diff --git a/pkg/webtests/huma_avatar_test.go b/pkg/webtests/huma_avatar_test.go index 16c481015..cffd13d72 100644 --- a/pkg/webtests/huma_avatar_test.go +++ b/pkg/webtests/huma_avatar_test.go @@ -20,6 +20,9 @@ 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" ) @@ -81,4 +84,68 @@ func TestAvatar(t *testing.T) { 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()) + }) }