package gateway import ( "bytes" "context" "encoding/base64" "encoding/json" "io" "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" "github.com/DeBrosOfficial/network/pkg/ipfs" "github.com/DeBrosOfficial/network/pkg/logging" ) // mockIPFSClient is a mock implementation of ipfs.IPFSClient for testing type mockIPFSClient struct { addFunc func(ctx context.Context, reader io.Reader, name string) (*ipfs.AddResponse, error) pinFunc func(ctx context.Context, cid string, name string, replicationFactor int) (*ipfs.PinResponse, error) pinStatusFunc func(ctx context.Context, cid string) (*ipfs.PinStatus, error) getFunc func(ctx context.Context, cid string, ipfsAPIURL string) (io.ReadCloser, error) unpinFunc func(ctx context.Context, cid string) error } func (m *mockIPFSClient) Add(ctx context.Context, reader io.Reader, name string) (*ipfs.AddResponse, error) { if m.addFunc != nil { return m.addFunc(ctx, reader, name) } return &ipfs.AddResponse{Cid: "QmTest123", Name: name, Size: 100}, nil } func (m *mockIPFSClient) Pin(ctx context.Context, cid string, name string, replicationFactor int) (*ipfs.PinResponse, error) { if m.pinFunc != nil { return m.pinFunc(ctx, cid, name, replicationFactor) } return &ipfs.PinResponse{Cid: cid, Name: name}, nil } func (m *mockIPFSClient) PinStatus(ctx context.Context, cid string) (*ipfs.PinStatus, error) { if m.pinStatusFunc != nil { return m.pinStatusFunc(ctx, cid) } return &ipfs.PinStatus{ Cid: cid, Name: "test", Status: "pinned", ReplicationMin: 3, ReplicationMax: 3, ReplicationFactor: 3, Peers: []string{"peer1", "peer2", "peer3"}, }, nil } func (m *mockIPFSClient) Get(ctx context.Context, cid string, ipfsAPIURL string) (io.ReadCloser, error) { if m.getFunc != nil { return m.getFunc(ctx, cid, ipfsAPIURL) } return io.NopCloser(strings.NewReader("test content")), nil } func (m *mockIPFSClient) Unpin(ctx context.Context, cid string) error { if m.unpinFunc != nil { return m.unpinFunc(ctx, cid) } return nil } func (m *mockIPFSClient) Health(ctx context.Context) error { return nil } func (m *mockIPFSClient) Close(ctx context.Context) error { return nil } func newTestGatewayWithIPFS(t *testing.T, ipfsClient ipfs.IPFSClient) *Gateway { logger, err := logging.NewColoredLogger(logging.ComponentGeneral, true) if err != nil { t.Fatalf("Failed to create logger: %v", err) } cfg := &Config{ ListenAddr: ":6001", ClientNamespace: "test", IPFSReplicationFactor: 3, IPFSEnableEncryption: true, IPFSAPIURL: "http://localhost:5001", } gw := &Gateway{ logger: logger, cfg: cfg, } if ipfsClient != nil { gw.ipfsClient = ipfsClient } return gw } func TestStorageUploadHandler_MissingIPFSClient(t *testing.T) { gw := newTestGatewayWithIPFS(t, nil) req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", nil) ctx := context.WithValue(req.Context(), ctxKeyNamespaceOverride, "test-ns") req = req.WithContext(ctx) w := httptest.NewRecorder() gw.storageUploadHandler(w, req) if w.Code != http.StatusServiceUnavailable { t.Errorf("Expected status %d, got %d", http.StatusServiceUnavailable, w.Code) } } func TestStorageUploadHandler_MethodNotAllowed(t *testing.T) { gw := newTestGatewayWithIPFS(t, &mockIPFSClient{}) req := httptest.NewRequest(http.MethodGet, "/v1/storage/upload", nil) ctx := context.WithValue(req.Context(), ctxKeyNamespaceOverride, "test-ns") req = req.WithContext(ctx) w := httptest.NewRecorder() gw.storageUploadHandler(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("Expected status %d, got %d", http.StatusMethodNotAllowed, w.Code) } } func TestStorageUploadHandler_MissingNamespace(t *testing.T) { gw := newTestGatewayWithIPFS(t, &mockIPFSClient{}) req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", nil) w := httptest.NewRecorder() gw.storageUploadHandler(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code) } } func TestStorageUploadHandler_MultipartUpload(t *testing.T) { expectedCID := "QmTest456" expectedName := "test.txt" expectedSize := int64(200) mockClient := &mockIPFSClient{ addFunc: func(ctx context.Context, reader io.Reader, name string) (*ipfs.AddResponse, error) { // Read and verify content data, _ := io.ReadAll(reader) if len(data) == 0 { return nil, io.ErrUnexpectedEOF } return &ipfs.AddResponse{ Cid: expectedCID, Name: name, Size: expectedSize, }, nil }, } gw := newTestGatewayWithIPFS(t, mockClient) var buf bytes.Buffer writer := multipart.NewWriter(&buf) part, _ := writer.CreateFormFile("file", expectedName) part.Write([]byte("test file content")) writer.Close() req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", &buf) req.Header.Set("Content-Type", writer.FormDataContentType()) ctx := context.WithValue(req.Context(), ctxKeyNamespaceOverride, "test-ns") req = req.WithContext(ctx) w := httptest.NewRecorder() gw.storageUploadHandler(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var resp StorageUploadResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("Failed to decode response: %v", err) } if resp.Cid != expectedCID { t.Errorf("Expected CID %s, got %s", expectedCID, resp.Cid) } if resp.Name != expectedName { t.Errorf("Expected name %s, got %s", expectedName, resp.Name) } if resp.Size != expectedSize { t.Errorf("Expected size %d, got %d", expectedSize, resp.Size) } } func TestStorageUploadHandler_JSONUpload(t *testing.T) { expectedCID := "QmTest789" expectedName := "test.json" testData := []byte("test json data") base64Data := base64.StdEncoding.EncodeToString(testData) mockClient := &mockIPFSClient{ addFunc: func(ctx context.Context, reader io.Reader, name string) (*ipfs.AddResponse, error) { data, _ := io.ReadAll(reader) if string(data) != string(testData) { return nil, io.ErrUnexpectedEOF } return &ipfs.AddResponse{ Cid: expectedCID, Name: name, Size: int64(len(testData)), }, nil }, } gw := newTestGatewayWithIPFS(t, mockClient) reqBody := StorageUploadRequest{ Name: expectedName, Data: base64Data, } bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") ctx := context.WithValue(req.Context(), ctxKeyNamespaceOverride, "test-ns") req = req.WithContext(ctx) w := httptest.NewRecorder() gw.storageUploadHandler(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var resp StorageUploadResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("Failed to decode response: %v", err) } if resp.Cid != expectedCID { t.Errorf("Expected CID %s, got %s", expectedCID, resp.Cid) } } func TestStorageUploadHandler_InvalidBase64(t *testing.T) { gw := newTestGatewayWithIPFS(t, &mockIPFSClient{}) reqBody := StorageUploadRequest{ Name: "test.txt", Data: "invalid base64!!!", } bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") ctx := context.WithValue(req.Context(), ctxKeyNamespaceOverride, "test-ns") req = req.WithContext(ctx) w := httptest.NewRecorder() gw.storageUploadHandler(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } } func TestStorageUploadHandler_IPFSError(t *testing.T) { mockClient := &mockIPFSClient{ addFunc: func(ctx context.Context, reader io.Reader, name string) (*ipfs.AddResponse, error) { return nil, io.ErrUnexpectedEOF }, } gw := newTestGatewayWithIPFS(t, mockClient) var buf bytes.Buffer writer := multipart.NewWriter(&buf) part, _ := writer.CreateFormFile("file", "test.txt") part.Write([]byte("test")) writer.Close() req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", &buf) req.Header.Set("Content-Type", writer.FormDataContentType()) ctx := context.WithValue(req.Context(), ctxKeyNamespaceOverride, "test-ns") req = req.WithContext(ctx) w := httptest.NewRecorder() gw.storageUploadHandler(w, req) if w.Code != http.StatusInternalServerError { t.Errorf("Expected status %d, got %d", http.StatusInternalServerError, w.Code) } } func TestStoragePinHandler_Success(t *testing.T) { expectedCID := "QmPin123" expectedName := "pinned-file" mockClient := &mockIPFSClient{ pinFunc: func(ctx context.Context, cid string, name string, replicationFactor int) (*ipfs.PinResponse, error) { if cid != expectedCID { return nil, io.ErrUnexpectedEOF } if replicationFactor != 3 { return nil, io.ErrUnexpectedEOF } return &ipfs.PinResponse{Cid: cid, Name: name}, nil }, } gw := newTestGatewayWithIPFS(t, mockClient) reqBody := StoragePinRequest{ Cid: expectedCID, Name: expectedName, } bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", bytes.NewReader(bodyBytes)) w := httptest.NewRecorder() gw.storagePinHandler(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var resp StoragePinResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("Failed to decode response: %v", err) } if resp.Cid != expectedCID { t.Errorf("Expected CID %s, got %s", expectedCID, resp.Cid) } if resp.Name != expectedName { t.Errorf("Expected name %s, got %s", expectedName, resp.Name) } } func TestStoragePinHandler_MissingCID(t *testing.T) { gw := newTestGatewayWithIPFS(t, &mockIPFSClient{}) reqBody := StoragePinRequest{} bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", bytes.NewReader(bodyBytes)) w := httptest.NewRecorder() gw.storagePinHandler(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } } func TestStorageStatusHandler_Success(t *testing.T) { expectedCID := "QmStatus123" mockClient := &mockIPFSClient{ pinStatusFunc: func(ctx context.Context, cid string) (*ipfs.PinStatus, error) { return &ipfs.PinStatus{ Cid: cid, Name: "test-file", Status: "pinned", ReplicationMin: 3, ReplicationMax: 3, ReplicationFactor: 3, Peers: []string{"peer1", "peer2", "peer3"}, }, nil }, } gw := newTestGatewayWithIPFS(t, mockClient) req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/"+expectedCID, nil) w := httptest.NewRecorder() gw.storageStatusHandler(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var resp StorageStatusResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("Failed to decode response: %v", err) } if resp.Cid != expectedCID { t.Errorf("Expected CID %s, got %s", expectedCID, resp.Cid) } if resp.Status != "pinned" { t.Errorf("Expected status 'pinned', got %s", resp.Status) } if resp.ReplicationFactor != 3 { t.Errorf("Expected replication factor 3, got %d", resp.ReplicationFactor) } } func TestStorageStatusHandler_MissingCID(t *testing.T) { gw := newTestGatewayWithIPFS(t, &mockIPFSClient{}) req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/", nil) w := httptest.NewRecorder() gw.storageStatusHandler(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } } func TestStorageGetHandler_Success(t *testing.T) { expectedCID := "QmGet123" expectedContent := "test content from IPFS" mockClient := &mockIPFSClient{ getFunc: func(ctx context.Context, cid string, ipfsAPIURL string) (io.ReadCloser, error) { if cid != expectedCID { return nil, io.ErrUnexpectedEOF } return io.NopCloser(strings.NewReader(expectedContent)), nil }, } gw := newTestGatewayWithIPFS(t, mockClient) req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/"+expectedCID, nil) ctx := context.WithValue(req.Context(), ctxKeyNamespaceOverride, "test-ns") req = req.WithContext(ctx) w := httptest.NewRecorder() gw.storageGetHandler(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } if w.Body.String() != expectedContent { t.Errorf("Expected content %s, got %s", expectedContent, w.Body.String()) } if w.Header().Get("Content-Type") != "application/octet-stream" { t.Errorf("Expected Content-Type 'application/octet-stream', got %s", w.Header().Get("Content-Type")) } } func TestStorageGetHandler_MissingNamespace(t *testing.T) { gw := newTestGatewayWithIPFS(t, &mockIPFSClient{}) req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/QmTest123", nil) w := httptest.NewRecorder() gw.storageGetHandler(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code) } } func TestStorageUnpinHandler_Success(t *testing.T) { expectedCID := "QmUnpin123" mockClient := &mockIPFSClient{ unpinFunc: func(ctx context.Context, cid string) error { if cid != expectedCID { return io.ErrUnexpectedEOF } return nil }, } gw := newTestGatewayWithIPFS(t, mockClient) req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/"+expectedCID, nil) w := httptest.NewRecorder() gw.storageUnpinHandler(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var resp map[string]any if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("Failed to decode response: %v", err) } if resp["cid"] != expectedCID { t.Errorf("Expected CID %s, got %v", expectedCID, resp["cid"]) } } func TestStorageUnpinHandler_MissingCID(t *testing.T) { gw := newTestGatewayWithIPFS(t, &mockIPFSClient{}) req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/", nil) w := httptest.NewRecorder() gw.storageUnpinHandler(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } } // Test helper functions func TestBase64Decode(t *testing.T) { testData := []byte("test data") encoded := base64.StdEncoding.EncodeToString(testData) decoded, err := base64Decode(encoded) if err != nil { t.Fatalf("Failed to decode: %v", err) } if string(decoded) != string(testData) { t.Errorf("Expected %s, got %s", string(testData), string(decoded)) } // Test invalid base64 _, err = base64Decode("invalid!!!") if err == nil { t.Error("Expected error for invalid base64") } } func TestGetNamespaceFromContext(t *testing.T) { gw := newTestGatewayWithIPFS(t, nil) // Test with namespace in context ctx := context.WithValue(context.Background(), ctxKeyNamespaceOverride, "test-ns") ns := gw.getNamespaceFromContext(ctx) if ns != "test-ns" { t.Errorf("Expected 'test-ns', got %s", ns) } // Test without namespace ctx2 := context.Background() ns2 := gw.getNamespaceFromContext(ctx2) if ns2 != "" { t.Errorf("Expected empty namespace, got %s", ns2) } }