383 lines
9.5 KiB
Go
383 lines
9.5 KiB
Go
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"
|
|
"git.mleku.dev/mleku/nostr/encoders/event"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
"git.mleku.dev/mleku/nostr/encoders/tag"
|
|
"git.mleku.dev/mleku/nostr/encoders/timestamp"
|
|
"git.mleku.dev/mleku/nostr/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")
|
|
}
|
|
}
|