summaryrefslogtreecommitdiffstats
path: root/internal/auth/auth_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/auth/auth_test.go')
-rw-r--r--internal/auth/auth_test.go306
1 files changed, 306 insertions, 0 deletions
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
new file mode 100644
index 0000000..1f0efee
--- /dev/null
+++ b/internal/auth/auth_test.go
@@ -0,0 +1,306 @@
1package auth
2
3import (
4 "context"
5 "encoding/base64"
6 "encoding/json"
7 "testing"
8 "time"
9
10 "google.golang.org/grpc/metadata"
11 "northwest.io/muxstr/internal/nostr"
12)
13
14func TestNostrCredentials(t *testing.T) {
15 key, err := nostr.GenerateKey()
16 if err != nil {
17 t.Fatalf("failed to generate key: %v", err)
18 }
19
20 creds := NewNostrCredentials(key)
21
22 // Test GetRequestMetadata
23 ctx := context.Background()
24 uri := "https://example.com/nostr.v1.NostrRelay/PublishEvent"
25
26 md, err := creds.GetRequestMetadata(ctx, uri)
27 if err != nil {
28 t.Fatalf("GetRequestMetadata failed: %v", err)
29 }
30
31 // Check authorization header exists
32 authHeader, ok := md["authorization"]
33 if !ok {
34 t.Fatal("missing authorization header")
35 }
36
37 // Parse and validate the event
38 event, err := ParseAuthHeader(authHeader)
39 if err != nil {
40 t.Fatalf("failed to parse auth header: %v", err)
41 }
42
43 if event.Kind != 27235 {
44 t.Errorf("wrong event kind: got %d, want 27235", event.Kind)
45 }
46
47 if event.PubKey != key.Public() {
48 t.Error("pubkey mismatch")
49 }
50
51 if !event.Verify() {
52 t.Error("event signature verification failed")
53 }
54
55 // Check tags
56 uTag := event.Tags.Find("u")
57 if uTag == nil {
58 t.Fatal("missing 'u' tag")
59 }
60 if uTag.Value() != uri {
61 t.Errorf("wrong URI in tag: got %s, want %s", uTag.Value(), uri)
62 }
63
64 methodTag := event.Tags.Find("method")
65 if methodTag == nil {
66 t.Fatal("missing 'method' tag")
67 }
68 if methodTag.Value() != "POST" {
69 t.Errorf("wrong method in tag: got %s, want POST", methodTag.Value())
70 }
71}
72
73func TestParseAuthHeader(t *testing.T) {
74 tests := []struct {
75 name string
76 header string
77 wantErr bool
78 }{
79 {
80 name: "empty header",
81 header: "",
82 wantErr: true,
83 },
84 {
85 name: "missing prefix",
86 header: "Bearer token",
87 wantErr: true,
88 },
89 {
90 name: "invalid base64",
91 header: "Nostr not-base64!",
92 wantErr: true,
93 },
94 {
95 name: "invalid json",
96 header: "Nostr " + base64.StdEncoding.EncodeToString([]byte("not json")),
97 wantErr: true,
98 },
99 }
100
101 for _, tt := range tests {
102 t.Run(tt.name, func(t *testing.T) {
103 _, err := ParseAuthHeader(tt.header)
104 if (err != nil) != tt.wantErr {
105 t.Errorf("ParseAuthHeader() error = %v, wantErr %v", err, tt.wantErr)
106 }
107 })
108 }
109}
110
111func TestValidateAuthEvent(t *testing.T) {
112 key, _ := nostr.GenerateKey()
113
114 // Create a valid event
115 event := &nostr.Event{
116 PubKey: key.Public(),
117 CreatedAt: time.Now().Unix(),
118 Kind: 27235,
119 Tags: nostr.Tags{
120 {"u", "https://example.com/test"},
121 {"method", "POST"},
122 },
123 Content: "",
124 }
125 key.Sign(event)
126
127 tests := []struct {
128 name string
129 event *nostr.Event
130 opts ValidationOptions
131 wantErr bool
132 }{
133 {
134 name: "valid event",
135 event: event,
136 opts: ValidationOptions{
137 TimestampWindow: 60,
138 ExpectedURI: "https://example.com/test",
139 ExpectedMethod: "POST",
140 },
141 wantErr: false,
142 },
143 {
144 name: "wrong kind",
145 event: &nostr.Event{
146 Kind: 1,
147 CreatedAt: time.Now().Unix(),
148 Tags: nostr.Tags{},
149 },
150 opts: ValidationOptions{},
151 wantErr: true,
152 },
153 {
154 name: "old timestamp",
155 event: &nostr.Event{
156 PubKey: key.Public(),
157 CreatedAt: time.Now().Unix() - 120, // 2 minutes ago
158 Kind: 27235,
159 Tags: nostr.Tags{},
160 Sig: event.Sig,
161 },
162 opts: ValidationOptions{
163 TimestampWindow: 60, // Only accept 60 seconds
164 },
165 wantErr: true,
166 },
167 {
168 name: "URI mismatch",
169 event: event,
170 opts: ValidationOptions{
171 TimestampWindow: 60,
172 ExpectedURI: "https://different.com/test",
173 },
174 wantErr: true,
175 },
176 {
177 name: "method mismatch",
178 event: event,
179 opts: ValidationOptions{
180 TimestampWindow: 60,
181 ExpectedMethod: "GET",
182 },
183 wantErr: true,
184 },
185 }
186
187 for _, tt := range tests {
188 t.Run(tt.name, func(t *testing.T) {
189 err := ValidateAuthEvent(tt.event, tt.opts)
190 if (err != nil) != tt.wantErr {
191 t.Errorf("ValidateAuthEvent() error = %v, wantErr %v", err, tt.wantErr)
192 }
193 })
194 }
195}
196
197func TestPubkeyFromContext(t *testing.T) {
198 ctx := context.Background()
199
200 // Test empty context
201 pubkey, ok := PubkeyFromContext(ctx)
202 if ok {
203 t.Error("expected ok=false for empty context")
204 }
205 if pubkey != "" {
206 t.Error("expected empty pubkey for empty context")
207 }
208
209 // Test context with pubkey
210 expectedPubkey := "test-pubkey-123"
211 ctx = context.WithValue(ctx, pubkeyContextKey, expectedPubkey)
212
213 pubkey, ok = PubkeyFromContext(ctx)
214 if !ok {
215 t.Error("expected ok=true for context with pubkey")
216 }
217 if pubkey != expectedPubkey {
218 t.Errorf("got pubkey %s, want %s", pubkey, expectedPubkey)
219 }
220}
221
222func TestValidateAuthFromContext(t *testing.T) {
223 key, _ := nostr.GenerateKey()
224
225 // Create valid auth event
226 event := &nostr.Event{
227 PubKey: key.Public(),
228 CreatedAt: time.Now().Unix(),
229 Kind: 27235,
230 Tags: nostr.Tags{
231 {"u", "https://example.com/test"},
232 {"method", "POST"},
233 },
234 Content: "",
235 }
236 key.Sign(event)
237
238 eventJSON, _ := json.Marshal(event)
239 authHeader := "Nostr " + base64.StdEncoding.EncodeToString(eventJSON)
240
241 // Create context with metadata
242 md := metadata.Pairs("authorization", authHeader)
243 ctx := metadata.NewIncomingContext(context.Background(), md)
244
245 opts := &InterceptorOptions{
246 TimestampWindow: 60,
247 Required: true,
248 }
249
250 pubkey, err := validateAuthFromContext(ctx, "/test.Service/Method", opts)
251 if err != nil {
252 t.Fatalf("validateAuthFromContext failed: %v", err)
253 }
254
255 if pubkey != key.Public() {
256 t.Errorf("got pubkey %s, want %s", pubkey, key.Public())
257 }
258}
259
260func TestShouldSkipAuth(t *testing.T) {
261 skipMethods := []string{
262 "/health/Check",
263 "/nostr.v1.NostrRelay/GetInfo",
264 }
265
266 tests := []struct {
267 method string
268 want bool
269 }{
270 {"/health/Check", true},
271 {"/nostr.v1.NostrRelay/GetInfo", true},
272 {"/nostr.v1.NostrRelay/PublishEvent", false},
273 {"/other/Method", false},
274 }
275
276 for _, tt := range tests {
277 t.Run(tt.method, func(t *testing.T) {
278 got := shouldSkipAuth(tt.method, skipMethods)
279 if got != tt.want {
280 t.Errorf("shouldSkipAuth(%s) = %v, want %v", tt.method, got, tt.want)
281 }
282 })
283 }
284}
285
286func TestHashPayload(t *testing.T) {
287 payload := []byte("test payload")
288 hash := HashPayload(payload)
289
290 // Should be a 64-character hex string (SHA256)
291 if len(hash) != 64 {
292 t.Errorf("hash length = %d, want 64", len(hash))
293 }
294
295 // Same payload should produce same hash
296 hash2 := HashPayload(payload)
297 if hash != hash2 {
298 t.Error("same payload produced different hashes")
299 }
300
301 // Different payload should produce different hash
302 hash3 := HashPayload([]byte("different payload"))
303 if hash == hash3 {
304 t.Error("different payloads produced same hash")
305 }
306}