Browse Source
- 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.main
8 changed files with 2226 additions and 134 deletions
@ -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}, " ") |
||||||
|
} |
||||||
|
|
||||||
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
@ -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") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
Loading…
Reference in new issue