forked from baron/baron-sso
test code 수정
This commit is contained in:
@@ -274,7 +274,7 @@ func main() {
|
|||||||
consentRepo := repository.NewClientConsentRepository(db)
|
consentRepo := repository.NewClientConsentRepository(db)
|
||||||
|
|
||||||
auditHandler := handler.NewAuditHandler(auditRepo)
|
auditHandler := handler.NewAuditHandler(auditRepo)
|
||||||
authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, oathkeeperRepo, tenantService, ketoService, ketoOutboxRepo, userRepo, consentRepo)
|
authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, oathkeeperRepo, tenantService, ketoService, ketoOutboxRepo, userRepo, consentRepo, kratosAdminService)
|
||||||
adminHandler := handler.NewAdminHandler(ketoService)
|
adminHandler := handler.NewAdminHandler(ketoService)
|
||||||
devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService)
|
devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService)
|
||||||
tenantHandler := handler.NewTenantHandler(db, tenantService, ketoService, ketoOutboxRepo, kratosAdminService)
|
tenantHandler := handler.NewTenantHandler(db, tenantService, ketoService, ketoOutboxRepo, kratosAdminService)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ type AuthHandler struct {
|
|||||||
SmsService domain.SmsService
|
SmsService domain.SmsService
|
||||||
EmailService domain.EmailService
|
EmailService domain.EmailService
|
||||||
RedisService domain.RedisRepository
|
RedisService domain.RedisRepository
|
||||||
KratosAdmin *service.KratosAdminService
|
KratosAdmin service.KratosAdminService
|
||||||
IdpProvider domain.IdentityProvider
|
IdpProvider domain.IdentityProvider
|
||||||
AuditRepo domain.AuditRepository
|
AuditRepo domain.AuditRepository
|
||||||
OathkeeperRepo domain.OathkeeperLogRepository
|
OathkeeperRepo domain.OathkeeperLogRepository
|
||||||
@@ -148,12 +148,12 @@ func checkPollInterval(redis domain.RedisRepository, key string, interval time.D
|
|||||||
return false, int(interval.Seconds())
|
return false, int(interval.Seconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthHandler(redisService domain.RedisRepository, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository, consentRepo repository.ClientConsentRepository) *AuthHandler {
|
func NewAuthHandler(redisService domain.RedisRepository, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository, consentRepo repository.ClientConsentRepository, kratos service.KratosAdminService) *AuthHandler {
|
||||||
return &AuthHandler{
|
return &AuthHandler{
|
||||||
SmsService: service.NewSmsService(),
|
SmsService: service.NewSmsService(),
|
||||||
EmailService: service.NewEmailService(),
|
EmailService: service.NewEmailService(),
|
||||||
RedisService: redisService,
|
RedisService: redisService,
|
||||||
KratosAdmin: service.NewKratosAdminService(),
|
KratosAdmin: kratos,
|
||||||
IdpProvider: idpProvider,
|
IdpProvider: idpProvider,
|
||||||
AuditRepo: auditRepo,
|
AuditRepo: auditRepo,
|
||||||
OathkeeperRepo: oathkeeperRepo,
|
OathkeeperRepo: oathkeeperRepo,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func (m *AsyncMockUserRepo) Create(ctx context.Context, user *domain.User) error
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error { return nil }
|
func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error { return nil }
|
||||||
|
func (m *AsyncMockUserRepo) Delete(ctx context.Context, id string) error { return nil }
|
||||||
func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Test Helpers ---
|
// --- Test Helpers ---
|
||||||
@@ -112,12 +113,15 @@ func TestGetConsentRequest_Skip_AutoAccept(t *testing.T) {
|
|||||||
AdminURL: "http://hydra.test",
|
AdminURL: "http://hydra.test",
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
},
|
},
|
||||||
KratosAdmin: &service.KratosAdminService{
|
KratosAdmin: new(MockKratosAdminService), // Reusing MockKratosAdminService if defined or use MockKratosAdminServiceShared
|
||||||
AdminURL: "http://kratos.test",
|
|
||||||
HTTPClient: client,
|
|
||||||
},
|
|
||||||
ConsentRepo: consentRepo,
|
ConsentRepo: consentRepo,
|
||||||
}
|
}
|
||||||
|
h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-123").Return(&service.KratosIdentity{
|
||||||
|
ID: "user-123",
|
||||||
|
Traits: map[string]interface{}{
|
||||||
|
"email": "user@test.com",
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
app := newConsentTestApp(h)
|
app := newConsentTestApp(h)
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-skip", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-skip", nil)
|
||||||
@@ -172,13 +176,16 @@ func TestAcceptConsentRequest_Normal(t *testing.T) {
|
|||||||
AdminURL: "http://hydra.test",
|
AdminURL: "http://hydra.test",
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
},
|
},
|
||||||
KratosAdmin: &service.KratosAdminService{
|
KratosAdmin: new(MockKratosAdminService),
|
||||||
AdminURL: "http://kratos.test",
|
|
||||||
HTTPClient: client,
|
|
||||||
},
|
|
||||||
AuditRepo: auditRepo,
|
AuditRepo: auditRepo,
|
||||||
ConsentRepo: consentRepo,
|
ConsentRepo: consentRepo,
|
||||||
}
|
}
|
||||||
|
h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-123").Return(&service.KratosIdentity{
|
||||||
|
ID: "user-123",
|
||||||
|
Traits: map[string]interface{}{
|
||||||
|
"email": "user@test.com",
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
app := newConsentTestApp(h)
|
app := newConsentTestApp(h)
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
AuditRepo: auditRepo,
|
AuditRepo: auditRepo,
|
||||||
ConsentRepo: consentRepo,
|
ConsentRepo: consentRepo,
|
||||||
KratosAdmin: &service.KratosAdminService{},
|
KratosAdmin: new(MockKratosAdminService),
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -81,15 +80,36 @@ func (m *MockIdentityProvider) UpdateUserPassword(loginID, newPassword string, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MockKratosAdminService struct {
|
type MockKratosAdminService struct {
|
||||||
// Simple mock for FindIdentityIDByIdentifier
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
func (m *MockKratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
||||||
// Always return a static ID for simplicity in this test
|
args := m.Called(ctx, identifier)
|
||||||
if identifier == "fail" {
|
return args.String(0), args.Error(1)
|
||||||
return "", errors.New("not found")
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminService) GetIdentity(ctx context.Context, id string) (*service.KratosIdentity, error) {
|
||||||
|
args := m.Called(ctx, id)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
}
|
}
|
||||||
return "kratos-identity-id", nil
|
return args.Get(0).(*service.KratosIdentity), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminService) ListIdentities(ctx context.Context) ([]service.KratosIdentity, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*service.KratosIdentity, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminService) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminService) DeleteIdentity(ctx context.Context, identityID string) error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper ---
|
// --- Helper ---
|
||||||
@@ -142,30 +162,17 @@ func TestPasswordLogin_OIDC_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)
|
||||||
|
|
||||||
h := &AuthHandler{
|
h := &AuthHandler{
|
||||||
IdpProvider: mockIdp,
|
IdpProvider: mockIdp,
|
||||||
KratosAdmin: service.NewKratosAdminService(), // We need to mock this better if resolveKratosIdentityIDFromLoginID calls real API
|
KratosAdmin: mockKratos,
|
||||||
Hydra: &service.HydraAdminService{
|
Hydra: &service.HydraAdminService{
|
||||||
AdminURL: "http://hydra.test",
|
AdminURL: "http://hydra.test",
|
||||||
HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
|
HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Inject Mock Kratos (Hack: overwrite the service field if it was an interface, but it's a struct pointer)
|
|
||||||
// AuthHandler uses *service.KratosAdminService struct pointer.
|
|
||||||
// KratosAdminService methods are real. We need to mock HTTP client inside KratosAdminService too.
|
|
||||||
|
|
||||||
kratosHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Mock FindIdentityIDByIdentifier response
|
|
||||||
if strings.Contains(r.URL.Path, "/identities") {
|
|
||||||
json.NewEncoder(w).Encode([]map[string]interface{}{
|
|
||||||
{"id": "kratos-identity-id"},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.NotFound(w, r)
|
|
||||||
})
|
|
||||||
h.KratosAdmin.HTTPClient = &http.Client{Transport: mockHydraTransport(kratosHandler)}
|
|
||||||
h.KratosAdmin.AdminURL = "http://kratos.test"
|
|
||||||
|
|
||||||
app := newAuthLoginTestApp(h)
|
app := newAuthLoginTestApp(h)
|
||||||
|
|
||||||
@@ -215,21 +222,18 @@ func TestPasswordLogin_OIDC_InactiveClient(t *testing.T) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)
|
||||||
|
|
||||||
h := &AuthHandler{
|
h := &AuthHandler{
|
||||||
IdpProvider: mockIdp,
|
IdpProvider: mockIdp,
|
||||||
KratosAdmin: service.NewKratosAdminService(),
|
KratosAdmin: mockKratos,
|
||||||
Hydra: &service.HydraAdminService{
|
Hydra: &service.HydraAdminService{
|
||||||
AdminURL: "http://hydra.test",
|
AdminURL: "http://hydra.test",
|
||||||
HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
|
HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
kratosHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
json.NewEncoder(w).Encode([]map[string]interface{}{{"id": "kratos-identity-id"}})
|
|
||||||
})
|
|
||||||
h.KratosAdmin.HTTPClient = &http.Client{Transport: mockHydraTransport(kratosHandler)}
|
|
||||||
h.KratosAdmin.AdminURL = "http://kratos.test"
|
|
||||||
|
|
||||||
app := newAuthLoginTestApp(h)
|
app := newAuthLoginTestApp(h)
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
body, _ := json.Marshal(map[string]string{
|
||||||
@@ -259,18 +263,15 @@ func TestPasswordLogin_NoOIDC_Success(t *testing.T) {
|
|||||||
Subject: "kratos-identity-id",
|
Subject: "kratos-identity-id",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)
|
||||||
|
|
||||||
h := &AuthHandler{
|
h := &AuthHandler{
|
||||||
IdpProvider: mockIdp,
|
IdpProvider: mockIdp,
|
||||||
KratosAdmin: service.NewKratosAdminService(),
|
KratosAdmin: mockKratos,
|
||||||
Hydra: service.NewHydraAdminService(),
|
Hydra: service.NewHydraAdminService(),
|
||||||
}
|
}
|
||||||
|
|
||||||
kratosHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
json.NewEncoder(w).Encode([]map[string]interface{}{{"id": "kratos-identity-id"}})
|
|
||||||
})
|
|
||||||
h.KratosAdmin.HTTPClient = &http.Client{Transport: mockHydraTransport(kratosHandler)}
|
|
||||||
h.KratosAdmin.AdminURL = "http://kratos.test"
|
|
||||||
|
|
||||||
app := newAuthLoginTestApp(h)
|
app := newAuthLoginTestApp(h)
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
body, _ := json.Marshal(map[string]string{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type DevHandler struct {
|
|||||||
Hydra *service.HydraAdminService
|
Hydra *service.HydraAdminService
|
||||||
Redis domain.RedisRepository
|
Redis domain.RedisRepository
|
||||||
SecretRepo domain.ClientSecretRepository
|
SecretRepo domain.ClientSecretRepository
|
||||||
KratosAdmin *service.KratosAdminService
|
KratosAdmin service.KratosAdminService
|
||||||
ConsentRepo repository.ClientConsentRepository
|
ConsentRepo repository.ClientConsentRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
|
|
||||||
type RelyingPartyHandler struct {
|
type RelyingPartyHandler struct {
|
||||||
Service service.RelyingPartyService
|
Service service.RelyingPartyService
|
||||||
KratosAdmin *service.KratosAdminService
|
KratosAdmin service.KratosAdminService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRelyingPartyHandler(s service.RelyingPartyService, kratos *service.KratosAdminService) *RelyingPartyHandler {
|
func NewRelyingPartyHandler(s service.RelyingPartyService, kratos service.KratosAdminService) *RelyingPartyHandler {
|
||||||
return &RelyingPartyHandler{Service: s, KratosAdmin: kratos}
|
return &RelyingPartyHandler{Service: s, KratosAdmin: kratos}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ type TenantHandler struct {
|
|||||||
Service service.TenantService
|
Service service.TenantService
|
||||||
Keto service.KetoService
|
Keto service.KetoService
|
||||||
KetoOutbox repository.KetoOutboxRepository
|
KetoOutbox repository.KetoOutboxRepository
|
||||||
KratosAdmin *service.KratosAdminService
|
KratosAdmin service.KratosAdminService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTenantHandler(db *gorm.DB, svc service.TenantService, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos *service.KratosAdminService) *TenantHandler {
|
func NewTenantHandler(db *gorm.DB, svc service.TenantService, keto service.KetoService, outbox repository.KetoOutboxRepository, kratos service.KratosAdminService) *TenantHandler {
|
||||||
return &TenantHandler{
|
return &TenantHandler{
|
||||||
DB: db,
|
DB: db,
|
||||||
Service: svc,
|
Service: svc,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func TestTenantHandler_CreateTenant(t *testing.T) {
|
|||||||
}
|
}
|
||||||
body, _ := json.Marshal(input)
|
body, _ := json.Marshal(input)
|
||||||
|
|
||||||
mockSvc.On("RegisterTenant", mock.Anything, "Test Tenant", "test-tenant", "", []string{"test.com"}).
|
mockSvc.On("RegisterTenant", mock.Anything, "Test Tenant", "test-tenant", "", []string{"test.com"}, (*string)(nil)).
|
||||||
Return(&domain.Tenant{ID: "t1", Name: "Test Tenant", Slug: "test-tenant"}, nil)
|
Return(&domain.Tenant{ID: "t1", Name: "Test Tenant", Slug: "test-tenant"}, nil)
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/tenants", bytes.NewReader(body))
|
req := httptest.NewRequest("POST", "/tenants", bytes.NewReader(body))
|
||||||
|
|||||||
@@ -20,16 +20,24 @@ type MockUserGroupService struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserGroupService) Create(ctx context.Context, group *domain.UserGroup) error {
|
func (m *MockUserGroupService) Create(ctx context.Context, tenantID string, parentID *string, name, description, unitType string) (*domain.UserGroup, error) {
|
||||||
return m.Called(ctx, group).Error(0)
|
args := m.Called(ctx, tenantID, parentID, name, description, unitType)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*domain.UserGroup), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserGroupService) Update(ctx context.Context, group *domain.UserGroup) error {
|
func (m *MockUserGroupService) Update(ctx context.Context, tenantID, groupID string, name, description, unitType string, parentID *string) (*domain.UserGroup, error) {
|
||||||
return m.Called(ctx, group).Error(0)
|
args := m.Called(ctx, tenantID, groupID, name, description, unitType, parentID)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*domain.UserGroup), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserGroupService) Delete(ctx context.Context, id string) error {
|
func (m *MockUserGroupService) Delete(ctx context.Context, tenantID, groupID string) error {
|
||||||
return m.Called(ctx, id).Error(0)
|
return m.Called(ctx, tenantID, groupID).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockUserGroupService) Get(ctx context.Context, id string) (*domain.UserGroup, error) {
|
func (m *MockUserGroupService) Get(ctx context.Context, id string) (*domain.UserGroup, error) {
|
||||||
@@ -95,9 +103,7 @@ func TestUserGroupHandler_Create(t *testing.T) {
|
|||||||
app.Post("/tenants/:tenantId/user-groups", h.Create)
|
app.Post("/tenants/:tenantId/user-groups", h.Create)
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]string{"name": "New Group"})
|
body, _ := json.Marshal(map[string]string{"name": "New Group"})
|
||||||
mockSvc.On("Create", mock.Anything, mock.MatchedBy(func(g *domain.UserGroup) bool {
|
mockSvc.On("Create", mock.Anything, "t1", mock.Anything, "New Group", mock.Anything, mock.Anything).Return(&domain.UserGroup{ID: "g1", Name: "New Group"}, nil)
|
||||||
return g.Name == "New Group" && g.TenantID == "t1"
|
|
||||||
})).Return(nil)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/tenants/t1/user-groups", bytes.NewReader(body))
|
req := httptest.NewRequest("POST", "/tenants/t1/user-groups", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserHandler struct {
|
type UserHandler struct {
|
||||||
KratosAdmin *service.KratosAdminService
|
KratosAdmin service.KratosAdminService
|
||||||
OryProvider *service.OryProvider
|
OryProvider *service.OryProvider
|
||||||
TenantService service.TenantService
|
TenantService service.TenantService
|
||||||
KetoService service.KetoService
|
KetoService service.KetoService
|
||||||
@@ -22,7 +22,7 @@ type UserHandler struct {
|
|||||||
UserRepo repository.UserRepository
|
UserRepo repository.UserRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository) *UserHandler {
|
func NewUserHandler(kratosAdmin service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository) *UserHandler {
|
||||||
return &UserHandler{
|
return &UserHandler{
|
||||||
KratosAdmin: kratosAdmin,
|
KratosAdmin: kratosAdmin,
|
||||||
OryProvider: oryProvider,
|
OryProvider: oryProvider,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"baron-sso-backend/internal/domain"
|
|
||||||
"baron-sso-backend/internal/service"
|
"baron-sso-backend/internal/service"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -32,13 +32,29 @@ func (m *MockKratosAdminForUser) ListIdentities(ctx context.Context) ([]service.
|
|||||||
return args.Get(0).([]service.KratosIdentity), args.Error(1)
|
return args.Get(0).([]service.KratosIdentity), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: In reality, KratosAdminService might not be an interface.
|
func (m *MockKratosAdminForUser) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
||||||
// If it's a struct, we'd need to mock the underlying client or use an interface.
|
return "", nil
|
||||||
// For the sake of this test, let's assume we can mock it or use a wrapper.
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminForUser) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*service.KratosIdentity, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminForUser) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminForUser) DeleteIdentity(ctx context.Context, identityID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestUserHandler_CreateUser_InvalidEmail(t *testing.T) {
|
func TestUserHandler_CreateUser_InvalidEmail(t *testing.T) {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
h := &UserHandler{}
|
mockKratos := new(MockKratosAdminForUser)
|
||||||
|
h := &UserHandler{
|
||||||
|
KratosAdmin: mockKratos,
|
||||||
|
OryProvider: &service.OryProvider{}, // Assuming it's a struct and non-nil is enough for this check
|
||||||
|
}
|
||||||
app.Post("/users", h.CreateUser)
|
app.Post("/users", h.CreateUser)
|
||||||
|
|
||||||
payload := map[string]string{
|
payload := map[string]string{
|
||||||
@@ -54,8 +70,8 @@ func TestUserHandler_CreateUser_InvalidEmail(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUserHandler_GetUser_Forbidden(t *testing.T) {
|
func TestUserHandler_GetUser_Forbidden(t *testing.T) {
|
||||||
app := fiber.New()
|
// app := fiber.New()
|
||||||
mockKratos := new(MockKratosAdminForUser)
|
// mockKratos := new(MockKratosAdminForUser)
|
||||||
// We need a way to inject mockKratos into UserHandler.
|
// We need a way to inject mockKratos into UserHandler.
|
||||||
// Since UserHandler uses *service.KratosAdminService (struct),
|
// Since UserHandler uses *service.KratosAdminService (struct),
|
||||||
// we'd typically use an interface here.
|
// we'd typically use an interface here.
|
||||||
|
|||||||
@@ -21,18 +21,27 @@ type KratosIdentity struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KratosAdminService struct {
|
type KratosAdminService interface {
|
||||||
|
ListIdentities(ctx context.Context) ([]KratosIdentity, error)
|
||||||
|
FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error)
|
||||||
|
GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error)
|
||||||
|
UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error)
|
||||||
|
UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error
|
||||||
|
DeleteIdentity(ctx context.Context, identityID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type kratosAdminService struct {
|
||||||
AdminURL string
|
AdminURL string
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKratosAdminService() *KratosAdminService {
|
func NewKratosAdminService() KratosAdminService {
|
||||||
return &KratosAdminService{
|
return &kratosAdminService{
|
||||||
AdminURL: getenvKratos("KRATOS_ADMIN_URL", "http://kratos:4434"),
|
AdminURL: getenvKratos("KRATOS_ADMIN_URL", "http://kratos:4434"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *KratosAdminService) ListIdentities(ctx context.Context) ([]KratosIdentity, error) {
|
func (s *kratosAdminService) ListIdentities(ctx context.Context) ([]KratosIdentity, error) {
|
||||||
endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
|
endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,7 +66,7 @@ func (s *KratosAdminService) ListIdentities(ctx context.Context) ([]KratosIdenti
|
|||||||
return identities, nil
|
return identities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *KratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
func (s *kratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
||||||
identifier = strings.TrimSpace(identifier)
|
identifier = strings.TrimSpace(identifier)
|
||||||
if identifier == "" {
|
if identifier == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -99,7 +108,7 @@ func (s *KratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, ide
|
|||||||
return identities[0].ID, nil
|
return identities[0].ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *KratosAdminService) GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error) {
|
func (s *kratosAdminService) GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error) {
|
||||||
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
|
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,7 +136,7 @@ func (s *KratosAdminService) GetIdentity(ctx context.Context, identityID string)
|
|||||||
return &identity, nil
|
return &identity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *KratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
|
func (s *kratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"schema_id": "default",
|
"schema_id": "default",
|
||||||
"traits": traits,
|
"traits": traits,
|
||||||
@@ -162,7 +171,7 @@ func (s *KratosAdminService) UpdateIdentity(ctx context.Context, identityID stri
|
|||||||
return &updated, nil
|
return &updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *KratosAdminService) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
|
func (s *kratosAdminService) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
|
||||||
patchOps := []map[string]interface{}{
|
patchOps := []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"op": "add",
|
"op": "add",
|
||||||
@@ -190,7 +199,7 @@ func (s *KratosAdminService) UpdateIdentityPassword(ctx context.Context, identit
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *KratosAdminService) DeleteIdentity(ctx context.Context, identityID string) error {
|
func (s *kratosAdminService) DeleteIdentity(ctx context.Context, identityID string) error {
|
||||||
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
|
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -210,7 +219,7 @@ func (s *KratosAdminService) DeleteIdentity(ctx context.Context, identityID stri
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *KratosAdminService) httpClient() *http.Client {
|
func (s *kratosAdminService) httpClient() *http.Client {
|
||||||
if s.HTTPClient != nil {
|
if s.HTTPClient != nil {
|
||||||
return s.HTTPClient
|
return s.HTTPClient
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,3 +72,41 @@ func (m *MockKetoServiceShared) ListObjects(ctx context.Context, namespace, rela
|
|||||||
}
|
}
|
||||||
return args.Get(0).([]string), args.Error(1)
|
return args.Get(0).([]string), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MockKratosAdminServiceShared struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminServiceShared) ListIdentities(ctx context.Context) ([]KratosIdentity, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Get(0).([]KratosIdentity), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminServiceShared) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
||||||
|
args := m.Called(ctx, identifier)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminServiceShared) GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error) {
|
||||||
|
args := m.Called(ctx, identityID)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*KratosIdentity), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminServiceShared) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
|
||||||
|
args := m.Called(ctx, identityID, traits, state)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*KratosIdentity), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminServiceShared) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
|
||||||
|
return m.Called(ctx, identityID, newPassword).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKratosAdminServiceShared) DeleteIdentity(ctx context.Context, identityID string) error {
|
||||||
|
return m.Called(ctx, identityID).Error(0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type orgChartService struct {
|
|||||||
userGroupRepo repository.UserGroupRepository
|
userGroupRepo repository.UserGroupRepository
|
||||||
userRepo repository.UserRepository
|
userRepo repository.UserRepository
|
||||||
ketoOutboxRepo repository.KetoOutboxRepository
|
ketoOutboxRepo repository.KetoOutboxRepository
|
||||||
kratos *KratosAdminService
|
kratos KratosAdminService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOrgChartService(
|
func NewOrgChartService(
|
||||||
@@ -30,7 +30,7 @@ func NewOrgChartService(
|
|||||||
userGroupRepo repository.UserGroupRepository,
|
userGroupRepo repository.UserGroupRepository,
|
||||||
userRepo repository.UserRepository,
|
userRepo repository.UserRepository,
|
||||||
ketoOutbox repository.KetoOutboxRepository,
|
ketoOutbox repository.KetoOutboxRepository,
|
||||||
kratos *KratosAdminService,
|
kratos KratosAdminService,
|
||||||
) OrgChartService {
|
) OrgChartService {
|
||||||
return &orgChartService{
|
return &orgChartService{
|
||||||
tenantRepo: tenantRepo,
|
tenantRepo: tenantRepo,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type userGroupService struct {
|
|||||||
tenantRepo repository.TenantRepository
|
tenantRepo repository.TenantRepository
|
||||||
ketoService KetoService
|
ketoService KetoService
|
||||||
outboxRepo repository.KetoOutboxRepository
|
outboxRepo repository.KetoOutboxRepository
|
||||||
kratos *KratosAdminService
|
kratos KratosAdminService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserGroupService(
|
func NewUserGroupService(
|
||||||
@@ -42,7 +42,7 @@ func NewUserGroupService(
|
|||||||
tenantRepo repository.TenantRepository,
|
tenantRepo repository.TenantRepository,
|
||||||
keto KetoService,
|
keto KetoService,
|
||||||
outbox repository.KetoOutboxRepository,
|
outbox repository.KetoOutboxRepository,
|
||||||
kratos *KratosAdminService,
|
kratos KratosAdminService,
|
||||||
) UserGroupService {
|
) UserGroupService {
|
||||||
return &userGroupService{
|
return &userGroupService{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
@@ -59,6 +59,12 @@ func (s *userGroupService) Create(ctx context.Context, tenantID string, parentID
|
|||||||
if parentID == nil || *parentID == "" {
|
if parentID == nil || *parentID == "" {
|
||||||
parentID = &tenantID
|
parentID = &tenantID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate parent tenant exists
|
||||||
|
if _, err := s.tenantRepo.FindByID(ctx, *parentID); err != nil {
|
||||||
|
return nil, fmt.Errorf("parent tenant not found or invalid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
unitID := uuid.NewString()
|
unitID := uuid.NewString()
|
||||||
|
|
||||||
// 1. Create Tenant (Type: USER_GROUP)
|
// 1. Create Tenant (Type: USER_GROUP)
|
||||||
@@ -199,6 +205,11 @@ func (s *userGroupService) List(ctx context.Context, tenantID string) ([]domain.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string) error {
|
func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string) error {
|
||||||
|
// Validate group exists
|
||||||
|
if _, err := s.repo.FindByID(ctx, groupID); err != nil {
|
||||||
|
return fmt.Errorf("user group not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Keto via Outbox: Tenant:<groupID>#members@User:<userID>
|
// Keto via Outbox: Tenant:<groupID>#members@User:<userID>
|
||||||
if s.outboxRepo != nil {
|
if s.outboxRepo != nil {
|
||||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||||
@@ -214,6 +225,11 @@ func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
|
func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
|
||||||
|
// Validate group exists
|
||||||
|
if _, err := s.repo.FindByID(ctx, groupID); err != nil {
|
||||||
|
return fmt.Errorf("user group not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Keto via Outbox: Delete relation
|
// Keto via Outbox: Delete relation
|
||||||
if s.outboxRepo != nil {
|
if s.outboxRepo != nil {
|
||||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||||
@@ -267,6 +283,11 @@ func (s *userGroupService) ListRoles(ctx context.Context, groupID string) ([]dom
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *userGroupService) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error {
|
func (s *userGroupService) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error {
|
||||||
|
// Validate group exists
|
||||||
|
if _, err := s.repo.FindByID(ctx, groupID); err != nil {
|
||||||
|
return fmt.Errorf("user group not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Keto via Outbox: Tenant:<tenantID>#<relation>@Tenant:<groupID>#members
|
// Keto via Outbox: Tenant:<tenantID>#<relation>@Tenant:<groupID>#members
|
||||||
if s.outboxRepo != nil {
|
if s.outboxRepo != nil {
|
||||||
subject := "Tenant:" + groupID + "#members"
|
subject := "Tenant:" + groupID + "#members"
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"baron-sso-backend/internal/domain"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|||||||
Reference in New Issue
Block a user