tinyrelayfile
tinyrelay/protocol/auth/replay_test.go/

protocol/auth/replay_test.go raw
1package auth
2
3import (
4 "encoding/base64"
5 "strings"
6 "sync"
7 "sync/atomic"
8 "testing"
9 "time"
10
11 "github.com/FelineStateMachine/tinyrelay/protocol/nostr"
12)
13
14func TestNIP98ReplayWindowBoundary(t *testing.T) {
15 now := time.Unix(1700000000, 0)
16 v := NewValidator(func() time.Time { return now })
17 // A future-dated proof is still fresh after its first replay window ends.
18 e := signedEvent(t, 27235, now.Unix()+60, "", [][]string{{"u", "https://relay.example/api"}, {"method", "GET"}})
19 header := token(t, e)
20 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err != nil {
21 t.Fatal(err)
22 }
23 now = now.Add(time.Minute)
24 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err == nil || !strings.Contains(err.Error(), "replay") {
25 t.Fatalf("proof must remain seen at exactly one minute: %v", err)
26 }
27 now = now.Add(time.Second)
28 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err != nil {
29 t.Fatalf("expired replay entry should be pruned: %v", err)
30 }
31}
32
33func TestNIP98AcceptsAllExistingBase64Forms(t *testing.T) {
34 now := time.Unix(1700000000, 0)
35 e := signedEvent(t, 27235, now.Unix(), "\u083e\u083f", [][]string{{"u", "https://relay.example/api"}, {"method", "GET"}})
36 raw, err := nostr.Canonical(e)
37 if err != nil {
38 t.Fatal(err)
39 }
40 for _, encoding := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} {
41 v := NewValidator(func() time.Time { return now })
42 header := "nOsTr " + encoding.EncodeToString(raw)
43 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err != nil {
44 t.Fatalf("base64 form rejected: %v", err)
45 }
46 }
47}
48
49func TestNIP42ChallengeExpiresAtOneMinute(t *testing.T) {
50 now := time.Unix(1700000000, 0)
51 m := NewChallengeManager("wss://relay.example", func() time.Time { return now })
52 challenge, err := m.Issue()
53 if err != nil {
54 t.Fatal(err)
55 }
56 e := signedEvent(t, 22242, now.Unix(), "", [][]string{{"relay", "wss://relay.example"}, {"challenge", challenge}})
57 now = now.Add(time.Minute)
58 if err := m.Verify(e); err == nil || !strings.Contains(err.Error(), "challenge") {
59 t.Fatalf("challenge must expire even while timestamp remains fresh: %v", err)
60 }
61}
62
63func TestConcurrentProofConsumptionIsSingleUse(t *testing.T) {
64 now := time.Unix(1700000000, 0)
65 v := NewValidator(func() time.Time { return now })
66 e := signedEvent(t, 27235, now.Unix(), "", [][]string{{"u", "https://relay.example/api"}, {"method", "GET"}})
67 header := token(t, e)
68 m := NewChallengeManager("wss://relay.example", func() time.Time { return now })
69 challenge, err := m.Issue()
70 if err != nil {
71 t.Fatal(err)
72 }
73 authEvent := signedEvent(t, 22242, now.Unix(), "", [][]string{{"relay", "wss://relay.example"}, {"challenge", challenge}})
74 for name, verify := range map[string]func() error{
75 "NIP-98": func() error { _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); return err },
76 "NIP-42": func() error { return m.Verify(authEvent) },
77 } {
78 t.Run(name, func(t *testing.T) {
79 var successes atomic.Int32
80 var wg sync.WaitGroup
81 for range 16 {
82 wg.Add(1)
83 go func() {
84 defer wg.Done()
85 if verify() == nil {
86 successes.Add(1)
87 }
88 }()
89 }
90 wg.Wait()
91 if got := successes.Load(); got != 1 {
92 t.Fatalf("successful consumers = %d, want 1", got)
93 }
94 })
95 }
96}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs