orama/pkg/gateway/handlers/storage/handlers_test.go
2026-02-13 16:18:22 +02:00

716 lines
20 KiB
Go

package storage
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"github.com/DeBrosOfficial/network/pkg/ipfs"
"github.com/DeBrosOfficial/network/pkg/logging"
)
// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------
// mockIPFSClient implements the IPFSClient interface for testing.
type mockIPFSClient struct {
addResp *ipfs.AddResponse
addErr error
pinResp *ipfs.PinResponse
pinErr error
pinStatus *ipfs.PinStatus
pinStatErr error
getReader io.ReadCloser
getErr error
unpinErr error
}
func (m *mockIPFSClient) Add(_ context.Context, _ io.Reader, _ string) (*ipfs.AddResponse, error) {
return m.addResp, m.addErr
}
func (m *mockIPFSClient) Pin(_ context.Context, _ string, _ string, _ int) (*ipfs.PinResponse, error) {
return m.pinResp, m.pinErr
}
func (m *mockIPFSClient) PinStatus(_ context.Context, _ string) (*ipfs.PinStatus, error) {
return m.pinStatus, m.pinStatErr
}
func (m *mockIPFSClient) Get(_ context.Context, _ string, _ string) (io.ReadCloser, error) {
return m.getReader, m.getErr
}
func (m *mockIPFSClient) Unpin(_ context.Context, _ string) error {
return m.unpinErr
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
func newTestLogger() *logging.ColoredLogger {
logger, _ := logging.NewColoredLogger(logging.ComponentStorage, false)
return logger
}
func newTestHandlers(client IPFSClient) *Handlers {
return New(client, newTestLogger(), Config{
IPFSReplicationFactor: 3,
IPFSAPIURL: "http://localhost:5001",
}, nil) // db=nil -> ownership checks bypassed
}
// withNamespace returns a request with the namespace context key set.
func withNamespace(r *http.Request, ns string) *http.Request {
ctx := context.WithValue(r.Context(), ctxkeys.NamespaceOverride, ns)
return r.WithContext(ctx)
}
// decodeBody decodes a JSON response body into a map.
func decodeBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]interface{} {
t.Helper()
var body map[string]interface{}
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
t.Fatalf("failed to decode response body: %v", err)
}
return body
}
// ---------------------------------------------------------------------------
// Tests: getNamespaceFromContext
// ---------------------------------------------------------------------------
func TestGetNamespaceFromContext_Present(t *testing.T) {
h := newTestHandlers(nil)
ctx := context.WithValue(context.Background(), ctxkeys.NamespaceOverride, "my-ns")
got := h.getNamespaceFromContext(ctx)
if got != "my-ns" {
t.Errorf("expected 'my-ns', got %q", got)
}
}
func TestGetNamespaceFromContext_Missing(t *testing.T) {
h := newTestHandlers(nil)
got := h.getNamespaceFromContext(context.Background())
if got != "" {
t.Errorf("expected empty string, got %q", got)
}
}
func TestGetNamespaceFromContext_WrongType(t *testing.T) {
h := newTestHandlers(nil)
ctx := context.WithValue(context.Background(), ctxkeys.NamespaceOverride, 12345)
got := h.getNamespaceFromContext(ctx)
if got != "" {
t.Errorf("expected empty string for wrong type, got %q", got)
}
}
// ---------------------------------------------------------------------------
// Tests: UploadHandler
// ---------------------------------------------------------------------------
func TestUploadHandler_NilIPFS(t *testing.T) {
h := newTestHandlers(nil) // nil IPFS client
req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503, got %d", rec.Code)
}
}
func TestUploadHandler_InvalidMethod(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/upload", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", rec.Code)
}
}
func TestUploadHandler_MissingNamespace(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
// No namespace in context
req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"data":"dGVzdA=="}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestUploadHandler_InvalidJSON(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader("not json"))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
}
func TestUploadHandler_MissingData(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"name":"test.txt"}`))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
body := decodeBody(t, rec)
errMsg, _ := body["error"].(string)
if !strings.Contains(errMsg, "data field required") {
t.Errorf("expected 'data field required' error, got %q", errMsg)
}
}
func TestUploadHandler_InvalidBase64(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"data":"!!!invalid!!!"}`))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
body := decodeBody(t, rec)
errMsg, _ := body["error"].(string)
if !strings.Contains(errMsg, "base64") {
t.Errorf("expected base64 decode error, got %q", errMsg)
}
}
func TestUploadHandler_PUTNotAllowed(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPut, "/v1/storage/upload", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", rec.Code)
}
}
func TestUploadHandler_Success(t *testing.T) {
mock := &mockIPFSClient{
addResp: &ipfs.AddResponse{
Cid: "QmTestCID1234567890123456789012345678901234",
Name: "test.txt",
Size: 4,
},
pinResp: &ipfs.PinResponse{
Cid: "QmTestCID1234567890123456789012345678901234",
Name: "test.txt",
},
}
h := newTestHandlers(mock)
// "dGVzdA==" is base64("test")
req := httptest.NewRequest(http.MethodPost, "/v1/storage/upload", strings.NewReader(`{"data":"dGVzdA==","name":"test.txt"}`))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UploadHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
}
body := decodeBody(t, rec)
if body["cid"] != "QmTestCID1234567890123456789012345678901234" {
t.Errorf("unexpected cid: %v", body["cid"])
}
}
// ---------------------------------------------------------------------------
// Tests: DownloadHandler
// ---------------------------------------------------------------------------
func TestDownloadHandler_NilIPFS(t *testing.T) {
h := newTestHandlers(nil)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/QmSomeCID", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.DownloadHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503, got %d", rec.Code)
}
}
func TestDownloadHandler_InvalidMethod(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/get/QmSomeCID", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.DownloadHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", rec.Code)
}
}
func TestDownloadHandler_MissingCID(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.DownloadHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
body := decodeBody(t, rec)
errMsg, _ := body["error"].(string)
if !strings.Contains(errMsg, "cid required") {
t.Errorf("expected 'cid required' error, got %q", errMsg)
}
}
func TestDownloadHandler_MissingNamespace(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
// No namespace in context
req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/QmSomeCID", nil)
rec := httptest.NewRecorder()
h.DownloadHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestDownloadHandler_Success(t *testing.T) {
mock := &mockIPFSClient{
getReader: io.NopCloser(strings.NewReader("file contents")),
}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/get/QmTestCID", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.DownloadHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
}
if ct := rec.Header().Get("Content-Type"); ct != "application/octet-stream" {
t.Errorf("expected application/octet-stream, got %q", ct)
}
if rec.Body.String() != "file contents" {
t.Errorf("expected 'file contents', got %q", rec.Body.String())
}
}
// ---------------------------------------------------------------------------
// Tests: StatusHandler
// ---------------------------------------------------------------------------
func TestStatusHandler_NilIPFS(t *testing.T) {
h := newTestHandlers(nil)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/QmSomeCID", nil)
rec := httptest.NewRecorder()
h.StatusHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503, got %d", rec.Code)
}
}
func TestStatusHandler_InvalidMethod(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/status/QmSomeCID", nil)
rec := httptest.NewRecorder()
h.StatusHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", rec.Code)
}
}
func TestStatusHandler_MissingCID(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/", nil)
rec := httptest.NewRecorder()
h.StatusHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
body := decodeBody(t, rec)
errMsg, _ := body["error"].(string)
if !strings.Contains(errMsg, "cid required") {
t.Errorf("expected 'cid required' error, got %q", errMsg)
}
}
func TestStatusHandler_Success(t *testing.T) {
mock := &mockIPFSClient{
pinStatus: &ipfs.PinStatus{
Cid: "QmTestCID",
Name: "test.txt",
Status: "pinned",
Peers: []string{"peer1", "peer2"},
},
}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/status/QmTestCID", nil)
rec := httptest.NewRecorder()
h.StatusHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rec.Code)
}
body := decodeBody(t, rec)
if body["cid"] != "QmTestCID" {
t.Errorf("expected cid='QmTestCID', got %v", body["cid"])
}
if body["status"] != "pinned" {
t.Errorf("expected status='pinned', got %v", body["status"])
}
}
// ---------------------------------------------------------------------------
// Tests: PinHandler
// ---------------------------------------------------------------------------
func TestPinHandler_NilIPFS(t *testing.T) {
h := newTestHandlers(nil)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"cid":"QmTest"}`))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.PinHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503, got %d", rec.Code)
}
}
func TestPinHandler_InvalidMethod(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/pin", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.PinHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", rec.Code)
}
}
func TestPinHandler_InvalidJSON(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader("bad json"))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.PinHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
}
func TestPinHandler_MissingCID(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"name":"test"}`))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.PinHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
body := decodeBody(t, rec)
errMsg, _ := body["error"].(string)
if !strings.Contains(errMsg, "cid required") {
t.Errorf("expected 'cid required' error, got %q", errMsg)
}
}
func TestPinHandler_MissingNamespace(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
// No namespace in context
req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"cid":"QmTest"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.PinHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestPinHandler_Success(t *testing.T) {
mock := &mockIPFSClient{
pinResp: &ipfs.PinResponse{
Cid: "QmTestCID",
Name: "test.txt",
},
}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/pin", strings.NewReader(`{"cid":"QmTestCID","name":"test.txt"}`))
req.Header.Set("Content-Type", "application/json")
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.PinHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
}
body := decodeBody(t, rec)
if body["cid"] != "QmTestCID" {
t.Errorf("expected cid='QmTestCID', got %v", body["cid"])
}
}
// ---------------------------------------------------------------------------
// Tests: UnpinHandler
// ---------------------------------------------------------------------------
func TestUnpinHandler_NilIPFS(t *testing.T) {
h := newTestHandlers(nil)
req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/QmTest", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UnpinHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503, got %d", rec.Code)
}
}
func TestUnpinHandler_InvalidMethod(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodGet, "/v1/storage/unpin/QmTest", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UnpinHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", rec.Code)
}
}
func TestUnpinHandler_MissingCID(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UnpinHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
body := decodeBody(t, rec)
errMsg, _ := body["error"].(string)
if !strings.Contains(errMsg, "cid required") {
t.Errorf("expected 'cid required' error, got %q", errMsg)
}
}
func TestUnpinHandler_MissingNamespace(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
// No namespace in context
req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/QmTest", nil)
rec := httptest.NewRecorder()
h.UnpinHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestUnpinHandler_POSTNotAllowed(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodPost, "/v1/storage/unpin/QmTest", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UnpinHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", rec.Code)
}
}
func TestUnpinHandler_Success(t *testing.T) {
mock := &mockIPFSClient{}
h := newTestHandlers(mock)
req := httptest.NewRequest(http.MethodDelete, "/v1/storage/unpin/QmTestCID", nil)
req = withNamespace(req, "test-ns")
rec := httptest.NewRecorder()
h.UnpinHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d; body: %s", rec.Code, rec.Body.String())
}
body := decodeBody(t, rec)
if body["status"] != "ok" {
t.Errorf("expected status='ok', got %v", body["status"])
}
if body["cid"] != "QmTestCID" {
t.Errorf("expected cid='QmTestCID', got %v", body["cid"])
}
}
// ---------------------------------------------------------------------------
// Tests: base64Decode helper
// ---------------------------------------------------------------------------
func TestBase64Decode_Valid(t *testing.T) {
// "dGVzdA==" is base64("test")
data, err := base64Decode("dGVzdA==")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(data) != "test" {
t.Errorf("expected 'test', got %q", string(data))
}
}
func TestBase64Decode_Invalid(t *testing.T) {
_, err := base64Decode("!!!not-valid-base64!!!")
if err == nil {
t.Error("expected error for invalid base64, got nil")
}
}
func TestBase64Decode_Empty(t *testing.T) {
data, err := base64Decode("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(data) != 0 {
t.Errorf("expected empty slice, got %d bytes", len(data))
}
}
// ---------------------------------------------------------------------------
// Tests: recordCIDOwnership / checkCIDOwnership / updatePinStatus with nil DB
// ---------------------------------------------------------------------------
func TestRecordCIDOwnership_NilDB(t *testing.T) {
h := newTestHandlers(&mockIPFSClient{})
err := h.recordCIDOwnership(context.Background(), "cid", "ns", "name", "uploader", 100)
if err != nil {
t.Errorf("expected nil error with nil db, got %v", err)
}
}
func TestCheckCIDOwnership_NilDB(t *testing.T) {
h := newTestHandlers(&mockIPFSClient{})
hasAccess, err := h.checkCIDOwnership(context.Background(), "cid", "ns")
if err != nil {
t.Errorf("expected nil error with nil db, got %v", err)
}
if !hasAccess {
t.Error("expected true (allow access) when db is nil")
}
}
func TestUpdatePinStatus_NilDB(t *testing.T) {
h := newTestHandlers(&mockIPFSClient{})
err := h.updatePinStatus(context.Background(), "cid", "ns", true)
if err != nil {
t.Errorf("expected nil error with nil db, got %v", err)
}
}