Files
lijiaoqiao/supply-api/internal/app/bootstrap_test.go

267 lines
7.5 KiB
Go

package app
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"lijiaoqiao/supply-api/internal/adapter"
"lijiaoqiao/supply-api/internal/audit"
auditservice "lijiaoqiao/supply-api/internal/audit/service"
"lijiaoqiao/supply-api/internal/config"
"lijiaoqiao/supply-api/internal/domain"
"lijiaoqiao/supply-api/internal/httpapi"
"lijiaoqiao/supply-api/internal/middleware"
)
type testLogger struct{}
func (testLogger) Debug(string, ...map[string]interface{}) {}
func (testLogger) Info(string, ...map[string]interface{}) {}
func (testLogger) Warn(string, ...map[string]interface{}) {}
func (testLogger) Error(string, ...map[string]interface{}) {}
func (testLogger) Fatal(string, ...map[string]interface{}) {}
func TestBuildServer_ReturnsErrorWhenSupplyAPIMissing(t *testing.T) {
_, alertAPI := mustBuildTestAPIs(t)
srv, err := BuildServer(BuildServerOptions{
Env: "dev",
Logger: testLogger{},
AlertAPI: alertAPI,
})
if err == nil {
t.Fatal("expected missing supply api to fail")
}
if srv != nil {
t.Fatal("expected nil server when bootstrap options are invalid")
}
if !strings.Contains(err.Error(), "supply api") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestBuildServer_ProdRequiresAuthMiddleware(t *testing.T) {
supplyAPI, alertAPI := mustBuildTestAPIs(t)
srv, err := BuildServer(BuildServerOptions{
Env: "prod",
Logger: testLogger{},
SupplyAPI: supplyAPI,
AlertAPI: alertAPI,
})
if err == nil {
t.Fatal("expected prod bootstrap to require auth middleware")
}
if srv != nil {
t.Fatal("expected nil server when auth middleware is missing")
}
if !strings.Contains(err.Error(), "auth middleware") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestBuildServer_RejectsUnsupportedEnv(t *testing.T) {
supplyAPI, alertAPI := mustBuildTestAPIs(t)
srv, err := BuildServer(BuildServerOptions{
Env: "qa",
Logger: testLogger{},
SupplyAPI: supplyAPI,
AlertAPI: alertAPI,
})
if err == nil {
t.Fatal("expected unsupported env to fail")
}
if srv != nil {
t.Fatal("expected nil server when env is unsupported")
}
if !strings.Contains(err.Error(), "unsupported env") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestBuildServer_RegistersHealthRoute(t *testing.T) {
supplyAPI, alertAPI := mustBuildTestAPIs(t)
srv, err := BuildServer(BuildServerOptions{
Env: "dev",
Logger: testLogger{},
SupplyAPI: supplyAPI,
AlertAPI: alertAPI,
ServerConfig: config.ServerConfig{
Addr: "127.0.0.1:18082",
ReadTimeout: 2 * time.Second,
WriteTimeout: 3 * time.Second,
IdleTimeout: 4 * time.Second,
ShutdownTimeout: 5 * time.Second,
},
})
if err != nil {
t.Fatalf("BuildServer returned error: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/actuator/health", nil)
rec := httptest.NewRecorder()
srv.Handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("unexpected status: got=%d want=%d", rec.Code, http.StatusOK)
}
if !strings.Contains(rec.Body.String(), `"healthy"`) {
t.Fatalf("unexpected health body: %s", rec.Body.String())
}
}
func TestBuildServer_DefaultsTimeoutsWhenUnset(t *testing.T) {
supplyAPI, alertAPI := mustBuildTestAPIs(t)
srv, err := BuildServer(BuildServerOptions{
Env: "dev",
Logger: testLogger{},
SupplyAPI: supplyAPI,
AlertAPI: alertAPI,
})
if err != nil {
t.Fatalf("BuildServer returned error: %v", err)
}
if srv.Addr != ":18082" {
t.Fatalf("unexpected addr: %s", srv.Addr)
}
if srv.ReadHeaderTimeout != 10*time.Second {
t.Fatalf("unexpected read header timeout: %s", srv.ReadHeaderTimeout)
}
if srv.ReadTimeout != 10*time.Second {
t.Fatalf("unexpected read timeout: %s", srv.ReadTimeout)
}
if srv.WriteTimeout != 15*time.Second {
t.Fatalf("unexpected write timeout: %s", srv.WriteTimeout)
}
if srv.IdleTimeout != 30*time.Second {
t.Fatalf("unexpected idle timeout: %s", srv.IdleTimeout)
}
}
func TestBuildRouteMux_RegistersHealthAndSupplyRoutes(t *testing.T) {
supplyAPI, alertAPI := mustBuildTestAPIs(t)
mux := buildRouteMux(buildRouteMuxOptions{
SupplyAPI: supplyAPI,
AlertAPI: alertAPI,
})
healthReq := httptest.NewRequest(http.MethodGet, "/actuator/health", nil)
healthRec := httptest.NewRecorder()
mux.ServeHTTP(healthRec, healthReq)
if healthRec.Code != http.StatusOK {
t.Fatalf("unexpected health status: got=%d want=%d", healthRec.Code, http.StatusOK)
}
supplyReq := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts/verify", nil)
supplyRec := httptest.NewRecorder()
mux.ServeHTTP(supplyRec, supplyReq)
if supplyRec.Code != http.StatusMethodNotAllowed {
t.Fatalf("unexpected supply status: got=%d want=%d", supplyRec.Code, http.StatusMethodNotAllowed)
}
}
func mustBuildTestAPIs(t *testing.T) (*httpapi.SupplyAPI, *httpapi.AlertAPI) {
t.Helper()
auditStore := audit.NewMemoryAuditStore()
accountStore := adapter.NewInMemoryAccountStoreAdapter()
packageStore := adapter.NewInMemoryPackageStoreAdapter()
settlementStore := adapter.NewInMemorySettlementStoreAdapter()
earningStore := adapter.NewInMemoryEarningStoreAdapter()
supplyAPI, err := httpapi.NewSupplyAPI(
domain.NewAccountService(accountStore, auditStore),
domain.NewPackageService(packageStore, accountStore, auditStore),
domain.NewSettlementService(settlementStore, earningStore, auditStore),
domain.NewEarningService(earningStore),
nil,
auditStore,
nil,
1,
"https://statements.example.com",
func() time.Time { return time.Unix(1712800000, 0).UTC() },
)
if err != nil {
t.Fatalf("expected supply api constructor to succeed: %v", err)
}
alertAPI, err := httpapi.NewAlertAPI(auditservice.NewAlertService(auditservice.NewInMemoryAlertStore()))
if err != nil {
t.Fatalf("expected alert api constructor to succeed: %v", err)
}
return supplyAPI, alertAPI
}
func TestBuildServer_ProdBuildsAuthenticatedHandler(t *testing.T) {
supplyAPI, alertAPI := mustBuildTestAPIs(t)
authMiddleware := middleware.NewAuthMiddleware(
middleware.AuthConfig{
SecretKey: "bootstrap-test-secret-key",
Algorithm: "HS256",
Issuer: "bootstrap-test",
Enabled: true,
},
middleware.NewTokenCache(),
nil,
nil,
)
srv, err := BuildServer(BuildServerOptions{
Env: "prod",
Logger: testLogger{},
SupplyAPI: supplyAPI,
AlertAPI: alertAPI,
AuthMiddleware: authMiddleware,
})
if err != nil {
t.Fatalf("BuildServer returned error: %v", err)
}
if srv == nil || srv.Handler == nil {
t.Fatal("expected non-nil server and handler")
}
}
func TestBuildMiddlewareChain_ProdRejectsQueryKey(t *testing.T) {
authMiddleware := middleware.NewAuthMiddleware(
middleware.AuthConfig{
SecretKey: "bootstrap-test-secret-key",
Algorithm: "HS256",
Issuer: "bootstrap-test",
Enabled: true,
},
middleware.NewTokenCache(),
nil,
nil,
)
handler := buildMiddlewareChain(middlewareChainOptions{
Env: "prod",
Logger: testLogger{},
AuthMiddleware: authMiddleware,
RateLimitConfig: &middleware.RateLimitConfig{
Enabled: false,
},
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts?token=bad", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("unexpected status: got=%d want=%d", rec.Code, http.StatusUnauthorized)
}
if !strings.Contains(rec.Body.String(), "QUERY_KEY_NOT_ALLOWED") {
t.Fatalf("unexpected body: %s", rec.Body.String())
}
}