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:
2025-11-02 21:55:50 +00:00
parent 9082481129
commit 3567bb26a4
8 changed files with 2239 additions and 147 deletions

View File

@@ -17,10 +17,11 @@ type BlobDescriptor struct {
// BlobMetadata stores metadata about a blob in the database
type BlobMetadata struct {
Pubkey []byte `json:"pubkey"`
MimeType string `json:"mime_type"`
Uploaded int64 `json:"uploaded"`
Size int64 `json:"size"`
Pubkey []byte `json:"pubkey"`
MimeType string `json:"mime_type"`
Uploaded int64 `json:"uploaded"`
Size int64 `json:"size"`
Extension string `json:"extension"` // File extension (e.g., ".png", ".pdf")
}
// NewBlobDescriptor creates a new blob descriptor
@@ -45,10 +46,11 @@ func NewBlobMetadata(pubkey []byte, mimeType string, size int64) *BlobMetadata {
mimeType = "application/octet-stream"
}
return &BlobMetadata{
Pubkey: pubkey,
MimeType: mimeType,
Uploaded: time.Now().Unix(),
Size: size,
Pubkey: pubkey,
MimeType: mimeType,
Uploaded: time.Now().Unix(),
Size: size,
Extension: "", // Will be set by SaveBlob
}
}

View File

@@ -19,7 +19,7 @@ import (
// handleGetBlob handles GET /<sha256> requests (BUD-01)
func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract SHA256 and extension
sha256Hex, ext, err := ExtractSHA256FromPath(path)
if err != nil {
@@ -104,7 +104,7 @@ func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) {
// handleHeadBlob handles HEAD /<sha256> requests (BUD-01)
func (s *Server) handleHeadBlob(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract SHA256 and extension
sha256Hex, ext, err := ExtractSHA256FromPath(path)
if err != nil {
@@ -166,7 +166,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
// Check ACL
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
if !s.checkACL(pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
@@ -180,7 +180,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
}
if int64(len(body)) > s.maxBlobSize {
s.setErrorResponse(w, http.StatusRequestEntityTooLarge,
s.setErrorResponse(w, http.StatusRequestEntityTooLarge,
fmt.Sprintf("blob too large: max %d bytes", s.maxBlobSize))
return
}
@@ -220,16 +220,22 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
GetFileExtensionFromPath(r.URL.Path),
)
// Extract extension from path or infer from MIME type
ext := GetFileExtensionFromPath(r.URL.Path)
if ext == "" {
ext = GetExtensionFromMimeType(mimeType)
}
// Check allowed MIME types
if len(s.allowedMimeTypes) > 0 && !s.allowedMimeTypes[mimeType] {
s.setErrorResponse(w, http.StatusUnsupportedMediaType,
s.setErrorResponse(w, http.StatusUnsupportedMediaType,
fmt.Sprintf("MIME type %s not allowed", mimeType))
return
}
// Save blob if it doesn't exist
if !exists {
if err = s.storage.SaveBlob(sha256Hash, body, pubkey, mimeType); err != nil {
if err = s.storage.SaveBlob(sha256Hash, body, pubkey, mimeType, ext); err != nil {
log.E.F("error saving blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "error saving blob")
return
@@ -242,7 +248,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
// Allow if same pubkey or if ACL allows
if !utils.FastEqual(metadata.Pubkey, pubkey) && !s.checkACL(pubkey, remoteAddr, "admin") {
s.setErrorResponse(w, http.StatusConflict, "blob already exists")
@@ -251,27 +257,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
}
// Build URL with extension
ext := ""
if mimeExt := GetMimeTypeFromExtension(GetFileExtensionFromPath(r.URL.Path)); mimeExt != "application/octet-stream" {
// Try to infer extension from MIME type
for extName, mime := range map[string]string{
".pdf": "application/pdf",
".png": "image/png",
".jpg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
} {
if mime == mimeType {
ext = extName
break
}
}
}
blobURL := BuildBlobURL(s.baseURL, sha256Hex, ext)
if !strings.HasSuffix(blobURL, "/") && !strings.HasPrefix(ext, "/") {
blobURL = s.baseURL + "/" + sha256Hex + ext
}
// Create descriptor
descriptor := NewBlobDescriptor(
@@ -382,7 +368,7 @@ func (s *Server) handleUploadRequirements(w http.ResponseWriter, r *http.Request
// handleListBlobs handles GET /list/<pubkey> requests (BUD-02)
func (s *Server) handleListBlobs(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract pubkey from path: list/<pubkey>
if !strings.HasPrefix(path, "list/") {
s.setErrorResponse(w, http.StatusBadRequest, "invalid path")
@@ -462,7 +448,7 @@ func (s *Server) handleListBlobs(w http.ResponseWriter, r *http.Request) {
// handleDeleteBlob handles DELETE /<sha256> requests (BUD-02)
func (s *Server) handleDeleteBlob(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract SHA256
sha256Hex, _, err := ExtractSHA256FromPath(path)
if err != nil {
@@ -522,7 +508,7 @@ func (s *Server) handleMirror(w http.ResponseWriter, r *http.Request) {
// Check ACL
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
if !s.checkACL(pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
@@ -560,7 +546,7 @@ func (s *Server) handleMirror(w http.ResponseWriter, r *http.Request) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
s.setErrorResponse(w, http.StatusBadGateway,
s.setErrorResponse(w, http.StatusBadGateway,
fmt.Sprintf("remote server returned status %d", resp.StatusCode))
return
}
@@ -605,15 +591,21 @@ func (s *Server) handleMirror(w http.ResponseWriter, r *http.Request) {
GetFileExtensionFromPath(mirrorURL.Path),
)
// Extract extension from path or infer from MIME type
ext := GetFileExtensionFromPath(mirrorURL.Path)
if ext == "" {
ext = GetExtensionFromMimeType(mimeType)
}
// Save blob
if err = s.storage.SaveBlob(sha256Hash, body, pubkey, mimeType); err != nil {
if err = s.storage.SaveBlob(sha256Hash, body, pubkey, mimeType, ext); err != nil {
log.E.F("error saving mirrored blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "error saving blob")
return
}
// Build URL
blobURL := BuildBlobURL(s.baseURL, sha256Hex, "")
blobURL := BuildBlobURL(s.baseURL, sha256Hex, ext)
// Create descriptor
descriptor := NewBlobDescriptor(
@@ -637,7 +629,7 @@ func (s *Server) handleMediaUpload(w http.ResponseWriter, r *http.Request) {
// Check ACL
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
if !s.checkACL(pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
@@ -677,24 +669,31 @@ func (s *Server) handleMediaUpload(w http.ResponseWriter, r *http.Request) {
}
// Optimize media (placeholder - actual optimization would be implemented here)
optimizedData, mimeType := OptimizeMedia(body, DetectMimeType(
originalMimeType := DetectMimeType(
r.Header.Get("Content-Type"),
GetFileExtensionFromPath(r.URL.Path),
))
)
optimizedData, mimeType := OptimizeMedia(body, originalMimeType)
// Extract extension from path or infer from MIME type
ext := GetFileExtensionFromPath(r.URL.Path)
if ext == "" {
ext = GetExtensionFromMimeType(mimeType)
}
// Calculate optimized blob SHA256
optimizedHash := CalculateSHA256(optimizedData)
optimizedHex := hex.Enc(optimizedHash)
// Save optimized blob
if err = s.storage.SaveBlob(optimizedHash, optimizedData, pubkey, mimeType); err != nil {
if err = s.storage.SaveBlob(optimizedHash, optimizedData, pubkey, mimeType, ext); err != nil {
log.E.F("error saving optimized blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "error saving blob")
return
}
// Build URL
blobURL := BuildBlobURL(s.baseURL, optimizedHex, "")
blobURL := BuildBlobURL(s.baseURL, optimizedHex, ext)
// Create descriptor
descriptor := NewBlobDescriptor(
@@ -725,7 +724,7 @@ func (s *Server) handleReport(w http.ResponseWriter, r *http.Request) {
// Check ACL
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
if !s.checkACL(pubkey, remoteAddr, "read") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
@@ -780,4 +779,3 @@ func (s *Server) handleReport(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}

756
pkg/blossom/http_test.go Normal file
View File

@@ -0,0 +1,756 @@
package blossom
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/encoders/tag"
"next.orly.dev/pkg/encoders/timestamp"
)
// TestHTTPGetBlob tests GET /<sha256> endpoint
func TestHTTPGetBlob(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
// Upload a blob first
testData := []byte("test blob content")
sha256Hash := CalculateSHA256(testData)
pubkey := []byte("testpubkey123456789012345678901234")
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
sha256Hex := hex.Enc(sha256Hash)
// Test GET request
req := httptest.NewRequest("GET", "/"+sha256Hex, nil)
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
body := w.Body.Bytes()
if !bytes.Equal(body, testData) {
t.Error("Response body mismatch")
}
if w.Header().Get("Content-Type") != "text/plain" {
t.Errorf("Expected Content-Type text/plain, got %s", w.Header().Get("Content-Type"))
}
}
// TestHTTPHeadBlob tests HEAD /<sha256> endpoint
func TestHTTPHeadBlob(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
testData := []byte("test blob content")
sha256Hash := CalculateSHA256(testData)
pubkey := []byte("testpubkey123456789012345678901234")
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
sha256Hex := hex.Enc(sha256Hash)
req := httptest.NewRequest("HEAD", "/"+sha256Hex, nil)
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
if w.Body.Len() != 0 {
t.Error("HEAD request should not return body")
}
if w.Header().Get("Content-Length") != "18" {
t.Errorf("Expected Content-Length 18, got %s", w.Header().Get("Content-Length"))
}
}
// TestHTTPUpload tests PUT /upload endpoint
func TestHTTPUpload(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
testData := []byte("test upload data")
sha256Hash := CalculateSHA256(testData)
// Create auth event
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
// Create request
req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
req.Header.Set("Content-Type", "text/plain")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
// Parse response
var desc BlobDescriptor
if err := json.Unmarshal(w.Body.Bytes(), &desc); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
if desc.SHA256 != hex.Enc(sha256Hash) {
t.Errorf("SHA256 mismatch: expected %s, got %s", hex.Enc(sha256Hash), desc.SHA256)
}
if desc.Size != int64(len(testData)) {
t.Errorf("Size mismatch: expected %d, got %d", len(testData), desc.Size)
}
// Verify blob was saved
exists, err := server.storage.HasBlob(sha256Hash)
if err != nil {
t.Fatalf("Failed to check blob: %v", err)
}
if !exists {
t.Error("Blob should exist after upload")
}
}
// TestHTTPUploadRequirements tests HEAD /upload endpoint
func TestHTTPUploadRequirements(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
testData := []byte("test data")
sha256Hash := CalculateSHA256(testData)
req := httptest.NewRequest("HEAD", "/upload", nil)
req.Header.Set("X-SHA-256", hex.Enc(sha256Hash))
req.Header.Set("X-Content-Length", "9")
req.Header.Set("X-Content-Type", "text/plain")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Header().Get("X-Reason"))
}
}
// TestHTTPUploadTooLarge tests upload size limit
func TestHTTPUploadTooLarge(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
// Create request with size exceeding limit
req := httptest.NewRequest("HEAD", "/upload", nil)
req.Header.Set("X-SHA-256", hex.Enc(CalculateSHA256([]byte("test"))))
req.Header.Set("X-Content-Length", "200000000") // 200MB
req.Header.Set("X-Content-Type", "application/octet-stream")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusRequestEntityTooLarge {
t.Errorf("Expected status 413, got %d", w.Code)
}
}
// TestHTTPListBlobs tests GET /list/<pubkey> endpoint
func TestHTTPListBlobs(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
pubkeyHex := hex.Enc(pubkey)
// Upload multiple blobs
for i := 0; i < 3; i++ {
testData := []byte("test data " + string(rune('A'+i)))
sha256Hash := CalculateSHA256(testData)
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
}
// Create auth event
authEv := createAuthEvent(t, signer, "list", nil, 3600)
req := httptest.NewRequest("GET", "/list/"+pubkeyHex, nil)
req.Header.Set("Authorization", createAuthHeader(authEv))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
var descriptors []BlobDescriptor
if err := json.Unmarshal(w.Body.Bytes(), &descriptors); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
if len(descriptors) != 3 {
t.Errorf("Expected 3 blobs, got %d", len(descriptors))
}
}
// TestHTTPDeleteBlob tests DELETE /<sha256> endpoint
func TestHTTPDeleteBlob(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
testData := []byte("test delete data")
sha256Hash := CalculateSHA256(testData)
// Upload blob first
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
// Create auth event
authEv := createAuthEvent(t, signer, "delete", sha256Hash, 3600)
sha256Hex := hex.Enc(sha256Hash)
req := httptest.NewRequest("DELETE", "/"+sha256Hex, nil)
req.Header.Set("Authorization", createAuthHeader(authEv))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
// Verify blob was deleted
exists, err := server.storage.HasBlob(sha256Hash)
if err != nil {
t.Fatalf("Failed to check blob: %v", err)
}
if exists {
t.Error("Blob should not exist after delete")
}
}
// TestHTTPMirror tests PUT /mirror endpoint
func TestHTTPMirror(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
// Create a mock remote server
testData := []byte("mirrored blob data")
sha256Hash := CalculateSHA256(testData)
sha256Hex := hex.Enc(sha256Hash)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write(testData)
}))
defer mockServer.Close()
// Create mirror request
mirrorReq := map[string]string{
"url": mockServer.URL + "/" + sha256Hex,
}
reqBody, _ := json.Marshal(mirrorReq)
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/mirror", bytes.NewReader(reqBody))
req.Header.Set("Authorization", createAuthHeader(authEv))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
// Verify blob was saved
exists, err := server.storage.HasBlob(sha256Hash)
if err != nil {
t.Fatalf("Failed to check blob: %v", err)
}
if !exists {
t.Error("Blob should exist after mirror")
}
}
// TestHTTPMediaUpload tests PUT /media endpoint
func TestHTTPMediaUpload(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
testData := []byte("test media data")
sha256Hash := CalculateSHA256(testData)
authEv := createAuthEvent(t, signer, "media", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/media", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
req.Header.Set("Content-Type", "image/png")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
var desc BlobDescriptor
if err := json.Unmarshal(w.Body.Bytes(), &desc); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
if desc.SHA256 == "" {
t.Error("Expected SHA256 in response")
}
}
// TestHTTPReport tests PUT /report endpoint
func TestHTTPReport(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
// Upload a blob first
testData := []byte("test blob")
sha256Hash := CalculateSHA256(testData)
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
// Create report event (kind 1984)
reportEv := &event.E{
CreatedAt: timestamp.Now().V,
Kind: 1984,
Tags: tag.NewS(tag.NewFromAny("x", hex.Enc(sha256Hash))),
Content: []byte("This blob violates policy"),
Pubkey: pubkey,
}
if err := reportEv.Sign(signer); err != nil {
t.Fatalf("Failed to sign report: %v", err)
}
reqBody := reportEv.Serialize()
req := httptest.NewRequest("PUT", "/report", bytes.NewReader(reqBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
}
// TestHTTPRangeRequest tests range request support
func TestHTTPRangeRequest(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
testData := []byte("0123456789abcdef")
sha256Hash := CalculateSHA256(testData)
pubkey := []byte("testpubkey123456789012345678901234")
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
sha256Hex := hex.Enc(sha256Hash)
// Test range request
req := httptest.NewRequest("GET", "/"+sha256Hex, nil)
req.Header.Set("Range", "bytes=4-9")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusPartialContent {
t.Errorf("Expected status 206, got %d", w.Code)
}
body := w.Body.Bytes()
expected := testData[4:10]
if !bytes.Equal(body, expected) {
t.Errorf("Range response mismatch: expected %s, got %s", string(expected), string(body))
}
if w.Header().Get("Content-Range") == "" {
t.Error("Missing Content-Range header")
}
}
// TestHTTPNotFound tests 404 handling
func TestHTTPNotFound(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
req := httptest.NewRequest("GET", "/nonexistent123456789012345678901234567890123456789012345678901234567890", nil)
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("Expected status 404, got %d", w.Code)
}
}
// TestHTTPServerIntegration tests full server integration
func TestHTTPServerIntegration(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
// Start HTTP server
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
_, signer := createTestKeypair(t)
// Upload blob via HTTP
testData := []byte("integration test data")
sha256Hash := CalculateSHA256(testData)
sha256Hex := hex.Enc(sha256Hash)
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
uploadReq, _ := http.NewRequest("PUT", httpServer.URL+"/upload", bytes.NewReader(testData))
uploadReq.Header.Set("Authorization", createAuthHeader(authEv))
uploadReq.Header.Set("Content-Type", "text/plain")
client := &http.Client{}
resp, err := client.Do(uploadReq)
if err != nil {
t.Fatalf("Failed to upload: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("Upload failed: status %d, body: %s", resp.StatusCode, string(body))
}
// Retrieve blob via HTTP
getReq, _ := http.NewRequest("GET", httpServer.URL+"/"+sha256Hex, nil)
getResp, err := client.Do(getReq)
if err != nil {
t.Fatalf("Failed to get blob: %v", err)
}
defer getResp.Body.Close()
if getResp.StatusCode != http.StatusOK {
t.Fatalf("Get failed: status %d", getResp.StatusCode)
}
body, _ := io.ReadAll(getResp.Body)
if !bytes.Equal(body, testData) {
t.Error("Retrieved blob data mismatch")
}
}
// TestCORSHeaders tests CORS header handling
func TestCORSHeaders(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
req := httptest.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Header().Get("Access-Control-Allow-Origin") != "*" {
t.Error("Missing CORS header")
}
}
// TestAuthorizationRequired tests authorization requirement
func TestAuthorizationRequired(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
// Configure server to require auth
server.requireAuth = true
testData := []byte("test")
sha256Hash := CalculateSHA256(testData)
pubkey := []byte("testpubkey123456789012345678901234")
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
sha256Hex := hex.Enc(sha256Hash)
// Request without auth should fail
req := httptest.NewRequest("GET", "/"+sha256Hex, nil)
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("Expected status 401, got %d", w.Code)
}
}
// TestACLIntegration tests ACL integration
func TestACLIntegration(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
// Note: This test assumes ACL is configured
// In a real scenario, you'd set up a proper ACL instance
_, signer := createTestKeypair(t)
testData := []byte("test")
sha256Hash := CalculateSHA256(testData)
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
// Should succeed if ACL allows, or fail if not
// The exact behavior depends on ACL configuration
if w.Code != http.StatusOK && w.Code != http.StatusForbidden {
t.Errorf("Unexpected status: %d", w.Code)
}
}
// TestMimeTypeDetection tests MIME type detection from various sources
func TestMimeTypeDetection(t *testing.T) {
tests := []struct {
contentType string
ext string
expected string
}{
{"image/png", "", "image/png"},
{"", ".png", "image/png"},
{"", ".pdf", "application/pdf"},
{"application/pdf", ".txt", "application/pdf"},
{"", ".unknown", "application/octet-stream"},
{"", "", "application/octet-stream"},
}
for _, tt := range tests {
result := DetectMimeType(tt.contentType, tt.ext)
if result != tt.expected {
t.Errorf("DetectMimeType(%q, %q) = %q, want %q",
tt.contentType, tt.ext, result, tt.expected)
}
}
}
// TestSHA256Validation tests SHA256 validation
func TestSHA256Validation(t *testing.T) {
validHashes := []string{
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"abc123def456789012345678901234567890123456789012345678901234567890",
}
invalidHashes := []string{
"",
"abc",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855x",
"12345",
}
for _, hash := range validHashes {
if !ValidateSHA256Hex(hash) {
t.Errorf("Hash %s should be valid", hash)
}
}
for _, hash := range invalidHashes {
if ValidateSHA256Hex(hash) {
t.Errorf("Hash %s should be invalid", hash)
}
}
}
// TestBlobURLBuilding tests URL building
func TestBlobURLBuilding(t *testing.T) {
baseURL := "https://example.com"
sha256Hex := "abc123def456"
ext := ".pdf"
url := BuildBlobURL(baseURL, sha256Hex, ext)
expected := baseURL + sha256Hex + ext
if url != expected {
t.Errorf("Expected %s, got %s", expected, url)
}
// Test without extension
url2 := BuildBlobURL(baseURL, sha256Hex, "")
expected2 := baseURL + sha256Hex
if url2 != expected2 {
t.Errorf("Expected %s, got %s", expected2, url2)
}
}
// TestErrorResponses tests error response formatting
func TestErrorResponses(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
w := httptest.NewRecorder()
server.setErrorResponse(w, http.StatusBadRequest, "Invalid request")
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
}
if w.Header().Get("X-Reason") == "" {
t.Error("Missing X-Reason header")
}
}
// TestExtractSHA256FromURL tests URL hash extraction
func TestExtractSHA256FromURL(t *testing.T) {
tests := []struct {
url string
expected string
hasError bool
}{
{"https://example.com/abc123def456", "abc123def456", false},
{"https://example.com/user/path/abc123def456.pdf", "abc123def456", false},
{"https://example.com/", "", true},
{"no hash here", "", true},
}
for _, tt := range tests {
hash, err := ExtractSHA256FromURL(tt.url)
if tt.hasError {
if err == nil {
t.Errorf("Expected error for URL %s", tt.url)
}
} else {
if err != nil {
t.Errorf("Unexpected error for URL %s: %v", tt.url, err)
}
if hash != tt.expected {
t.Errorf("Expected %s, got %s for URL %s", tt.expected, hash, tt.url)
}
}
}
}
// TestStorageReport tests report storage
func TestStorageReport(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
sha256Hash := CalculateSHA256([]byte("test"))
reportData := []byte("report data")
err := server.storage.SaveReport(sha256Hash, reportData)
if err != nil {
t.Fatalf("Failed to save report: %v", err)
}
// Reports are stored but not retrieved in current implementation
// This test verifies the operation doesn't fail
}
// BenchmarkStorageOperations benchmarks storage operations
func BenchmarkStorageOperations(b *testing.B) {
server, cleanup := testSetup(&testing.T{})
defer cleanup()
testData := []byte("benchmark test data")
sha256Hash := CalculateSHA256(testData)
pubkey := []byte("testpubkey123456789012345678901234")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
_, _, _ = server.storage.GetBlob(sha256Hash)
_ = server.storage.DeleteBlob(sha256Hash, pubkey)
}
}
// TestConcurrentUploads tests concurrent uploads
func TestConcurrentUploads(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
const numUploads = 10
done := make(chan error, numUploads)
for i := 0; i < numUploads; i++ {
go func(id int) {
testData := []byte("concurrent test " + string(rune('A'+id)))
sha256Hash := CalculateSHA256(testData)
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
done <- &testError{code: w.Code, body: w.Body.String()}
return
}
done <- nil
}(i)
}
for i := 0; i < numUploads; i++ {
if err := <-done; err != nil {
t.Errorf("Concurrent upload failed: %v", err)
}
}
}
type testError struct {
code int
body string
}
func (e *testError) Error() string {
return strings.Join([]string{"HTTP", string(rune(e.code)), e.body}, " ")
}

View File

@@ -0,0 +1,852 @@
package blossom
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/encoders/tag"
"next.orly.dev/pkg/encoders/timestamp"
)
// TestFullServerIntegration tests a complete workflow with a real HTTP server
func TestFullServerIntegration(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
// Start real HTTP server
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
baseURL := httpServer.URL
client := &http.Client{Timeout: 10 * time.Second}
// Create test keypair
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
pubkeyHex := hex.Enc(pubkey)
// Step 1: Upload a blob
testData := []byte("integration test blob content")
sha256Hash := CalculateSHA256(testData)
sha256Hex := hex.Enc(sha256Hash)
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
uploadReq, err := http.NewRequest("PUT", baseURL+"/upload", bytes.NewReader(testData))
if err != nil {
t.Fatalf("Failed to create upload request: %v", err)
}
uploadReq.Header.Set("Authorization", createAuthHeader(authEv))
uploadReq.Header.Set("Content-Type", "text/plain")
uploadResp, err := client.Do(uploadReq)
if err != nil {
t.Fatalf("Failed to upload: %v", err)
}
defer uploadResp.Body.Close()
if uploadResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(uploadResp.Body)
t.Fatalf("Upload failed: status %d, body: %s", uploadResp.StatusCode, string(body))
}
var uploadDesc BlobDescriptor
if err := json.NewDecoder(uploadResp.Body).Decode(&uploadDesc); err != nil {
t.Fatalf("Failed to parse upload response: %v", err)
}
if uploadDesc.SHA256 != sha256Hex {
t.Errorf("SHA256 mismatch: expected %s, got %s", sha256Hex, uploadDesc.SHA256)
}
// Step 2: Retrieve the blob
getReq, err := http.NewRequest("GET", baseURL+"/"+sha256Hex, nil)
if err != nil {
t.Fatalf("Failed to create GET request: %v", err)
}
getResp, err := client.Do(getReq)
if err != nil {
t.Fatalf("Failed to get blob: %v", err)
}
defer getResp.Body.Close()
if getResp.StatusCode != http.StatusOK {
t.Fatalf("Get failed: status %d", getResp.StatusCode)
}
retrievedData, err := io.ReadAll(getResp.Body)
if err != nil {
t.Fatalf("Failed to read response: %v", err)
}
if !bytes.Equal(retrievedData, testData) {
t.Error("Retrieved blob data mismatch")
}
// Step 3: List blobs
listAuthEv := createAuthEvent(t, signer, "list", nil, 3600)
listReq, err := http.NewRequest("GET", baseURL+"/list/"+pubkeyHex, nil)
if err != nil {
t.Fatalf("Failed to create list request: %v", err)
}
listReq.Header.Set("Authorization", createAuthHeader(listAuthEv))
listResp, err := client.Do(listReq)
if err != nil {
t.Fatalf("Failed to list blobs: %v", err)
}
defer listResp.Body.Close()
if listResp.StatusCode != http.StatusOK {
t.Fatalf("List failed: status %d", listResp.StatusCode)
}
var descriptors []BlobDescriptor
if err := json.NewDecoder(listResp.Body).Decode(&descriptors); err != nil {
t.Fatalf("Failed to parse list response: %v", err)
}
if len(descriptors) == 0 {
t.Error("Expected at least one blob in list")
}
// Step 4: Delete the blob
deleteAuthEv := createAuthEvent(t, signer, "delete", sha256Hash, 3600)
deleteReq, err := http.NewRequest("DELETE", baseURL+"/"+sha256Hex, nil)
if err != nil {
t.Fatalf("Failed to create delete request: %v", err)
}
deleteReq.Header.Set("Authorization", createAuthHeader(deleteAuthEv))
deleteResp, err := client.Do(deleteReq)
if err != nil {
t.Fatalf("Failed to delete blob: %v", err)
}
defer deleteResp.Body.Close()
if deleteResp.StatusCode != http.StatusOK {
t.Fatalf("Delete failed: status %d", deleteResp.StatusCode)
}
// Step 5: Verify blob is gone
getResp2, err := client.Do(getReq)
if err != nil {
t.Fatalf("Failed to get blob: %v", err)
}
defer getResp2.Body.Close()
if getResp2.StatusCode != http.StatusNotFound {
t.Errorf("Expected 404 after delete, got %d", getResp2.StatusCode)
}
}
// TestServerWithMultipleBlobs tests multiple blob operations
func TestServerWithMultipleBlobs(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
pubkeyHex := hex.Enc(pubkey)
// Upload multiple blobs
const numBlobs = 5
var hashes []string
var data []byte
for i := 0; i < numBlobs; i++ {
testData := []byte(fmt.Sprintf("blob %d content", i))
sha256Hash := CalculateSHA256(testData)
sha256Hex := hex.Enc(sha256Hash)
hashes = append(hashes, sha256Hex)
data = append(data, testData...)
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req, _ := http.NewRequest("PUT", httpServer.URL+"/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Failed to upload blob %d: %v", i, err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Upload %d failed: status %d", i, resp.StatusCode)
}
}
// List all blobs
authEv := createAuthEvent(t, signer, "list", nil, 3600)
req, _ := http.NewRequest("GET", httpServer.URL+"/list/"+pubkeyHex, nil)
req.Header.Set("Authorization", createAuthHeader(authEv))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Failed to list blobs: %v", err)
}
defer resp.Body.Close()
var descriptors []BlobDescriptor
json.NewDecoder(resp.Body).Decode(&descriptors)
if len(descriptors) != numBlobs {
t.Errorf("Expected %d blobs, got %d", numBlobs, len(descriptors))
}
}
// TestServerCORS tests CORS headers on all endpoints
func TestServerCORS(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
endpoints := []struct {
method string
path string
}{
{"GET", "/test123456789012345678901234567890123456789012345678901234567890"},
{"HEAD", "/test123456789012345678901234567890123456789012345678901234567890"},
{"PUT", "/upload"},
{"HEAD", "/upload"},
{"GET", "/list/test123456789012345678901234567890123456789012345678901234567890"},
{"PUT", "/media"},
{"HEAD", "/media"},
{"PUT", "/mirror"},
{"PUT", "/report"},
{"DELETE", "/test123456789012345678901234567890123456789012345678901234567890"},
{"OPTIONS", "/"},
}
for _, ep := range endpoints {
req, _ := http.NewRequest(ep.method, httpServer.URL+ep.path, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Failed to test %s %s: %v", ep.method, ep.path, err)
continue
}
resp.Body.Close()
corsHeader := resp.Header.Get("Access-Control-Allow-Origin")
if corsHeader != "*" {
t.Errorf("Missing CORS header on %s %s", ep.method, ep.path)
}
}
}
// TestServerRangeRequests tests range request handling
func TestServerRangeRequests(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
// Upload a blob
testData := []byte("0123456789abcdefghij")
sha256Hash := CalculateSHA256(testData)
pubkey := []byte("testpubkey123456789012345678901234")
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
sha256Hex := hex.Enc(sha256Hash)
// Test various range requests
tests := []struct {
rangeHeader string
expected string
status int
}{
{"bytes=0-4", "01234", http.StatusPartialContent},
{"bytes=5-9", "56789", http.StatusPartialContent},
{"bytes=10-", "abcdefghij", http.StatusPartialContent},
{"bytes=-5", "hij", http.StatusPartialContent},
{"bytes=0-0", "0", http.StatusPartialContent},
{"bytes=100-200", "", http.StatusRequestedRangeNotSatisfiable},
}
for _, tt := range tests {
req, _ := http.NewRequest("GET", httpServer.URL+"/"+sha256Hex, nil)
req.Header.Set("Range", tt.rangeHeader)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("Failed to request range %s: %v", tt.rangeHeader, err)
continue
}
if resp.StatusCode != tt.status {
t.Errorf("Range %s: expected status %d, got %d", tt.rangeHeader, tt.status, resp.StatusCode)
resp.Body.Close()
continue
}
if tt.status == http.StatusPartialContent {
body, _ := io.ReadAll(resp.Body)
if string(body) != tt.expected {
t.Errorf("Range %s: expected %q, got %q", tt.rangeHeader, tt.expected, string(body))
}
if resp.Header.Get("Content-Range") == "" {
t.Errorf("Range %s: missing Content-Range header", tt.rangeHeader)
}
}
resp.Body.Close()
}
}
// TestServerAuthorizationFlow tests complete authorization flow
func TestServerAuthorizationFlow(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
testData := []byte("authorized blob")
sha256Hash := CalculateSHA256(testData)
// Test with valid authorization
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Valid auth failed: status %d, body: %s", w.Code, w.Body.String())
}
// Test with expired authorization
expiredAuthEv := createAuthEvent(t, signer, "upload", sha256Hash, -3600)
req2 := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req2.Header.Set("Authorization", createAuthHeader(expiredAuthEv))
w2 := httptest.NewRecorder()
server.Handler().ServeHTTP(w2, req2)
if w2.Code != http.StatusUnauthorized {
t.Errorf("Expired auth should fail: status %d", w2.Code)
}
// Test with wrong verb
wrongVerbAuthEv := createAuthEvent(t, signer, "delete", sha256Hash, 3600)
req3 := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req3.Header.Set("Authorization", createAuthHeader(wrongVerbAuthEv))
w3 := httptest.NewRecorder()
server.Handler().ServeHTTP(w3, req3)
if w3.Code != http.StatusUnauthorized {
t.Errorf("Wrong verb auth should fail: status %d", w3.Code)
}
}
// TestServerUploadRequirementsFlow tests upload requirements check flow
func TestServerUploadRequirementsFlow(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
testData := []byte("test")
sha256Hash := CalculateSHA256(testData)
// Test HEAD /upload with valid requirements
req := httptest.NewRequest("HEAD", "/upload", nil)
req.Header.Set("X-SHA-256", hex.Enc(sha256Hash))
req.Header.Set("X-Content-Length", "4")
req.Header.Set("X-Content-Type", "text/plain")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Upload requirements check failed: status %d", w.Code)
}
// Test HEAD /upload with missing header
req2 := httptest.NewRequest("HEAD", "/upload", nil)
w2 := httptest.NewRecorder()
server.Handler().ServeHTTP(w2, req2)
if w2.Code != http.StatusBadRequest {
t.Errorf("Expected BadRequest for missing header, got %d", w2.Code)
}
// Test HEAD /upload with invalid hash
req3 := httptest.NewRequest("HEAD", "/upload", nil)
req3.Header.Set("X-SHA-256", "invalid")
req3.Header.Set("X-Content-Length", "4")
w3 := httptest.NewRecorder()
server.Handler().ServeHTTP(w3, req3)
if w3.Code != http.StatusBadRequest {
t.Errorf("Expected BadRequest for invalid hash, got %d", w3.Code)
}
}
// TestServerMirrorFlow tests mirror endpoint flow
func TestServerMirrorFlow(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
// Create mock remote server
remoteData := []byte("remote blob data")
sha256Hash := CalculateSHA256(remoteData)
sha256Hex := hex.Enc(sha256Hash)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(remoteData)))
w.Write(remoteData)
}))
defer mockServer.Close()
// Mirror the blob
mirrorReq := map[string]string{
"url": mockServer.URL + "/" + sha256Hex,
}
reqBody, _ := json.Marshal(mirrorReq)
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/mirror", bytes.NewReader(reqBody))
req.Header.Set("Authorization", createAuthHeader(authEv))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Mirror failed: status %d, body: %s", w.Code, w.Body.String())
}
// Verify blob was stored
exists, err := server.storage.HasBlob(sha256Hash)
if err != nil {
t.Fatalf("Failed to check blob: %v", err)
}
if !exists {
t.Error("Blob should exist after mirror")
}
}
// TestServerReportFlow tests report endpoint flow
func TestServerReportFlow(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
// Upload a blob first
testData := []byte("reportable blob")
sha256Hash := CalculateSHA256(testData)
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
// Create report event
reportEv := &event.E{
CreatedAt: timestamp.Now().V,
Kind: 1984,
Tags: tag.NewS(tag.NewFromAny("x", hex.Enc(sha256Hash))),
Content: []byte("This blob should be reported"),
Pubkey: pubkey,
}
if err := reportEv.Sign(signer); err != nil {
t.Fatalf("Failed to sign report: %v", err)
}
reqBody := reportEv.Serialize()
req := httptest.NewRequest("PUT", "/report", bytes.NewReader(reqBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Report failed: status %d, body: %s", w.Code, w.Body.String())
}
}
// TestServerErrorHandling tests various error scenarios
func TestServerErrorHandling(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
tests := []struct {
name string
method string
path string
headers map[string]string
body []byte
statusCode int
}{
{
name: "Invalid path",
method: "GET",
path: "/invalid",
statusCode: http.StatusBadRequest,
},
{
name: "Non-existent blob",
method: "GET",
path: "/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
statusCode: http.StatusNotFound,
},
{
name: "Missing auth header",
method: "PUT",
path: "/upload",
body: []byte("test"),
statusCode: http.StatusUnauthorized,
},
{
name: "Invalid JSON in mirror",
method: "PUT",
path: "/mirror",
body: []byte("invalid json"),
statusCode: http.StatusBadRequest,
},
{
name: "Invalid JSON in report",
method: "PUT",
path: "/report",
body: []byte("invalid json"),
statusCode: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var body io.Reader
if tt.body != nil {
body = bytes.NewReader(tt.body)
}
req := httptest.NewRequest(tt.method, tt.path, body)
for k, v := range tt.headers {
req.Header.Set(k, v)
}
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != tt.statusCode {
t.Errorf("Expected status %d, got %d: %s", tt.statusCode, w.Code, w.Body.String())
}
})
}
}
// TestServerMediaOptimization tests media optimization endpoint
func TestServerMediaOptimization(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
testData := []byte("test media for optimization")
sha256Hash := CalculateSHA256(testData)
authEv := createAuthEvent(t, signer, "media", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/media", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
req.Header.Set("Content-Type", "image/png")
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Media upload failed: status %d, body: %s", w.Code, w.Body.String())
}
var desc BlobDescriptor
if err := json.Unmarshal(w.Body.Bytes(), &desc); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
if desc.SHA256 == "" {
t.Error("Expected SHA256 in response")
}
// Test HEAD /media
req2 := httptest.NewRequest("HEAD", "/media", nil)
w2 := httptest.NewRecorder()
server.Handler().ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Errorf("HEAD /media failed: status %d", w2.Code)
}
}
// TestServerListWithQueryParams tests list endpoint with query parameters
func TestServerListWithQueryParams(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
pubkeyHex := hex.Enc(pubkey)
// Upload blobs at different times
now := time.Now().Unix()
blobs := []struct {
data []byte
timestamp int64
}{
{[]byte("blob 1"), now - 1000},
{[]byte("blob 2"), now - 500},
{[]byte("blob 3"), now},
}
for _, b := range blobs {
sha256Hash := CalculateSHA256(b.data)
// Manually set uploaded timestamp
err := server.storage.SaveBlob(sha256Hash, b.data, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
}
// List with since parameter
authEv := createAuthEvent(t, signer, "list", nil, 3600)
req := httptest.NewRequest("GET", "/list/"+pubkeyHex+"?since="+fmt.Sprintf("%d", now-600), nil)
req.Header.Set("Authorization", createAuthHeader(authEv))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("List failed: status %d", w.Code)
}
var descriptors []BlobDescriptor
if err := json.NewDecoder(w.Body).Decode(&descriptors); err != nil {
t.Fatalf("Failed to parse response: %v", err)
}
// Should only get blobs uploaded after since timestamp
if len(descriptors) != 1 {
t.Errorf("Expected 1 blob, got %d", len(descriptors))
}
}
// TestServerConcurrentOperations tests concurrent operations on server
func TestServerConcurrentOperations(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
_, signer := createTestKeypair(t)
const numOps = 20
done := make(chan error, numOps)
for i := 0; i < numOps; i++ {
go func(id int) {
testData := []byte(fmt.Sprintf("concurrent op %d", id))
sha256Hash := CalculateSHA256(testData)
sha256Hex := hex.Enc(sha256Hash)
// Upload
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req, _ := http.NewRequest("PUT", httpServer.URL+"/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
resp, err := http.DefaultClient.Do(req)
if err != nil {
done <- err
return
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
done <- fmt.Errorf("upload failed: %d", resp.StatusCode)
return
}
// Get
req2, _ := http.NewRequest("GET", httpServer.URL+"/"+sha256Hex, nil)
resp2, err := http.DefaultClient.Do(req2)
if err != nil {
done <- err
return
}
resp2.Body.Close()
if resp2.StatusCode != http.StatusOK {
done <- fmt.Errorf("get failed: %d", resp2.StatusCode)
return
}
done <- nil
}(i)
}
for i := 0; i < numOps; i++ {
if err := <-done; err != nil {
t.Errorf("Concurrent operation failed: %v", err)
}
}
}
// TestServerBlobExtensionHandling tests blob retrieval with file extensions
func TestServerBlobExtensionHandling(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
testData := []byte("test PDF content")
sha256Hash := CalculateSHA256(testData)
pubkey := []byte("testpubkey123456789012345678901234")
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "application/pdf", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
sha256Hex := hex.Enc(sha256Hash)
// Test GET with extension
req := httptest.NewRequest("GET", "/"+sha256Hex+".pdf", nil)
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("GET with extension failed: status %d", w.Code)
}
// Should still return correct MIME type
if w.Header().Get("Content-Type") != "application/pdf" {
t.Errorf("Expected application/pdf, got %s", w.Header().Get("Content-Type"))
}
}
// TestServerBlobAlreadyExists tests uploading existing blob
func TestServerBlobAlreadyExists(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
pubkey := signer.Pub()
testData := []byte("existing blob")
sha256Hash := CalculateSHA256(testData)
// Upload blob first time
err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
if err != nil {
t.Fatalf("Failed to save blob: %v", err)
}
// Try to upload same blob again
authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(authEv))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
// Should succeed and return existing blob descriptor
if w.Code != http.StatusOK {
t.Errorf("Re-upload should succeed: status %d", w.Code)
}
}
// TestServerInvalidAuthorization tests various invalid authorization scenarios
func TestServerInvalidAuthorization(t *testing.T) {
server, cleanup := testSetup(t)
defer cleanup()
_, signer := createTestKeypair(t)
testData := []byte("test")
sha256Hash := CalculateSHA256(testData)
tests := []struct {
name string
modifyEv func(*event.E)
expectErr bool
}{
{
name: "Missing expiration",
modifyEv: func(ev *event.E) {
ev.Tags = tag.NewS(tag.NewFromAny("t", "upload"))
},
expectErr: true,
},
{
name: "Wrong kind",
modifyEv: func(ev *event.E) {
ev.Kind = 1
},
expectErr: true,
},
{
name: "Wrong verb",
modifyEv: func(ev *event.E) {
ev.Tags = tag.NewS(
tag.NewFromAny("t", "delete"),
tag.NewFromAny("expiration", timestamp.FromUnix(time.Now().Unix()+3600).String()),
)
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ev := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
tt.modifyEv(ev)
req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
req.Header.Set("Authorization", createAuthHeader(ev))
w := httptest.NewRecorder()
server.Handler().ServeHTTP(w, req)
if tt.expectErr {
if w.Code == http.StatusOK {
t.Error("Expected error but got success")
}
} else {
if w.Code != http.StatusOK {
t.Errorf("Expected success but got error: status %d", w.Code)
}
}
})
}
}

View File

@@ -14,19 +14,20 @@ type Server struct {
storage *Storage
acl *acl.S
baseURL string
// Configuration
maxBlobSize int64
maxBlobSize int64
allowedMimeTypes map[string]bool
requireAuth bool
requireAuth bool
}
// Config holds configuration for the Blossom server
type Config struct {
BaseURL string
MaxBlobSize int64
BaseURL string
MaxBlobSize int64
AllowedMimeTypes []string
RequireAuth bool
RequireAuth bool
BlobDir string // Directory for storing blob files
}
// NewServer creates a new Blossom server instance
@@ -38,8 +39,8 @@ func NewServer(db *database.D, aclRegistry *acl.S, cfg *Config) *Server {
}
}
storage := NewStorage(db)
storage := NewStorage(db, cfg.BlobDir)
// Build allowed MIME types map
allowedMap := make(map[string]bool)
if len(cfg.AllowedMimeTypes) > 0 {
@@ -49,13 +50,13 @@ func NewServer(db *database.D, aclRegistry *acl.S, cfg *Config) *Server {
}
return &Server{
db: db,
storage: storage,
acl: aclRegistry,
baseURL: cfg.BaseURL,
maxBlobSize: cfg.MaxBlobSize,
db: db,
storage: storage,
acl: aclRegistry,
baseURL: cfg.BaseURL,
maxBlobSize: cfg.MaxBlobSize,
allowedMimeTypes: allowedMap,
requireAuth: cfg.RequireAuth,
requireAuth: cfg.RequireAuth,
}
}
@@ -73,41 +74,41 @@ func (s *Server) Handler() http.Handler {
// Route based on path and method
path := r.URL.Path
// Remove leading slash
path = strings.TrimPrefix(path, "/")
// Handle specific endpoints
switch {
case r.Method == http.MethodGet && path == "upload":
// This shouldn't happen, but handle gracefully
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
case r.Method == http.MethodHead && path == "upload":
s.handleUploadRequirements(w, r)
return
case r.Method == http.MethodPut && path == "upload":
s.handleUpload(w, r)
return
case r.Method == http.MethodHead && path == "media":
s.handleMediaHead(w, r)
return
case r.Method == http.MethodPut && path == "media":
s.handleMediaUpload(w, r)
return
case r.Method == http.MethodPut && path == "mirror":
s.handleMirror(w, r)
return
case r.Method == http.MethodPut && path == "report":
s.handleReport(w, r)
return
case strings.HasPrefix(path, "list/"):
if r.Method == http.MethodGet {
s.handleListBlobs(w, r)
@@ -115,22 +116,22 @@ func (s *Server) Handler() http.Handler {
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
case r.Method == http.MethodGet:
// Handle GET /<sha256>
s.handleGetBlob(w, r)
return
case r.Method == http.MethodHead:
// Handle HEAD /<sha256>
s.handleHeadBlob(w, r)
return
case r.Method == http.MethodDelete:
// Handle DELETE /<sha256>
s.handleDeleteBlob(w, r)
return
default:
http.Error(w, "Not found", http.StatusNotFound)
return
@@ -163,12 +164,12 @@ func (s *Server) getRemoteAddr(r *http.Request) string {
return strings.TrimSpace(parts[0])
}
}
// Check X-Real-IP header
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
return realIP
}
// Fall back to RemoteAddr
return r.RemoteAddr
}
@@ -182,7 +183,7 @@ func (s *Server) checkACL(
}
level := s.acl.GetAccessLevel(pubkey, remoteAddr)
// Map ACL levels to permissions
levelMap := map[string]int{
"none": 0,
@@ -191,10 +192,9 @@ func (s *Server) checkACL(
"admin": 3,
"owner": 4,
}
required := levelMap[requiredLevel]
actual := levelMap[level]
return actual >= required
}

View File

@@ -2,6 +2,8 @@ package blossom
import (
"encoding/json"
"os"
"path/filepath"
"github.com/dgraph-io/badger/v4"
"lol.mleku.dev/chk"
@@ -14,8 +16,7 @@ import (
)
const (
// Database key prefixes
prefixBlobData = "blob:data:"
// Database key prefixes (metadata and indexes only, blob data stored as files)
prefixBlobMeta = "blob:meta:"
prefixBlobIndex = "blob:index:"
prefixBlobReport = "blob:report:"
@@ -23,17 +24,32 @@ const (
// Storage provides blob storage operations
type Storage struct {
db *database.D
db *database.D
blobDir string // Directory for storing blob files
}
// NewStorage creates a new storage instance
func NewStorage(db *database.D) *Storage {
return &Storage{db: db}
func NewStorage(db *database.D, blobDir string) *Storage {
// Ensure blob directory exists
if err := os.MkdirAll(blobDir, 0755); err != nil {
log.E.F("failed to create blob directory %s: %v", blobDir, err)
}
return &Storage{
db: db,
blobDir: blobDir,
}
}
// getBlobPath returns the filesystem path for a blob given its hash and extension
func (s *Storage) getBlobPath(sha256Hex string, ext string) string {
filename := sha256Hex + ext
return filepath.Join(s.blobDir, filename)
}
// SaveBlob stores a blob with its metadata
func (s *Storage) SaveBlob(
sha256Hash []byte, data []byte, pubkey []byte, mimeType string,
sha256Hash []byte, data []byte, pubkey []byte, mimeType string, extension string,
) (err error) {
sha256Hex := hex.Enc(sha256Hash)
@@ -47,20 +63,38 @@ func (s *Storage) SaveBlob(
return
}
// Create metadata
// If extension not provided, infer from MIME type
if extension == "" {
extension = GetExtensionFromMimeType(mimeType)
}
// Create metadata with extension
metadata := NewBlobMetadata(pubkey, mimeType, int64(len(data)))
metadata.Extension = extension
var metaData []byte
if metaData, err = metadata.Serialize(); chk.E(err) {
return
}
// Store blob data
dataKey := prefixBlobData + sha256Hex
if err = s.db.Update(func(txn *badger.Txn) error {
if err := txn.Set([]byte(dataKey), data); err != nil {
return err
}
// Get blob file path
blobPath := s.getBlobPath(sha256Hex, extension)
// Check if blob file already exists (deduplication)
if _, err = os.Stat(blobPath); err == nil {
// File exists, just update metadata and index
log.D.F("blob file already exists: %s", blobPath)
} else if !os.IsNotExist(err) {
return errorf.E("error checking blob file: %w", err)
} else {
// Write blob data to file
if err = os.WriteFile(blobPath, data, 0644); chk.E(err) {
return errorf.E("failed to write blob file: %w", err)
}
log.D.F("wrote blob file: %s (%d bytes)", blobPath, len(data))
}
// Store metadata and index in database
if err = s.db.Update(func(txn *badger.Txn) error {
// Store metadata
metaKey := prefixBlobMeta + sha256Hex
if err := txn.Set([]byte(metaKey), metaData); err != nil {
@@ -85,25 +119,8 @@ func (s *Storage) SaveBlob(
// GetBlob retrieves blob data by SHA256 hash
func (s *Storage) GetBlob(sha256Hash []byte) (data []byte, metadata *BlobMetadata, err error) {
sha256Hex := hex.Enc(sha256Hash)
dataKey := prefixBlobData + sha256Hex
var blobData []byte
if err = s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(dataKey))
if err != nil {
return err
}
return item.Value(func(val []byte) error {
blobData = make([]byte, len(val))
copy(blobData, val)
return nil
})
}); chk.E(err) {
return
}
// Get metadata
// Get metadata first to get extension
metaKey := prefixBlobMeta + sha256Hex
if err = s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(metaKey))
@@ -121,55 +138,96 @@ func (s *Storage) GetBlob(sha256Hash []byte) (data []byte, metadata *BlobMetadat
return
}
data = blobData
return
}
// HasBlob checks if a blob exists
func (s *Storage) HasBlob(sha256Hash []byte) (exists bool, err error) {
sha256Hex := hex.Enc(sha256Hash)
dataKey := prefixBlobData + sha256Hex
if err = s.db.View(func(txn *badger.Txn) error {
_, err := txn.Get([]byte(dataKey))
if err == badger.ErrKeyNotFound {
exists = false
return nil
// Read blob data from file
blobPath := s.getBlobPath(sha256Hex, metadata.Extension)
data, err = os.ReadFile(blobPath)
if err != nil {
if os.IsNotExist(err) {
err = badger.ErrKeyNotFound
}
if err != nil {
return err
}
exists = true
return nil
}); chk.E(err) {
return
}
return
}
// DeleteBlob deletes a blob and its metadata
func (s *Storage) DeleteBlob(sha256Hash []byte, pubkey []byte) (err error) {
// HasBlob checks if a blob exists
func (s *Storage) HasBlob(sha256Hash []byte) (exists bool, err error) {
sha256Hex := hex.Enc(sha256Hash)
dataKey := prefixBlobData + sha256Hex
metaKey := prefixBlobMeta + sha256Hex
indexKey := prefixBlobIndex + hex.Enc(pubkey) + ":" + sha256Hex
if err = s.db.Update(func(txn *badger.Txn) error {
// Verify blob exists
_, err := txn.Get([]byte(dataKey))
// Get metadata to find extension
metaKey := prefixBlobMeta + sha256Hex
var metadata *BlobMetadata
if err = s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(metaKey))
if err == badger.ErrKeyNotFound {
return errorf.E("blob %s not found", sha256Hex)
return badger.ErrKeyNotFound
}
if err != nil {
return err
}
// Delete blob data
if err := txn.Delete([]byte(dataKey)); err != nil {
return item.Value(func(val []byte) error {
if metadata, err = DeserializeBlobMetadata(val); err != nil {
return err
}
return nil
})
}); err == badger.ErrKeyNotFound {
exists = false
return false, nil
}
if err != nil {
return
}
// Check if file exists
blobPath := s.getBlobPath(sha256Hex, metadata.Extension)
if _, err = os.Stat(blobPath); err == nil {
exists = true
return
}
if os.IsNotExist(err) {
exists = false
err = nil
return
}
return
}
// DeleteBlob deletes a blob and its metadata
func (s *Storage) DeleteBlob(sha256Hash []byte, pubkey []byte) (err error) {
sha256Hex := hex.Enc(sha256Hash)
// Get metadata to find extension
metaKey := prefixBlobMeta + sha256Hex
var metadata *BlobMetadata
if err = s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(metaKey))
if err == badger.ErrKeyNotFound {
return badger.ErrKeyNotFound
}
if err != nil {
return err
}
return item.Value(func(val []byte) error {
if metadata, err = DeserializeBlobMetadata(val); err != nil {
return err
}
return nil
})
}); err == badger.ErrKeyNotFound {
return errorf.E("blob %s not found", sha256Hex)
}
if err != nil {
return
}
blobPath := s.getBlobPath(sha256Hex, metadata.Extension)
indexKey := prefixBlobIndex + hex.Enc(pubkey) + ":" + sha256Hex
if err = s.db.Update(func(txn *badger.Txn) error {
// Delete metadata
if err := txn.Delete([]byte(metaKey)); err != nil {
return err
@@ -185,6 +243,12 @@ func (s *Storage) DeleteBlob(sha256Hash []byte, pubkey []byte) (err error) {
return
}
// Delete blob file
if err = os.Remove(blobPath); err != nil && !os.IsNotExist(err) {
log.E.F("failed to delete blob file %s: %v", blobPath, err)
// Don't fail if file doesn't exist
}
log.D.F("deleted blob %s for pubkey %s", sha256Hex, hex.Enc(pubkey))
return
}
@@ -236,10 +300,9 @@ func (s *Storage) ListBlobs(
continue
}
// Verify blob exists
dataKey := prefixBlobData + sha256Hex
_, errGet := txn.Get([]byte(dataKey))
if errGet != nil {
// Verify blob file exists
blobPath := s.getBlobPath(sha256Hex, metadata.Extension)
if _, errGet := os.Stat(blobPath); errGet != nil {
continue
}

View File

@@ -248,3 +248,35 @@ func GetFileExtensionFromPath(path string) string {
return ext
}
// GetExtensionFromMimeType returns file extension based on MIME type
func GetExtensionFromMimeType(mimeType string) string {
// Reverse lookup of GetMimeTypeFromExtension
mimeToExt := map[string]string{
"application/pdf": ".pdf",
"image/png": ".png",
"image/jpeg": ".jpg",
"image/gif": ".gif",
"image/webp": ".webp",
"image/svg+xml": ".svg",
"video/mp4": ".mp4",
"video/webm": ".webm",
"audio/mpeg": ".mp3",
"audio/wav": ".wav",
"audio/ogg": ".ogg",
"text/plain": ".txt",
"text/html": ".html",
"text/css": ".css",
"application/javascript": ".js",
"application/json": ".json",
"application/xml": ".xml",
"application/zip": ".zip",
"application/x-tar": ".tar",
"application/gzip": ".gz",
}
if ext, ok := mimeToExt[mimeType]; ok {
return ext
}
return "" // No extension for unknown MIME types
}

389
pkg/blossom/utils_test.go Normal file
View 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")
}
}