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