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:
@@ -21,6 +21,7 @@ type BlobMetadata struct {
|
||||
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
|
||||
@@ -49,6 +50,7 @@ func NewBlobMetadata(pubkey []byte, mimeType string, size int64) *BlobMetadata {
|
||||
MimeType: mimeType,
|
||||
Uploaded: time.Now().Unix(),
|
||||
Size: size,
|
||||
Extension: "", // Will be set by SaveBlob
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -220,6 +220,12 @@ 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,
|
||||
@@ -229,7 +235,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 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
|
||||
@@ -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(
|
||||
@@ -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(
|
||||
@@ -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(
|
||||
@@ -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
756
pkg/blossom/http_test.go
Normal 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}, " ")
|
||||
}
|
||||
|
||||
852
pkg/blossom/integration_test.go
Normal file
852
pkg/blossom/integration_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type Config struct {
|
||||
MaxBlobSize int64
|
||||
AllowedMimeTypes []string
|
||||
RequireAuth bool
|
||||
BlobDir string // Directory for storing blob files
|
||||
}
|
||||
|
||||
// NewServer creates a new Blossom server instance
|
||||
@@ -38,7 +39,7 @@ 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)
|
||||
@@ -197,4 +198,3 @@ func (s *Server) checkACL(
|
||||
|
||||
return actual >= required
|
||||
}
|
||||
|
||||
|
||||
@@ -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:"
|
||||
@@ -24,16 +25,31 @@ const (
|
||||
// Storage provides blob storage operations
|
||||
type Storage struct {
|
||||
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
|
||||
// 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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// HasBlob checks if a blob exists
|
||||
func (s *Storage) HasBlob(sha256Hash []byte) (exists bool, err error) {
|
||||
sha256Hex := hex.Enc(sha256Hash)
|
||||
dataKey := prefixBlobData + sha256Hex
|
||||
|
||||
// Get metadata to find extension
|
||||
metaKey := prefixBlobMeta + sha256Hex
|
||||
var metadata *BlobMetadata
|
||||
if err = s.db.View(func(txn *badger.Txn) error {
|
||||
_, err := txn.Get([]byte(dataKey))
|
||||
item, err := txn.Get([]byte(metaKey))
|
||||
if err == badger.ErrKeyNotFound {
|
||||
exists = false
|
||||
return nil
|
||||
return badger.ErrKeyNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exists = true
|
||||
|
||||
return item.Value(func(val []byte) error {
|
||||
if metadata, err = DeserializeBlobMetadata(val); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); chk.E(err) {
|
||||
})
|
||||
}); 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)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
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