From fd2828ce9453f2df398229304ac32bd3fa8357ae Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 18 Jan 2026 14:51:38 -0300 Subject: [PATCH] nipb0/blossom: tests. --- nipb0/blossom/blossom_test.go | 329 ++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 nipb0/blossom/blossom_test.go diff --git a/nipb0/blossom/blossom_test.go b/nipb0/blossom/blossom_test.go new file mode 100644 index 0000000..5e52e55 --- /dev/null +++ b/nipb0/blossom/blossom_test.go @@ -0,0 +1,329 @@ +package blossom_test + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/keyer" + "fiatjaf.com/nostr/khatru" + khatru_blossom "fiatjaf.com/nostr/khatru/blossom" + blossomclient "fiatjaf.com/nostr/nipb0/blossom" +) + +func TestBlossomBasicOperations(t *testing.T) { + // setup two test servers + server1 := setupTestServer(t, ":38081") + defer server1.Close() + + server2 := setupTestServer(t, ":38082") + defer server2.Close() + + // create signers for two different pubkeys + secretKey1 := nostr.Generate() + secretKey2 := nostr.Generate() + signer1 := keyer.NewPlainKeySigner(secretKey1) + signer2 := keyer.NewPlainKeySigner(secretKey2) + + // create clients + client1 := blossomclient.NewClient(server1.URL, signer1) + client2 := blossomclient.NewClient(server1.URL, signer2) + client2Server := blossomclient.NewClient(server2.URL, signer2) + + ctx := context.Background() + + // test content + testContent := []byte("Hello, Blossom World!") + contentType := "text/plain" + + // 1. Upload blob from first pubkey + t.Run("UploadBlob", func(t *testing.T) { + reader := bytes.NewReader(testContent) + bd, err := client1.UploadBlob(ctx, reader, contentType) + if err != nil { + t.Fatalf("Failed to upload blob: %v", err) + } + + if bd.Size != len(testContent) { + t.Errorf("Expected size %d, got %d", len(testContent), bd.Size) + } + + // content type may include charset, just check that it starts with our expected type + if !strings.HasPrefix(bd.Type, contentType) { + t.Errorf("Expected type to start with %s, got %s", contentType, bd.Type) + } + + // verify SHA256 is calculated correctly + if len(bd.SHA256) != 64 { + t.Errorf("Expected SHA256 to be 64 characters, got %d", len(bd.SHA256)) + } + }) + + // 2. Download the blob + t.Run("DownloadBlob", func(t *testing.T) { + // first get the list to find the hash + blobs, err := client1.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs: %v", err) + } + + if len(blobs) != 1 { + t.Fatalf("Expected 1 blob, got %d", len(blobs)) + } + + hash := blobs[0].SHA256 + downloaded, err := client1.Download(ctx, hash) + if err != nil { + t.Fatalf("Failed to download blob: %v", err) + } + + if !bytes.Equal(downloaded, testContent) { + t.Errorf("Downloaded content mismatch. Expected %q, got %q", testContent, downloaded) + } + }) + + // 3. Upload same blob from different pubkey + t.Run("UploadSameBlobDifferentPubkey", func(t *testing.T) { + // get the hash from the first upload + blobs, err := client1.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs: %v", err) + } + + if len(blobs) != 1 { + t.Fatalf("Expected 1 blob, got %d", len(blobs)) + } + + hash := blobs[0].SHA256 + reader := bytes.NewReader(testContent) + bd, err := client2.UploadBlob(ctx, reader, contentType) + if err != nil { + t.Fatalf("Failed to upload same blob with different pubkey: %v", err) + } + + if bd.SHA256 != hash { + t.Errorf("Hash mismatch for same content. Expected %s, got %s", hash, bd.SHA256) + } + }) + + // 4. Check list for both pubkeys + t.Run("ListBlobs", func(t *testing.T) { + // list from first pubkey + blobs1, err := client1.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs from pubkey1: %v", err) + } + + // list from second pubkey + blobs2, err := client2.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs from pubkey2: %v", err) + } + + // both should see the same blob + if len(blobs1) != 1 || len(blobs2) != 1 { + t.Errorf("Expected both pubkeys to see 1 blob, got %d and %d", len(blobs1), len(blobs2)) + } + + if blobs1[0].SHA256 != blobs2[0].SHA256 { + t.Errorf("Hash mismatch between pubkey lists: %s vs %s", blobs1[0].SHA256, blobs2[0].SHA256) + } + }) + + // 5. Delete from first pubkey + t.Run("DeleteFromFirstPubkey", func(t *testing.T) { + // get the hash + blobs, err := client1.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs: %v", err) + } + + hash := blobs[0].SHA256 + + // delete from first pubkey + err = client1.Delete(ctx, hash) + if err != nil { + t.Fatalf("Failed to delete blob from pubkey1: %v", err) + } + + // first pubkey should see no blobs + blobs1, err := client1.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs after delete: %v", err) + } + + // second pubkey should still see the blob + blobs2, err := client2.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs from pubkey2: %v", err) + } + + if len(blobs1) != 0 { + t.Errorf("Expected pubkey1 to see 0 blobs after delete, got %d", len(blobs1)) + } + + if len(blobs2) != 1 { + t.Errorf("Expected pubkey2 to still see 1 blob after pubkey1 delete, got %d", len(blobs2)) + } + + // download should still work + downloaded, err := client2.Download(ctx, hash) + if err != nil { + t.Fatalf("Failed to download blob after pubkey1 delete: %v", err) + } + + if !bytes.Equal(downloaded, testContent) { + t.Errorf("Downloaded content mismatch after delete. Expected %q, got %q", testContent, downloaded) + } + }) + + // 6. Upload different blobs to second server + t.Run("UploadToSecondServer", func(t *testing.T) { + testContent2 := []byte("Hello from server 2!") + reader := bytes.NewReader(testContent2) + bd, err := client2Server.UploadBlob(ctx, reader, "text/plain") + if err != nil { + t.Fatalf("Failed to upload blob to server2: %v", err) + } + + // verify it appears in list + blobs, err := client2Server.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs from server2: %v", err) + } + + if len(blobs) != 1 { + t.Errorf("Expected 1 blob on server2, got %d", len(blobs)) + } + + // verify download + downloaded, err := client2Server.Download(ctx, bd.SHA256) + if err != nil { + t.Fatalf("Failed to download from server2: %v", err) + } + + if !bytes.Equal(downloaded, testContent2) { + t.Errorf("Downloaded content mismatch from server2") + } + }) + + // 7. Mirror from server1 to server2 + t.Run("MirrorBetweenServers", func(t *testing.T) { + // get a blob from server1 (should still be there from pubkey2) + blobs1, err := client2.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs from server1: %v", err) + } + + if len(blobs1) == 0 { + t.Skip("No blobs to mirror") + } + + sourceURL := server1.URL + "/" + blobs1[0].SHA256 + + // mirror to server2 + bd, err := client2Server.MirrorBlob(ctx, sourceURL) + if err != nil { + t.Fatalf("Failed to mirror blob: %v", err) + } + + // verify mirrored blob appears in server2 list + blobs2, err := client2Server.List(ctx) + if err != nil { + t.Fatalf("Failed to list blobs from server2 after mirror: %v", err) + } + + // should now have 2 blobs (original + mirrored) + if len(blobs2) < 2 { + t.Errorf("Expected at least 2 blobs on server2 after mirror, got %d", len(blobs2)) + } + + // verify the mirrored blob can be downloaded + downloaded, err := client2Server.Download(ctx, bd.SHA256) + if err != nil { + t.Fatalf("Failed to download mirrored blob: %v", err) + } + + // should match the original content + originalDownloaded, err := client2.Download(ctx, blobs1[0].SHA256) + if err != nil { + t.Fatalf("Failed to download original blob for comparison: %v", err) + } + + if !bytes.Equal(downloaded, originalDownloaded) { + t.Errorf("Mirrored blob content mismatch") + } + }) + + // 8. Final list verification + t.Run("FinalListVerification", func(t *testing.T) { + // check final state of both servers + blobs1, err := client2.List(ctx) // server1 with pubkey2 + if err != nil { + t.Fatalf("Failed final list from server1: %v", err) + } + + blobs2, err := client2Server.List(ctx) // server2 with pubkey2 + if err != nil { + t.Fatalf("Failed final list from server2: %v", err) + } + + // server1 should have the original blob + if len(blobs1) == 0 { + t.Errorf("Expected server1 to have blobs, got 0") + } + + // server2 should have at least the original blob + mirrored blob + if len(blobs2) < 2 { + t.Errorf("Expected server2 to have at least 2 blobs, got %d", len(blobs2)) + } + + t.Logf("Final state - Server1: %d blobs, Server2: %d blobs", len(blobs1), len(blobs2)) + }) +} + +func setupTestServer(t *testing.T, addr string) *httptest.Server { + relay := khatru.NewRelay() + + // use memory-based blob index for testing + memoryIndex := khatru_blossom.NewMemoryBlobIndex() + + // setup blob storage in memory + blobStorage := make(map[string][]byte) + + bl := khatru_blossom.New(relay, "http://localhost"+addr) + bl.Store = memoryIndex + + bl.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error { + blobStorage[sha256] = make([]byte, len(body)) + copy(blobStorage[sha256], body) + return nil + } + + bl.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) { + if data, ok := blobStorage[sha256]; ok { + return bytes.NewReader(data), nil, nil + } + return nil, nil, fmt.Errorf("blob not found") + } + + bl.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error { + delete(blobStorage, sha256) + return nil + } + + server := httptest.NewUnstartedServer(relay) + server.Start() + + // wait a moment for server to be ready + time.Sleep(10 * time.Millisecond) + + return server +}