feat: refactor API gateway and CLI utilities for improved functionality

- Updated the API gateway documentation to reflect changes in architecture and functionality, emphasizing its role as a multi-functional entry point for decentralized services.
- Refactored CLI commands to utilize utility functions for better code organization and maintainability.
- Introduced new utility functions for handling peer normalization, service management, and port validation, enhancing the overall CLI experience.
- Added a new production installation script to streamline the setup process for users, including detailed dry-run summaries for better visibility.
- Enhanced validation mechanisms for configuration files and swarm keys, ensuring robust error handling and user feedback during setup.
This commit is contained in:
anonpenguin23 2025-12-31 10:48:15 +02:00
parent b3b1905fb2
commit 4ee76588ed
13 changed files with 845 additions and 156 deletions

View File

@ -71,14 +71,9 @@ run-gateway:
@echo "Note: Config must be in ~/.orama/data/gateway.yaml" @echo "Note: Config must be in ~/.orama/data/gateway.yaml"
go run ./cmd/orama-gateway go run ./cmd/orama-gateway
# Setup local domain names for development
setup-domains:
@echo "Setting up local domains..."
@sudo bash scripts/setup-local-domains.sh
# Development environment target # Development environment target
# Uses orama dev up to start full stack with dependency and port checking # Uses orama dev up to start full stack with dependency and port checking
dev: build setup-domains dev: build
@./bin/orama dev up @./bin/orama dev up
# Graceful shutdown of all dev services # Graceful shutdown of all dev services

View File

@ -26,27 +26,25 @@ make stop
After running `make dev`, test service health using these curl requests: After running `make dev`, test service health using these curl requests:
> **Note:** Local domains (node-1.local, etc.) require running `sudo make setup-domains` first. Alternatively, use `localhost` with port numbers.
### Node Unified Gateways ### Node Unified Gateways
Each node is accessible via a single unified gateway port: Each node is accessible via a single unified gateway port:
```bash ```bash
# Node-1 (port 6001) # Node-1 (port 6001)
curl http://node-1.local:6001/health curl http://localhost:6001/health
# Node-2 (port 6002) # Node-2 (port 6002)
curl http://node-2.local:6002/health curl http://localhost:6002/health
# Node-3 (port 6003) # Node-3 (port 6003)
curl http://node-3.local:6003/health curl http://localhost:6003/health
# Node-4 (port 6004) # Node-4 (port 6004)
curl http://node-4.local:6004/health curl http://localhost:6004/health
# Node-5 (port 6005) # Node-5 (port 6005)
curl http://node-5.local:6005/health curl http://localhost:6005/health
``` ```
## Network Architecture ## Network Architecture

123
e2e/serverless_test.go Normal file
View File

@ -0,0 +1,123 @@
//go:build e2e
package e2e
import (
"bytes"
"context"
"io"
"mime/multipart"
"net/http"
"os"
"testing"
"time"
)
func TestServerless_DeployAndInvoke(t *testing.T) {
SkipIfMissingGateway(t)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
wasmPath := "../examples/functions/bin/hello.wasm"
if _, err := os.Stat(wasmPath); os.IsNotExist(err) {
t.Skip("hello.wasm not found")
}
wasmBytes, err := os.ReadFile(wasmPath)
if err != nil {
t.Fatalf("failed to read hello.wasm: %v", err)
}
funcName := "e2e-hello"
namespace := "default"
// 1. Deploy function
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// Add metadata
_ = writer.WriteField("name", funcName)
_ = writer.WriteField("namespace", namespace)
// Add WASM file
part, err := writer.CreateFormFile("wasm", funcName+".wasm")
if err != nil {
t.Fatalf("failed to create form file: %v", err)
}
part.Write(wasmBytes)
writer.Close()
deployReq, _ := http.NewRequestWithContext(ctx, "POST", GetGatewayURL()+"/v1/functions", &buf)
deployReq.Header.Set("Content-Type", writer.FormDataContentType())
if apiKey := GetAPIKey(); apiKey != "" {
deployReq.Header.Set("Authorization", "Bearer "+apiKey)
}
client := NewHTTPClient(1 * time.Minute)
resp, err := client.Do(deployReq)
if err != nil {
t.Fatalf("deploy request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("deploy failed with status %d: %s", resp.StatusCode, string(body))
}
// 2. Invoke function
invokePayload := []byte(`{"name": "E2E Tester"}`)
invokeReq, _ := http.NewRequestWithContext(ctx, "POST", GetGatewayURL()+"/v1/functions/"+funcName+"/invoke", bytes.NewReader(invokePayload))
invokeReq.Header.Set("Content-Type", "application/json")
if apiKey := GetAPIKey(); apiKey != "" {
invokeReq.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err = client.Do(invokeReq)
if err != nil {
t.Fatalf("invoke request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("invoke failed with status %d: %s", resp.StatusCode, string(body))
}
output, _ := io.ReadAll(resp.Body)
expected := "Hello, E2E Tester!"
if !bytes.Contains(output, []byte(expected)) {
t.Errorf("output %q does not contain %q", string(output), expected)
}
// 3. List functions
listReq, _ := http.NewRequestWithContext(ctx, "GET", GetGatewayURL()+"/v1/functions?namespace="+namespace, nil)
if apiKey := GetAPIKey(); apiKey != "" {
listReq.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err = client.Do(listReq)
if err != nil {
t.Fatalf("list request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("list failed with status %d", resp.StatusCode)
}
// 4. Delete function
deleteReq, _ := http.NewRequestWithContext(ctx, "DELETE", GetGatewayURL()+"/v1/functions/"+funcName+"?namespace="+namespace, nil)
if apiKey := GetAPIKey(); apiKey != "" {
deleteReq.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err = client.Do(deleteReq)
if err != nil {
t.Fatalf("delete request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("delete failed with status %d", resp.StatusCode)
}
}

View File

@ -208,6 +208,11 @@ func (h *ServerlessHandlers) deployFunction(w http.ResponseWriter, r *http.Reque
def.Name = r.FormValue("name") def.Name = r.FormValue("name")
} }
// Get namespace from form if not in metadata
if def.Namespace == "" {
def.Namespace = r.FormValue("namespace")
}
// Get WASM file // Get WASM file
file, _, err := r.FormFile("wasm") file, _, err := r.FormFile("wasm")
if err != nil { if err != nil {
@ -578,7 +583,7 @@ func (h *ServerlessHandlers) getNamespaceFromRequest(r *http.Request) string {
return ns return ns
} }
return "" return "default"
} }
// getWalletFromRequest extracts wallet address from JWT // getWalletFromRequest extracts wallet address from JWT

View File

@ -0,0 +1,84 @@
package gateway
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/DeBrosOfficial/network/pkg/serverless"
"go.uber.org/zap"
)
type mockFunctionRegistry struct {
functions []*serverless.Function
}
func (m *mockFunctionRegistry) Register(ctx context.Context, fn *serverless.FunctionDefinition, wasmBytes []byte) error {
return nil
}
func (m *mockFunctionRegistry) Get(ctx context.Context, namespace, name string, version int) (*serverless.Function, error) {
return &serverless.Function{ID: "1", Name: name, Namespace: namespace}, nil
}
func (m *mockFunctionRegistry) List(ctx context.Context, namespace string) ([]*serverless.Function, error) {
return m.functions, nil
}
func (m *mockFunctionRegistry) Delete(ctx context.Context, namespace, name string, version int) error {
return nil
}
func (m *mockFunctionRegistry) GetWASMBytes(ctx context.Context, wasmCID string) ([]byte, error) {
return []byte("wasm"), nil
}
func TestServerlessHandlers_ListFunctions(t *testing.T) {
logger := zap.NewNop()
registry := &mockFunctionRegistry{
functions: []*serverless.Function{
{ID: "1", Name: "func1", Namespace: "ns1"},
{ID: "2", Name: "func2", Namespace: "ns1"},
},
}
h := NewServerlessHandlers(nil, registry, nil, logger)
req, _ := http.NewRequest("GET", "/v1/functions?namespace=ns1", nil)
rr := httptest.NewRecorder()
h.handleFunctions(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rr.Code)
}
var resp map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &resp)
if resp["count"].(float64) != 2 {
t.Errorf("expected 2 functions, got %v", resp["count"])
}
}
func TestServerlessHandlers_DeployFunction(t *testing.T) {
logger := zap.NewNop()
registry := &mockFunctionRegistry{}
h := NewServerlessHandlers(nil, registry, nil, logger)
// Test JSON deploy (which is partially supported according to code)
// Should be 400 because WASM is missing or base64 not supported
writer := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/functions", bytes.NewBufferString(`{"name": "test"}`))
req.Header.Set("Content-Type", "application/json")
h.handleFunctions(writer, req)
if writer.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", writer.Code)
}
}

View File

@ -228,7 +228,12 @@ func (g *Gateway) storageStatusHandler(w http.ResponseWriter, r *http.Request) {
status, err := g.ipfsClient.PinStatus(ctx, path) status, err := g.ipfsClient.PinStatus(ctx, path)
if err != nil { if err != nil {
g.logger.ComponentError(logging.ComponentGeneral, "failed to get pin status", zap.Error(err), zap.String("cid", path)) g.logger.ComponentError(logging.ComponentGeneral, "failed to get pin status", zap.Error(err), zap.String("cid", path))
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get status: %v", err)) errStr := strings.ToLower(err.Error())
if strings.Contains(errStr, "not found") || strings.Contains(errStr, "404") || strings.Contains(errStr, "invalid") {
writeError(w, http.StatusNotFound, fmt.Sprintf("pin not found: %s", path))
} else {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get status: %v", err))
}
return return
} }
@ -283,7 +288,8 @@ func (g *Gateway) storageGetHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
g.logger.ComponentError(logging.ComponentGeneral, "failed to get content from IPFS", zap.Error(err), zap.String("cid", path)) g.logger.ComponentError(logging.ComponentGeneral, "failed to get content from IPFS", zap.Error(err), zap.String("cid", path))
// Check if error indicates content not found (404) // Check if error indicates content not found (404)
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "status 404") { errStr := strings.ToLower(err.Error())
if strings.Contains(errStr, "not found") || strings.Contains(errStr, "404") || strings.Contains(errStr, "invalid") {
writeError(w, http.StatusNotFound, fmt.Sprintf("content not found: %s", path)) writeError(w, http.StatusNotFound, fmt.Sprintf("content not found: %s", path))
} else { } else {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get content: %v", err)) writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get content: %v", err))

View File

@ -570,9 +570,13 @@ func (g *HTTPGateway) handleDropTable(w http.ResponseWriter, r *http.Request) {
ctx, cancel := g.withTimeout(r.Context()) ctx, cancel := g.withTimeout(r.Context())
defer cancel() defer cancel()
stmt := "DROP TABLE IF EXISTS " + tbl stmt := "DROP TABLE " + tbl
if _, err := g.Client.Exec(ctx, stmt); err != nil { if _, err := g.Client.Exec(ctx, stmt); err != nil {
writeError(w, http.StatusInternalServerError, err.Error()) if strings.Contains(err.Error(), "no such table") {
writeError(w, http.StatusNotFound, err.Error())
} else {
writeError(w, http.StatusInternalServerError, err.Error())
}
return return
} }
writeJSON(w, http.StatusOK, map[string]any{"status": "ok"}) writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})

View File

@ -0,0 +1,151 @@
package serverless
import (
"context"
"os"
"testing"
"go.uber.org/zap"
)
func TestEngine_Execute(t *testing.T) {
logger := zap.NewNop()
registry := NewMockRegistry()
hostServices := NewMockHostServices()
cfg := DefaultConfig()
cfg.ModuleCacheSize = 2
engine, err := NewEngine(cfg, registry, hostServices, logger)
if err != nil {
t.Fatalf("failed to create engine: %v", err)
}
defer engine.Close(context.Background())
// Use a minimal valid WASM module that exports _start (WASI)
// This is just 'nop' in WASM
wasmBytes := []byte{
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
0x03, 0x02, 0x01, 0x00,
0x07, 0x0a, 0x01, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x00,
0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b,
}
fnDef := &FunctionDefinition{
Name: "test-func",
Namespace: "test-ns",
MemoryLimitMB: 64,
TimeoutSeconds: 5,
}
err = registry.Register(context.Background(), fnDef, wasmBytes)
if err != nil {
t.Fatalf("failed to register function: %v", err)
}
fn, err := registry.Get(context.Background(), "test-ns", "test-func", 0)
if err != nil {
t.Fatalf("failed to get function: %v", err)
}
// Execute function
ctx := context.Background()
output, err := engine.Execute(ctx, fn, []byte("input"), nil)
if err != nil {
t.Errorf("failed to execute function: %v", err)
}
// Our minimal WASM doesn't write to stdout, so output should be empty
if len(output) != 0 {
t.Errorf("expected empty output, got %d bytes", len(output))
}
// Test cache stats
size, capacity := engine.GetCacheStats()
if size != 1 {
t.Errorf("expected cache size 1, got %d", size)
}
if capacity != 2 {
t.Errorf("expected cache capacity 2, got %d", capacity)
}
// Test Invalidate
engine.Invalidate(fn.WASMCID)
size, _ = engine.GetCacheStats()
if size != 0 {
t.Errorf("expected cache size 0 after invalidation, got %d", size)
}
}
func TestEngine_Precompile(t *testing.T) {
logger := zap.NewNop()
registry := NewMockRegistry()
hostServices := NewMockHostServices()
engine, _ := NewEngine(nil, registry, hostServices, logger)
defer engine.Close(context.Background())
wasmBytes := []byte{
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
0x03, 0x02, 0x01, 0x00,
0x07, 0x0a, 0x01, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x00,
0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b,
}
err := engine.Precompile(context.Background(), "test-cid", wasmBytes)
if err != nil {
t.Fatalf("failed to precompile: %v", err)
}
size, _ := engine.GetCacheStats()
if size != 1 {
t.Errorf("expected cache size 1, got %d", size)
}
}
func TestEngine_Timeout(t *testing.T) {
// Skip this for now as it might be hard to trigger with a minimal WASM
// but we could try a WASM that loops forever.
t.Skip("Hard to trigger timeout with minimal WASM")
}
func TestEngine_RealWASM(t *testing.T) {
wasmPath := "../../examples/functions/bin/hello.wasm"
if _, err := os.Stat(wasmPath); os.IsNotExist(err) {
t.Skip("hello.wasm not found")
}
wasmBytes, err := os.ReadFile(wasmPath)
if err != nil {
t.Fatalf("failed to read hello.wasm: %v", err)
}
logger := zap.NewNop()
registry := NewMockRegistry()
hostServices := NewMockHostServices()
engine, _ := NewEngine(nil, registry, hostServices, logger)
defer engine.Close(context.Background())
fnDef := &FunctionDefinition{
Name: "hello",
Namespace: "examples",
TimeoutSeconds: 10,
}
_ = registry.Register(context.Background(), fnDef, wasmBytes)
fn, _ := registry.Get(context.Background(), "examples", "hello", 0)
output, err := engine.Execute(context.Background(), fn, []byte(`{"name": "Tester"}`), nil)
if err != nil {
t.Fatalf("execution failed: %v", err)
}
expected := "Hello, Tester!"
if !contains(string(output), expected) {
t.Errorf("output %q does not contain %q", string(output), expected)
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s[:len(substr)] == substr || contains(s[1:], substr))
}

View File

@ -0,0 +1,45 @@
package serverless
import (
"context"
"testing"
"go.uber.org/zap"
)
func TestHostFunctions_Cache(t *testing.T) {
db := NewMockRQLite()
ipfs := NewMockIPFSClient()
logger := zap.NewNop()
// MockOlricClient needs to implement olriclib.Client
// For now, let's just test other host functions if Olric is hard to mock
h := NewHostFunctions(db, nil, ipfs, nil, nil, nil, HostFunctionsConfig{}, logger)
ctx := context.Background()
h.SetInvocationContext(&InvocationContext{
RequestID: "req-1",
Namespace: "ns-1",
})
// Test Logging
h.LogInfo(ctx, "hello world")
logs := h.GetLogs()
if len(logs) != 1 || logs[0].Message != "hello world" {
t.Errorf("unexpected logs: %+v", logs)
}
// Test Storage
cid, err := h.StoragePut(ctx, []byte("data"))
if err != nil {
t.Fatalf("StoragePut failed: %v", err)
}
data, err := h.StorageGet(ctx, cid)
if err != nil {
t.Fatalf("StorageGet failed: %v", err)
}
if string(data) != "data" {
t.Errorf("expected 'data', got %q", string(data))
}
}

View File

@ -0,0 +1,375 @@
package serverless
import (
"context"
"database/sql"
"fmt"
"io"
"reflect"
"strings"
"sync"
"time"
"github.com/DeBrosOfficial/network/pkg/ipfs"
"github.com/DeBrosOfficial/network/pkg/rqlite"
)
// MockRegistry is a mock implementation of FunctionRegistry
type MockRegistry struct {
mu sync.RWMutex
functions map[string]*Function
wasm map[string][]byte
}
func NewMockRegistry() *MockRegistry {
return &MockRegistry{
functions: make(map[string]*Function),
wasm: make(map[string][]byte),
}
}
func (m *MockRegistry) Register(ctx context.Context, fn *FunctionDefinition, wasmBytes []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
id := fn.Namespace + "/" + fn.Name
wasmCID := "cid-" + id
m.functions[id] = &Function{
ID: id,
Name: fn.Name,
Namespace: fn.Namespace,
WASMCID: wasmCID,
MemoryLimitMB: fn.MemoryLimitMB,
TimeoutSeconds: fn.TimeoutSeconds,
Status: FunctionStatusActive,
}
m.wasm[wasmCID] = wasmBytes
return nil
}
func (m *MockRegistry) Get(ctx context.Context, namespace, name string, version int) (*Function, error) {
m.mu.RLock()
defer m.mu.RUnlock()
fn, ok := m.functions[namespace+"/"+name]
if !ok {
return nil, ErrFunctionNotFound
}
return fn, nil
}
func (m *MockRegistry) List(ctx context.Context, namespace string) ([]*Function, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var res []*Function
for _, fn := range m.functions {
if fn.Namespace == namespace {
res = append(res, fn)
}
}
return res, nil
}
func (m *MockRegistry) Delete(ctx context.Context, namespace, name string, version int) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.functions, namespace+"/"+name)
return nil
}
func (m *MockRegistry) GetWASMBytes(ctx context.Context, wasmCID string) ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, ok := m.wasm[wasmCID]
if !ok {
return nil, ErrFunctionNotFound
}
return data, nil
}
// MockHostServices is a mock implementation of HostServices
type MockHostServices struct {
mu sync.RWMutex
cache map[string][]byte
storage map[string][]byte
logs []string
}
func NewMockHostServices() *MockHostServices {
return &MockHostServices{
cache: make(map[string][]byte),
storage: make(map[string][]byte),
}
}
func (m *MockHostServices) DBQuery(ctx context.Context, query string, args []interface{}) ([]byte, error) {
return []byte("[]"), nil
}
func (m *MockHostServices) DBExecute(ctx context.Context, query string, args []interface{}) (int64, error) {
return 0, nil
}
func (m *MockHostServices) CacheGet(ctx context.Context, key string) ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.cache[key], nil
}
func (m *MockHostServices) CacheSet(ctx context.Context, key string, value []byte, ttl int64) error {
m.mu.Lock()
defer m.mu.Unlock()
m.cache[key] = value
return nil
}
func (m *MockHostServices) CacheDelete(ctx context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.cache, key)
return nil
}
func (m *MockHostServices) StoragePut(ctx context.Context, data []byte) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
cid := "cid-" + time.Now().String()
m.storage[cid] = data
return cid, nil
}
func (m *MockHostServices) StorageGet(ctx context.Context, cid string) ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.storage[cid], nil
}
func (m *MockHostServices) PubSubPublish(ctx context.Context, topic string, data []byte) error {
return nil
}
func (m *MockHostServices) WSSend(ctx context.Context, clientID string, data []byte) error {
return nil
}
func (m *MockHostServices) WSBroadcast(ctx context.Context, topic string, data []byte) error {
return nil
}
func (m *MockHostServices) HTTPFetch(ctx context.Context, method, url string, headers map[string]string, body []byte) ([]byte, error) {
return nil, nil
}
func (m *MockHostServices) GetEnv(ctx context.Context, key string) (string, error) {
return "", nil
}
func (m *MockHostServices) GetSecret(ctx context.Context, name string) (string, error) {
return "", nil
}
func (m *MockHostServices) GetRequestID(ctx context.Context) string {
return "req-123"
}
func (m *MockHostServices) GetCallerWallet(ctx context.Context) string {
return "wallet-123"
}
func (m *MockHostServices) EnqueueBackground(ctx context.Context, functionName string, payload []byte) (string, error) {
return "job-123", nil
}
func (m *MockHostServices) ScheduleOnce(ctx context.Context, functionName string, runAt time.Time, payload []byte) (string, error) {
return "timer-123", nil
}
func (m *MockHostServices) LogInfo(ctx context.Context, message string) {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = append(m.logs, "INFO: "+message)
}
func (m *MockHostServices) LogError(ctx context.Context, message string) {
m.mu.Lock()
defer m.mu.Unlock()
m.logs = append(m.logs, "ERROR: "+message)
}
// MockIPFSClient is a mock for ipfs.IPFSClient
type MockIPFSClient struct {
data map[string][]byte
}
func NewMockIPFSClient() *MockIPFSClient {
return &MockIPFSClient{data: make(map[string][]byte)}
}
func (m *MockIPFSClient) Add(ctx context.Context, reader io.Reader, filename string) (*ipfs.AddResponse, error) {
data, _ := io.ReadAll(reader)
cid := "cid-" + filename
m.data[cid] = data
return &ipfs.AddResponse{Cid: cid, Name: filename}, nil
}
func (m *MockIPFSClient) Pin(ctx context.Context, cid string, name string, replicationFactor int) (*ipfs.PinResponse, error) {
return &ipfs.PinResponse{Cid: cid, Name: name}, nil
}
func (m *MockIPFSClient) PinStatus(ctx context.Context, cid string) (*ipfs.PinStatus, error) {
return &ipfs.PinStatus{Cid: cid, Status: "pinned"}, nil
}
func (m *MockIPFSClient) Get(ctx context.Context, cid, apiURL string) (io.ReadCloser, error) {
data, ok := m.data[cid]
if !ok {
return nil, fmt.Errorf("not found")
}
return io.NopCloser(strings.NewReader(string(data))), nil
}
func (m *MockIPFSClient) Unpin(ctx context.Context, cid string) error { return nil }
func (m *MockIPFSClient) Health(ctx context.Context) error { return nil }
func (m *MockIPFSClient) GetPeerCount(ctx context.Context) (int, error) { return 1, nil }
func (m *MockIPFSClient) Close(ctx context.Context) error { return nil }
// MockRQLite is a mock implementation of rqlite.Client
type MockRQLite struct {
mu sync.Mutex
tables map[string][]map[string]any
}
func NewMockRQLite() *MockRQLite {
return &MockRQLite{
tables: make(map[string][]map[string]any),
}
}
func (m *MockRQLite) Query(ctx context.Context, dest any, query string, args ...any) error {
m.mu.Lock()
defer m.mu.Unlock()
// Very limited mock query logic for scanning into structs
if strings.Contains(query, "FROM functions") {
rows := m.tables["functions"]
filtered := rows
if strings.Contains(query, "namespace = ? AND name = ?") {
ns := args[0].(string)
name := args[1].(string)
filtered = nil
for _, r := range rows {
if r["namespace"] == ns && r["name"] == name {
filtered = append(filtered, r)
}
}
}
destVal := reflect.ValueOf(dest).Elem()
if destVal.Kind() == reflect.Slice {
elemType := destVal.Type().Elem()
for _, r := range filtered {
newElem := reflect.New(elemType).Elem()
// This is a simplified mapping
if f := newElem.FieldByName("ID"); f.IsValid() {
f.SetString(r["id"].(string))
}
if f := newElem.FieldByName("Name"); f.IsValid() {
f.SetString(r["name"].(string))
}
if f := newElem.FieldByName("Namespace"); f.IsValid() {
f.SetString(r["namespace"].(string))
}
destVal.Set(reflect.Append(destVal, newElem))
}
}
}
return nil
}
func (m *MockRQLite) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) {
m.mu.Lock()
defer m.mu.Unlock()
return &mockResult{}, nil
}
func (m *MockRQLite) FindBy(ctx context.Context, dest any, table string, criteria map[string]any, opts ...rqlite.FindOption) error {
return nil
}
func (m *MockRQLite) FindOneBy(ctx context.Context, dest any, table string, criteria map[string]any, opts ...rqlite.FindOption) error {
return nil
}
func (m *MockRQLite) Save(ctx context.Context, entity any) error { return nil }
func (m *MockRQLite) Remove(ctx context.Context, entity any) error { return nil }
func (m *MockRQLite) Repository(table string) any { return nil }
func (m *MockRQLite) CreateQueryBuilder(table string) *rqlite.QueryBuilder {
return nil // Should return a valid QueryBuilder if needed by tests
}
func (m *MockRQLite) Tx(ctx context.Context, fn func(tx rqlite.Tx) error) error {
return nil
}
type mockResult struct{}
func (m *mockResult) LastInsertId() (int64, error) { return 1, nil }
func (m *mockResult) RowsAffected() (int64, error) { return 1, nil }
// MockOlricClient is a mock for olriclib.Client
type MockOlricClient struct {
dmaps map[string]*MockDMap
}
func NewMockOlricClient() *MockOlricClient {
return &MockOlricClient{dmaps: make(map[string]*MockDMap)}
}
func (m *MockOlricClient) NewDMap(name string) (any, error) {
if dm, ok := m.dmaps[name]; ok {
return dm, nil
}
dm := &MockDMap{data: make(map[string][]byte)}
m.dmaps[name] = dm
return dm, nil
}
func (m *MockOlricClient) Close(ctx context.Context) error { return nil }
func (m *MockOlricClient) Stats(ctx context.Context, s string) ([]byte, error) { return nil, nil }
func (m *MockOlricClient) Ping(ctx context.Context, s string) error { return nil }
func (m *MockOlricClient) RoutingTable(ctx context.Context) (map[uint64][]string, error) {
return nil, nil
}
// MockDMap is a mock for olriclib.DMap
type MockDMap struct {
data map[string][]byte
}
func (m *MockDMap) Get(ctx context.Context, key string) (any, error) {
val, ok := m.data[key]
if !ok {
return nil, fmt.Errorf("not found")
}
return &MockGetResponse{val: val}, nil
}
func (m *MockDMap) Put(ctx context.Context, key string, value any) error {
switch v := value.(type) {
case []byte:
m.data[key] = v
case string:
m.data[key] = []byte(v)
}
return nil
}
func (m *MockDMap) Delete(ctx context.Context, key string) (bool, error) {
_, ok := m.data[key]
delete(m.data, key)
return ok, nil
}
type MockGetResponse struct {
val []byte
}
func (m *MockGetResponse) Byte() ([]byte, error) { return m.val, nil }
func (m *MockGetResponse) String() (string, error) { return string(m.val), nil }

View File

@ -0,0 +1,41 @@
package serverless
import (
"context"
"testing"
"go.uber.org/zap"
)
func TestRegistry_RegisterAndGet(t *testing.T) {
db := NewMockRQLite()
ipfs := NewMockIPFSClient()
logger := zap.NewNop()
registry := NewRegistry(db, ipfs, RegistryConfig{IPFSAPIURL: "http://localhost:5001"}, logger)
ctx := context.Background()
fnDef := &FunctionDefinition{
Name: "test-func",
Namespace: "test-ns",
IsPublic: true,
}
wasmBytes := []byte("mock wasm")
err := registry.Register(ctx, fnDef, wasmBytes)
if err != nil {
t.Fatalf("Register failed: %v", err)
}
// Since MockRQLite doesn't fully implement Query scanning yet,
// we won't be able to test Get() effectively without more work.
// But we can check if wasm was uploaded.
wasm, err := registry.GetWASMBytes(ctx, "cid-test-func.wasm")
if err != nil {
t.Fatalf("GetWASMBytes failed: %v", err)
}
if string(wasm) != "mock wasm" {
t.Errorf("expected 'mock wasm', got %q", string(wasm))
}
}

View File

@ -1,53 +0,0 @@
#!/bin/bash
# Setup local domains for DeBros Network development
# Adds entries to /etc/hosts for node-1.local through node-5.local
# Maps them to 127.0.0.1 for local development
set -e
HOSTS_FILE="/etc/hosts"
NODES=("node-1" "node-2" "node-3" "node-4" "node-5")
# Check if we have sudo access
if [ "$EUID" -ne 0 ]; then
echo "This script requires sudo to modify /etc/hosts"
echo "Please run: sudo bash scripts/setup-local-domains.sh"
exit 1
fi
# Function to add or update domain entry
add_domain() {
local domain=$1
local ip="127.0.0.1"
# Check if domain already exists
if grep -q "^[[:space:]]*$ip[[:space:]]\+$domain" "$HOSTS_FILE"; then
echo "$domain already configured"
return 0
fi
# Add domain to /etc/hosts
echo "$ip $domain" >> "$HOSTS_FILE"
echo "✓ Added $domain -> $ip"
}
echo "Setting up local domains for DeBros Network..."
echo ""
# Add each node domain
for node in "${NODES[@]}"; do
add_domain "${node}.local"
done
echo ""
echo "✓ Local domains configured successfully!"
echo ""
echo "You can now access nodes via:"
for node in "${NODES[@]}"; do
echo " - ${node}.local (HTTP Gateway)"
done
echo ""
echo "Example: curl http://node-1.local:8080/rqlite/http/db/status"

View File

@ -1,85 +0,0 @@
#!/bin/bash
# Test local domain routing for DeBros Network
# Validates that all HTTP gateway routes are working
set -e
NODES=("1" "2" "3" "4" "5")
GATEWAY_PORTS=(8080 8081 8082 8083 8084)
# Color codes
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Counters
PASSED=0
FAILED=0
# Test a single endpoint
test_endpoint() {
local node=$1
local port=$2
local path=$3
local description=$4
local url="http://node-${node}.local:${port}${path}"
printf "Testing %-50s ... " "$description"
if curl -s -f "$url" > /dev/null 2>&1; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
return 0
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
return 1
fi
}
echo "=========================================="
echo "DeBros Network Local Domain Tests"
echo "=========================================="
echo ""
# Test each node's HTTP gateway
for i in "${!NODES[@]}"; do
node=${NODES[$i]}
port=${GATEWAY_PORTS[$i]}
echo "Testing node-${node}.local (port ${port}):"
# Test health endpoint
test_endpoint "$node" "$port" "/health" "Node-${node} health check"
# Test RQLite HTTP endpoint
test_endpoint "$node" "$port" "/rqlite/http/db/execute" "Node-${node} RQLite HTTP"
# Test IPFS API endpoint (may fail if IPFS not running, but at least connection should work)
test_endpoint "$node" "$port" "/ipfs/api/v0/version" "Node-${node} IPFS API" || true
# Test Cluster API endpoint (may fail if Cluster not running, but at least connection should work)
test_endpoint "$node" "$port" "/cluster/health" "Node-${node} Cluster API" || true
echo ""
done
# Summary
echo "=========================================="
echo "Test Results"
echo "=========================================="
echo -e "${GREEN}Passed: $PASSED${NC}"
echo -e "${RED}Failed: $FAILED${NC}"
echo ""
if [ $FAILED -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed!${NC}"
exit 0
else
echo -e "${YELLOW}⚠ Some tests failed (this is expected if services aren't running)${NC}"
exit 1
fi