package communities import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestUpload_Success(t *testing.T) { // Arrange a fake Codex server that validates headers and returns a CID server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if r.URL.Path != "/api/codex/v1/data" { w.WriteHeader(http.StatusNotFound) return } if ct := r.Header.Get("Content-Type"); ct != "application/octet-stream" { w.WriteHeader(http.StatusBadRequest) return } if cd := r.Header.Get("Content-Disposition"); cd != "filename=\"hello.txt\"" { w.WriteHeader(http.StatusBadRequest) return } _, _ = io.ReadAll(r.Body) // consume body _ = r.Body.Close() w.WriteHeader(http.StatusOK) // Codex returns CIDv1 base58btc // prefix: zDv // - z = multibase prefix for base58btc // - Dv = CIDv1 prefix for raw codex // we add a newline to simulate real response _, _ = w.Write([]byte("zDvZRwzmTestCID123\n")) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL // Act cid, err := client.Upload(bytes.NewReader([]byte("payload")), "hello.txt") // Assert if err != nil { t.Fatalf("unexpected error: %v", err) } // Codex uses CIDv1 with base58btc encoding (prefix: zDv) if cid != "zDvZRwzmTestCID123" { t.Fatalf("unexpected cid: %q", cid) } } func TestDownload_Success(t *testing.T) { const wantCID = "zDvZRwzm" const payload = "hello from codex" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } if r.URL.Path != "/api/codex/v1/data/"+wantCID+"/network/stream" { w.WriteHeader(http.StatusNotFound) return } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(payload)) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL var buf bytes.Buffer if err := client.Download(wantCID, &buf); err != nil { t.Fatalf("unexpected error: %v", err) } if got := buf.String(); got != payload { t.Fatalf("unexpected payload: %q", got) } } func TestDownloadWithContext_Cancel(t *testing.T) { const cid = "zDvZRwzm" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/codex/v1/data/"+cid+"/network/stream" { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "application/octet-stream") flusher, _ := w.(http.Flusher) w.WriteHeader(http.StatusOK) // Stream data slowly so the request can be canceled for i := 0; i < 1000; i++ { select { case <-r.Context().Done(): return default: } if _, err := w.Write([]byte("x")); err != nil { // Client likely went away; stop writing return } if flusher != nil { flusher.Flush() } time.Sleep(10 * time.Millisecond) } })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond) defer cancel() err := client.DownloadWithContext(ctx, cid, io.Discard) if err == nil { t.Fatalf("expected cancellation error, got nil") } // Accept either canceled or deadline exceeded depending on timing if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { // net/http may wrap the context error; check error string as a fallback es := err.Error() if !(es == context.Canceled.Error() || es == context.DeadlineExceeded.Error()) { t.Fatalf("expected context cancellation, got: %v", err) } } } func TestHasCid_Success(t *testing.T) { tests := []struct { name string cid string hasIt bool wantBool bool }{ {"has CID returns true", "zDvZRwzmTestCID", true, true}, {"has CID returns false", "zDvZRwzmTestCID", false, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/codex/v1/data/"+tt.cid+"/exists" { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Return JSON: {"": } fmt.Fprintf(w, `{"%s": %t}`, tt.cid, tt.hasIt) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL got, err := client.HasCid(tt.cid) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != tt.wantBool { t.Fatalf("HasCid(%q) = %v, want %v", tt.cid, got, tt.wantBool) } }) } } func TestHasCid_RequestError(t *testing.T) { // Create a server and immediately close it to trigger connection error server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() // Close immediately so connection fails client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL // Use the closed server's URL got, err := client.HasCid("zDvZRwzmTestCID") if err == nil { t.Fatal("expected error, got nil") } if got != false { t.Fatalf("expected false on error, got %v", got) } } func TestHasCid_CidMismatch(t *testing.T) { const requestCid = "zDvZRwzmRequestCID" const responseCid = "zDvZRwzmDifferentCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Return a different CID in the response fmt.Fprintf(w, `{"%s": true}`, responseCid) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL got, err := client.HasCid(requestCid) if err == nil { t.Fatal("expected error for CID mismatch, got nil") } if got != false { t.Fatalf("expected false on CID mismatch, got %v", got) } // Check error message mentions the missing/mismatched CID if !strings.Contains(err.Error(), requestCid) { t.Fatalf("error should mention request CID %q, got: %v", requestCid, err) } } func TestRemoveCid_Success(t *testing.T) { const testCid = "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { w.WriteHeader(http.StatusMethodNotAllowed) return } if r.URL.Path != "/api/codex/v1/data/"+testCid { w.WriteHeader(http.StatusNotFound) return } // DELETE should return 204 No Content w.WriteHeader(http.StatusNoContent) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL err := client.RemoveCid(testCid) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestRemoveCid_Error(t *testing.T) { const testCid = "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Return error status w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("server error")) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL err := client.RemoveCid(testCid) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "500") { t.Fatalf("error should mention status 500, got: %v", err) } } func TestTriggerDownload(t *testing.T) { const testCid = "zDvZRwzmTestCID" const expectedManifest = `{ "cid": "zDvZRwzmTestCID", "manifest": { "treeCid": "zDvZRwzmTreeCID", "datasetSize": 1024, "blockSize": 65536, "protected": false, "filename": "test-file.bin", "mimetype": "application/octet-stream" } }` server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if r.URL.Path != "/api/codex/v1/data/"+testCid+"/network" { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(expectedManifest)) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx := context.Background() manifest, err := client.TriggerDownloadWithContext(ctx, testCid) if err != nil { t.Fatalf("unexpected error: %v", err) } if manifest.CID != testCid { t.Fatalf("expected CID %q, got %q", testCid, manifest.CID) } if manifest.Manifest.TreeCid != "zDvZRwzmTreeCID" { t.Fatalf("expected TreeCid %q, got %q", "zDvZRwzmTreeCID", manifest.Manifest.TreeCid) } if manifest.Manifest.DatasetSize != 1024 { t.Fatalf("expected DatasetSize %d, got %d", 1024, manifest.Manifest.DatasetSize) } if manifest.Manifest.Filename != "test-file.bin" { t.Fatalf("expected Filename %q, got %q", "test-file.bin", manifest.Manifest.Filename) } } func TestTriggerDownloadWithContext_RequestError(t *testing.T) { // Create a server and immediately close it to trigger connection error server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx := context.Background() manifest, err := client.TriggerDownloadWithContext(ctx, "zDvZRwzmRigWseNB7WqmudkKAPgZmrDCE9u5cY4KvCqhRo9Ki") if err == nil { t.Fatal("expected error, got nil") } if manifest != nil { t.Fatalf("expected nil manifest on error, got %v", manifest) } } func TestTriggerDownloadWithContext_JSONParseError(t *testing.T) { const testCid = "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Return invalid JSON w.Write([]byte(`{"invalid": json}`)) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx := context.Background() manifest, err := client.TriggerDownloadWithContext(ctx, testCid) if err == nil { t.Fatal("expected JSON parse error, got nil") } if manifest != nil { t.Fatalf("expected nil manifest on parse error, got %v", manifest) } if !strings.Contains(err.Error(), "failed to parse download manifest") { t.Fatalf("error should mention parse failure, got: %v", err) } } func TestTriggerDownloadWithContext_HTTPError(t *testing.T) { const testCid = "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("CID not found")) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx := context.Background() manifest, err := client.TriggerDownloadWithContext(ctx, testCid) if err == nil { t.Fatal("expected error for 404 status, got nil") } if manifest != nil { t.Fatalf("expected nil manifest on HTTP error, got %v", manifest) } if !strings.Contains(err.Error(), "404") { t.Fatalf("error should mention status 404, got: %v", err) } } func TestTriggerDownloadWithContext_Cancellation(t *testing.T) { const testCid = "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate slow response to allow cancellation select { case <-r.Context().Done(): return case <-time.After(200 * time.Millisecond): w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"cid": "test"}`)) } })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL // Cancel after 50ms (before server responds) ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() manifest, err := client.TriggerDownloadWithContext(ctx, testCid) if err == nil { t.Fatal("expected cancellation error, got nil") } if manifest != nil { t.Fatalf("expected nil manifest on cancellation, got %v", manifest) } // Accept either canceled or deadline exceeded depending on timing if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { // net/http may wrap the context error; check error string as a fallback es := err.Error() if !(es == context.Canceled.Error() || es == context.DeadlineExceeded.Error()) { t.Fatalf("expected context cancellation, got: %v", err) } } } func TestLocalDownload(t *testing.T) { testData := []byte("test data for local download") testCid := "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify request method and path if r.Method != "GET" { t.Errorf("Expected GET request, got %s", r.Method) } expectedPath := "/api/codex/v1/data/" + testCid if r.URL.Path != expectedPath { t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) } w.WriteHeader(http.StatusOK) w.Write(testData) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL var buf bytes.Buffer err := client.LocalDownload(testCid, &buf) if err != nil { t.Fatalf("LocalDownload failed: %v", err) } if !bytes.Equal(buf.Bytes(), testData) { t.Errorf("Downloaded data mismatch. Expected %q, got %q", string(testData), buf.String()) } } func TestLocalDownloadWithContext_Success(t *testing.T) { testData := []byte("test data for local download with context") testCid := "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify request method and path if r.Method != "GET" { t.Errorf("Expected GET request, got %s", r.Method) } expectedPath := "/api/codex/v1/data/" + testCid if r.URL.Path != expectedPath { t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) } w.WriteHeader(http.StatusOK) w.Write(testData) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx := context.Background() var buf bytes.Buffer err := client.LocalDownloadWithContext(ctx, testCid, &buf) if err != nil { t.Fatalf("LocalDownloadWithContext failed: %v", err) } if !bytes.Equal(buf.Bytes(), testData) { t.Errorf("Downloaded data mismatch. Expected %q, got %q", string(testData), buf.String()) } } func TestLocalDownloadWithContext_RequestError(t *testing.T) { // Create a server and immediately close it to trigger connection error server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx := context.Background() var buf bytes.Buffer err := client.LocalDownloadWithContext(ctx, "zDvZRwzmTestCID", &buf) if err == nil { t.Fatal("Expected error due to closed server, got nil") } if !strings.Contains(err.Error(), "failed to download from codex") { t.Errorf("Expected 'failed to download from codex' in error, got: %v", err) } } func TestLocalDownloadWithContext_HTTPError(t *testing.T) { testCid := "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("CID not found in local storage")) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL ctx := context.Background() var buf bytes.Buffer err := client.LocalDownloadWithContext(ctx, testCid, &buf) if err == nil { t.Fatal("Expected error for HTTP 404, got nil") } if !strings.Contains(err.Error(), "404") { t.Errorf("Expected '404' in error message, got: %v", err) } } func TestLocalDownloadWithContext_Cancellation(t *testing.T) { testCid := "zDvZRwzmTestCID" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Simulate a slow response time.Sleep(100 * time.Millisecond) w.WriteHeader(http.StatusOK) w.Write([]byte("slow response")) })) defer server.Close() client := NewCodexClient("localhost", "8080") client.BaseURL = server.URL // Create a context with a very short timeout ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() var buf bytes.Buffer err := client.LocalDownloadWithContext(ctx, testCid, &buf) if err == nil { t.Fatal("Expected context cancellation error, got nil") } // Accept either canceled or deadline exceeded depending on timing if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { // net/http may wrap the context error; check error string as a fallback es := err.Error() if !(es == context.Canceled.Error() || es == context.DeadlineExceeded.Error()) { t.Fatalf("expected context cancellation, got: %v", err) } } }