You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
8.3 KiB
Go

package tests
// integration_http_test.go contains backend tests for package behavior, error paths, and regressions.
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v3"
"github.com/prometheus/client_golang/prometheus"
appadmin "knowfoolery/backend/services/admin-service/internal/application/admin"
"knowfoolery/backend/services/admin-service/internal/domain/audit"
httpapi "knowfoolery/backend/services/admin-service/internal/interfaces/http"
"knowfoolery/backend/shared/infra/auth/zitadel"
"knowfoolery/backend/shared/infra/observability/logging"
sharedmetrics "knowfoolery/backend/shared/infra/observability/metrics"
sharedhttpx "knowfoolery/backend/shared/testutil/httpx"
)
type inMemoryAuditRepo struct {
entries []audit.Entry
countErr error
listErr error
appendErr error
}
// newInMemoryAuditRepo creates in-memory test doubles and deterministic fixtures.
func newInMemoryAuditRepo() *inMemoryAuditRepo {
return &inMemoryAuditRepo{entries: make([]audit.Entry, 0)}
}
// EnsureSchema initializes schema state required before repository operations.
func (r *inMemoryAuditRepo) EnsureSchema(ctx context.Context) error { return nil }
// Append appends audit records to the in-memory repository.
func (r *inMemoryAuditRepo) Append(ctx context.Context, e audit.Entry) error {
if r.appendErr != nil {
return r.appendErr
}
r.entries = append(r.entries, e)
return nil
}
// List returns filtered collections from the in-memory repository.
func (r *inMemoryAuditRepo) List(ctx context.Context, limit, offset int) ([]audit.Entry, error) {
if r.listErr != nil {
return nil, r.listErr
}
if offset >= len(r.entries) {
return []audit.Entry{}, nil
}
end := offset + limit
if end > len(r.entries) {
end = len(r.entries)
}
out := make([]audit.Entry, end-offset)
copy(out, r.entries[offset:end])
return out, nil
}
// Count returns aggregate counts from the in-memory repository.
func (r *inMemoryAuditRepo) Count(ctx context.Context) (int64, error) {
if r.countErr != nil {
return 0, r.countErr
}
return int64(len(r.entries)), nil
}
// PruneBefore supports prune before test setup and assertions.
func (r *inMemoryAuditRepo) PruneBefore(ctx context.Context, before time.Time) (int64, error) {
return 0, nil
}
// setupApp wires the test application with mocked dependencies.
func setupApp(t *testing.T, repo *inMemoryAuditRepo) *fiber.App {
t.Helper()
svc := appadmin.NewService(repo, 90)
logger := logging.NewLogger(logging.DefaultConfig())
m := sharedmetrics.NewMetrics(sharedmetrics.Config{
ServiceName: "admin-service-test",
Enabled: true,
Registry: prometheus.NewRegistry(),
})
h := httpapi.NewHandler(svc, logger, m)
app := fiber.New()
auth := func(c fiber.Ctx) error {
switch c.Get("Authorization") {
case "Bearer admin":
c.Locals(string(zitadel.ContextKeyUserID), "admin-1")
c.Locals(string(zitadel.ContextKeyUserEmail), "admin@example.com")
c.Locals(string(zitadel.ContextKeyUserRoles), []string{"admin"})
c.Locals(string(zitadel.ContextKeyMFAVerified), true)
return c.Next()
case "Bearer player":
c.Locals(string(zitadel.ContextKeyUserID), "player-1")
c.Locals(string(zitadel.ContextKeyUserEmail), "player@example.com")
c.Locals(string(zitadel.ContextKeyUserRoles), []string{"player"})
c.Locals(string(zitadel.ContextKeyMFAVerified), false)
return c.Next()
default:
return c.SendStatus(http.StatusUnauthorized)
}
}
httpapi.RegisterRoutes(app, h, auth)
return app
}
// TestAdminAuthRoute ensures admin auth route behavior is handled correctly.
func TestAdminAuthRoute(t *testing.T) {
repo := newInMemoryAuditRepo()
app := setupApp(t, repo)
req := httptest.NewRequest(http.MethodPost, "/admin/auth", nil)
resp := sharedhttpx.MustTest(t, app, req)
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusUnauthorized, "expected unauthorized without token")
defer resp.Body.Close()
req = httptest.NewRequest(http.MethodPost, "/admin/auth", nil)
req.Header.Set("Authorization", "Bearer admin")
resp = sharedhttpx.MustTest(t, app, req)
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusOK, "expected admin auth success")
defer resp.Body.Close()
if len(repo.entries) == 0 {
t.Fatalf("expected audit entries after /admin/auth")
}
}
// TestDashboardRouteSuccessAndErrors ensures dashboard route success and errors behavior is handled correctly.
func TestDashboardRouteSuccessAndErrors(t *testing.T) {
t.Run("forbidden for non-admin", func(t *testing.T) {
repo := newInMemoryAuditRepo()
app := setupApp(t, repo)
req := httptest.NewRequest(http.MethodGet, "/admin/dashboard", nil)
req.Header.Set("Authorization", "Bearer player")
resp := sharedhttpx.MustTest(t, app, req)
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusForbidden, "expected player forbidden")
defer resp.Body.Close()
})
t.Run("success for admin", func(t *testing.T) {
repo := newInMemoryAuditRepo()
repo.entries = append(repo.entries, audit.Entry{ID: "seed"})
app := setupApp(t, repo)
req := httptest.NewRequest(http.MethodGet, "/admin/dashboard", nil)
req.Header.Set("Authorization", "Bearer admin")
resp := sharedhttpx.MustTest(t, app, req)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var body map[string]any
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatalf("decode response: %v", err)
}
if _, ok := body["audit_entries_total"]; !ok {
t.Fatalf("expected audit_entries_total in dashboard response")
}
})
t.Run("internal error when count fails", func(t *testing.T) {
repo := newInMemoryAuditRepo()
repo.countErr = errors.New("boom")
app := setupApp(t, repo)
req := httptest.NewRequest(http.MethodGet, "/admin/dashboard", nil)
req.Header.Set("Authorization", "Bearer admin")
resp := sharedhttpx.MustTest(t, app, req)
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusInternalServerError, "expected dashboard error mapping")
defer resp.Body.Close()
})
}
// TestAuditRouteSuccessAndErrors ensures audit route success and errors behavior is handled correctly.
func TestAuditRouteSuccessAndErrors(t *testing.T) {
t.Run("forbidden for non-admin", func(t *testing.T) {
repo := newInMemoryAuditRepo()
app := setupApp(t, repo)
req := httptest.NewRequest(http.MethodGet, "/admin/audit", nil)
req.Header.Set("Authorization", "Bearer player")
resp := sharedhttpx.MustTest(t, app, req)
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusForbidden, "expected player forbidden")
defer resp.Body.Close()
})
t.Run("success with pagination", func(t *testing.T) {
repo := newInMemoryAuditRepo()
repo.entries = append(
repo.entries,
audit.Entry{ID: "1", Action: "admin.auth"},
audit.Entry{ID: "2", Action: "admin.dashboard.view"},
)
app := setupApp(t, repo)
req := httptest.NewRequest(http.MethodGet, "/admin/audit?limit=1&offset=0", nil)
req.Header.Set("Authorization", "Bearer admin")
resp := sharedhttpx.MustTest(t, app, req)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var body map[string]any
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatalf("decode response: %v", err)
}
if body["limit"] != float64(1) {
t.Fatalf("expected limit=1, got %v", body["limit"])
}
})
t.Run("internal error when list fails", func(t *testing.T) {
repo := newInMemoryAuditRepo()
repo.listErr = errors.New("boom")
app := setupApp(t, repo)
req := httptest.NewRequest(http.MethodGet, "/admin/audit", nil)
req.Header.Set("Authorization", "Bearer admin")
resp := sharedhttpx.MustTest(t, app, req)
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusInternalServerError, "expected audit list error mapping")
defer resp.Body.Close()
})
}
// TestRegisterRoutesDoesNotPanic ensures register routes does not panic behavior is handled correctly.
func TestRegisterRoutesDoesNotPanic(t *testing.T) {
app := fiber.New()
repo := newInMemoryAuditRepo()
svc := appadmin.NewService(repo, 90)
logger := logging.NewLogger(logging.DefaultConfig())
m := sharedmetrics.NewMetrics(sharedmetrics.Config{
ServiceName: "admin-service-test",
Enabled: true,
Registry: prometheus.NewRegistry(),
})
h := httpapi.NewHandler(svc, logger, m)
httpapi.RegisterRoutes(app, h, nil)
}