Files
verdande/internal/httpapi/views_test.go
T

203 lines
6.9 KiB
Go
Raw Normal View History

2026-08-17 17:05:06 +02:00
package httpapi
import (
"net/http"
"testing"
)
2026-08-17 17:05:06 +02:00
// A label is how you find a task whose wording you have forgotten. Search
// covered the text only, so "regnskab" found nothing even with the task
// labelled @regnskab in front of you.
func TestSearchFindsTasksByLabel(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
ts.do(t, "POST", "/api/v1/tasks/quick-add",
map[string]any{"text": "ring til revisoren @regnskab"})
ts.do(t, "POST", "/api/v1/tasks/quick-add",
map[string]any{"text": "køb kaffe"})
_, body := ts.do(t, "GET", "/api/v1/search?q=regnskab", nil)
tasks, _ := body["tasks"].([]any)
if len(tasks) != 1 {
t.Fatalf("found %d tasks searching for a label, want 1: %v", len(tasks), body)
}
if got := tasks[0].(map[string]any)["content"]; got != "ring til revisoren" {
t.Errorf("found %v", got)
}
}
// The text search still works, and a label match does not swallow it.
func TestSearchStillFindsTasksByText(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
ts.do(t, "POST", "/api/v1/tasks/quick-add", map[string]any{"text": "køb grøn maling"})
// Diacritic folding still applies to the text side.
_, body := ts.do(t, "GET", "/api/v1/search?q=gron", nil)
if tasks, _ := body["tasks"].([]any); len(tasks) != 1 {
t.Errorf("searching \"gron\" found %d tasks, want 1", len(tasks))
}
}
// Somebody else's label must not pull their task into your results — the label
// join has to stay inside the visibility the outer query already applies.
func TestSearchByLabelStaysWithinWhatYouCanSee(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
other := ts.newUser(t, "anden@example.dk", "Anden")
other.do(t, "POST", "/api/v1/tasks/quick-add",
map[string]any{"text": "deres hemmelighed @regnskab"})
_, body := ts.do(t, "GET", "/api/v1/search?q=regnskab", nil)
if tasks, _ := body["tasks"].([]any); len(tasks) != 0 {
t.Errorf("found somebody else's task: %v", tasks)
}
}
// "Waiting on others" is the other half of an assignee filter, and the half that
// is easy to get backwards: what somebody has handed over, not what they have
// been handed.
func TestDelegatedListsWhatOtherPeopleAreSittingOn(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
ts.newUser(t, "anders@example.dk", "Anders")
anders, err := ts.db.UserByEmail(t.Context(), "anders@example.dk")
if err != nil {
t.Fatal(err)
}
me, err := ts.db.UserByEmail(t.Context(), "kristian@example.dk")
if err != nil {
t.Fatal(err)
}
_, project := ts.do(t, "POST", "/api/v1/projects", map[string]any{"name": "Delt"})
projectID := project["id"].(string)
ts.do(t, "POST", "/api/v1/projects/"+projectID+"/invites", map[string]any{
"email": "anders@example.dk", "role": "editor",
})
ts.do(t, "POST", "/api/v1/tasks", map[string]any{
"content": "anders skriver rapporten", "project_id": projectID, "assignee_id": anders.ID,
})
// Mine, and unassigned: neither belongs in a view about other people.
ts.do(t, "POST", "/api/v1/tasks", map[string]any{
"content": "jeg læser den", "project_id": projectID, "assignee_id": me.ID,
})
ts.do(t, "POST", "/api/v1/tasks", map[string]any{
"content": "ingen har den", "project_id": projectID,
})
resp, body := ts.do(t, "GET", "/api/v1/delegated", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("delegated: %d %v", resp.StatusCode, body)
}
people, _ := body["people"].([]any)
if len(people) != 1 {
t.Fatalf("want one person, got %v", people)
}
person := people[0].(map[string]any)
if person["name"] != "Anders" {
t.Errorf("name = %v — the view has to carry the name, or the client has no way to ask for it",
person["name"])
}
if person["avatar_color"] == nil || person["avatar_color"] == "" {
t.Errorf("no avatar colour: %v", person)
}
tasks, _ := person["tasks"].([]any)
if len(tasks) != 1 {
t.Fatalf("want one task, got %v", tasks)
}
if tasks[0].(map[string]any)["content"] != "anders skriver rapporten" {
t.Errorf("content = %v", tasks[0])
}
}
// The view is per person: Anders is not waiting on himself, and he cannot see
// what somebody has delegated in a project he is not in.
func TestDelegatedIsPerPersonAndRespectsAccess(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
anders := ts.newUser(t, "anders@example.dk", "Anders")
andersUser, err := ts.db.UserByEmail(t.Context(), "anders@example.dk")
if err != nil {
t.Fatal(err)
}
// A project Anders is not a member of.
_, project := ts.do(t, "POST", "/api/v1/projects", map[string]any{"name": "Privat"})
projectID := project["id"].(string)
ts.do(t, "POST", "/api/v1/projects/"+projectID+"/invites", map[string]any{
"email": "anders@example.dk", "role": "editor",
})
ts.do(t, "POST", "/api/v1/tasks", map[string]any{
"content": "anders gør det", "project_id": projectID, "assignee_id": andersUser.ID,
})
// It is assigned to him, so it is not something he is waiting on.
_, body := anders.do(t, "GET", "/api/v1/delegated", nil)
if people, _ := body["people"].([]any); len(people) != 0 {
t.Errorf("Anders is waiting on himself: %v", people)
}
// And somebody with no access to the project sees none of it.
other := ts.newUser(t, "tredje@example.dk", "Tredje")
_, third := other.do(t, "GET", "/api/v1/delegated", nil)
if people, _ := third["people"].([]any); len(people) != 0 {
t.Errorf("a task in a project they cannot see: %v", people)
}
}
// /people is what puts a name on an assignee id. It must be the people you share
// work with — not the instance's user list, which is the administrator's page.
func TestPeopleIsWhoYouShareWorkWith(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
colleague := ts.newUser(t, "anders@example.dk", "Anders")
ts.newUser(t, "fremmed@example.dk", "Fremmed")
_, project := ts.do(t, "POST", "/api/v1/projects", map[string]any{"name": "Delt"})
ts.do(t, "POST", "/api/v1/projects/"+project["id"].(string)+"/invites", map[string]any{
"email": "anders@example.dk", "role": "editor",
})
_, body := ts.do(t, "GET", "/api/v1/people", nil)
names := map[string]bool{}
for _, raw := range body["people"].([]any) {
p := raw.(map[string]any)
names[p["name"].(string)] = true
if p["avatar_color"] == "" {
t.Errorf("no avatar colour for %v — the row cannot draw a face", p["name"])
}
}
if !names["Kristian"] {
t.Error("yourself is missing, so a client cannot tell 'me' from 'somebody else' against one list")
}
if !names["Anders"] {
t.Error("somebody sharing a project is missing")
}
if names["Fremmed"] {
t.Error("an account with no project in common is listed — this is not the user directory")
}
// And it is symmetric: the person invited sees the person who invited them.
_, theirs := colleague.do(t, "GET", "/api/v1/people", nil)
seen := map[string]bool{}
for _, raw := range theirs["people"].([]any) {
seen[raw.(map[string]any)["name"].(string)] = true
}
if !seen["Kristian"] || !seen["Anders"] {
t.Errorf("the member cannot see who they share with: %v", seen)
}
if seen["Fremmed"] {
t.Errorf("a stranger is listed: %v", seen)
}
}