Enhance blob storage functionality with file extension support
- Added an `Extension` field to `BlobMetadata` to store file extensions alongside existing metadata. - Updated the `SaveBlob` method to handle file extensions, ensuring they are stored and retrieved correctly. - Modified the `GetBlob` method to read blob data from the filesystem based on the stored extension. - Enhanced the `Storage` struct to manage blob files in a specified directory, improving organization and access. - Introduced utility functions for determining file extensions from MIME types, facilitating better file handling. - Added comprehensive tests for new functionalities, ensuring robust behavior across blob operations.
This commit is contained in:
389
pkg/blossom/utils_test.go
Normal file
389
pkg/blossom/utils_test.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/acl"
|
||||
"next.orly.dev/pkg/crypto/p256k"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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
|
||||
aclRegistry := acl.Registry
|
||||
|
||||
// Create temporary directory for blob storage
|
||||
blobDir, err := os.MkdirTemp("", "blossom-blobs-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create blob temp dir: %v", err)
|
||||
}
|
||||
|
||||
// Create server
|
||||
cfg := &Config{
|
||||
BaseURL: "http://localhost:8080",
|
||||
MaxBlobSize: 100 * 1024 * 1024, // 100MB
|
||||
AllowedMimeTypes: nil,
|
||||
RequireAuth: false,
|
||||
BlobDir: blobDir,
|
||||
}
|
||||
|
||||
server := NewServer(db, aclRegistry, cfg)
|
||||
|
||||
cleanup := func() {
|
||||
cancel()
|
||||
db.Close()
|
||||
os.RemoveAll(tempDir)
|
||||
os.RemoveAll(blobDir)
|
||||
}
|
||||
|
||||
return server, cleanup
|
||||
}
|
||||
|
||||
// createTestKeypair creates a test keypair for signing events
|
||||
func createTestKeypair(t *testing.T) ([]byte, *p256k.Signer) {
|
||||
signer := &p256k.Signer{}
|
||||
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 *p256k.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
|
||||
sha256Hex, ext, err := ExtractSHA256FromPath("abc123def456")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract SHA256: %v", err)
|
||||
}
|
||||
if sha256Hex != "abc123def456" {
|
||||
t.Errorf("Expected %s, got %s", "abc123def456", sha256Hex)
|
||||
}
|
||||
if ext != "" {
|
||||
t.Errorf("Expected empty ext, got %s", ext)
|
||||
}
|
||||
|
||||
sha256Hex, ext, err = ExtractSHA256FromPath("abc123def456.pdf")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to extract SHA256: %v", err)
|
||||
}
|
||||
if sha256Hex != "abc123def456" {
|
||||
t.Errorf("Expected %s, got %s", "abc123def456", 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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user