utils_test.go raw
1 package blossom
2
3 import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "testing"
11 "time"
12
13 "next.orly.dev/pkg/acl"
14 "next.orly.dev/pkg/database"
15 "next.orly.dev/pkg/nostr/encoders/event"
16 "next.orly.dev/pkg/nostr/encoders/hex"
17 "next.orly.dev/pkg/nostr/encoders/tag"
18 "next.orly.dev/pkg/nostr/encoders/timestamp"
19 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
20 )
21
22 // testSetup creates a test database, ACL, and server
23 func testSetup(t *testing.T) (*Server, func()) {
24 // Create temporary directory for database
25 tempDir, err := os.MkdirTemp("", "blossom-test-*")
26 if err != nil {
27 t.Fatalf("Failed to create temp dir: %v", err)
28 }
29
30 ctx, cancel := context.WithCancel(context.Background())
31
32 // Create database
33 db, err := database.New(ctx, cancel, tempDir, "error")
34 if err != nil {
35 os.RemoveAll(tempDir)
36 t.Fatalf("Failed to create database: %v", err)
37 }
38
39 // Create ACL registry and set to "none" mode for tests
40 aclRegistry := acl.Registry
41 aclRegistry.SetMode("none") // Allow all access for tests
42
43 // Create server
44 cfg := &Config{
45 BaseURL: "http://localhost:8080",
46 MaxBlobSize: 100 * 1024 * 1024, // 100MB
47 AllowedMimeTypes: nil,
48 RequireAuth: false,
49 }
50
51 server := NewServer(db, aclRegistry, cfg)
52
53 cleanup := func() {
54 cancel()
55 db.Close()
56 os.RemoveAll(tempDir)
57 }
58
59 return server, cleanup
60 }
61
62 // createTestKeypair creates a test keypair for signing events
63 func createTestKeypair(t *testing.T) ([]byte, *p8k.Signer) {
64 signer := p8k.MustNew()
65 if err := signer.Generate(); err != nil {
66 t.Fatalf("Failed to generate keypair: %v", err)
67 }
68 pubkey := signer.Pub()
69 return pubkey, signer
70 }
71
72 // createAuthEvent creates a valid kind 24242 authorization event
73 func createAuthEvent(
74 t *testing.T, signer *p8k.Signer, verb string,
75 sha256Hash []byte, expiresIn int64,
76 ) *event.E {
77 now := time.Now().Unix()
78 expires := now + expiresIn
79
80 tags := tag.NewS()
81 tags.Append(tag.NewFromAny("t", verb))
82 tags.Append(tag.NewFromAny("expiration", timestamp.FromUnix(expires).String()))
83
84 if sha256Hash != nil {
85 tags.Append(tag.NewFromAny("x", hex.Enc(sha256Hash)))
86 }
87
88 ev := &event.E{
89 CreatedAt: now,
90 Kind: BlossomAuthKind,
91 Tags: tags,
92 Content: []byte("Test authorization"),
93 Pubkey: signer.Pub(),
94 }
95
96 // Sign event
97 if err := ev.Sign(signer); err != nil {
98 t.Fatalf("Failed to sign event: %v", err)
99 }
100
101 return ev
102 }
103
104 // createAuthHeader creates an Authorization header from an event
105 func createAuthHeader(ev *event.E) string {
106 eventJSON := ev.Serialize()
107 b64 := base64.StdEncoding.EncodeToString(eventJSON)
108 return "Nostr " + b64
109 }
110
111 // makeRequest creates an HTTP request with optional authorization
112 func makeRequest(
113 t *testing.T, method, path string, body []byte, authEv *event.E,
114 ) *http.Request {
115 req := httptest.NewRequest(method, path, nil)
116 if body != nil {
117 req.Body = httptest.NewRequest(method, path, nil).Body
118 req.ContentLength = int64(len(body))
119 }
120
121 if authEv != nil {
122 req.Header.Set("Authorization", createAuthHeader(authEv))
123 }
124
125 return req
126 }
127
128 // TestBlobDescriptor tests BlobDescriptor creation and serialization
129 func TestBlobDescriptor(t *testing.T) {
130 desc := NewBlobDescriptor(
131 "https://example.com/blob.pdf",
132 "abc123",
133 1024,
134 "application/pdf",
135 1234567890,
136 )
137
138 if desc.URL != "https://example.com/blob.pdf" {
139 t.Errorf("Expected URL %s, got %s", "https://example.com/blob.pdf", desc.URL)
140 }
141 if desc.SHA256 != "abc123" {
142 t.Errorf("Expected SHA256 %s, got %s", "abc123", desc.SHA256)
143 }
144 if desc.Size != 1024 {
145 t.Errorf("Expected Size %d, got %d", 1024, desc.Size)
146 }
147 if desc.Type != "application/pdf" {
148 t.Errorf("Expected Type %s, got %s", "application/pdf", desc.Type)
149 }
150
151 // Test default MIME type
152 desc2 := NewBlobDescriptor("url", "hash", 0, "", 0)
153 if desc2.Type != "application/octet-stream" {
154 t.Errorf("Expected default MIME type, got %s", desc2.Type)
155 }
156 }
157
158 // TestBlobMetadata tests BlobMetadata serialization
159 func TestBlobMetadata(t *testing.T) {
160 pubkey := []byte("testpubkey123456789012345678901234")
161 meta := NewBlobMetadata(pubkey, "image/png", 2048)
162
163 if meta.Size != 2048 {
164 t.Errorf("Expected Size %d, got %d", 2048, meta.Size)
165 }
166 if meta.MimeType != "image/png" {
167 t.Errorf("Expected MIME type %s, got %s", "image/png", meta.MimeType)
168 }
169
170 // Test serialization
171 data, err := meta.Serialize()
172 if err != nil {
173 t.Fatalf("Failed to serialize metadata: %v", err)
174 }
175
176 // Test deserialization
177 meta2, err := DeserializeBlobMetadata(data)
178 if err != nil {
179 t.Fatalf("Failed to deserialize metadata: %v", err)
180 }
181
182 if meta2.Size != meta.Size {
183 t.Errorf("Size mismatch after deserialize")
184 }
185 if meta2.MimeType != meta.MimeType {
186 t.Errorf("MIME type mismatch after deserialize")
187 }
188 }
189
190 // TestUtils tests utility functions
191 func TestUtils(t *testing.T) {
192 data := []byte("test data")
193 hash := CalculateSHA256(data)
194 if len(hash) != 32 {
195 t.Errorf("Expected hash length 32, got %d", len(hash))
196 }
197
198 hashHex := CalculateSHA256Hex(data)
199 if len(hashHex) != 64 {
200 t.Errorf("Expected hex hash length 64, got %d", len(hashHex))
201 }
202
203 // Test ExtractSHA256FromPath
204 testHash := "abc123def456789012345678901234567890123456789012345678901234abcd"
205 sha256Hex, ext, err := ExtractSHA256FromPath(testHash)
206 if err != nil {
207 t.Fatalf("Failed to extract SHA256: %v", err)
208 }
209 if sha256Hex != testHash {
210 t.Errorf("Expected %s, got %s", testHash, sha256Hex)
211 }
212 if ext != "" {
213 t.Errorf("Expected empty ext, got %s", ext)
214 }
215
216 sha256Hex, ext, err = ExtractSHA256FromPath(testHash + ".pdf")
217 if err != nil {
218 t.Fatalf("Failed to extract SHA256: %v", err)
219 }
220 if sha256Hex != testHash {
221 t.Errorf("Expected %s, got %s", testHash, sha256Hex)
222 }
223 if ext != ".pdf" {
224 t.Errorf("Expected .pdf, got %s", ext)
225 }
226
227 // Test MIME type detection
228 mime := GetMimeTypeFromExtension(".pdf")
229 if mime != "application/pdf" {
230 t.Errorf("Expected application/pdf, got %s", mime)
231 }
232
233 mime = DetectMimeType("image/png", ".png")
234 if mime != "image/png" {
235 t.Errorf("Expected image/png, got %s", mime)
236 }
237
238 mime = DetectMimeType("", ".jpg")
239 if mime != "image/jpeg" {
240 t.Errorf("Expected image/jpeg, got %s", mime)
241 }
242 }
243
244 // TestStorage tests storage operations
245 func TestStorage(t *testing.T) {
246 server, cleanup := testSetup(t)
247 defer cleanup()
248
249 storage := server.storage
250
251 // Create test data
252 testData := []byte("test blob data")
253 sha256Hash := CalculateSHA256(testData)
254 pubkey := []byte("testpubkey123456789012345678901234")
255
256 // Test SaveBlob
257 err := storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
258 if err != nil {
259 t.Fatalf("Failed to save blob: %v", err)
260 }
261
262 // Test HasBlob
263 exists, err := storage.HasBlob(sha256Hash)
264 if err != nil {
265 t.Fatalf("Failed to check blob existence: %v", err)
266 }
267 if !exists {
268 t.Error("Blob should exist after save")
269 }
270
271 // Test GetBlob
272 blobData, metadata, err := storage.GetBlob(sha256Hash)
273 if err != nil {
274 t.Fatalf("Failed to get blob: %v", err)
275 }
276 if string(blobData) != string(testData) {
277 t.Error("Blob data mismatch")
278 }
279 if metadata.Size != int64(len(testData)) {
280 t.Errorf("Size mismatch: expected %d, got %d", len(testData), metadata.Size)
281 }
282
283 // Test ListBlobs
284 descriptors, err := storage.ListBlobs(pubkey, 0, 0)
285 if err != nil {
286 t.Fatalf("Failed to list blobs: %v", err)
287 }
288 if len(descriptors) != 1 {
289 t.Errorf("Expected 1 blob, got %d", len(descriptors))
290 }
291
292 // Test DeleteBlob
293 err = storage.DeleteBlob(sha256Hash, pubkey)
294 if err != nil {
295 t.Fatalf("Failed to delete blob: %v", err)
296 }
297
298 exists, err = storage.HasBlob(sha256Hash)
299 if err != nil {
300 t.Fatalf("Failed to check blob existence: %v", err)
301 }
302 if exists {
303 t.Error("Blob should not exist after delete")
304 }
305 }
306
307 // TestAuthEvent tests authorization event validation
308 func TestAuthEvent(t *testing.T) {
309 pubkey, signer := createTestKeypair(t)
310 sha256Hash := CalculateSHA256([]byte("test"))
311
312 // Create valid auth event
313 authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
314
315 // Create HTTP request
316 req := httptest.NewRequest("PUT", "/upload", nil)
317 req.Header.Set("Authorization", createAuthHeader(authEv))
318
319 // Extract and validate
320 ev, err := ExtractAuthEvent(req)
321 if err != nil {
322 t.Fatalf("Failed to extract auth event: %v", err)
323 }
324
325 if ev.Kind != BlossomAuthKind {
326 t.Errorf("Expected kind %d, got %d", BlossomAuthKind, ev.Kind)
327 }
328
329 // Validate auth event
330 authEv2, err := ValidateAuthEvent(req, "upload", sha256Hash)
331 if err != nil {
332 t.Fatalf("Failed to validate auth event: %v", err)
333 }
334
335 if authEv2.Verb != "upload" {
336 t.Errorf("Expected verb 'upload', got '%s'", authEv2.Verb)
337 }
338
339 // Verify pubkey matches
340 if !bytes.Equal(authEv2.Pubkey, pubkey) {
341 t.Error("Pubkey mismatch")
342 }
343 }
344
345 // TestAuthEventExpired tests expired authorization events
346 func TestAuthEventExpired(t *testing.T) {
347 _, signer := createTestKeypair(t)
348 sha256Hash := CalculateSHA256([]byte("test"))
349
350 // Create expired auth event
351 authEv := createAuthEvent(t, signer, "upload", sha256Hash, -3600)
352
353 req := httptest.NewRequest("PUT", "/upload", nil)
354 req.Header.Set("Authorization", createAuthHeader(authEv))
355
356 _, err := ValidateAuthEvent(req, "upload", sha256Hash)
357 if err == nil {
358 t.Error("Expected error for expired auth event")
359 }
360 }
361
362 // TestServerHandler tests the server handler routing
363 func TestServerHandler(t *testing.T) {
364 server, cleanup := testSetup(t)
365 defer cleanup()
366
367 handler := server.Handler()
368
369 // Test OPTIONS request (CORS preflight)
370 req := httptest.NewRequest("OPTIONS", "/", nil)
371 w := httptest.NewRecorder()
372 handler.ServeHTTP(w, req)
373
374 if w.Code != http.StatusOK {
375 t.Errorf("Expected status 200, got %d", w.Code)
376 }
377
378 // Check CORS headers
379 if w.Header().Get("Access-Control-Allow-Origin") != "*" {
380 t.Error("Missing CORS header")
381 }
382 }
383