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
7.8 KiB
Go
251 lines
7.8 KiB
Go
package tests
|
|
|
|
// integration_http_test.go contains tests for backend behavior.
|
|
|
|
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 is a test helper.
|
|
func newInMemoryAuditRepo() *inMemoryAuditRepo {
|
|
return &inMemoryAuditRepo{entries: make([]audit.Entry, 0)}
|
|
}
|
|
|
|
// EnsureSchema is a test helper.
|
|
func (r *inMemoryAuditRepo) EnsureSchema(ctx context.Context) error { return nil }
|
|
|
|
// Append is a test helper.
|
|
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 is a test helper.
|
|
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 is a test helper.
|
|
func (r *inMemoryAuditRepo) Count(ctx context.Context) (int64, error) {
|
|
if r.countErr != nil {
|
|
return 0, r.countErr
|
|
}
|
|
return int64(len(r.entries)), nil
|
|
}
|
|
|
|
// PruneBefore is a test helper.
|
|
func (r *inMemoryAuditRepo) PruneBefore(ctx context.Context, before time.Time) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
// setupApp is a test helper.
|
|
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 verifies expected behavior.
|
|
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 verifies expected behavior.
|
|
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 verifies expected behavior.
|
|
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 verifies expected behavior.
|
|
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)
|
|
}
|