You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
9.5 KiB
382 lines
9.5 KiB
package blossom |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"encoding/base64" |
|
"net/http" |
|
"net/http/httptest" |
|
"os" |
|
"testing" |
|
"time" |
|
|
|
"next.orly.dev/pkg/acl" |
|
"next.orly.dev/pkg/database" |
|
"next.orly.dev/pkg/encoders/event" |
|
"next.orly.dev/pkg/encoders/hex" |
|
"next.orly.dev/pkg/encoders/tag" |
|
"next.orly.dev/pkg/encoders/timestamp" |
|
"next.orly.dev/pkg/interfaces/signer/p8k" |
|
) |
|
|
|
// testSetup creates a test database, ACL, and server |
|
func testSetup(t *testing.T) (*Server, func()) { |
|
// Create temporary directory for database |
|
tempDir, err := os.MkdirTemp("", "blossom-test-*") |
|
if err != nil { |
|
t.Fatalf("Failed to create temp dir: %v", err) |
|
} |
|
|
|
ctx, cancel := context.WithCancel(context.Background()) |
|
|
|
// Create database |
|
db, err := database.New(ctx, cancel, tempDir, "error") |
|
if err != nil { |
|
os.RemoveAll(tempDir) |
|
t.Fatalf("Failed to create database: %v", err) |
|
} |
|
|
|
// Create ACL registry and set to "none" mode for tests |
|
aclRegistry := acl.Registry |
|
aclRegistry.Active.Store("none") // Allow all access for tests |
|
|
|
// Create server |
|
cfg := &Config{ |
|
BaseURL: "http://localhost:8080", |
|
MaxBlobSize: 100 * 1024 * 1024, // 100MB |
|
AllowedMimeTypes: nil, |
|
RequireAuth: false, |
|
} |
|
|
|
server := NewServer(db, aclRegistry, cfg) |
|
|
|
cleanup := func() { |
|
cancel() |
|
db.Close() |
|
os.RemoveAll(tempDir) |
|
} |
|
|
|
return server, cleanup |
|
} |
|
|
|
// createTestKeypair creates a test keypair for signing events |
|
func createTestKeypair(t *testing.T) ([]byte, *p8k.Signer) { |
|
signer := p8k.MustNew() |
|
if err := signer.Generate(); err != nil { |
|
t.Fatalf("Failed to generate keypair: %v", err) |
|
} |
|
pubkey := signer.Pub() |
|
return pubkey, signer |
|
} |
|
|
|
// createAuthEvent creates a valid kind 24242 authorization event |
|
func createAuthEvent( |
|
t *testing.T, signer *p8k.Signer, verb string, |
|
sha256Hash []byte, expiresIn int64, |
|
) *event.E { |
|
now := time.Now().Unix() |
|
expires := now + expiresIn |
|
|
|
tags := tag.NewS() |
|
tags.Append(tag.NewFromAny("t", verb)) |
|
tags.Append(tag.NewFromAny("expiration", timestamp.FromUnix(expires).String())) |
|
|
|
if sha256Hash != nil { |
|
tags.Append(tag.NewFromAny("x", hex.Enc(sha256Hash))) |
|
} |
|
|
|
ev := &event.E{ |
|
CreatedAt: now, |
|
Kind: BlossomAuthKind, |
|
Tags: tags, |
|
Content: []byte("Test authorization"), |
|
Pubkey: signer.Pub(), |
|
} |
|
|
|
// Sign event |
|
if err := ev.Sign(signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
return ev |
|
} |
|
|
|
// createAuthHeader creates an Authorization header from an event |
|
func createAuthHeader(ev *event.E) string { |
|
eventJSON := ev.Serialize() |
|
b64 := base64.StdEncoding.EncodeToString(eventJSON) |
|
return "Nostr " + b64 |
|
} |
|
|
|
// makeRequest creates an HTTP request with optional authorization |
|
func makeRequest( |
|
t *testing.T, method, path string, body []byte, authEv *event.E, |
|
) *http.Request { |
|
req := httptest.NewRequest(method, path, nil) |
|
if body != nil { |
|
req.Body = httptest.NewRequest(method, path, nil).Body |
|
req.ContentLength = int64(len(body)) |
|
} |
|
|
|
if authEv != nil { |
|
req.Header.Set("Authorization", createAuthHeader(authEv)) |
|
} |
|
|
|
return req |
|
} |
|
|
|
// TestBlobDescriptor tests BlobDescriptor creation and serialization |
|
func TestBlobDescriptor(t *testing.T) { |
|
desc := NewBlobDescriptor( |
|
"https://example.com/blob.pdf", |
|
"abc123", |
|
1024, |
|
"application/pdf", |
|
1234567890, |
|
) |
|
|
|
if desc.URL != "https://example.com/blob.pdf" { |
|
t.Errorf("Expected URL %s, got %s", "https://example.com/blob.pdf", desc.URL) |
|
} |
|
if desc.SHA256 != "abc123" { |
|
t.Errorf("Expected SHA256 %s, got %s", "abc123", desc.SHA256) |
|
} |
|
if desc.Size != 1024 { |
|
t.Errorf("Expected Size %d, got %d", 1024, desc.Size) |
|
} |
|
if desc.Type != "application/pdf" { |
|
t.Errorf("Expected Type %s, got %s", "application/pdf", desc.Type) |
|
} |
|
|
|
// Test default MIME type |
|
desc2 := NewBlobDescriptor("url", "hash", 0, "", 0) |
|
if desc2.Type != "application/octet-stream" { |
|
t.Errorf("Expected default MIME type, got %s", desc2.Type) |
|
} |
|
} |
|
|
|
// TestBlobMetadata tests BlobMetadata serialization |
|
func TestBlobMetadata(t *testing.T) { |
|
pubkey := []byte("testpubkey123456789012345678901234") |
|
meta := NewBlobMetadata(pubkey, "image/png", 2048) |
|
|
|
if meta.Size != 2048 { |
|
t.Errorf("Expected Size %d, got %d", 2048, meta.Size) |
|
} |
|
if meta.MimeType != "image/png" { |
|
t.Errorf("Expected MIME type %s, got %s", "image/png", meta.MimeType) |
|
} |
|
|
|
// Test serialization |
|
data, err := meta.Serialize() |
|
if err != nil { |
|
t.Fatalf("Failed to serialize metadata: %v", err) |
|
} |
|
|
|
// Test deserialization |
|
meta2, err := DeserializeBlobMetadata(data) |
|
if err != nil { |
|
t.Fatalf("Failed to deserialize metadata: %v", err) |
|
} |
|
|
|
if meta2.Size != meta.Size { |
|
t.Errorf("Size mismatch after deserialize") |
|
} |
|
if meta2.MimeType != meta.MimeType { |
|
t.Errorf("MIME type mismatch after deserialize") |
|
} |
|
} |
|
|
|
// TestUtils tests utility functions |
|
func TestUtils(t *testing.T) { |
|
data := []byte("test data") |
|
hash := CalculateSHA256(data) |
|
if len(hash) != 32 { |
|
t.Errorf("Expected hash length 32, got %d", len(hash)) |
|
} |
|
|
|
hashHex := CalculateSHA256Hex(data) |
|
if len(hashHex) != 64 { |
|
t.Errorf("Expected hex hash length 64, got %d", len(hashHex)) |
|
} |
|
|
|
// Test ExtractSHA256FromPath |
|
testHash := "abc123def456789012345678901234567890123456789012345678901234abcd" |
|
sha256Hex, ext, err := ExtractSHA256FromPath(testHash) |
|
if err != nil { |
|
t.Fatalf("Failed to extract SHA256: %v", err) |
|
} |
|
if sha256Hex != testHash { |
|
t.Errorf("Expected %s, got %s", testHash, sha256Hex) |
|
} |
|
if ext != "" { |
|
t.Errorf("Expected empty ext, got %s", ext) |
|
} |
|
|
|
sha256Hex, ext, err = ExtractSHA256FromPath(testHash + ".pdf") |
|
if err != nil { |
|
t.Fatalf("Failed to extract SHA256: %v", err) |
|
} |
|
if sha256Hex != testHash { |
|
t.Errorf("Expected %s, got %s", testHash, sha256Hex) |
|
} |
|
if ext != ".pdf" { |
|
t.Errorf("Expected .pdf, got %s", ext) |
|
} |
|
|
|
// Test MIME type detection |
|
mime := GetMimeTypeFromExtension(".pdf") |
|
if mime != "application/pdf" { |
|
t.Errorf("Expected application/pdf, got %s", mime) |
|
} |
|
|
|
mime = DetectMimeType("image/png", ".png") |
|
if mime != "image/png" { |
|
t.Errorf("Expected image/png, got %s", mime) |
|
} |
|
|
|
mime = DetectMimeType("", ".jpg") |
|
if mime != "image/jpeg" { |
|
t.Errorf("Expected image/jpeg, got %s", mime) |
|
} |
|
} |
|
|
|
// TestStorage tests storage operations |
|
func TestStorage(t *testing.T) { |
|
server, cleanup := testSetup(t) |
|
defer cleanup() |
|
|
|
storage := server.storage |
|
|
|
// Create test data |
|
testData := []byte("test blob data") |
|
sha256Hash := CalculateSHA256(testData) |
|
pubkey := []byte("testpubkey123456789012345678901234") |
|
|
|
// Test SaveBlob |
|
err := storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "") |
|
if err != nil { |
|
t.Fatalf("Failed to save blob: %v", err) |
|
} |
|
|
|
// Test HasBlob |
|
exists, err := storage.HasBlob(sha256Hash) |
|
if err != nil { |
|
t.Fatalf("Failed to check blob existence: %v", err) |
|
} |
|
if !exists { |
|
t.Error("Blob should exist after save") |
|
} |
|
|
|
// Test GetBlob |
|
blobData, metadata, err := storage.GetBlob(sha256Hash) |
|
if err != nil { |
|
t.Fatalf("Failed to get blob: %v", err) |
|
} |
|
if string(blobData) != string(testData) { |
|
t.Error("Blob data mismatch") |
|
} |
|
if metadata.Size != int64(len(testData)) { |
|
t.Errorf("Size mismatch: expected %d, got %d", len(testData), metadata.Size) |
|
} |
|
|
|
// Test ListBlobs |
|
descriptors, err := storage.ListBlobs(pubkey, 0, 0) |
|
if err != nil { |
|
t.Fatalf("Failed to list blobs: %v", err) |
|
} |
|
if len(descriptors) != 1 { |
|
t.Errorf("Expected 1 blob, got %d", len(descriptors)) |
|
} |
|
|
|
// Test DeleteBlob |
|
err = storage.DeleteBlob(sha256Hash, pubkey) |
|
if err != nil { |
|
t.Fatalf("Failed to delete blob: %v", err) |
|
} |
|
|
|
exists, err = storage.HasBlob(sha256Hash) |
|
if err != nil { |
|
t.Fatalf("Failed to check blob existence: %v", err) |
|
} |
|
if exists { |
|
t.Error("Blob should not exist after delete") |
|
} |
|
} |
|
|
|
// TestAuthEvent tests authorization event validation |
|
func TestAuthEvent(t *testing.T) { |
|
pubkey, signer := createTestKeypair(t) |
|
sha256Hash := CalculateSHA256([]byte("test")) |
|
|
|
// Create valid auth event |
|
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600) |
|
|
|
// Create HTTP request |
|
req := httptest.NewRequest("PUT", "/upload", nil) |
|
req.Header.Set("Authorization", createAuthHeader(authEv)) |
|
|
|
// Extract and validate |
|
ev, err := ExtractAuthEvent(req) |
|
if err != nil { |
|
t.Fatalf("Failed to extract auth event: %v", err) |
|
} |
|
|
|
if ev.Kind != BlossomAuthKind { |
|
t.Errorf("Expected kind %d, got %d", BlossomAuthKind, ev.Kind) |
|
} |
|
|
|
// Validate auth event |
|
authEv2, err := ValidateAuthEvent(req, "upload", sha256Hash) |
|
if err != nil { |
|
t.Fatalf("Failed to validate auth event: %v", err) |
|
} |
|
|
|
if authEv2.Verb != "upload" { |
|
t.Errorf("Expected verb 'upload', got '%s'", authEv2.Verb) |
|
} |
|
|
|
// Verify pubkey matches |
|
if !bytes.Equal(authEv2.Pubkey, pubkey) { |
|
t.Error("Pubkey mismatch") |
|
} |
|
} |
|
|
|
// TestAuthEventExpired tests expired authorization events |
|
func TestAuthEventExpired(t *testing.T) { |
|
_, signer := createTestKeypair(t) |
|
sha256Hash := CalculateSHA256([]byte("test")) |
|
|
|
// Create expired auth event |
|
authEv := createAuthEvent(t, signer, "upload", sha256Hash, -3600) |
|
|
|
req := httptest.NewRequest("PUT", "/upload", nil) |
|
req.Header.Set("Authorization", createAuthHeader(authEv)) |
|
|
|
_, err := ValidateAuthEvent(req, "upload", sha256Hash) |
|
if err == nil { |
|
t.Error("Expected error for expired auth event") |
|
} |
|
} |
|
|
|
// TestServerHandler tests the server handler routing |
|
func TestServerHandler(t *testing.T) { |
|
server, cleanup := testSetup(t) |
|
defer cleanup() |
|
|
|
handler := server.Handler() |
|
|
|
// Test OPTIONS request (CORS preflight) |
|
req := httptest.NewRequest("OPTIONS", "/", nil) |
|
w := httptest.NewRecorder() |
|
handler.ServeHTTP(w, req) |
|
|
|
if w.Code != http.StatusOK { |
|
t.Errorf("Expected status 200, got %d", w.Code) |
|
} |
|
|
|
// Check CORS headers |
|
if w.Header().Get("Access-Control-Allow-Origin") != "*" { |
|
t.Error("Missing CORS header") |
|
} |
|
}
|
|
|