mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 14:33:03 +00:00
555 lines
16 KiB
Go
555 lines
16 KiB
Go
package gateway
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
|
|
"github.com/DeBrosOfficial/network/pkg/gateway/handlers/storage"
|
|
"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)
|
|
addDirectoryFunc func(ctx context.Context, dirPath 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
|
|
getPeerCountFunc func(ctx context.Context) (int, 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) AddDirectory(ctx context.Context, dirPath string) (*ipfs.AddResponse, error) {
|
|
if m.addDirectoryFunc != nil {
|
|
return m.addDirectoryFunc(ctx, dirPath)
|
|
}
|
|
return &ipfs.AddResponse{Cid: "QmTestDir123", Name: dirPath, Size: 1000}, 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) GetPeerCount(ctx context.Context) (int, error) {
|
|
if m.getPeerCountFunc != nil {
|
|
return m.getPeerCountFunc(ctx)
|
|
}
|
|
return 3, 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
|
|
// Initialize storage handlers with the IPFS client
|
|
gw.storageHandlers = storage.New(ipfsClient, logger, storage.Config{
|
|
IPFSReplicationFactor: cfg.IPFSReplicationFactor,
|
|
IPFSAPIURL: cfg.IPFSAPIURL,
|
|
}, nil) // nil db client for tests
|
|
}
|
|
|
|
return gw
|
|
}
|
|
|
|
func TestStorageUploadHandler_MissingIPFSClient(t *testing.T) {
|
|
logger, err := logging.NewColoredLogger(logging.ComponentGeneral, true)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create logger: %v", err)
|
|
}
|
|
|
|
// Create storage handlers with nil IPFS client
|
|
handlers := storage.New(nil, logger, storage.Config{
|
|
IPFSReplicationFactor: 3,
|
|
IPFSAPIURL: "http://localhost:5001",
|
|
}, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", nil)
|
|
ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
handlers.UploadHandler(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(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.UploadHandler(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.storageHandlers.UploadHandler(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(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.UploadHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
var resp storage.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 := storage.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(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.UploadHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
var resp storage.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 := storage.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(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.UploadHandler(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(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.UploadHandler(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 := storage.StoragePinRequest{
|
|
Cid: expectedCID,
|
|
Name: expectedName,
|
|
}
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", bytes.NewReader(bodyBytes))
|
|
ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.PinHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
var resp storage.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 := storage.StoragePinRequest{}
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", bytes.NewReader(bodyBytes))
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.PinHandler(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.storageHandlers.StatusHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
|
|
}
|
|
|
|
var resp storage.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.storageHandlers.StatusHandler(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(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.DownloadHandler(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.storageHandlers.DownloadHandler(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)
|
|
ctx := context.WithValue(req.Context(), ctxkeys.NamespaceOverride, "test-ns")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
gw.storageHandlers.UnpinHandler(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.storageHandlers.UnpinHandler(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
|
|
}
|
|
}
|
|
|
|
// Helper function tests removed - base64Decode and getNamespaceFromContext
|
|
// are now private methods in the storage package and are tested indirectly
|
|
// through the handler tests.
|