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 "strconv"
7 "strings"
8 "testing"
9)
10
11// sealLinkToken/openLinkToken round-trip, and a wrong key fails closed
12// (GCM authentication, not string comparison, decides "invalid").
13func TestLinkTokenRoundTrip(t *testing.T) {
14 key := make([]byte, 32)
15 tok, err := sealLinkToken(key, "thread-abc", 42)
16 if err != nil {
17 t.Fatalf("seal: %v", err)
18 }
19 tid, mid, ok := openLinkToken(key, tok)
20 if !ok || tid != "thread-abc" || mid != 42 {
21 t.Fatalf("open: got tid=%q mid=%d ok=%v", tid, mid, ok)
22 }
23 otherKey := make([]byte, 32)
24 otherKey[0] = 1
25 if _, _, ok := openLinkToken(otherKey, tok); ok {
26 t.Fatal("token opened under the wrong key")
27 }
28 if _, _, ok := openLinkToken(key, "not-a-real-token"); ok {
29 t.Fatal("garbage token should not open")
30 }
31}
32
33// Two seals of the same pair produce different ciphertext (random nonce per
34// seal) — links to the same message aren't correlatable by their bytes alone.
35func TestLinkTokenNotCorrelatable(t *testing.T) {
36 key := make([]byte, 32)
37 a, _ := sealLinkToken(key, "thread-abc", 42)
38 b, _ := sealLinkToken(key, "thread-abc", 42)
39 if a == b {
40 t.Fatal("two seals of the same pair should not be identical")
41 }
42}
43
44// End-to-end: mint a link as a thread member, resolve it back to the same
45// ids. A non-member of the thread gets a generic not-found, same as an
46// invalid token — the endpoint must not be a membership oracle.
47func TestMintAndResolveMessageLink(t *testing.T) {
48 ts, srv := newTestServer(t)
49
50 tokA, uidA, tidA := joinedMember(t, srv, "Alice")
51 tokB, _, _ := joinedMember(t, srv, "Bob") // a member of a different thread
52
53 m, err := srv.db.insertMessage(tidA, "blob", uidA)
54 if err != nil {
55 t.Fatalf("insertMessage: %v", err)
56 }
57
58 mint := func(tok, threadID string, msgID int64) (string, int) {
59 resp, err := http.Post(
60 ts.URL+"/api/messages/"+strconv.FormatInt(msgID, 10)+"/link?token="+tok,
61 "application/json",
62 strings.NewReader(`{"thread_id":"`+threadID+`"}`),
63 )
64 if err != nil {
65 t.Fatalf("mint post: %v", err)
66 }
67 defer resp.Body.Close()
68 var out struct {
69 Token string `json:"token"`
70 }
71 json.NewDecoder(resp.Body).Decode(&out)
72 return out.Token, resp.StatusCode
73 }
74 resolve := func(tok, linkToken string) (string, int64, int) {
75 resp, err := http.Post(ts.URL+"/api/resolve-link?token="+tok, "application/json",
76 strings.NewReader(`{"token":"`+linkToken+`"}`))
77 if err != nil {
78 t.Fatalf("resolve post: %v", err)
79 }
80 defer resp.Body.Close()
81 var out struct {
82 ThreadID string `json:"thread_id"`
83 MessageID int64 `json:"message_id"`
84 }
85 json.NewDecoder(resp.Body).Decode(&out)
86 return out.ThreadID, out.MessageID, resp.StatusCode
87 }
88
89 linkTok, code := mint(tokA, tidA, m.ID)
90 if code != http.StatusOK || linkTok == "" {
91 t.Fatalf("mint: status=%d token=%q", code, linkTok)
92 }
93
94 if tid, mid, code := resolve(tokA, linkTok); code != http.StatusOK || tid != tidA || mid != m.ID {
95 t.Fatalf("resolve as member: status=%d tid=%q mid=%d", code, tid, mid)
96 }
97
98 if _, _, code := resolve(tokB, linkTok); code != http.StatusNotFound {
99 t.Fatalf("resolve as non-member: want 404, got %d", code)
100 }
101 if _, _, code := resolve(tokA, "garbage"); code != http.StatusNotFound {
102 t.Fatalf("resolve garbage token: want 404, got %d", code)
103 }
104
105 // Bob can't mint a link for a message in a thread he's not in.
106 if _, code := mint(tokB, tidA, m.ID); code != http.StatusForbidden {
107 t.Fatalf("mint as non-member: want 403, got %d", code)
108 }
109}
110