eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "encoding/json"
5 "net/http"
6 "strings"
7 "testing"
8)
9
10// Muting a chat is a per-user marker synced via getThreads, and its whole
11// server-side effect is subtractive: the push fan-out drops every one of the
12// muter's devices for that thread. Nobody else's list or pushes change.
13func TestThreadMuted(t *testing.T) {
14 ts, srv := newTestServer(t)
15
16 tokA, uidA, _ := joinedMember(t, srv, "Alice")
17 tokB, uidB, _ := joinedMember(t, srv, "Bob")
18 _ = tokB
19 tid, err := srv.db.createThread("group", "", []ThreadMemberKey{
20 {UserID: uidA, Wrapped: "wa"}, {UserID: uidB, Wrapped: "wb"},
21 })
22 if err != nil {
23 t.Fatalf("createThread: %v", err)
24 }
25
26 post := func(path, body string) int {
27 resp, err := http.Post(ts.URL+path, "application/json", strings.NewReader(body))
28 if err != nil {
29 t.Fatalf("post %s: %v", path, err)
30 }
31 resp.Body.Close()
32 return resp.StatusCode
33 }
34 mutedForAlice := func() bool {
35 resp, err := http.Get(ts.URL + "/api/threads?token=" + tokA)
36 if err != nil {
37 t.Fatalf("getThreads: %v", err)
38 }
39 defer resp.Body.Close()
40 var out struct {
41 Threads []Thread `json:"threads"`
42 }
43 if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
44 t.Fatalf("decode: %v", err)
45 }
46 for _, th := range out.Threads {
47 if th.ID == tid {
48 return th.Muted
49 }
50 }
51 t.Fatalf("thread %q missing from Alice's list", tid)
52 return false
53 }
54
55 if mutedForAlice() {
56 t.Fatal("fresh thread should not be muted")
57 }
58 if code := post("/api/threads/"+tid+"/muted", `{"token":"`+tokA+`","muted":true}`); code != 200 {
59 t.Fatalf("mute: got %d", code)
60 }
61 if !mutedForAlice() {
62 t.Fatal("mute should stick and sync via getThreads")
63 }
64
65 // The push recipient set loses the muter — and only the muter.
66 set := map[string]bool{uidA: true, uidB: true}
67 srv.dropMutedRecipients(tid, set)
68 if set[uidA] || !set[uidB] {
69 t.Fatalf("muted member should drop from push recipients, others stay: %v", set)
70 }
71
72 if code := post("/api/threads/"+tid+"/muted", `{"token":"`+tokA+`","muted":false}`); code != 200 {
73 t.Fatalf("unmute: got %d", code)
74 }
75 if mutedForAlice() {
76 t.Fatal("unmute should clear the marker")
77 }
78 set = map[string]bool{uidA: true, uidB: true}
79 srv.dropMutedRecipients(tid, set)
80 if !set[uidA] {
81 t.Fatal("after unmuting, pushes should reach the member again")
82 }
83
84 // Non-members can't mute a thread they're not in.
85 tokC, _, _ := joinedMember(t, srv, "Carol")
86 if code := post("/api/threads/"+tid+"/muted", `{"token":"`+tokC+`","muted":true}`); code != 403 {
87 t.Fatalf("non-member mute should be 403, got %d", code)
88 }
89}
90
91// The prefs blob (muted people) round-trips opaquely and CASes like the
92// nicknames blob: a write from a stale version 409s with the current version
93// instead of clobbering another device's write.
94func TestPrefsBlobRoundTrip(t *testing.T) {
95 ts, srv := newTestServer(t)
96 tok, _, _ := joinedMember(t, srv, "Alice")
97
98 get := func() (string, int64) {
99 resp, err := http.Get(ts.URL + "/api/prefs?token=" + tok)
100 if err != nil {
101 t.Fatalf("get prefs: %v", err)
102 }
103 defer resp.Body.Close()
104 var out struct {
105 PrefsBlob string `json:"prefs_blob"`
106 Version int64 `json:"version"`
107 }
108 if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
109 t.Fatalf("decode: %v", err)
110 }
111 return out.PrefsBlob, out.Version
112 }
113 post := func(body string) (int, int64) {
114 resp, err := http.Post(ts.URL+"/api/prefs", "application/json", strings.NewReader(body))
115 if err != nil {
116 t.Fatalf("post prefs: %v", err)
117 }
118 defer resp.Body.Close()
119 var out struct {
120 Version int64 `json:"version"`
121 }
122 _ = json.NewDecoder(resp.Body).Decode(&out)
123 return resp.StatusCode, out.Version
124 }
125
126 if blob, v := get(); blob != "" || v != 0 {
127 t.Fatalf("fresh prefs should be empty at version 0, got %q v%d", blob, v)
128 }
129 if code, v := post(`{"token":"` + tok + `","prefs_blob":"sealed1","version":0}`); code != 200 || v != 1 {
130 t.Fatalf("first write: got %d v%d", code, v)
131 }
132 if blob, v := get(); blob != "sealed1" || v != 1 {
133 t.Fatalf("round trip: got %q v%d", blob, v)
134 }
135 // Stale base: another device already wrote v1, so a writer holding v0 loses.
136 if code, v := post(`{"token":"` + tok + `","prefs_blob":"stale","version":0}`); code != 409 || v != 1 {
137 t.Fatalf("stale write should 409 with current version, got %d v%d", code, v)
138 }
139 if blob, _ := get(); blob != "sealed1" {
140 t.Fatalf("stale write must not clobber, got %q", blob)
141 }
142 // Versionless write (pre-versioning client) stays unconditional.
143 if code, _ := post(`{"token":"` + tok + `","prefs_blob":"sealed2"}`); code != 200 {
144 t.Fatalf("versionless write: got %d", code)
145 }
146 if blob, v := get(); blob != "sealed2" || v != 2 {
147 t.Fatalf("after versionless write: got %q v%d", blob, v)
148 }
149}
150