| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | func TestChatActivitySharesApprovalStateAndScopesRooms(t *testing.T) { |
| 10 | h := newRoomHarness(t) |
| 11 | request := h.must("alice" , 5000, [][]string{{"h" , "main" }, {"p" , h.keys["bob" ]}, {"subject" , "Build preview" }}, "Build preview" ) |
| 12 | h.must("bob" , 7000, [][]string{{"h" , "main" }, {"e" , request.ID}, {"p" , h.keys["alice" ]}, {"status" , "processing" , "Running checks" }}, "2 of 3 checks complete" ) |
| 13 | decision := h.must("alice" , 9, [][]string{{"h" , "main" }, {"request" , "approve" }, {"p" , h.keys["bob" ]}, {"subject" , "Publish preview" }}, "Publish it?" ) |
| 14 | read := func(who, room string) map[string]any { |
| 15 | t.Helper() |
| 16 | raw, err := h.tenant.Execute(h.ctx, h.keys[who], "browsechatactivity" , []json.RawMessage{rawJSON(map[string]string{"room" : room})}) |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | return raw.(map[string]any) |
| 21 | } |
| 22 | data := read("bob" , "main" ) |
| 23 | jobs := data["jobs" ].([]jobItem) |
| 24 | if len(jobs) != 1 || jobs[0].Status != "processing" { |
| 25 | t.Fatalf("jobs=%v" , jobs) |
| 26 | } |
| 27 | approvals := data["approvals" ].([]approvalItem) |
| 28 | if len(approvals) != 1 || approvals[0].State != "open" { |
| 29 | t.Fatalf("approvals=%v" , approvals) |
| 30 | } |
| 31 | h.must("bob" , 7, [][]string{{"h" , "main" }, {"e" , decision.ID}, {"p" , h.keys["alice" ]}}, "+" ) |
| 32 | data = read("bob" , "main" ) |
| 33 | if data["approvals" ].([]approvalItem)[0].State != "answered" { |
| 34 | t.Fatal("inline approval not settled" ) |
| 35 | } |
| 36 | detail, err := h.tenant.Execute(h.ctx, h.keys["bob" ], "browseapproval" , []json.RawMessage{rawJSON(map[string]string{"id" : decision.ID})}) |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | if detail.(map[string]any)["item" ].(approvalItem).State != "answered" { |
| 41 | t.Fatal("Approvals disagrees" ) |
| 42 | } |
| 43 | h.must("alice" , event.KIND_CREATE_GROUP, [][]string{{"h" , "private-chat" }, {"name" , "Private" }, {"visibility" , "members" }}, "" ) |
| 44 | private := h.must("alice" , 5000, [][]string{{"h" , "private-chat" }, {"p" , h.keys["alice" ]}}, "Private work" ) |
| 45 | if _, err := h.tenant.Execute(h.ctx, h.keys["bob" ], "browsechatactivity" , []json.RawMessage{rawJSON(map[string]string{"room" : "private-chat" })}); err == nil { |
| 46 | t.Fatal("outsider read private activity" ) |
| 47 | } |
| 48 | for _, job := range read("bob" , "main" )["jobs" ].([]jobItem) { |
| 49 | if job.ID == private.ID { |
| 50 | t.Fatal("private job leaked" ) |
| 51 | } |
| 52 | } |
| 53 | if _, err := h.publish("alice" , 7000, [][]string{{"e" , private.ID}, {"p" , h.keys["alice" ]}, {"status" , "processing" }}, "private progress" ); err == nil { |
| 54 | t.Fatal("unscoped progress accepted for a private room task" ) |
| 55 | } |
| 56 | } |
| 57 | func TestChatPresenceIsEphemeralAndRoomAuthorized(t *testing.T) { |
| 58 | h := newRoomHarness(t) |
| 59 | h.must("alice" , event.KIND_CREATE_GROUP, [][]string{{"h" , "private-chat" }, {"visibility" , "members" }}, "" ) |
| 60 | for _, kind := range []int{20001, 20002} { |
| 61 | e := h.must("alice" , kind, [][]string{{"h" , "private-chat" }}, "typing" ) |
| 62 | if rows := h.query("alice" , event.Filter{IDs: []string{e.ID}}); len(rows) != 0 { |
| 63 | t.Fatal("presence persisted" ) |
| 64 | } |
| 65 | if _, err := h.publish("bob" , kind, [][]string{{"h" , "private-chat" }}, "typing" ); err == nil { |
| 66 | t.Fatal("outsider presence accepted" ) |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestChatActivityIncludesParentOnlyRequestsInCanonicalThread(t *testing.T) { |
| 72 | h := newRoomHarness(t) |
| 73 | root := h.must("alice" , 9, [][]string{{"h" , "main" }}, "Root" ) |
| 74 | parent := h.must("bob" , 9, [][]string{{"h" , "main" }, {"e" , root.ID, "" , "reply" }}, "Parent" ) |
| 75 | approval := h.must("alice" , 9, [][]string{{"h" , "main" }, {"e" , parent.ID, "" , "reply" }, {"request" , "approve" }, {"p" , h.keys["bob" ]}}, "Approve the nested action?" ) |
| 76 | job := h.must("alice" , 5000, [][]string{{"h" , "main" }, {"e" , parent.ID}, {"p" , h.keys["bob" ]}}, "Nested job" ) |
| 77 | other := h.must("alice" , 9, [][]string{{"h" , "main" }}, "Another root" ) |
| 78 | h.must("alice" , 9, [][]string{{"h" , "main" }, {"e" , other.ID, "" , "root" }, {"request" , "approve" }, {"p" , h.keys["bob" ]}}, "Unrelated action" ) |
| 79 | raw, err := h.tenant.Execute(h.ctx, h.keys["bob" ], "browsechatactivity" , []json.RawMessage{rawJSON(map[string]string{"room" : "main" , "root" : root.ID})}) |
| 80 | if err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | data := raw.(map[string]any) |
| 84 | approvals, jobs := data["approvals" ].([]approvalItem), data["jobs" ].([]jobItem) |
| 85 | if len(approvals) != 1 || approvals[0].ID != approval.ID || len(jobs) != 1 || jobs[0].ID != job.ID { |
| 86 | t.Fatalf("activity omitted nested request or included another thread: %v" , data) |
| 87 | } |
| 88 | } |