mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
Complete MPC wallet management platform rivaling Fireblocks/Utila/Fordefi: Backend (Go): - REST API with 60+ endpoints on chi router (pkg/api/) - PostgreSQL schema with 13 tables (pkg/db/) - JWT + MFA auth, bcrypt passwords, API key management - Policy engine (deny/approve/require_approval) - Transaction signing flow with approval workflow - Subscription scheduler for recurring payments - Payment requests with public pay pages - Safe + ERC-4337 smart wallet integration (pkg/smart/) - Rate limiting, webhook delivery (HMAC-SHA256), audit logging - 27 passing tests (auth, policy engine, middleware) Frontend (Next.js 14): - 27 routes with monochrome dark theme (Tailwind + Geist fonts) - Full API client with JWT auto-refresh - React Query hooks for all resources - Pages: dashboard, vaults, wallets, transactions, policies, whitelist, team, settings, API keys, webhooks, audit, subscriptions, payments, smart wallets Infrastructure: - Dockerfiles for API (Go) and dashboard (Next.js standalone) - compose.yml for local development (PostgreSQL + API + dashboard) - K8s manifests for mpc.lux.network + cloud.lux.network - GitHub Actions deploy workflow (build → push GHCR → kubectl apply) - TLS via cert-manager, Traefik IngressRoute for both domains
291 lines
7.6 KiB
Go
291 lines
7.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestAuthMiddleware_ValidToken(t *testing.T) {
|
|
s := newTestServer("middleware-secret")
|
|
|
|
token, err := s.generateJWT("user-mid", "org-mid", "admin")
|
|
if err != nil {
|
|
t.Fatalf("generateJWT: %v", err)
|
|
}
|
|
|
|
// Handler that checks context values were set
|
|
var gotUserID, gotOrgID, gotRole string
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotUserID = getUserID(r.Context())
|
|
gotOrgID = getOrgID(r.Context())
|
|
gotRole = getRole(r.Context())
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler := s.authMiddleware(inner)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if gotUserID != "user-mid" {
|
|
t.Errorf("UserID = %q, want %q", gotUserID, "user-mid")
|
|
}
|
|
if gotOrgID != "org-mid" {
|
|
t.Errorf("OrgID = %q, want %q", gotOrgID, "org-mid")
|
|
}
|
|
if gotRole != "admin" {
|
|
t.Errorf("Role = %q, want %q", gotRole, "admin")
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_NoToken(t *testing.T) {
|
|
s := newTestServer("middleware-secret")
|
|
|
|
called := false
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
})
|
|
|
|
handler := s.authMiddleware(inner)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
// No Authorization header, no X-API-Key header
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
|
|
}
|
|
if called {
|
|
t.Error("inner handler should not have been called")
|
|
}
|
|
|
|
var body map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if body["error"] != "missing authorization" {
|
|
t.Errorf("error = %q, want %q", body["error"], "missing authorization")
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_InvalidToken(t *testing.T) {
|
|
s := newTestServer("middleware-secret")
|
|
|
|
called := false
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
})
|
|
|
|
handler := s.authMiddleware(inner)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer invalid-token-data")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
|
|
}
|
|
if called {
|
|
t.Error("inner handler should not have been called")
|
|
}
|
|
|
|
var body map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if body["error"] != "invalid token" {
|
|
t.Errorf("error = %q, want %q", body["error"], "invalid token")
|
|
}
|
|
}
|
|
|
|
func TestRequireRole_Allowed(t *testing.T) {
|
|
called := false
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler := requireRole("admin", "operator")(inner)
|
|
|
|
// Set role=admin in context
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
ctx := context.WithValue(req.Context(), ctxRole, "admin")
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if !called {
|
|
t.Error("inner handler should have been called")
|
|
}
|
|
}
|
|
|
|
func TestRequireRole_Denied(t *testing.T) {
|
|
called := false
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
})
|
|
|
|
handler := requireRole("admin")(inner)
|
|
|
|
// Set role=viewer in context (not in allowed list)
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
ctx := context.WithValue(req.Context(), ctxRole, "viewer")
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
|
}
|
|
if called {
|
|
t.Error("inner handler should not have been called")
|
|
}
|
|
|
|
var body map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if body["error"] != "insufficient permissions" {
|
|
t.Errorf("error = %q, want %q", body["error"], "insufficient permissions")
|
|
}
|
|
}
|
|
|
|
func TestRequireRole_NoRoleInContext(t *testing.T) {
|
|
called := false
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
})
|
|
|
|
handler := requireRole("admin")(inner)
|
|
|
|
// No role in context at all
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
|
}
|
|
if called {
|
|
t.Error("inner handler should not have been called")
|
|
}
|
|
}
|
|
|
|
func TestRequireRole_MultipleAllowed(t *testing.T) {
|
|
called := false
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler := requireRole("admin", "operator", "api")(inner)
|
|
|
|
// role=operator is in the allowed list
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
ctx := context.WithValue(req.Context(), ctxRole, "operator")
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if !called {
|
|
t.Error("inner handler should have been called")
|
|
}
|
|
}
|
|
|
|
func TestContextHelpers(t *testing.T) {
|
|
// Empty context returns zero values
|
|
ctx := context.Background()
|
|
if v := getUserID(ctx); v != "" {
|
|
t.Errorf("getUserID(empty) = %q, want empty", v)
|
|
}
|
|
if v := getOrgID(ctx); v != "" {
|
|
t.Errorf("getOrgID(empty) = %q, want empty", v)
|
|
}
|
|
if v := getRole(ctx); v != "" {
|
|
t.Errorf("getRole(empty) = %q, want empty", v)
|
|
}
|
|
|
|
// With values
|
|
ctx = context.WithValue(ctx, ctxUserID, "u1")
|
|
ctx = context.WithValue(ctx, ctxOrgID, "o1")
|
|
ctx = context.WithValue(ctx, ctxRole, "admin")
|
|
if v := getUserID(ctx); v != "u1" {
|
|
t.Errorf("getUserID = %q, want %q", v, "u1")
|
|
}
|
|
if v := getOrgID(ctx); v != "o1" {
|
|
t.Errorf("getOrgID = %q, want %q", v, "o1")
|
|
}
|
|
if v := getRole(ctx); v != "admin" {
|
|
t.Errorf("getRole = %q, want %q", v, "admin")
|
|
}
|
|
}
|
|
|
|
func TestWriteJSON(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
writeJSON(rec, http.StatusCreated, map[string]string{"msg": "ok"})
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
|
|
t.Errorf("Content-Type = %q, want %q", ct, "application/json")
|
|
}
|
|
|
|
var body map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body["msg"] != "ok" {
|
|
t.Errorf("body[msg] = %q, want %q", body["msg"], "ok")
|
|
}
|
|
}
|
|
|
|
func TestWriteError(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
writeError(rec, http.StatusBadRequest, "bad input")
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
var body map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body["error"] != "bad input" {
|
|
t.Errorf("body[error] = %q, want %q", body["error"], "bad input")
|
|
}
|
|
}
|
|
|
|
func TestNilIfEmpty(t *testing.T) {
|
|
if v := nilIfEmpty(""); v != nil {
|
|
t.Errorf("nilIfEmpty(\"\") = %v, want nil", v)
|
|
}
|
|
if v := nilIfEmpty("hello"); v == nil || *v != "hello" {
|
|
t.Errorf("nilIfEmpty(\"hello\") = %v, want *\"hello\"", v)
|
|
}
|
|
}
|