From 7bd561ded81c03ed7046e184977669c3f211bd3f Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 20 Apr 2026 11:05:39 +0200 Subject: [PATCH] test(spike): Label round-trip through Huma OAS 3.1 routes --- pkg/webtests/huma_label_test.go | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 pkg/webtests/huma_label_test.go diff --git a/pkg/webtests/huma_label_test.go b/pkg/webtests/huma_label_test.go new file mode 100644 index 000000000..eb495953e --- /dev/null +++ b/pkg/webtests/huma_label_test.go @@ -0,0 +1,157 @@ +// 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 . + +package webtests + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "code.vikunja.io/api/pkg/modules/auth" + "code.vikunja.io/api/pkg/user" + + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// tokenForUser returns a JWT bearer token for the given user, suitable for the +// "Authorization: Bearer ..." header on full ServeHTTP requests. +func tokenForUser(t *testing.T, u *user.User) string { + token, err := auth.NewUserJWTAuthtoken(u, "test-session-id") + require.NoError(t, err) + return token +} + +// TestHumaLabel_Create_ReadOne_via_OAS31Route exercises the Huma-served +// /labels endpoint with the same auth + fixtures the legacy tests use. +// This proves: +// 1. humaecho5 adapter dispatches correctly +// 2. JWT middleware still populates the echo.Context +// 3. auth.GetAuthFromContext fishes it back out +// 4. DoCreate / DoReadOne wire up through to the model +// 5. Response JSON shape matches what the frontend expects +func TestHumaLabel_Create_ReadOne_via_OAS31Route(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + token := tokenForUser(t, &testuser1) + + // 1) PUT /api/v1/oas3/labels — create a label via the Huma-mounted route + createReq := httptest.NewRequest(http.MethodPut, "/api/v1/oas3/labels", + strings.NewReader(`{"title":"spike","hex_color":"abcdef"}`)) + createReq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + createReq.Header.Set(echo.HeaderAuthorization, "Bearer "+token) + createRec := httptest.NewRecorder() + e.ServeHTTP(createRec, createReq) + + require.Equalf(t, http.StatusOK, createRec.Code, + "unexpected status %d; body=%q", createRec.Code, createRec.Body.String()) + + var created map[string]any + require.NoError(t, json.Unmarshal(createRec.Body.Bytes(), &created)) + assert.Equal(t, "spike", created["title"]) + + rawID, ok := created["id"] + require.Truef(t, ok, "response body has no id field: %q", createRec.Body.String()) + // JSON numbers decode to float64. + idFloat, ok := rawID.(float64) + require.True(t, ok, "id field is not a number") + require.NotZero(t, int64(idFloat)) + id := strconv.FormatInt(int64(idFloat), 10) + + // 2) GET /api/v1/oas3/labels/{id} — read it back + readReq := httptest.NewRequest(http.MethodGet, "/api/v1/oas3/labels/"+id, nil) + readReq.Header.Set(echo.HeaderAuthorization, "Bearer "+token) + readRec := httptest.NewRecorder() + e.ServeHTTP(readRec, readReq) + + require.Equalf(t, http.StatusOK, readRec.Code, + "unexpected status %d; body=%q", readRec.Code, readRec.Body.String()) + + var fetched map[string]any + require.NoError(t, json.Unmarshal(readRec.Body.Bytes(), &fetched)) + assert.Equal(t, "spike", fetched["title"]) + assert.Equal(t, idFloat, fetched["id"]) +} + +// TestHumaLabel_OpenAPISpecContainsLabelPaths proves the spec is served +// and includes the Label routes. +func TestHumaLabel_OpenAPISpecContainsLabelPaths(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/oas3/openapi.json", nil) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + require.Equalf(t, http.StatusOK, rec.Code, + "unexpected status %d; body=%q", rec.Code, rec.Body.String()) + + body := rec.Body.String() + assert.Contains(t, body, `"openapi":"3.1`) + assert.Contains(t, body, `/labels`) + assert.Contains(t, body, `/labels/{id}`) +} + +// TestHumaLabel_ForbiddenErrorShape ensures a 403 returns +// {"message": "Forbidden"} and NOT RFC 9457 problem+json. +func TestHumaLabel_ForbiddenErrorShape(t *testing.T) { + e, err := setupTestEnv() + require.NoError(t, err) + + // user 1 creates a label via the Huma route... + creatorToken := tokenForUser(t, &testuser1) + createReq := httptest.NewRequest(http.MethodPut, "/api/v1/oas3/labels", + strings.NewReader(`{"title":"forbidden-target"}`)) + createReq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + createReq.Header.Set(echo.HeaderAuthorization, "Bearer "+creatorToken) + createRec := httptest.NewRecorder() + e.ServeHTTP(createRec, createReq) + require.Equalf(t, http.StatusOK, createRec.Code, + "create failed with %d: %q", createRec.Code, createRec.Body.String()) + + var created map[string]any + require.NoError(t, json.Unmarshal(createRec.Body.Bytes(), &created)) + idFloat, ok := created["id"].(float64) + require.True(t, ok) + id := strconv.FormatInt(int64(idFloat), 10) + + // ...another user (user 10) tries to delete it. + attackerToken := tokenForUser(t, &testuser10) + delReq := httptest.NewRequest(http.MethodDelete, "/api/v1/oas3/labels/"+id, nil) + delReq.Header.Set(echo.HeaderAuthorization, "Bearer "+attackerToken) + delRec := httptest.NewRecorder() + e.ServeHTTP(delRec, delReq) + + assert.Equalf(t, http.StatusForbidden, delRec.Code, + "expected 403, got %d; body=%q", delRec.Code, delRec.Body.String()) + + var body map[string]any + require.NoError(t, json.Unmarshal(delRec.Body.Bytes(), &body)) + assert.Equal(t, "Forbidden", body["message"]) + + // RFC 9457 problem+json would put these on the payload; Vikunja's legacy + // shape must stay clean. + _, hasType := body["type"] + _, hasTitle := body["title"] + assert.Falsef(t, hasType, "unexpected RFC 9457 field 'type' in body %q", delRec.Body.String()) + assert.Falsef(t, hasTitle, "unexpected RFC 9457 field 'title' in body %q", delRec.Body.String()) +}