mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-07-15 22:32:29 -05:00
Bring the v2 avatar webtest to 1:1 parity with the v1 avatar tests so the v1 routes and tests can be removed without losing coverage: - link-share auth path: a request authenticated as a link-share user (not a regular JWT) returns 200 + non-empty image bytes, porting v1's TestLinkShareAvatar. - bot user: the botmarble provider path returns 200 + SVG bytes, a distinct rendering v1 never exercised; asserts the marble mask id so it cannot silently fall through to the default placeholder. - non-numeric size: rejected with 422 (Huma's int64 query validation) rather than v1's 400 ErrInvalidModel, both being client errors that refuse the malformed input.
152 lines
7.1 KiB
Go
152 lines
7.1 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package webtests
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"code.vikunja.io/api/pkg/models"
|
|
"code.vikunja.io/api/pkg/modules/auth"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestAvatar covers the v2 binary-response endpoint GET /api/v2/avatar/{username}.
|
|
// It is the reference for serving raw bytes with a runtime-chosen Content-Type.
|
|
// Unlike v1's CRUD resources there is no model — the input is a path username and
|
|
// an optional size query param. The endpoint is authenticated (global security),
|
|
// so an anonymous request must be rejected with 401.
|
|
func TestAvatar(t *testing.T) {
|
|
t.Run("Authenticated known user returns bytes and a content type", func(t *testing.T) {
|
|
e, err := setupTestEnv()
|
|
require.NoError(t, err)
|
|
token := humaTokenFor(t, &testuser1)
|
|
|
|
rec := humaRequest(t, e, http.MethodGet, "/api/v2/avatar/user1", "", token, "")
|
|
require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String())
|
|
assert.NotEmpty(t, rec.Body.Bytes(), "avatar bytes must be returned")
|
|
assert.NotEmpty(t, rec.Header().Get("Content-Type"), "a content type must be set")
|
|
// user1 has no avatar_provider configured, so the empty provider serves the default SVG.
|
|
assert.Equal(t, "image/svg+xml", rec.Header().Get("Content-Type"))
|
|
})
|
|
|
|
t.Run("Size query param is accepted and clamped", func(t *testing.T) {
|
|
e, err := setupTestEnv()
|
|
require.NoError(t, err)
|
|
token := humaTokenFor(t, &testuser1)
|
|
|
|
// A size far above config.ServiceMaxAvatarSize must be clamped, not rejected.
|
|
rec := humaRequest(t, e, http.MethodGet, "/api/v2/avatar/user1?size=99999", "", token, "")
|
|
require.Equal(t, http.StatusOK, rec.Code, "oversized size must clamp, not error; body: %s", rec.Body.String())
|
|
assert.NotEmpty(t, rec.Body.Bytes())
|
|
|
|
// A normal size is honored.
|
|
rec = humaRequest(t, e, http.MethodGet, "/api/v2/avatar/user1?size=64", "", token, "")
|
|
require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String())
|
|
assert.NotEmpty(t, rec.Body.Bytes())
|
|
})
|
|
|
|
t.Run("Anonymous request is rejected with 401", func(t *testing.T) {
|
|
e, err := setupTestEnv()
|
|
require.NoError(t, err)
|
|
|
|
// Empty token => no Authorization header => anonymous. Proves the endpoint
|
|
// inherits the global security and is NOT public.
|
|
rec := humaRequest(t, e, http.MethodGet, "/api/v2/avatar/user1", "", "", "")
|
|
assert.Equal(t, http.StatusUnauthorized, rec.Code, "anonymous must get 401; body: %s", rec.Body.String())
|
|
})
|
|
|
|
t.Run("Unknown user falls back to the default avatar like v1", func(t *testing.T) {
|
|
e, err := setupTestEnv()
|
|
require.NoError(t, err)
|
|
token := humaTokenFor(t, &testuser1)
|
|
|
|
// v1 GetAvatar does not 404 for an unknown user — it serves the empty
|
|
// provider's default SVG with a 200. v2 must match that behaviour.
|
|
rec := humaRequest(t, e, http.MethodGet, "/api/v2/avatar/this-user-does-not-exist", "", token, "")
|
|
require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String())
|
|
assert.NotEmpty(t, rec.Body.Bytes())
|
|
assert.Equal(t, "image/svg+xml", rec.Header().Get("Content-Type"))
|
|
})
|
|
|
|
t.Run("Authenticated as a link share returns bytes and a content type", func(t *testing.T) {
|
|
// Ports v1's TestLinkShareAvatar: a request authenticated as a link-share
|
|
// user (not a regular JWT) must succeed. The avatar endpoint inherits the
|
|
// global security; a link-share JWT is a valid credential and resolves to a
|
|
// link-share web.Auth in authFromCtx, so the request must not 401. The
|
|
// requested username here is the synthetic link-share-1 handle (matching v1):
|
|
// it is not a real user row, so GetAvatarForUsername serves the empty
|
|
// provider's default SVG with a 200 — proving the link-share auth path works.
|
|
e, err := setupTestEnv()
|
|
require.NoError(t, err)
|
|
|
|
share := &models.LinkSharing{
|
|
ID: 1,
|
|
Hash: "test",
|
|
ProjectID: 1,
|
|
Permission: models.PermissionRead,
|
|
SharingType: models.SharingTypeWithoutPassword,
|
|
SharedByID: 1,
|
|
}
|
|
token, err := auth.NewLinkShareJWTAuthtoken(share)
|
|
require.NoError(t, err)
|
|
|
|
rec := humaRequest(t, e, http.MethodGet, "/api/v2/avatar/link-share-1", "", token, "")
|
|
require.Equal(t, http.StatusOK, rec.Code, "link-share auth must succeed; body: %s", rec.Body.String())
|
|
assert.NotEmpty(t, rec.Body.Bytes(), "avatar bytes must be returned for a link-share caller")
|
|
assert.NotEmpty(t, rec.Header().Get("Content-Type"), "a content type must be set")
|
|
})
|
|
|
|
t.Run("Bot user avatar is rendered by the botmarble provider", func(t *testing.T) {
|
|
// A bot user (bot_owner_id set) routes through the botmarble provider rather
|
|
// than its configured provider — a distinct rendering path v1's single test
|
|
// never exercised. It must still return 200 + non-empty bytes. botmarble
|
|
// renders a marble-style SVG, so the content type is image/svg+xml.
|
|
e, err := setupTestEnv()
|
|
require.NoError(t, err)
|
|
token := humaTokenFor(t, &testuser1)
|
|
|
|
rec := humaRequest(t, e, http.MethodGet, "/api/v2/avatar/bot-owner-a-assistant", "", token, "")
|
|
require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String())
|
|
assert.NotEmpty(t, rec.Body.Bytes(), "bot avatar bytes must be returned")
|
|
assert.Equal(t, "image/svg+xml", rec.Header().Get("Content-Type"), "botmarble renders an SVG")
|
|
// The marble mask id is unique to the (bot)marble renderer; the empty
|
|
// fallback SVG never contains it. Asserting on it proves IsBot() routed
|
|
// to botmarble rather than silently falling through to the default avatar.
|
|
assert.Contains(t, rec.Body.String(), "mask__marble",
|
|
"bot avatar must be rendered by the (bot)marble provider, not the default placeholder")
|
|
})
|
|
|
|
t.Run("Non-numeric size is rejected", func(t *testing.T) {
|
|
// v1 parses ?size with strconv.ParseInt and returns ErrInvalidModel
|
|
// ("Invalid size parameter") -> 400 for a non-numeric value. v2 declares
|
|
// Size as int64, so Huma's own request validation rejects the malformed
|
|
// query value before the handler runs and returns 422 Unprocessable Entity.
|
|
// Either client-error status proves the malformed input is refused rather
|
|
// than silently coerced; assert v2's actual behaviour (422).
|
|
e, err := setupTestEnv()
|
|
require.NoError(t, err)
|
|
token := humaTokenFor(t, &testuser1)
|
|
|
|
rec := humaRequest(t, e, http.MethodGet, "/api/v2/avatar/user1?size=notanumber", "", token, "")
|
|
require.Equal(t, http.StatusUnprocessableEntity, rec.Code,
|
|
"non-numeric size must be rejected by Huma's int64 validation; body: %s", rec.Body.String())
|
|
})
|
|
}
|