2026-08-17 17:05:06 +02:00
|
|
|
package httpapi
|
|
|
|
|
|
2026-08-17 19:36:37 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-17 19:36:37 +02:00
|
|
|
|
|
|
|
|
// "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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-17 22:31:43 +02:00
|
|
|
|
|
|
|
|
// /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)
|
|
|
|
|
}
|
|
|
|
|
}
|