test(api/v2): isolate project tests per-handler, not via shared harness

The project test port had added db.LoadFixtures() into the shared
webHandlerTestV2.serve(), reloading fixtures before every request. That
wiped runtime-created rows between requests within a test, breaking the
create-then-read-back contract every v2 resource relies on (e.g.
TestHumaTeam/Create/Public read its freshly-created team back and got 403).

Revert that shared-harness change and isolate the project/archived tests
the way the team and label tests do: each subtest builds its own handler
via handlerFor, so it runs against freshly loaded fixtures (setupTestEnv
reloads once per handler), while a create-then-read-back sequence reuses
one handler within the subtest.
This commit is contained in:
kolaente 2026-06-04 23:57:50 +02:00 committed by kolaente
parent bec991288b
commit 33b9aa6292
3 changed files with 61 additions and 16 deletions

View File

@ -21,6 +21,7 @@ import (
"testing" "testing"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -49,16 +50,17 @@ import (
// HTTP surface to exercise them through. They remain proven by the v1 webtest // HTTP surface to exercise them through. They remain proven by the v1 webtest
// and by the model-level TestCheckIsArchived until those resources are ported. // and by the model-level TestCheckIsArchived until those resources are ported.
func TestHumaArchived(t *testing.T) { func TestHumaArchived(t *testing.T) {
testHandler := webHandlerTestV2{ // Each subtest gets a pristine handler: the shared serve() does not reload
user: &testuser1, // fixtures per request, so the un-archive/archive mutations below must not
basePath: "/api/v2/projects", // leak across subtests (mirrors huma_team_test.go's per-subtest isolation).
idParam: "project", handlerFor := func(u *user.User) *webHandlerTestV2 {
t: t, return &webHandlerTestV2{user: u, basePath: "/api/v2/projects", idParam: "project", t: t}
} }
// The project belongs to an archived parent project. // The project belongs to an archived parent project.
t.Run("archived parent project", func(t *testing.T) { t.Run("archived parent project", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) { t.Run("not editable", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"TestIpsum","is_archived":true}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"TestIpsum","is_archived":true}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusPreconditionFailed, getHTTPErrorCode(err)) assert.Equal(t, http.StatusPreconditionFailed, getHTTPErrorCode(err))
@ -67,6 +69,7 @@ func TestHumaArchived(t *testing.T) {
t.Run("not unarchivable", func(t *testing.T) { t.Run("not unarchivable", func(t *testing.T) {
// The un-archive exception only applies to the self-archived // The un-archive exception only applies to the self-archived
// project; here the archived ancestor (22) still blocks it. // project; here the archived ancestor (22) still blocks it.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"LoremIpsum","is_archived":false}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "21"}, `{"title":"LoremIpsum","is_archived":false}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusPreconditionFailed, getHTTPErrorCode(err)) assert.Equal(t, http.StatusPreconditionFailed, getHTTPErrorCode(err))
@ -77,12 +80,14 @@ func TestHumaArchived(t *testing.T) {
// The project itself is archived. // The project itself is archived.
t.Run("archived individually", func(t *testing.T) { t.Run("archived individually", func(t *testing.T) {
t.Run("not editable", func(t *testing.T) { t.Run("not editable", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"TestIpsum","is_archived":true}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"TestIpsum","is_archived":true}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusPreconditionFailed, getHTTPErrorCode(err)) assert.Equal(t, http.StatusPreconditionFailed, getHTTPErrorCode(err))
assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived) assertHandlerErrorCode(t, err, models.ErrCodeProjectIsArchived)
}) })
t.Run("unarchivable", func(t *testing.T) { t.Run("unarchivable", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"LoremIpsum","is_archived":false}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "22"}, `{"title":"LoremIpsum","is_archived":false}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"is_archived":false`) assert.Contains(t, rec.Body.String(), `"is_archived":false`)
@ -91,6 +96,7 @@ func TestHumaArchived(t *testing.T) {
// Archiving a non-archived project should work. // Archiving a non-archived project should work.
t.Run("archive non-archived project", func(t *testing.T) { t.Run("archive non-archived project", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Test1","is_archived":true}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Test1","is_archived":true}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"is_archived":true`) assert.Contains(t, rec.Body.String(), `"is_archived":true`)

View File

@ -25,6 +25,7 @@ import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -43,16 +44,19 @@ const runelength250Title = `Lorem ipsum dolor sit amet, consetetur sadipscing el
// codes, absent ETag, expand=permissions). Status-code differences from v1 are // codes, absent ETag, expand=permissions). Status-code differences from v1 are
// noted inline. All cases drive testuser1, whose share fixtures (projects 617, // noted inline. All cases drive testuser1, whose share fixtures (projects 617,
// 20, 3234, 911) exercise every share-kind×level just like the v1 test. // 20, 3234, 911) exercise every share-kind×level just like the v1 test.
//
// Each subtest builds its own handler via handlerFor so it runs against freshly
// loaded fixtures (setupTestEnv reloads them once per handler). The shared
// serve() does not reload per request, so mutating and exact-cardinality
// subtests must not share a handler — mirrors huma_team_test.go's isolation.
func TestHumaProject(t *testing.T) { func TestHumaProject(t *testing.T) {
testHandler := webHandlerTestV2{ handlerFor := func(u *user.User) *webHandlerTestV2 {
user: &testuser1, return &webHandlerTestV2{user: u, basePath: "/api/v2/projects", idParam: "project", t: t}
basePath: "/api/v2/projects",
idParam: "project",
t: t,
} }
t.Run("ReadAll", func(t *testing.T) { t.Run("ReadAll", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testReadAllWithUser(nil, nil) rec, err := testHandler.testReadAllWithUser(nil, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`) assert.Contains(t, rec.Body.String(), `Test1`)
@ -64,6 +68,7 @@ func TestHumaProject(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `Test22`) // Archived directly assert.NotContains(t, rec.Body.String(), `Test22`) // Archived directly
}) })
t.Run("Search", func(t *testing.T) { t.Run("Search", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testReadAllWithUser(url.Values{"q": []string{"Test1"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"q": []string{"Test1"}}, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`) assert.Contains(t, rec.Body.String(), `Test1`)
@ -94,6 +99,7 @@ func TestHumaProject(t *testing.T) {
} }
}) })
t.Run("Normal with archived projects", func(t *testing.T) { t.Run("Normal with archived projects", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`) assert.Contains(t, rec.Body.String(), `Test1`)
@ -119,6 +125,7 @@ func TestHumaProject(t *testing.T) {
assert.True(t, found21, "Project 21 should be present when listing archived projects") assert.True(t, found21, "Project 21 should be present when listing archived projects")
}) })
t.Run("Expand permissions", func(t *testing.T) { t.Run("Expand permissions", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testReadAllWithUser(url.Values{"expand": []string{"permissions"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"expand": []string{"permissions"}}, nil)
require.NoError(t, err) require.NoError(t, err)
// User 1 owns Test1 → admin (2). With expand the field carries a real value. // User 1 owns Test1 → admin (2). With expand the field carries a real value.
@ -128,6 +135,7 @@ func TestHumaProject(t *testing.T) {
t.Run("ReadOne", func(t *testing.T) { t.Run("ReadOne", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "1"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "1"})
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test1"`) assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
@ -148,6 +156,7 @@ func TestHumaProject(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
// Projects return 404 here (CanRead → GetProjectSimpleByID → ErrProjectDoesNotExist), // Projects return 404 here (CanRead → GetProjectSimpleByID → ErrProjectDoesNotExist),
// unlike labels which return 403 from the read branch. // unlike labels which return 403 from the read branch.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "9999"}) _, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "9999"})
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err)) assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err))
@ -156,6 +165,7 @@ func TestHumaProject(t *testing.T) {
t.Run("Permissions check", func(t *testing.T) { t.Run("Permissions check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Project 20 exists but is owned by user13: CanRead returns false → 403. // Project 20 exists but is owned by user13: CanRead returns false → 403.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "20"}) _, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": "20"})
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
@ -165,6 +175,7 @@ func TestHumaProject(t *testing.T) {
// granted level via the always-present max_permission field, the v2 // granted level via the always-present max_permission field, the v2
// equivalent of v1's x-max-permission header assertion. // equivalent of v1's x-max-permission header assertion.
readOneWithMaxPermission := func(t *testing.T, projectID, title string, want models.Permission) { readOneWithMaxPermission := func(t *testing.T, projectID, title string, want models.Permission) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": projectID}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"project": projectID})
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"`+title+`"`) assert.Contains(t, rec.Body.String(), `"title":"`+title+`"`)
@ -218,6 +229,7 @@ func TestHumaProject(t *testing.T) {
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem"}`) rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, http.StatusCreated, rec.Code)
@ -232,6 +244,7 @@ func TestHumaProject(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"max_permission":null`) assert.Contains(t, rec.Body.String(), `"max_permission":null`)
}) })
t.Run("Normal with description", func(t *testing.T) { t.Run("Normal with description", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","description":"Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","description":"Ipsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, http.StatusCreated, rec.Code)
@ -241,6 +254,7 @@ func TestHumaProject(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `"tasks":`) assert.NotContains(t, rec.Body.String(), `"tasks":`)
}) })
t.Run("Nonexisting parent project", func(t *testing.T) { t.Run("Nonexisting parent project", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":99999}`) _, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":99999}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err)) assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err))
@ -248,6 +262,7 @@ func TestHumaProject(t *testing.T) {
}) })
t.Run("Empty title", func(t *testing.T) { t.Run("Empty title", func(t *testing.T) {
// v2 returns 422, not v1's 400; full body shape asserted in TestHuma_ErrorShapeIsRFC9457. // v2 returns 422, not v1's 400; full body shape asserted in TestHuma_ErrorShapeIsRFC9457.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":""}`) _, err := testHandler.testCreateWithUser(nil, nil, `{"title":""}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err)) assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err))
@ -255,6 +270,7 @@ func TestHumaProject(t *testing.T) {
t.Run("Title too long", func(t *testing.T) { t.Run("Title too long", func(t *testing.T) {
// v1 hit govalidator runelength(1|250); v2 enforces maxLength:250 at // v1 hit govalidator runelength(1|250); v2 enforces maxLength:250 at
// the schema layer → 422 before the handler. // the schema layer → 422 before the handler.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"`+runelength250Title+`"}`) _, err := testHandler.testCreateWithUser(nil, nil, `{"title":"`+runelength250Title+`"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err)) assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err))
@ -264,17 +280,20 @@ func TestHumaProject(t *testing.T) {
// write access to that parent. // write access to that parent.
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Parent 20 is owned by user13; user1 has no access. // Parent 20 is owned by user13; user1 has no access.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":20}`) _, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":20}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
}) })
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) { t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
// Read-only on parent 32 is not enough to create a child. // Read-only on parent 32 is not enough to create a child.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":32}`) _, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":32}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
}) })
t.Run("Shared Via Parent Project Team write", func(t *testing.T) { t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":33}`) rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":33}`)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, http.StatusCreated, rec.Code)
@ -284,6 +303,7 @@ func TestHumaProject(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `"tasks":`) assert.NotContains(t, rec.Body.String(), `"tasks":`)
}) })
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) { t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":34}`) rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":34}`)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, http.StatusCreated, rec.Code)
@ -295,11 +315,13 @@ func TestHumaProject(t *testing.T) {
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) { t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
// Read-only on parent 9 is not enough to create a child. // Read-only on parent 9 is not enough to create a child.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":9}`) _, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":9}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
}) })
t.Run("Shared Via Parent Project User write", func(t *testing.T) { t.Run("Shared Via Parent Project User write", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":10}`) rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":10}`)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, http.StatusCreated, rec.Code)
@ -309,6 +331,7 @@ func TestHumaProject(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `"tasks":`) assert.NotContains(t, rec.Body.String(), `"tasks":`)
}) })
t.Run("Shared Via Parent Project User admin", func(t *testing.T) { t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":11}`) rec, err := testHandler.testCreateWithUser(nil, nil, `{"title":"Lorem","parent_project_id":11}`)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, http.StatusCreated, rec.Code)
@ -322,6 +345,7 @@ func TestHumaProject(t *testing.T) {
t.Run("Update", func(t *testing.T) { t.Run("Update", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
@ -331,23 +355,27 @@ func TestHumaProject(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"max_permission":null`) assert.Contains(t, rec.Body.String(), `"max_permission":null`)
}) })
t.Run("Normal with updating the description", func(t *testing.T) { t.Run("Normal with updating the description", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`) assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"TestLoremIpsum"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err)) assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err))
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
}) })
t.Run("Empty title", func(t *testing.T) { t.Run("Empty title", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":""}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":""}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err)) assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err))
}) })
t.Run("Title too long", func(t *testing.T) { t.Run("Title too long", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"`+runelength250Title+`"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "1"}, `{"title":"`+runelength250Title+`"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err)) assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err))
@ -355,6 +383,7 @@ func TestHumaProject(t *testing.T) {
t.Run("Permissions check", func(t *testing.T) { t.Run("Permissions check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Owned by user13. // Owned by user13.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "20"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "20"}, `{"title":"TestLoremIpsum"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
@ -362,64 +391,76 @@ func TestHumaProject(t *testing.T) {
t.Run("Shared Via Team readonly", func(t *testing.T) { t.Run("Shared Via Team readonly", func(t *testing.T) {
// Read access is not enough to update. // Read access is not enough to update.
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "6"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "6"}, `{"title":"TestLoremIpsum"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "7"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "7"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "8"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "8"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "9"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "9"}, `{"title":"TestLoremIpsum"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "10"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "10"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "11"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "11"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) { t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "12"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "12"}, `{"title":"TestLoremIpsum"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
}) })
t.Run("Shared Via Parent Project Team write", func(t *testing.T) { t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "13"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "13"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) { t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "14"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "14"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) { t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "15"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "15"}, `{"title":"TestLoremIpsum"}`)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
}) })
t.Run("Shared Via Parent Project User write", func(t *testing.T) { t.Run("Shared Via Parent Project User write", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "16"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "16"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via Parent Project User admin", func(t *testing.T) { t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "17"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"project": "17"}, `{"title":"TestLoremIpsum"}`)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
@ -429,6 +470,7 @@ func TestHumaProject(t *testing.T) {
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "1"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "1"})
require.NoError(t, err) require.NoError(t, err)
// v2 delete is 204 No Content; v1 returned 200 + a message body. // v2 delete is 204 No Content; v1 returned 200 + a message body.
@ -436,6 +478,7 @@ func TestHumaProject(t *testing.T) {
assert.Empty(t, rec.Body.String()) assert.Empty(t, rec.Body.String())
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "999"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "999"})
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err)) assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err))
@ -444,11 +487,13 @@ func TestHumaProject(t *testing.T) {
t.Run("Permissions check", func(t *testing.T) { t.Run("Permissions check", func(t *testing.T) {
// Delete needs admin everywhere: read and write must be refused, admin allowed. // Delete needs admin everywhere: read and write must be refused, admin allowed.
deleteForbidden := func(t *testing.T, projectID string) { deleteForbidden := func(t *testing.T, projectID string) {
testHandler := handlerFor(&testuser1)
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": projectID}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": projectID})
require.Error(t, err) require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err)) assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
} }
deleteAllowed := func(t *testing.T, projectID string) { deleteAllowed := func(t *testing.T, projectID string) {
testHandler := handlerFor(&testuser1)
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": projectID}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": projectID})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusNoContent, rec.Code) assert.Equal(t, http.StatusNoContent, rec.Code)

View File

@ -447,12 +447,6 @@ func (h *webHandlerTestV2) buildURL(queryParams url.Values, urlParams map[string
func (h *webHandlerTestV2) serve(method, path, payload string) (*httptest.ResponseRecorder, error) { func (h *webHandlerTestV2) serve(method, path, payload string) (*httptest.ResponseRecorder, error) {
require.NoError(h.t, h.ensureEnv()) require.NoError(h.t, h.ensureEnv())
// Reload fixtures before every request so each subtest sees a pristine
// database, mirroring v1's webHandlerTest (which calls setupTestEnv ->
// LoadFixtures per request via bootstrapTestRequest). Without this, mutating
// subtests (create/update/delete) would leak state into later ones in the
// shared Echo instance and break permission-matrix assertions.
require.NoError(h.t, db.LoadFixtures())
token, err := auth.NewUserJWTAuthtoken(h.user, "test-session-id") token, err := auth.NewUserJWTAuthtoken(h.user, "test-session-id")
require.NoError(h.t, err) require.NoError(h.t, err)
var reader *strings.Reader var reader *strings.Reader