eleven / messenger Public

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

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 "time"
9)
10
11func testGate(t *testing.T, bits int) *powGate {
12 t.Helper()
13 keys, err := newStateKeys([]byte("test-secret"))
14 if err != nil {
15 t.Fatal(err)
16 }
17 return newPowGate(bits, keys)
18}
19
20func TestPowMintVerify(t *testing.T) {
21 g := testGate(t, 8) // 8 bits ≈ 256 hashes — instant in tests
22 challenge, err := g.mint("uid-1")
23 if err != nil {
24 t.Fatal(err)
25 }
26 nonce := solvePow(challenge, 8)
27
28 // Wrong account, wrong nonce, then the real thing — which consumes it.
29 if err := g.verify("uid-2", challenge, nonce); err == nil {
30 t.Error("someone else's challenge verified")
31 }
32 if err := g.verify("uid-1", challenge, "not-the-nonce"); err == nil {
33 t.Error("bad nonce verified")
34 }
35 if err := g.verify("uid-1", challenge, nonce); err != nil {
36 t.Fatalf("valid solution rejected: %v", err)
37 }
38 // Replay: one challenge, one space.
39 if err := g.verify("uid-1", challenge, nonce); err == nil {
40 t.Error("replayed challenge verified")
41 }
42
43 // A tampered challenge never opens.
44 if err := g.verify("uid-1", challenge+"x", nonce); err == nil {
45 t.Error("tampered challenge verified")
46 }
47 // An expired one is refused even with a valid solution.
48 stale, err := g.mintAt("uid-1", time.Now().Add(-time.Minute))
49 if err != nil {
50 t.Fatal(err)
51 }
52 if err := g.verify("uid-1", stale, solvePow(stale, 8)); err == nil {
53 t.Error("expired challenge verified")
54 }
55
56 if newPowGate(0, g.keys) != nil {
57 t.Error("bits 0 should disable the gate")
58 }
59}
60
61func TestLeadingZeroBits(t *testing.T) {
62 for _, c := range []struct {
63 in []byte
64 want int
65 }{
66 {[]byte{0x80}, 0}, {[]byte{0x01}, 7}, {[]byte{0x00, 0xff}, 8},
67 {[]byte{0x00, 0x0f}, 12}, {[]byte{0x00, 0x00}, 16},
68 } {
69 if got := leadingZeroBits(c.in); got != c.want {
70 t.Errorf("leadingZeroBits(%v) = %d, want %d", c.in, got, c.want)
71 }
72 }
73}
74
75func TestCreateSpaceRequiresPow(t *testing.T) {
76 ts, s := newTestServer(t)
77 uid, token := seedMember(t, s)
78 cloud, _, _ := fakeCloud(t, http.StatusCreated, map[string]any{
79 "host": "h.example.com", "url": "https://h.example.com", "setup_link": "https://h.example.com/join/setup#S",
80 })
81 s.cloud = newCloudProvisioner(cloud.URL, "caller-token")
82 s.pow = newPowGate(8, s.keys)
83
84 post := func(body string) *http.Response {
85 req, _ := http.NewRequest("POST", ts.URL+"/api/spaces", strings.NewReader(body))
86 req.Header.Set("Authorization", "Bearer "+token)
87 resp, err := http.DefaultClient.Do(req)
88 if err != nil {
89 t.Fatal(err)
90 }
91 t.Cleanup(func() { resp.Body.Close() })
92 return resp
93 }
94
95 // No proof / garbage proof → 403 with the stable codes.
96 if r := post(""); r.StatusCode != http.StatusForbidden {
97 t.Fatalf("no proof: %d, want 403", r.StatusCode)
98 }
99 if r := post(`{"challenge":"junk","nonce":"1"}`); r.StatusCode != http.StatusForbidden {
100 t.Fatalf("junk proof: %d, want 403", r.StatusCode)
101 }
102
103 // The real flow: challenge → solve → create.
104 creq, _ := http.NewRequest("POST", ts.URL+"/api/spaces/challenge", nil)
105 creq.Header.Set("Authorization", "Bearer "+token)
106 cresp, err := http.DefaultClient.Do(creq)
107 if err != nil {
108 t.Fatal(err)
109 }
110 defer cresp.Body.Close()
111 var ch struct {
112 Bits int `json:"bits"`
113 Challenge string `json:"challenge"`
114 }
115 json.NewDecoder(cresp.Body).Decode(&ch)
116 if ch.Bits != 8 || ch.Challenge == "" {
117 t.Fatalf("challenge = %+v", ch)
118 }
119 body, _ := json.Marshal(map[string]string{"challenge": ch.Challenge, "nonce": solvePow(ch.Challenge, ch.Bits)})
120 if r := post(string(body)); r.StatusCode != http.StatusCreated {
121 t.Fatalf("solved create: %d, want 201", r.StatusCode)
122 }
123 // The same proof can't buy a second space.
124 if r := post(string(body)); r.StatusCode != http.StatusForbidden {
125 t.Fatalf("replayed proof: %d, want 403", r.StatusCode)
126 }
127
128 // Gate off → bits 0 and bodyless creation works (the deployed default).
129 s.pow = nil
130 cresp2, err := http.DefaultClient.Do(creq.Clone(creq.Context()))
131 if err != nil {
132 t.Fatal(err)
133 }
134 defer cresp2.Body.Close()
135 var off struct {
136 Bits int `json:"bits"`
137 }
138 json.NewDecoder(cresp2.Body).Decode(&off)
139 if off.Bits != 0 {
140 t.Errorf("gate off: bits = %d, want 0", off.Bits)
141 }
142 if r := post(""); r.StatusCode != http.StatusCreated {
143 t.Errorf("gate off create: %d, want 201", r.StatusCode)
144 }
145 _ = uid
146}
147