vikunja/pkg/webtests/huma_team_test.go

200 lines
8.0 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"
"net/http/httptest"
"net/url"
"strings"
"testing"
"code.vikunja.io/api/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestHumaTeam mirrors v1's model TestTeam shape so v2 contract parity is
// readable side-by-side. Named TestHumaTeam to avoid clashing with the v1
// model test (pkg/models/teams_test.go TestTeam).
//
// Fixture facts (pkg/db/fixtures/team_members.yml): testuser1 is an admin of
// team 1 and a non-admin member of teams 2-8. Team 9 (created by user 7) has
// only user 2 as a member, so user1 is not a member at all.
func TestHumaTeam(t *testing.T) {
testHandler := webHandlerTestV2{
user: &testuser1,
basePath: "/api/v2/teams",
idParam: "team",
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(nil, nil)
require.NoError(t, err)
// User 1 is a member of teams 1-8.
assert.Contains(t, rec.Body.String(), `testteam1`)
// User 1 is not a member of team 9 (only user 2 is).
assert.NotContains(t, rec.Body.String(), `testteam9`)
})
// testteam13 and testteam15 are public (teams.yml) and user1 is not a
// member of either (team_members.yml only lists user 10 there).
t.Run("Include public, but public teams disabled", func(t *testing.T) {
// The config gate is off by default: include_public must be a no-op
// so public teams the user is not a member of stay hidden.
require.False(t, config.ServiceEnablePublicTeams.GetBool())
rec, err := testHandler.testReadAllWithUser(url.Values{"include_public": []string{"true"}}, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `testteam1`)
assert.NotContains(t, rec.Body.String(), `testteam13`)
assert.NotContains(t, rec.Body.String(), `testteam15`)
})
t.Run("Include public when public teams enabled", func(t *testing.T) {
prev := config.ServiceEnablePublicTeams.GetBool()
config.ServiceEnablePublicTeams.Set(true)
defer config.ServiceEnablePublicTeams.Set(prev)
// Without include_public the public teams stay hidden even with the
// instance setting on.
rec, err := testHandler.testReadAllWithUser(nil, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `testteam1`)
assert.NotContains(t, rec.Body.String(), `testteam13`)
assert.NotContains(t, rec.Body.String(), `testteam15`)
// With include_public=true the public teams the user is not a member
// of are surfaced.
rec, err = testHandler.testReadAllWithUser(url.Values{"include_public": []string{"true"}}, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `testteam1`)
assert.Contains(t, rec.Body.String(), `testteam13`)
assert.Contains(t, rec.Body.String(), `testteam15`)
})
})
t.Run("ReadOne", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"team": "1"})
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"name":"testteam1"`)
assert.NotEmpty(t, rec.Result().Header.Get("ETag"))
})
t.Run("Nonexisting", func(t *testing.T) {
// CanRead refuses non-members before existence is checked, so a
// missing team returns 403, not 404.
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"team": "9999"})
require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
})
t.Run("Permissions check", func(t *testing.T) {
t.Run("Forbidden non-member", func(t *testing.T) {
// Team 9: user1 is not a member.
_, err := testHandler.testReadOneWithUser(nil, map[string]string{"team": "9"})
require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
})
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, nil, `{"name":"Lorem","description":"Ipsum"}`)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code)
assert.Contains(t, rec.Body.String(), `"name":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":"Ipsum"`)
})
t.Run("Empty name", func(t *testing.T) {
// Name has minLength:1, so Huma rejects an empty name with 422
// before the model is touched.
_, err := testHandler.testCreateWithUser(nil, nil, `{"name":""}`)
require.Error(t, err)
assert.Equal(t, http.StatusUnprocessableEntity, getHTTPErrorCode(err))
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Team 1: user1 is admin.
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"team": "1"}, `{"name":"TestLoremIpsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"name":"TestLoremIpsum"`)
})
t.Run("Nonexisting", func(t *testing.T) {
// CanUpdate -> IsAdmin -> GetTeamByID surfaces ErrTeamDoesNotExist (404).
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"team": "9999"}, `{"name":"TestLoremIpsum"}`)
require.Error(t, err)
assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err))
})
t.Run("Permissions check", func(t *testing.T) {
t.Run("Forbidden non-admin", func(t *testing.T) {
// Team 2: user1 is a member but not an admin.
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"team": "2"}, `{"name":"TestLoremIpsum"}`)
require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
})
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
// Team 1: user1 is admin.
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"team": "1"})
require.NoError(t, err)
// v2 delete is 204 No Content; v1 returned 200 + a message body.
assert.Equal(t, http.StatusNoContent, rec.Code)
assert.Empty(t, rec.Body.String())
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"team": "9999"})
require.Error(t, err)
assert.Equal(t, http.StatusNotFound, getHTTPErrorCode(err))
})
t.Run("Permissions check", func(t *testing.T) {
t.Run("Forbidden non-admin", func(t *testing.T) {
// Team 2: user1 is a member but not an admin.
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"team": "2"})
require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
})
})
})
}
// TestHumaTeam_ETagReturns304 covers the v2-only conditional-request behaviour
// (ETag + If-None-Match -> 304) with no v1 counterpart.
func TestHumaTeam_ETagReturns304(t *testing.T) {
e, err := setupTestEnv()
require.NoError(t, err)
token := humaTokenFor(t, &testuser1)
rec := humaRequest(t, e, http.MethodGet, "/api/v2/teams/1", "", token, "")
require.Equal(t, http.StatusOK, rec.Code, "body: %s", rec.Body.String())
etag := rec.Header().Get("ETag")
require.NotEmpty(t, etag, "GET must return an ETag header")
req := httptest.NewRequest(http.MethodGet, "/api/v2/teams/1", strings.NewReader(""))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("If-None-Match", etag)
rec = httptest.NewRecorder()
e.ServeHTTP(rec, req)
require.Equal(t, http.StatusNotModified, rec.Code, "body: %s", rec.Body.String())
}