tinyrelayfile
tinyrelay/internal/daemon/membership_protocol_test.go/

internal/daemon/membership_protocol_test.go raw
1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "net/http/httptest"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/event"
12 "github.com/coder/websocket"
13)
14
15func TestNIP43InviteMountedRequestIsRelaySignedAndScoped(t *testing.T) {
16 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
17 defer cancel()
18 app, tenant := testTenant(t)
19 secret := strings.Repeat("0", 63) + "1"
20 server := httptest.NewServer(app)
21 wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/r/main"
22 conn, _, err := websocket.Dial(ctx, wsURL, nil)
23 if err != nil {
24 t.Fatal(err)
25 }
26 t.Cleanup(func() {
27 _ = conn.CloseNow()
28 server.Close()
29 })
30
31 _, challengeRaw, err := conn.Read(ctx)
32 if err != nil {
33 t.Fatal(err)
34 }
35 var challengeMessage []json.RawMessage
36 if err := json.Unmarshal(challengeRaw, &challengeMessage); err != nil || len(challengeMessage) < 2 {
37 t.Fatalf("AUTH challenge: %s", challengeRaw)
38 }
39 var challenge string
40 if err := json.Unmarshal(challengeMessage[1], &challenge); err != nil {
41 t.Fatal(err)
42 }
43 auth := event.Event{Kind: event.KIND_AUTH, CreatedAt: time.Now().Unix(), Tags: [][]string{{"relay", wsURL}, {"challenge", challenge}}}
44 if err := event.Sign(&auth, secret); err != nil {
45 t.Fatal(err)
46 }
47 if err := writeRelayMessage(ctx, conn, []any{"AUTH", auth}); err != nil {
48 t.Fatal(err)
49 }
50 if data := readRelayMessage(t, ctx, conn); !strings.Contains(string(data), `"OK","`+auth.ID+`",true`) {
51 t.Fatalf("AUTH rejected: %s", data)
52 }
53
54 if err := writeRelayMessage(ctx, conn, []any{"REQ", "zero", map[string]any{"kinds": []int{event.KIND_NIP43_INVITE}, "limit": 0}}); err != nil {
55 t.Fatal(err)
56 }
57 if data := readRelayMessage(t, ctx, conn); !strings.Contains(string(data), `"EOSE","zero"`) {
58 t.Fatalf("limit zero response: %s", data)
59 }
60 var claims int
61 if err := tenant.store.DB().QueryRow(`SELECT count(*) FROM community_invites`).Scan(&claims); err != nil {
62 t.Fatal(err)
63 }
64 if claims != 0 {
65 t.Fatalf("limit zero minted %d claims", claims)
66 }
67 now := time.Now().Unix()
68 impossible := []struct {
69 name string
70 filter map[string]any
71 }{
72 {name: "wrong id", filter: map[string]any{"kinds": []int{event.KIND_NIP43_INVITE}, "ids": []string{strings.Repeat("f", 64)}}},
73 {name: "wrong author", filter: map[string]any{"kinds": []int{event.KIND_NIP43_INVITE}, "authors": []string{strings.Repeat("e", 64)}}},
74 {name: "wrong tag", filter: map[string]any{"kinds": []int{event.KIND_NIP43_INVITE}, "#p": []string{strings.Repeat("d", 64)}}},
75 {name: "expired", filter: map[string]any{"kinds": []int{event.KIND_NIP43_INVITE}, "until": now - 60}},
76 {name: "future", filter: map[string]any{"kinds": []int{event.KIND_NIP43_INVITE}, "since": now + 60}},
77 }
78 for _, tc := range impossible {
79 if err := writeRelayMessage(ctx, conn, []any{"REQ", tc.name, tc.filter}); err != nil {
80 t.Fatal(err)
81 }
82 if data := readRelayMessage(t, ctx, conn); !strings.Contains(string(data), `"EOSE","`+tc.name+`"`) {
83 t.Fatalf("%s response: %s", tc.name, data)
84 }
85 }
86 if err := tenant.store.DB().QueryRow(`SELECT count(*) FROM community_invites`).Scan(&claims); err != nil {
87 t.Fatal(err)
88 }
89 if claims != 0 {
90 t.Fatalf("impossible filters minted %d claims", claims)
91 }
92
93 if err := writeRelayMessage(ctx, conn, []any{"REQ", "mixed", map[string]any{"kinds": []int{event.KIND_NIP43_INVITE, 1}, "limit": 1}}); err != nil {
94 t.Fatal(err)
95 }
96 data := readRelayMessage(t, ctx, conn)
97 var message []json.RawMessage
98 if err := json.Unmarshal(data, &message); err != nil || len(message) < 3 {
99 t.Fatalf("invite response: %s", data)
100 }
101 var responseKind string
102 if err := json.Unmarshal(message[0], &responseKind); err != nil || responseKind != "EVENT" {
103 t.Fatalf("expected EVENT, got %s", data)
104 }
105 var invite event.Event
106 if err := json.Unmarshal(message[2], &invite); err != nil {
107 t.Fatal(err)
108 }
109 if invite.Kind != event.KIND_NIP43_INVITE || invite.PubKey != tenant.records.PublicKey() {
110 t.Fatalf("unexpected invite identity: %+v", invite)
111 }
112 if err := event.Validate(invite); err != nil {
113 t.Fatalf("relay signature invalid: %v", err)
114 }
115 if event.Tag(invite, "claim") == "" {
116 t.Fatal("invite omitted claim")
117 }
118}
119
120func writeRelayMessage(ctx context.Context, conn *websocket.Conn, value any) error {
121 data, err := json.Marshal(value)
122 if err != nil {
123 return err
124 }
125 return conn.Write(ctx, websocket.MessageText, data)
126}
127
128func readRelayMessage(t *testing.T, ctx context.Context, conn *websocket.Conn) []byte {
129 t.Helper()
130 _, data, err := conn.Read(ctx)
131 if err != nil {
132 t.Fatal(err)
133 }
134 return data
135}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs