clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / internal / daemon / membership_protocol_test.go
membership_protocol_test.go 4.5K
1package daemon23import (4 "context"5 "encoding/json"6 "net/http/httptest"7 "strings"8 "testing"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/internal/event"12 "github.com/coder/websocket"13)1415func 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 })3031 _, challengeRaw, err := conn.Read(ctx)32 if err != nil {33 t.Fatal(err)34 }35 var challengeMessage []json.RawMessage36 if err := json.Unmarshal(challengeRaw, &challengeMessage); err != nil || len(challengeMessage) < 2 {37 t.Fatalf("AUTH challenge: %s", challengeRaw)38 }39 var challenge string40 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 }5354 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 int61 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 string70 filter map[string]any71 }{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 }9293 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.RawMessage98 if err := json.Unmarshal(data, &message); err != nil || len(message) < 3 {99 t.Fatalf("invite response: %s", data)100 }101 var responseKind string102 if err := json.Unmarshal(message[0], &responseKind); err != nil || responseKind != "EVENT" {103 t.Fatalf("expected EVENT, got %s", data)104 }105 var invite event.Event106 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}119120func writeRelayMessage(ctx context.Context, conn *websocket.Conn, value any) error {121 data, err := json.Marshal(value)122 if err != nil {123 return err124 }125 return conn.Write(ctx, websocket.MessageText, data)126}127128func 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 data135}