http_test.go raw

   1  package blossom
   2  
   3  import (
   4  	"bytes"
   5  	"encoding/json"
   6  	"io"
   7  	"net/http"
   8  	"net/http/httptest"
   9  	"strings"
  10  	"testing"
  11  
  12  	"next.orly.dev/pkg/nostr/encoders/event"
  13  	"next.orly.dev/pkg/nostr/encoders/hex"
  14  	"next.orly.dev/pkg/nostr/encoders/tag"
  15  	"next.orly.dev/pkg/nostr/encoders/timestamp"
  16  )
  17  
  18  // TestHTTPGetBlob tests GET /<sha256> endpoint
  19  func TestHTTPGetBlob(t *testing.T) {
  20  	server, cleanup := testSetup(t)
  21  	defer cleanup()
  22  
  23  	// Upload a blob first
  24  	testData := []byte("test blob content")
  25  	sha256Hash := CalculateSHA256(testData)
  26  	pubkey := []byte("testpubkey123456789012345678901234")
  27  
  28  	err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
  29  	if err != nil {
  30  		t.Fatalf("Failed to save blob: %v", err)
  31  	}
  32  
  33  	sha256Hex := hex.Enc(sha256Hash)
  34  
  35  	// Test GET request
  36  	req := httptest.NewRequest("GET", "/"+sha256Hex, nil)
  37  	w := httptest.NewRecorder()
  38  	server.Handler().ServeHTTP(w, req)
  39  
  40  	if w.Code != http.StatusOK {
  41  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
  42  	}
  43  
  44  	body := w.Body.Bytes()
  45  	if !bytes.Equal(body, testData) {
  46  		t.Error("Response body mismatch")
  47  	}
  48  
  49  	if w.Header().Get("Content-Type") != "text/plain" {
  50  		t.Errorf("Expected Content-Type text/plain, got %s", w.Header().Get("Content-Type"))
  51  	}
  52  }
  53  
  54  // TestHTTPHeadBlob tests HEAD /<sha256> endpoint
  55  func TestHTTPHeadBlob(t *testing.T) {
  56  	server, cleanup := testSetup(t)
  57  	defer cleanup()
  58  
  59  	testData := []byte("test blob content")
  60  	sha256Hash := CalculateSHA256(testData)
  61  	pubkey := []byte("testpubkey123456789012345678901234")
  62  
  63  	err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
  64  	if err != nil {
  65  		t.Fatalf("Failed to save blob: %v", err)
  66  	}
  67  
  68  	sha256Hex := hex.Enc(sha256Hash)
  69  
  70  	req := httptest.NewRequest("HEAD", "/"+sha256Hex, nil)
  71  	w := httptest.NewRecorder()
  72  	server.Handler().ServeHTTP(w, req)
  73  
  74  	if w.Code != http.StatusOK {
  75  		t.Errorf("Expected status 200, got %d", w.Code)
  76  	}
  77  
  78  	if w.Body.Len() != 0 {
  79  		t.Error("HEAD request should not return body")
  80  	}
  81  
  82  	if w.Header().Get("Content-Length") != "17" {
  83  		t.Errorf("Expected Content-Length 17, got %s", w.Header().Get("Content-Length"))
  84  	}
  85  }
  86  
  87  // TestHTTPUpload tests PUT /upload endpoint
  88  func TestHTTPUpload(t *testing.T) {
  89  	server, cleanup := testSetup(t)
  90  	defer cleanup()
  91  
  92  	_, signer := createTestKeypair(t)
  93  
  94  	testData := []byte("test upload data")
  95  	sha256Hash := CalculateSHA256(testData)
  96  
  97  	// Create auth event
  98  	authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
  99  
 100  	// Create request
 101  	req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
 102  	req.Header.Set("Authorization", createAuthHeader(authEv))
 103  	req.Header.Set("Content-Type", "text/plain")
 104  
 105  	w := httptest.NewRecorder()
 106  	server.Handler().ServeHTTP(w, req)
 107  
 108  	if w.Code != http.StatusOK {
 109  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
 110  	}
 111  
 112  	// Parse response
 113  	var desc BlobDescriptor
 114  	if err := json.Unmarshal(w.Body.Bytes(), &desc); err != nil {
 115  		t.Fatalf("Failed to parse response: %v", err)
 116  	}
 117  
 118  	if desc.SHA256 != hex.Enc(sha256Hash) {
 119  		t.Errorf("SHA256 mismatch: expected %s, got %s", hex.Enc(sha256Hash), desc.SHA256)
 120  	}
 121  
 122  	if desc.Size != int64(len(testData)) {
 123  		t.Errorf("Size mismatch: expected %d, got %d", len(testData), desc.Size)
 124  	}
 125  
 126  	// Verify blob was saved
 127  	exists, err := server.storage.HasBlob(sha256Hash)
 128  	if err != nil {
 129  		t.Fatalf("Failed to check blob: %v", err)
 130  	}
 131  	if !exists {
 132  		t.Error("Blob should exist after upload")
 133  	}
 134  }
 135  
 136  // TestHTTPUploadRequirements tests HEAD /upload endpoint
 137  func TestHTTPUploadRequirements(t *testing.T) {
 138  	server, cleanup := testSetup(t)
 139  	defer cleanup()
 140  
 141  	testData := []byte("test data")
 142  	sha256Hash := CalculateSHA256(testData)
 143  
 144  	req := httptest.NewRequest("HEAD", "/upload", nil)
 145  	req.Header.Set("X-SHA-256", hex.Enc(sha256Hash))
 146  	req.Header.Set("X-Content-Length", "9")
 147  	req.Header.Set("X-Content-Type", "text/plain")
 148  
 149  	w := httptest.NewRecorder()
 150  	server.Handler().ServeHTTP(w, req)
 151  
 152  	if w.Code != http.StatusOK {
 153  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Header().Get("X-Reason"))
 154  	}
 155  }
 156  
 157  // TestHTTPUploadTooLarge tests upload size limit
 158  func TestHTTPUploadTooLarge(t *testing.T) {
 159  	server, cleanup := testSetup(t)
 160  	defer cleanup()
 161  
 162  	// Create request with size exceeding limit
 163  	req := httptest.NewRequest("HEAD", "/upload", nil)
 164  	req.Header.Set("X-SHA-256", hex.Enc(CalculateSHA256([]byte("test"))))
 165  	req.Header.Set("X-Content-Length", "200000000") // 200MB
 166  	req.Header.Set("X-Content-Type", "application/octet-stream")
 167  
 168  	w := httptest.NewRecorder()
 169  	server.Handler().ServeHTTP(w, req)
 170  
 171  	if w.Code != http.StatusRequestEntityTooLarge {
 172  		t.Errorf("Expected status 413, got %d", w.Code)
 173  	}
 174  }
 175  
 176  // TestHTTPListBlobs tests GET /list/<pubkey> endpoint
 177  func TestHTTPListBlobs(t *testing.T) {
 178  	server, cleanup := testSetup(t)
 179  	defer cleanup()
 180  
 181  	_, signer := createTestKeypair(t)
 182  	pubkey := signer.Pub()
 183  	pubkeyHex := hex.Enc(pubkey)
 184  
 185  	// Upload multiple blobs
 186  	for i := 0; i < 3; i++ {
 187  		testData := []byte("test data " + string(rune('A'+i)))
 188  		sha256Hash := CalculateSHA256(testData)
 189  		err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
 190  		if err != nil {
 191  			t.Fatalf("Failed to save blob: %v", err)
 192  		}
 193  	}
 194  
 195  	// Create auth event
 196  	authEv := createAuthEvent(t, signer, "list", nil, 3600)
 197  
 198  	req := httptest.NewRequest("GET", "/list/"+pubkeyHex, nil)
 199  	req.Header.Set("Authorization", createAuthHeader(authEv))
 200  
 201  	w := httptest.NewRecorder()
 202  	server.Handler().ServeHTTP(w, req)
 203  
 204  	if w.Code != http.StatusOK {
 205  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
 206  	}
 207  
 208  	var descriptors []BlobDescriptor
 209  	if err := json.Unmarshal(w.Body.Bytes(), &descriptors); err != nil {
 210  		t.Fatalf("Failed to parse response: %v", err)
 211  	}
 212  
 213  	if len(descriptors) != 3 {
 214  		t.Errorf("Expected 3 blobs, got %d", len(descriptors))
 215  	}
 216  }
 217  
 218  // TestHTTPDeleteBlob tests DELETE /<sha256> endpoint
 219  func TestHTTPDeleteBlob(t *testing.T) {
 220  	server, cleanup := testSetup(t)
 221  	defer cleanup()
 222  
 223  	_, signer := createTestKeypair(t)
 224  	pubkey := signer.Pub()
 225  
 226  	testData := []byte("test delete data")
 227  	sha256Hash := CalculateSHA256(testData)
 228  
 229  	// Upload blob first
 230  	err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
 231  	if err != nil {
 232  		t.Fatalf("Failed to save blob: %v", err)
 233  	}
 234  
 235  	// Create auth event
 236  	authEv := createAuthEvent(t, signer, "delete", sha256Hash, 3600)
 237  
 238  	sha256Hex := hex.Enc(sha256Hash)
 239  	req := httptest.NewRequest("DELETE", "/"+sha256Hex, nil)
 240  	req.Header.Set("Authorization", createAuthHeader(authEv))
 241  
 242  	w := httptest.NewRecorder()
 243  	server.Handler().ServeHTTP(w, req)
 244  
 245  	if w.Code != http.StatusOK {
 246  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
 247  	}
 248  
 249  	// Verify blob was deleted
 250  	exists, err := server.storage.HasBlob(sha256Hash)
 251  	if err != nil {
 252  		t.Fatalf("Failed to check blob: %v", err)
 253  	}
 254  	if exists {
 255  		t.Error("Blob should not exist after delete")
 256  	}
 257  }
 258  
 259  // TestHTTPMirror tests PUT /mirror endpoint
 260  func TestHTTPMirror(t *testing.T) {
 261  	server, cleanup := testSetup(t)
 262  	defer cleanup()
 263  
 264  	_, signer := createTestKeypair(t)
 265  
 266  	// Create a mock remote server
 267  	testData := []byte("mirrored blob data")
 268  	sha256Hash := CalculateSHA256(testData)
 269  	sha256Hex := hex.Enc(sha256Hash)
 270  
 271  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 272  		w.Header().Set("Content-Type", "text/plain")
 273  		w.Write(testData)
 274  	}))
 275  	defer mockServer.Close()
 276  
 277  	// Create mirror request
 278  	mirrorReq := map[string]string{
 279  		"url": mockServer.URL + "/" + sha256Hex,
 280  	}
 281  	reqBody, _ := json.Marshal(mirrorReq)
 282  
 283  	authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
 284  
 285  	req := httptest.NewRequest("PUT", "/mirror", bytes.NewReader(reqBody))
 286  	req.Header.Set("Authorization", createAuthHeader(authEv))
 287  	req.Header.Set("Content-Type", "application/json")
 288  
 289  	w := httptest.NewRecorder()
 290  	server.Handler().ServeHTTP(w, req)
 291  
 292  	if w.Code != http.StatusOK {
 293  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
 294  	}
 295  
 296  	// Verify blob was saved
 297  	exists, err := server.storage.HasBlob(sha256Hash)
 298  	if err != nil {
 299  		t.Fatalf("Failed to check blob: %v", err)
 300  	}
 301  	if !exists {
 302  		t.Error("Blob should exist after mirror")
 303  	}
 304  }
 305  
 306  // TestHTTPMediaUpload tests PUT /media endpoint
 307  func TestHTTPMediaUpload(t *testing.T) {
 308  	server, cleanup := testSetup(t)
 309  	defer cleanup()
 310  
 311  	_, signer := createTestKeypair(t)
 312  
 313  	testData := []byte("test media data")
 314  	sha256Hash := CalculateSHA256(testData)
 315  
 316  	authEv := createAuthEvent(t, signer, "media", sha256Hash, 3600)
 317  
 318  	req := httptest.NewRequest("PUT", "/media", bytes.NewReader(testData))
 319  	req.Header.Set("Authorization", createAuthHeader(authEv))
 320  	req.Header.Set("Content-Type", "image/png")
 321  
 322  	w := httptest.NewRecorder()
 323  	server.Handler().ServeHTTP(w, req)
 324  
 325  	if w.Code != http.StatusOK {
 326  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
 327  	}
 328  
 329  	var desc BlobDescriptor
 330  	if err := json.Unmarshal(w.Body.Bytes(), &desc); err != nil {
 331  		t.Fatalf("Failed to parse response: %v", err)
 332  	}
 333  
 334  	if desc.SHA256 == "" {
 335  		t.Error("Expected SHA256 in response")
 336  	}
 337  }
 338  
 339  // TestHTTPReport tests PUT /report endpoint
 340  func TestHTTPReport(t *testing.T) {
 341  	server, cleanup := testSetup(t)
 342  	defer cleanup()
 343  
 344  	_, signer := createTestKeypair(t)
 345  	pubkey := signer.Pub()
 346  
 347  	// Upload a blob first
 348  	testData := []byte("test blob")
 349  	sha256Hash := CalculateSHA256(testData)
 350  
 351  	err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
 352  	if err != nil {
 353  		t.Fatalf("Failed to save blob: %v", err)
 354  	}
 355  
 356  	// Create report event (kind 1984)
 357  	reportEv := &event.E{
 358  		CreatedAt: timestamp.Now().V,
 359  		Kind:      1984,
 360  		Tags:      tag.NewS(tag.NewFromAny("x", hex.Enc(sha256Hash))),
 361  		Content:   []byte("This blob violates policy"),
 362  		Pubkey:    pubkey,
 363  	}
 364  
 365  	if err := reportEv.Sign(signer); err != nil {
 366  		t.Fatalf("Failed to sign report: %v", err)
 367  	}
 368  
 369  	reqBody := reportEv.Serialize()
 370  	req := httptest.NewRequest("PUT", "/report", bytes.NewReader(reqBody))
 371  	req.Header.Set("Content-Type", "application/json")
 372  
 373  	w := httptest.NewRecorder()
 374  	server.Handler().ServeHTTP(w, req)
 375  
 376  	if w.Code != http.StatusOK {
 377  		t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
 378  	}
 379  }
 380  
 381  // TestHTTPRangeRequest tests range request support
 382  func TestHTTPRangeRequest(t *testing.T) {
 383  	server, cleanup := testSetup(t)
 384  	defer cleanup()
 385  
 386  	testData := []byte("0123456789abcdef")
 387  	sha256Hash := CalculateSHA256(testData)
 388  	pubkey := []byte("testpubkey123456789012345678901234")
 389  
 390  	err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
 391  	if err != nil {
 392  		t.Fatalf("Failed to save blob: %v", err)
 393  	}
 394  
 395  	sha256Hex := hex.Enc(sha256Hash)
 396  
 397  	// Test range request
 398  	req := httptest.NewRequest("GET", "/"+sha256Hex, nil)
 399  	req.Header.Set("Range", "bytes=4-9")
 400  
 401  	w := httptest.NewRecorder()
 402  	server.Handler().ServeHTTP(w, req)
 403  
 404  	if w.Code != http.StatusPartialContent {
 405  		t.Errorf("Expected status 206, got %d", w.Code)
 406  	}
 407  
 408  	body := w.Body.Bytes()
 409  	expected := testData[4:10]
 410  	if !bytes.Equal(body, expected) {
 411  		t.Errorf("Range response mismatch: expected %s, got %s", string(expected), string(body))
 412  	}
 413  
 414  	if w.Header().Get("Content-Range") == "" {
 415  		t.Error("Missing Content-Range header")
 416  	}
 417  }
 418  
 419  // TestHTTPNotFound tests 404 handling
 420  func TestHTTPNotFound(t *testing.T) {
 421  	server, cleanup := testSetup(t)
 422  	defer cleanup()
 423  
 424  	req := httptest.NewRequest("GET", "/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", nil)
 425  	w := httptest.NewRecorder()
 426  	server.Handler().ServeHTTP(w, req)
 427  
 428  	if w.Code != http.StatusNotFound {
 429  		t.Errorf("Expected status 404, got %d", w.Code)
 430  	}
 431  }
 432  
 433  // TestHTTPServerIntegration tests full server integration
 434  func TestHTTPServerIntegration(t *testing.T) {
 435  	server, cleanup := testSetup(t)
 436  	defer cleanup()
 437  
 438  	// Start HTTP server
 439  	httpServer := httptest.NewServer(server.Handler())
 440  	defer httpServer.Close()
 441  
 442  	_, signer := createTestKeypair(t)
 443  
 444  	// Upload blob via HTTP
 445  	testData := []byte("integration test data")
 446  	sha256Hash := CalculateSHA256(testData)
 447  	sha256Hex := hex.Enc(sha256Hash)
 448  
 449  	authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
 450  
 451  	uploadReq, _ := http.NewRequest("PUT", httpServer.URL+"/upload", bytes.NewReader(testData))
 452  	uploadReq.Header.Set("Authorization", createAuthHeader(authEv))
 453  	uploadReq.Header.Set("Content-Type", "text/plain")
 454  
 455  	client := &http.Client{}
 456  	resp, err := client.Do(uploadReq)
 457  	if err != nil {
 458  		t.Fatalf("Failed to upload: %v", err)
 459  	}
 460  	defer resp.Body.Close()
 461  
 462  	if resp.StatusCode != http.StatusOK {
 463  		body, _ := io.ReadAll(resp.Body)
 464  		t.Fatalf("Upload failed: status %d, body: %s", resp.StatusCode, string(body))
 465  	}
 466  
 467  	// Retrieve blob via HTTP
 468  	getReq, _ := http.NewRequest("GET", httpServer.URL+"/"+sha256Hex, nil)
 469  	getResp, err := client.Do(getReq)
 470  	if err != nil {
 471  		t.Fatalf("Failed to get blob: %v", err)
 472  	}
 473  	defer getResp.Body.Close()
 474  
 475  	if getResp.StatusCode != http.StatusOK {
 476  		t.Fatalf("Get failed: status %d", getResp.StatusCode)
 477  	}
 478  
 479  	body, _ := io.ReadAll(getResp.Body)
 480  	if !bytes.Equal(body, testData) {
 481  		t.Error("Retrieved blob data mismatch")
 482  	}
 483  }
 484  
 485  // TestCORSHeaders tests CORS header handling
 486  func TestCORSHeaders(t *testing.T) {
 487  	server, cleanup := testSetup(t)
 488  	defer cleanup()
 489  
 490  	req := httptest.NewRequest("GET", "/test", nil)
 491  	w := httptest.NewRecorder()
 492  
 493  	server.Handler().ServeHTTP(w, req)
 494  
 495  	if w.Header().Get("Access-Control-Allow-Origin") != "*" {
 496  		t.Error("Missing CORS header")
 497  	}
 498  }
 499  
 500  // TestAuthorizationRequired tests authorization requirement
 501  func TestAuthorizationRequired(t *testing.T) {
 502  	server, cleanup := testSetup(t)
 503  	defer cleanup()
 504  
 505  	// Configure server to require auth
 506  	server.requireAuth = true
 507  
 508  	testData := []byte("test")
 509  	sha256Hash := CalculateSHA256(testData)
 510  	pubkey := []byte("testpubkey123456789012345678901234")
 511  
 512  	err := server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
 513  	if err != nil {
 514  		t.Fatalf("Failed to save blob: %v", err)
 515  	}
 516  
 517  	sha256Hex := hex.Enc(sha256Hash)
 518  
 519  	// Request without auth should fail
 520  	req := httptest.NewRequest("GET", "/"+sha256Hex, nil)
 521  	w := httptest.NewRecorder()
 522  	server.Handler().ServeHTTP(w, req)
 523  
 524  	if w.Code != http.StatusUnauthorized {
 525  		t.Errorf("Expected status 401, got %d", w.Code)
 526  	}
 527  }
 528  
 529  // TestACLIntegration tests ACL integration
 530  func TestACLIntegration(t *testing.T) {
 531  	server, cleanup := testSetup(t)
 532  	defer cleanup()
 533  
 534  	// Note: This test assumes ACL is configured
 535  	// In a real scenario, you'd set up a proper ACL instance
 536  
 537  	_, signer := createTestKeypair(t)
 538  	testData := []byte("test")
 539  	sha256Hash := CalculateSHA256(testData)
 540  
 541  	authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
 542  
 543  	req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(testData))
 544  	req.Header.Set("Authorization", createAuthHeader(authEv))
 545  
 546  	w := httptest.NewRecorder()
 547  	server.Handler().ServeHTTP(w, req)
 548  
 549  	// Should succeed if ACL allows, or fail if not
 550  	// The exact behavior depends on ACL configuration
 551  	if w.Code != http.StatusOK && w.Code != http.StatusForbidden {
 552  		t.Errorf("Unexpected status: %d", w.Code)
 553  	}
 554  }
 555  
 556  // TestMimeTypeDetection tests MIME type detection from various sources
 557  func TestMimeTypeDetection(t *testing.T) {
 558  	tests := []struct {
 559  		contentType string
 560  		ext         string
 561  		expected    string
 562  	}{
 563  		{"image/png", "", "image/png"},
 564  		{"", ".png", "image/png"},
 565  		{"", ".pdf", "application/pdf"},
 566  		{"application/pdf", ".txt", "application/pdf"},
 567  		{"", ".unknown", "application/octet-stream"},
 568  		{"", "", "application/octet-stream"},
 569  	}
 570  
 571  	for _, tt := range tests {
 572  		result := DetectMimeType(tt.contentType, tt.ext)
 573  		if result != tt.expected {
 574  			t.Errorf("DetectMimeType(%q, %q) = %q, want %q",
 575  				tt.contentType, tt.ext, result, tt.expected)
 576  		}
 577  	}
 578  }
 579  
 580  // TestSHA256Validation tests SHA256 validation
 581  func TestSHA256Validation(t *testing.T) {
 582  	validHashes := []string{
 583  		"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
 584  		"abc123def456789012345678901234567890123456789012345678901234abcd",
 585  	}
 586  
 587  	invalidHashes := []string{
 588  		"",
 589  		"abc",
 590  		"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855x",
 591  		"12345",
 592  	}
 593  
 594  	for _, hash := range validHashes {
 595  		if !ValidateSHA256Hex(hash) {
 596  			t.Errorf("Hash %s should be valid", hash)
 597  		}
 598  	}
 599  
 600  	for _, hash := range invalidHashes {
 601  		if ValidateSHA256Hex(hash) {
 602  			t.Errorf("Hash %s should be invalid", hash)
 603  		}
 604  	}
 605  }
 606  
 607  // TestBlobURLBuilding tests URL building
 608  func TestBlobURLBuilding(t *testing.T) {
 609  	baseURL := "https://example.com"
 610  	sha256Hex := "abc123def456"
 611  	ext := ".pdf"
 612  
 613  	url := BuildBlobURL(baseURL, sha256Hex, ext)
 614  	expected := baseURL + "/" + sha256Hex + ext
 615  
 616  	if url != expected {
 617  		t.Errorf("Expected %s, got %s", expected, url)
 618  	}
 619  
 620  	// Test without extension
 621  	url2 := BuildBlobURL(baseURL, sha256Hex, "")
 622  	expected2 := baseURL + "/" + sha256Hex
 623  
 624  	if url2 != expected2 {
 625  		t.Errorf("Expected %s, got %s", expected2, url2)
 626  	}
 627  }
 628  
 629  // TestErrorResponses tests error response formatting
 630  func TestErrorResponses(t *testing.T) {
 631  	server, cleanup := testSetup(t)
 632  	defer cleanup()
 633  
 634  	w := httptest.NewRecorder()
 635  
 636  	server.setErrorResponse(w, http.StatusBadRequest, "Invalid request")
 637  
 638  	if w.Code != http.StatusBadRequest {
 639  		t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
 640  	}
 641  
 642  	if w.Header().Get("X-Reason") == "" {
 643  		t.Error("Missing X-Reason header")
 644  	}
 645  }
 646  
 647  // TestExtractSHA256FromURL tests URL hash extraction
 648  func TestExtractSHA256FromURL(t *testing.T) {
 649  	tests := []struct {
 650  		url      string
 651  		expected string
 652  		hasError bool
 653  	}{
 654  		{"https://example.com/abc123def456789012345678901234567890123456789012345678901234abcd", "abc123def456789012345678901234567890123456789012345678901234abcd", false},
 655  		{"https://example.com/user/path/abc123def456789012345678901234567890123456789012345678901234abcd.pdf", "abc123def456789012345678901234567890123456789012345678901234abcd", false},
 656  		{"https://example.com/", "", true},
 657  		{"no hash here", "", true},
 658  	}
 659  
 660  	for _, tt := range tests {
 661  		hash, err := ExtractSHA256FromURL(tt.url)
 662  		if tt.hasError {
 663  			if err == nil {
 664  				t.Errorf("Expected error for URL %s", tt.url)
 665  			}
 666  		} else {
 667  			if err != nil {
 668  				t.Errorf("Unexpected error for URL %s: %v", tt.url, err)
 669  			}
 670  			if hash != tt.expected {
 671  				t.Errorf("Expected %s, got %s for URL %s", tt.expected, hash, tt.url)
 672  			}
 673  		}
 674  	}
 675  }
 676  
 677  // TestStorageReport tests report storage
 678  func TestStorageReport(t *testing.T) {
 679  	server, cleanup := testSetup(t)
 680  	defer cleanup()
 681  
 682  	sha256Hash := CalculateSHA256([]byte("test"))
 683  	reportData := []byte("report data")
 684  
 685  	err := server.storage.SaveReport(sha256Hash, reportData)
 686  	if err != nil {
 687  		t.Fatalf("Failed to save report: %v", err)
 688  	}
 689  
 690  	// Reports are stored but not retrieved in current implementation
 691  	// This test verifies the operation doesn't fail
 692  }
 693  
 694  // BenchmarkStorageOperations benchmarks storage operations
 695  func BenchmarkStorageOperations(b *testing.B) {
 696  	server, cleanup := testSetup(&testing.T{})
 697  	defer cleanup()
 698  
 699  	testData := []byte("benchmark test data")
 700  	sha256Hash := CalculateSHA256(testData)
 701  	pubkey := []byte("testpubkey123456789012345678901234")
 702  
 703  	b.ResetTimer()
 704  	for i := 0; i < b.N; i++ {
 705  		_ = server.storage.SaveBlob(sha256Hash, testData, pubkey, "text/plain", "")
 706  		_, _, _ = server.storage.GetBlob(sha256Hash)
 707  		_ = server.storage.DeleteBlob(sha256Hash, pubkey)
 708  	}
 709  }
 710  
 711  // TestConcurrentUploads tests concurrent uploads
 712  func TestConcurrentUploads(t *testing.T) {
 713  	server, cleanup := testSetup(t)
 714  	defer cleanup()
 715  
 716  	// Prepare all auth events sequentially to avoid race in p256k1.TaggedHash
 717  	// (the package-level SHA256 hasher is not safe for concurrent Sign() calls).
 718  	// The test is about concurrent HTTP uploads, not concurrent signing.
 719  	const numUploads = 10
 720  	type uploadPrep struct {
 721  		data    []byte
 722  		authHdr string
 723  	}
 724  	preps := make([]uploadPrep, numUploads)
 725  	for i := range preps {
 726  		_, signer := createTestKeypair(t)
 727  		data := []byte("concurrent test " + string(rune('A'+i)))
 728  		sha256Hash := CalculateSHA256(data)
 729  		authEv := createAuthEvent(t, signer, "upload", sha256Hash, 3600)
 730  		preps[i] = uploadPrep{data: data, authHdr: createAuthHeader(authEv)}
 731  	}
 732  
 733  	done := make(chan error, numUploads)
 734  
 735  	for i := 0; i < numUploads; i++ {
 736  		go func(id int) {
 737  			req := httptest.NewRequest("PUT", "/upload", bytes.NewReader(preps[id].data))
 738  			req.Header.Set("Authorization", preps[id].authHdr)
 739  
 740  			w := httptest.NewRecorder()
 741  			server.Handler().ServeHTTP(w, req)
 742  
 743  			if w.Code != http.StatusOK {
 744  				done <- &testError{code: w.Code, body: w.Body.String()}
 745  				return
 746  			}
 747  			done <- nil
 748  		}(i)
 749  	}
 750  
 751  	for i := 0; i < numUploads; i++ {
 752  		if err := <-done; err != nil {
 753  			t.Errorf("Concurrent upload failed: %v", err)
 754  		}
 755  	}
 756  }
 757  
 758  type testError struct {
 759  	code int
 760  	body string
 761  }
 762  
 763  func (e *testError) Error() string {
 764  	return strings.Join([]string{"HTTP", string(rune(e.code)), e.body}, " ")
 765  }
 766