2026-09-08 11:40:43 +02:00
|
|
|
package httpapi
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// notifications reads somebody's bell.
|
|
|
|
|
func notifications(t *testing.T, ts *testServer) ([]any, float64) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
resp, out := ts.do(t, "GET", "/api/v1/notifications", nil)
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatalf("list notifications: status %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
list, _ := out["notifications"].([]any)
|
|
|
|
|
unread, _ := out["unread"].(float64)
|
|
|
|
|
return list, unread
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func firstOf(t *testing.T, list []any) map[string]any {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if len(list) == 0 {
|
|
|
|
|
t.Fatal("ingen beskeder")
|
|
|
|
|
}
|
|
|
|
|
n, _ := list[0].(map[string]any)
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// At blive tildelt en opgave giver besked — og kun ved selve skiftet.
|
|
|
|
|
func TestBeingGivenATaskNotifiesTheAssignee(t *testing.T) {
|
|
|
|
|
owner := newTestServer(t)
|
|
|
|
|
owner.bootstrap(t)
|
|
|
|
|
other := owner.newUser(t, "sofie@example.dk", "Sofie")
|
|
|
|
|
sofie := userID(t, owner, "sofie@example.dk")
|
|
|
|
|
|
|
|
|
|
_, project := owner.do(t, "POST", "/api/v1/projects", map[string]any{"name": "Fælles"})
|
|
|
|
|
projectID, _ := project["id"].(string)
|
|
|
|
|
// Medlemskab kommer gennem en invitation; en konto, der allerede findes, bliver
|
|
|
|
|
// medlem med det samme.
|
|
|
|
|
if resp, _ := owner.do(t, "POST", "/api/v1/projects/"+projectID+"/invites",
|
|
|
|
|
map[string]any{"email": "sofie@example.dk", "role": "editor"}); resp.StatusCode >= 300 {
|
|
|
|
|
t.Fatalf("invitér: %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, task := owner.do(t, "POST", "/api/v1/tasks",
|
|
|
|
|
map[string]any{"project_id": projectID, "content": "ring til revisor"})
|
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
|
|
|
t.Fatalf("opret opgave: %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
taskID, _ := task["id"].(string)
|
|
|
|
|
|
|
|
|
|
// Hvad hun havde, før nogen tildelte hende noget. Ikke nødvendigvis nul —
|
|
|
|
|
// invitationen kan selv have sagt noget — og prøven handler om forskellen.
|
|
|
|
|
before, _ := notifications(t, other)
|
|
|
|
|
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/tasks/"+taskID,
|
|
|
|
|
map[string]any{"assignee_id": sofie}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatalf("tildel: %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list, _ := notifications(t, other)
|
|
|
|
|
if len(list) != len(before)+1 {
|
|
|
|
|
t.Fatalf("Sofie fik %d beskeder, want %d", len(list), len(before)+1)
|
|
|
|
|
}
|
|
|
|
|
n := firstOf(t, list)
|
|
|
|
|
if n["kind"] != "assigned" {
|
|
|
|
|
t.Errorf("kind = %v, want assigned", n["kind"])
|
|
|
|
|
}
|
|
|
|
|
if n["task_id"] != taskID {
|
|
|
|
|
t.Errorf("beskeden pegede ikke på opgaven: %v", n["task_id"])
|
|
|
|
|
}
|
|
|
|
|
if n["actor_name"] != "Kristian" {
|
|
|
|
|
t.Errorf("actor_name = %v", n["actor_name"])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// En rettelse af noget andet, mens den samme tildeling står i forespørgslen,
|
|
|
|
|
// er ikke en ny besked. Det er dét, en fladet, der sender hele opgaven ved hver
|
|
|
|
|
// ændring, gør — og så bliver klokken ubrugelig på en dag.
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/tasks/"+taskID,
|
|
|
|
|
map[string]any{"assignee_id": sofie, "content": "ring til revisoren"}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatalf("ret: %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
if now, _ := notifications(t, other); len(now) != len(before)+1 {
|
|
|
|
|
t.Errorf("en uændret tildeling gav besked igen: %d beskeder", len(now))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Og den, der tildeler sig selv, får ingenting.
|
|
|
|
|
me := userID(t, owner, "kristian@example.dk")
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/tasks/"+taskID,
|
|
|
|
|
map[string]any{"assignee_id": me}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatalf("tildel til sig selv: %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
if list, _ := notifications(t, owner); len(list) != 0 {
|
|
|
|
|
t.Errorf("man fik besked om noget, man selv gjorde: %d beskeder", len(list))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// En rettelse i en delt note giver besked — og gentagne rettelser bliver til én,
|
|
|
|
|
// så længe den ikke er læst.
|
|
|
|
|
func TestEditingASharedNoteNotifiesTheOthers(t *testing.T) {
|
|
|
|
|
owner := newTestServer(t)
|
|
|
|
|
owner.bootstrap(t)
|
|
|
|
|
other := owner.newUser(t, "sofie@example.dk", "Sofie")
|
|
|
|
|
sofie := userID(t, owner, "sofie@example.dk")
|
|
|
|
|
|
|
|
|
|
noteID := owner.createNote(t, "# Aftale om levering\nfirst")
|
|
|
|
|
if resp, _ := owner.do(t, "POST", "/api/v1/notes/"+noteID+"/shares",
|
|
|
|
|
map[string]string{"user_id": sofie, "role": "editor"}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatal("del noten")
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 11:58:47 +02:00
|
|
|
// Selve delingen er den første besked: Sofie skal vide, at noten er der, før
|
|
|
|
|
// nogen retter i den. Den tælles med i alt, hvad der følger.
|
|
|
|
|
if list, unread := notifications(t, other); len(list) != 1 || unread != 1 ||
|
|
|
|
|
firstOf(t, list)["kind"] != "note.shared" {
|
|
|
|
|
t.Fatalf("delingen gav %d beskeder (%v ulæste): %v", len(list), unread, list)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 11:40:43 +02:00
|
|
|
// Ejeren skriver. Sofie skal høre det.
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/notes/"+noteID,
|
|
|
|
|
map[string]any{"body": "# Aftale om levering\nanden linje"}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatal("ret noten")
|
|
|
|
|
}
|
|
|
|
|
list, unread := notifications(t, other)
|
2026-09-08 11:58:47 +02:00
|
|
|
if unread != 2 || len(list) != 2 {
|
|
|
|
|
t.Fatalf("Sofie fik %d beskeder (%v ulæste), want 2 — delingen og rettelsen",
|
|
|
|
|
len(list), unread)
|
2026-09-08 11:40:43 +02:00
|
|
|
}
|
|
|
|
|
n := firstOf(t, list)
|
|
|
|
|
if n["kind"] != "note.changed" || n["note_id"] != noteID {
|
|
|
|
|
t.Errorf("besked = %v", n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// En gemning mere, mens den er ulæst, må ikke blive til en linje mere. En note
|
|
|
|
|
// gemmes ved hver tastepause, så uden det her er fem minutters skriveri tyve
|
|
|
|
|
// ens linjer i klokken.
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/notes/"+noteID,
|
|
|
|
|
map[string]any{"body": "# Aftale om levering\nlinje nummer " + string(rune('a'+i))}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatal("ret noten igen")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-08 11:58:47 +02:00
|
|
|
if list, unread := notifications(t, other); len(list) != 2 || unread != 2 {
|
|
|
|
|
t.Errorf("tre gemninger blev til %d beskeder, want 2 — delingen og én rettelse",
|
|
|
|
|
len(list))
|
2026-09-08 11:40:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Læst, og så en rettelse mere: dét er en ny ting at få at vide.
|
|
|
|
|
if resp, _ := other.do(t, "POST", "/api/v1/notifications/read", nil); resp.StatusCode != http.StatusNoContent {
|
|
|
|
|
t.Fatal("markér som læst")
|
|
|
|
|
}
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/notes/"+noteID,
|
|
|
|
|
map[string]any{"body": "# Aftale om levering\nefter læsningen"}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatal("ret noten efter læsningen")
|
|
|
|
|
}
|
2026-09-08 11:58:47 +02:00
|
|
|
if list, unread := notifications(t, other); len(list) != 3 || unread != 1 {
|
|
|
|
|
t.Errorf("efter læsningen gav en rettelse %d beskeder (%v ulæste), want 3 og 1", len(list), unread)
|
2026-09-08 11:40:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ejeren har ikke fået besked om sine egne rettelser undervejs.
|
|
|
|
|
if list, _ := notifications(t, owner); len(list) != 0 {
|
|
|
|
|
t.Errorf("ejeren fik %d beskeder om sine egne rettelser", len(list))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Og den anden vej: Sofie skriver, ejeren hører det.
|
|
|
|
|
if resp, _ := other.do(t, "PATCH", "/api/v1/notes/"+noteID,
|
|
|
|
|
map[string]any{"body": "# Aftale om levering\nfra Sofie"}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatal("Sofie retter")
|
|
|
|
|
}
|
|
|
|
|
list, _ = notifications(t, owner)
|
|
|
|
|
if len(list) != 1 {
|
|
|
|
|
t.Fatalf("ejeren fik %d beskeder, da noten blev rettet af en anden, want 1", len(list))
|
|
|
|
|
}
|
|
|
|
|
if firstOf(t, list)["actor_name"] != "Sofie" {
|
|
|
|
|
t.Errorf("beskeden nævnte ikke hvem: %v", list[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// En ændring, der ikke er tekst, er ikke en rettelse at give besked om.
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/notes/"+noteID,
|
|
|
|
|
map[string]any{"pinned": true}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatal("stjernemarkér")
|
|
|
|
|
}
|
2026-09-08 11:58:47 +02:00
|
|
|
if list, _ := notifications(t, other); len(list) != 3 {
|
|
|
|
|
t.Errorf("en stjernemarkering gav besked: %d beskeder, want 3", len(list))
|
2026-09-08 11:40:43 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// En note, ingen deler, er ingen at give besked.
|
|
|
|
|
func TestEditingAPrivateNoteNotifiesNobody(t *testing.T) {
|
|
|
|
|
owner := newTestServer(t)
|
|
|
|
|
owner.bootstrap(t)
|
|
|
|
|
other := owner.newUser(t, "sofie@example.dk", "Sofie")
|
|
|
|
|
|
|
|
|
|
noteID := owner.createNote(t, "# Min egen\nintet")
|
|
|
|
|
if resp, _ := owner.do(t, "PATCH", "/api/v1/notes/"+noteID,
|
|
|
|
|
map[string]any{"body": "# Min egen\nnoget"}); resp.StatusCode != http.StatusOK {
|
|
|
|
|
t.Fatal("ret noten")
|
|
|
|
|
}
|
|
|
|
|
if list, _ := notifications(t, other); len(list) != 0 {
|
|
|
|
|
t.Errorf("Sofie hørte om en note, hun ikke kan se: %d beskeder", len(list))
|
|
|
|
|
}
|
|
|
|
|
if list, _ := notifications(t, owner); len(list) != 0 {
|
|
|
|
|
t.Errorf("ejeren fik besked om sin egen note: %d beskeder", len(list))
|
|
|
|
|
}
|
|
|
|
|
}
|