1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/handler/admin_handler_test.go
2026-06-12 18:36:18 +09:00

335 lines
10 KiB
Go

package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/require"
)
type fakeRPUsageQueryRepo struct {
query domain.RPUsageQuery
items []domain.RPUsageDailyMetric
}
func (f *fakeRPUsageQueryRepo) FindRPUsage(ctx context.Context, query domain.RPUsageQuery) ([]domain.RPUsageDailyMetric, error) {
f.query = query
return f.items, nil
}
type fakeAdminKeto struct {
allowed bool
subject string
object string
relation string
}
func (f *fakeAdminKeto) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) {
f.subject = subject
f.object = object
f.relation = relation
return f.allowed, nil
}
func (f *fakeAdminKeto) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
return nil
}
func (f *fakeAdminKeto) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
return nil
}
func (f *fakeAdminKeto) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
return nil, nil
}
func (f *fakeAdminKeto) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
return nil, nil
}
type fakeOverviewAuditRepo struct {
mockAuditRepo
since time.Time
count int64
}
func (f *fakeOverviewAuditRepo) CountEventsSince(ctx context.Context, since time.Time) (int64, error) {
f.since = since
return f.count, nil
}
type fakeAdminUserProjectionRepo struct {
status domain.UserProjectionStatus
}
func (f *fakeAdminUserProjectionRepo) IsReady(ctx context.Context) (bool, error) {
return f.status.Ready, nil
}
func (f *fakeAdminUserProjectionRepo) CountTenantMembers(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
return nil, nil
}
func (f *fakeAdminUserProjectionRepo) CountTenantMembersRecursive(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
return nil, nil
}
func (f *fakeAdminUserProjectionRepo) ReplaceAllFromKratos(ctx context.Context, users []domain.User) error {
return nil
}
func (f *fakeAdminUserProjectionRepo) MarkFailed(ctx context.Context, syncErr error) error {
return nil
}
func (f *fakeAdminUserProjectionRepo) GetStatus(ctx context.Context) (domain.UserProjectionStatus, error) {
return f.status, nil
}
type fakeIdentityCacheAdmin struct {
status domain.IdentityCacheStatus
flush domain.IdentityCacheFlushResult
err error
statusHit int
flushCalls int
}
func (f *fakeIdentityCacheAdmin) GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error) {
f.statusHit++
return f.status, f.err
}
func (f *fakeIdentityCacheAdmin) FlushIdentityCache(ctx context.Context) (domain.IdentityCacheFlushResult, error) {
f.flushCalls++
return f.flush, f.err
}
func TestAdminHandler_GetRPUsageDaily(t *testing.T) {
repo := &fakeRPUsageQueryRepo{
items: []domain.RPUsageDailyMetric{
{
Date: "2026-05-06",
TenantID: "tenant-1",
TenantType: domain.TenantTypeCompany,
ClientID: "orgfront",
ClientName: "OrgFront",
LoginRequests: 12,
OtherRequests: 4,
UniqueSubjects: 8,
},
},
}
h := &AdminHandler{RPUsageQueries: repo}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin})
return c.Next()
})
app.Get("/api/v1/admin/rp-usage/daily", h.GetRPUsageDaily)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/rp-usage/daily?days=7&period=week&tenantId=tenant-1", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, 7, repo.query.Days)
require.Equal(t, "week", repo.query.Period)
require.Equal(t, "tenant-1", repo.query.TenantID)
var body struct {
Items []domain.RPUsageDailyMetric `json:"items"`
Days int `json:"days"`
Period string `json:"period"`
TenantID string `json:"tenantId"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, 7, body.Days)
require.Equal(t, "week", body.Period)
require.Equal(t, "tenant-1", body.TenantID)
require.Len(t, body.Items, 1)
require.Equal(t, "orgfront", body.Items[0].ClientID)
require.Equal(t, uint64(12), body.Items[0].LoginRequests)
}
func TestAdminHandler_UserProjectionStatusRequiresSuperAdmin(t *testing.T) {
h := &AdminHandler{
UserProjectionRepo: &fakeAdminUserProjectionRepo{
status: domain.UserProjectionStatus{Name: domain.UserProjectionNameKratos, Status: domain.UserProjectionStatusReady, Ready: true},
},
}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{ID: "tenant-admin", Role: "tenant_admin"})
return c.Next()
})
app.Get("/api/v1/admin/projections/users", h.GetUserProjectionStatus)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/projections/users", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
}
func TestAdminHandler_UserProjectionStatusReturnsProjectionStateForSuperAdmin(t *testing.T) {
syncedAt := time.Date(2026, 5, 11, 3, 0, 0, 0, time.UTC)
h := &AdminHandler{
UserProjectionRepo: &fakeAdminUserProjectionRepo{
status: domain.UserProjectionStatus{
Name: domain.UserProjectionNameKratos,
Status: domain.UserProjectionStatusReady,
Ready: true,
LastSyncedAt: &syncedAt,
ProjectedUsers: 152,
},
},
}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin})
return c.Next()
})
app.Get("/api/v1/admin/projections/users", h.GetUserProjectionStatus)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/projections/users", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var body domain.UserProjectionStatus
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, domain.UserProjectionNameKratos, body.Name)
require.Equal(t, domain.UserProjectionStatusReady, body.Status)
require.True(t, body.Ready)
require.Equal(t, int64(152), body.ProjectedUsers)
}
func TestAdminHandler_GetOrySSOTSystemStatusReturnsIdentityCacheOnly(t *testing.T) {
syncedAt := time.Date(2026, 5, 11, 3, 0, 0, 0, time.UTC)
cache := &fakeIdentityCacheAdmin{
status: domain.IdentityCacheStatus{
Status: "ready",
RedisReady: true,
ObservedCount: 151,
KeyCount: 153,
LastRefreshedAt: &syncedAt,
UpdatedAt: &syncedAt,
},
}
h := &AdminHandler{
IdentityCache: cache,
}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin})
return c.Next()
})
app.Get("/api/v1/admin/ory/ssot", h.GetOrySSOTSystemStatus)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/ory/ssot", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var body struct {
UserProjection *domain.UserProjectionStatus `json:"userProjection,omitempty"`
IdentityCache domain.IdentityCacheStatus `json:"identityCache"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Nil(t, body.UserProjection)
require.True(t, body.IdentityCache.RedisReady)
require.Equal(t, int64(151), body.IdentityCache.ObservedCount)
require.Equal(t, int64(153), body.IdentityCache.KeyCount)
require.Equal(t, 1, cache.statusHit)
}
func TestAdminHandler_FlushIdentityCacheRequiresSuperAdminAndFlushesCacheOnly(t *testing.T) {
cache := &fakeIdentityCacheAdmin{
flush: domain.IdentityCacheFlushResult{
Status: "success",
FlushedKeys: 7,
UpdatedAt: time.Date(2026, 5, 11, 3, 2, 0, 0, time.UTC),
},
}
h := &AdminHandler{
IdentityCache: cache,
}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin})
return c.Next()
})
app.Post("/api/v1/admin/ory/ssot/identity-cache/flush", h.FlushIdentityCache)
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/ory/ssot/identity-cache/flush", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var body domain.IdentityCacheFlushResult
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Equal(t, int64(7), body.FlushedKeys)
require.Equal(t, 1, cache.flushCalls)
}
func TestAdminHandler_GetRPUsageDailyChecksTenantPermission(t *testing.T) {
repo := &fakeRPUsageQueryRepo{}
keto := &fakeAdminKeto{allowed: true}
h := &AdminHandler{RPUsageQueries: repo, Keto: keto}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "user-1",
Role: "tenant_admin",
})
return c.Next()
})
app.Get("/api/v1/admin/rp-usage/daily", h.GetRPUsageDaily)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/rp-usage/daily?tenantId=tenant-allowed", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "User:user-1", keto.subject)
require.Equal(t, "tenant-allowed", keto.object)
require.Equal(t, "view_rp_usage_stats", keto.relation)
require.Equal(t, "tenant-allowed", repo.query.TenantID)
}
func TestAdminHandler_GetSystemStatsIncludesOverviewMetrics(t *testing.T) {
auditRepo := &fakeOverviewAuditRepo{count: 22}
h := &AdminHandler{
AuditRepo: auditRepo,
UserProjectionRepo: &fakeAdminUserProjectionRepo{
status: domain.UserProjectionStatus{
Name: domain.UserProjectionNameKratos,
Status: domain.UserProjectionStatusReady,
Ready: true,
ProjectedUsers: 152,
},
},
}
app := fiber.New()
app.Get("/api/v1/admin/stats", h.GetSystemStats)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/stats", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
require.Contains(t, body, "totalTenants")
require.Contains(t, body, "totalUsers")
require.Contains(t, body, "oidcClients")
require.Contains(t, body, "auditEvents24h")
require.Equal(t, float64(152), body["totalUsers"])
require.Equal(t, float64(22), body["auditEvents24h"])
require.Equal(t, time.UTC, auditRepo.since.Location())
}