forked from baron/baron-sso
세션 카드 디버그용 시나리오 및 테스트 추가
This commit is contained in:
@@ -7,6 +7,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"baron-sso-backend/internal/domain"
|
"baron-sso-backend/internal/domain"
|
||||||
|
"baron-sso-backend/internal/middleware"
|
||||||
"baron-sso-backend/internal/service"
|
"baron-sso-backend/internal/service"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
@@ -637,6 +638,156 @@ func TestPasswordLogin_OIDC_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPasswordLogin_OIDC_AuditIncludesClientMetadata(t *testing.T) {
|
||||||
|
mockIdp := new(MockIdentityProvider)
|
||||||
|
mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
|
||||||
|
SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"},
|
||||||
|
Subject: "kratos-identity-id",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
|
||||||
|
json.NewEncoder(w).Encode(domain.HydraLoginRequest{
|
||||||
|
Challenge: "challenge-123",
|
||||||
|
Client: domain.HydraClient{
|
||||||
|
ClientID: "devfront",
|
||||||
|
ClientName: "DevFront",
|
||||||
|
Metadata: map[string]interface{}{"status": "active"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)
|
||||||
|
|
||||||
|
auditRepo := &mockAuditRepo{}
|
||||||
|
h := &AuthHandler{
|
||||||
|
IdpProvider: mockIdp,
|
||||||
|
KratosAdmin: mockKratos,
|
||||||
|
AuditRepo: auditRepo,
|
||||||
|
Hydra: &service.HydraAdminService{
|
||||||
|
AdminURL: "http://hydra.test",
|
||||||
|
HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(middleware.AuditMiddleware(middleware.AuditConfig{
|
||||||
|
Repo: auditRepo,
|
||||||
|
BodyDump: true,
|
||||||
|
}))
|
||||||
|
app.Post("/api/v1/auth/password/login", h.PasswordLogin)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]string{
|
||||||
|
"loginId": "user@example.com",
|
||||||
|
"password": "password",
|
||||||
|
"login_challenge": "challenge-123",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/login", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(auditRepo.logs) != 1 {
|
||||||
|
t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs))
|
||||||
|
}
|
||||||
|
|
||||||
|
log := auditRepo.logs[0]
|
||||||
|
if log.EventType != "POST /api/v1/auth/password/login" {
|
||||||
|
t.Fatalf("expected password login audit event, got %q", log.EventType)
|
||||||
|
}
|
||||||
|
if log.UserID != "kratos-identity-id" {
|
||||||
|
t.Fatalf("expected audit user_id kratos-identity-id, got %q", log.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
details, err := parseAuditDetails(log.Details)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse audit details: %v", err)
|
||||||
|
}
|
||||||
|
if got, _ := details["client_id"].(string); got != "devfront" {
|
||||||
|
t.Fatalf("expected client_id devfront, got %v", details["client_id"])
|
||||||
|
}
|
||||||
|
if got, _ := details["client_name"].(string); got != "DevFront" {
|
||||||
|
t.Fatalf("expected client_name DevFront, got %v", details["client_name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPasswordLogin_UserFront_AuditIncludesDefaultClientMetadata(t *testing.T) {
|
||||||
|
mockIdp := new(MockIdentityProvider)
|
||||||
|
mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{
|
||||||
|
SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"},
|
||||||
|
Subject: "kratos-identity-id",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil)
|
||||||
|
|
||||||
|
auditRepo := &mockAuditRepo{}
|
||||||
|
h := &AuthHandler{
|
||||||
|
IdpProvider: mockIdp,
|
||||||
|
KratosAdmin: mockKratos,
|
||||||
|
AuditRepo: auditRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Use(middleware.AuditMiddleware(middleware.AuditConfig{
|
||||||
|
Repo: auditRepo,
|
||||||
|
BodyDump: true,
|
||||||
|
}))
|
||||||
|
app.Post("/api/v1/auth/password/login", h.PasswordLogin)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]string{
|
||||||
|
"loginId": "user@example.com",
|
||||||
|
"password": "password",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/login", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(auditRepo.logs) != 1 {
|
||||||
|
t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs))
|
||||||
|
}
|
||||||
|
if auditRepo.logs[0].UserID != "kratos-identity-id" {
|
||||||
|
t.Fatalf("expected audit user_id kratos-identity-id, got %q", auditRepo.logs[0].UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
details, err := parseAuditDetails(auditRepo.logs[0].Details)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse audit details: %v", err)
|
||||||
|
}
|
||||||
|
if got, _ := details["client_id"].(string); got != "userfront" {
|
||||||
|
t.Fatalf("expected client_id userfront, got %v", details["client_id"])
|
||||||
|
}
|
||||||
|
if got, _ := details["client_name"].(string); got != "UserFront" {
|
||||||
|
t.Fatalf("expected client_name UserFront, got %v", details["client_name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) {
|
func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) {
|
||||||
mockIdp := new(MockIdentityProvider)
|
mockIdp := new(MockIdentityProvider)
|
||||||
mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
|
mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
|
||||||
|
|||||||
@@ -103,6 +103,250 @@ func TestListMySessions_Success(t *testing.T) {
|
|||||||
mockKratos.AssertExpectations(t)
|
mockKratos.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListMySessions_UsesConsentGrantForAppName(t *testing.T) {
|
||||||
|
now := time.Date(2026, 4, 2, 4, 40, 0, 0, time.UTC)
|
||||||
|
setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.URL.Path == "/sessions/whoami" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"id": "current-sid",
|
||||||
|
"authenticated_at": now.Format(time.RFC3339),
|
||||||
|
"identity": map[string]any{
|
||||||
|
"id": "user-123",
|
||||||
|
"traits": map[string]any{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "User",
|
||||||
|
"role": "user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
|
||||||
|
{
|
||||||
|
ID: "current-sid",
|
||||||
|
Active: true,
|
||||||
|
AuthenticatedAt: now,
|
||||||
|
ExpiresAt: now.Add(24 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "c7c721ea-session",
|
||||||
|
Active: true,
|
||||||
|
AuthenticatedAt: now.Add(-5 * time.Minute),
|
||||||
|
ExpiresAt: now.Add(23*time.Hour + 55*time.Minute),
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
auditRepo := &mockAuditRepo{
|
||||||
|
logs: []domain.AuditLog{
|
||||||
|
{
|
||||||
|
UserID: "user-123",
|
||||||
|
EventType: "consent.granted",
|
||||||
|
SessionID: "c7c721ea-session",
|
||||||
|
Timestamp: now,
|
||||||
|
Details: `{"client_id":"devfront","client_name":"DevFront","session_id":"c7c721ea-session","approved_session_id":"c7c721ea-session"}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &AuthHandler{
|
||||||
|
KratosAdmin: mockKratos,
|
||||||
|
AuditRepo: auditRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Get("/api/v1/user/sessions", h.ListMySessions)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
|
||||||
|
req.Header.Set("Cookie", "ory_kratos_session=valid")
|
||||||
|
|
||||||
|
resp, err := app.Test(req, -1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Items []struct {
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
AppName string `json:"app_name"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, body.Items, 2) {
|
||||||
|
assert.Equal(t, "c7c721ea-session", body.Items[1].SessionID)
|
||||||
|
assert.Equal(t, "DevFront", body.Items[1].AppName)
|
||||||
|
assert.Equal(t, "devfront", body.Items[1].ClientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockKratos.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListMySessions_PreservesAppNameFromOlderConsentGrant(t *testing.T) {
|
||||||
|
now := time.Date(2026, 4, 2, 4, 40, 0, 0, time.UTC)
|
||||||
|
setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.URL.Path == "/sessions/whoami" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"id": "current-sid",
|
||||||
|
"authenticated_at": now.Format(time.RFC3339),
|
||||||
|
"identity": map[string]any{
|
||||||
|
"id": "user-123",
|
||||||
|
"traits": map[string]any{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "User",
|
||||||
|
"role": "user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
|
||||||
|
{
|
||||||
|
ID: "current-sid",
|
||||||
|
Active: true,
|
||||||
|
AuthenticatedAt: now,
|
||||||
|
ExpiresAt: now.Add(24 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "c7c721ea-session",
|
||||||
|
Active: true,
|
||||||
|
AuthenticatedAt: now.Add(-5 * time.Minute),
|
||||||
|
ExpiresAt: now.Add(23*time.Hour + 55*time.Minute),
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
auditRepo := &mockAuditRepo{
|
||||||
|
logs: []domain.AuditLog{
|
||||||
|
{
|
||||||
|
UserID: "user-123",
|
||||||
|
EventType: "consent.granted",
|
||||||
|
SessionID: "c7c721ea-session",
|
||||||
|
Timestamp: now.Add(-30 * time.Second),
|
||||||
|
IPAddress: "203.0.113.10",
|
||||||
|
Details: `{"client_id":"devfront","client_name":"DevFront","session_id":"c7c721ea-session"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserID: "user-123",
|
||||||
|
EventType: "login_success",
|
||||||
|
SessionID: "c7c721ea-session",
|
||||||
|
Timestamp: now,
|
||||||
|
IPAddress: "10.0.0.12",
|
||||||
|
UserAgent: "Mozilla/5.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h := &AuthHandler{
|
||||||
|
KratosAdmin: mockKratos,
|
||||||
|
AuditRepo: auditRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Get("/api/v1/user/sessions", h.ListMySessions)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
|
||||||
|
req.Header.Set("Cookie", "ory_kratos_session=valid")
|
||||||
|
|
||||||
|
resp, err := app.Test(req, -1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Items []struct {
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
AppName string `json:"app_name"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, body.Items, 2) {
|
||||||
|
assert.Equal(t, "c7c721ea-session", body.Items[1].SessionID)
|
||||||
|
assert.Equal(t, "DevFront", body.Items[1].AppName)
|
||||||
|
assert.Equal(t, "devfront", body.Items[1].ClientID)
|
||||||
|
assert.Equal(t, "203.0.113.10", body.Items[1].IPAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockKratos.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListMySessions_CurrentSessionFallsBackToRequestMetadata(t *testing.T) {
|
||||||
|
now := time.Date(2026, 4, 6, 1, 2, 3, 0, time.UTC)
|
||||||
|
setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.URL.Path == "/sessions/whoami" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"id": "current-sid",
|
||||||
|
"authenticated_at": now.Format(time.RFC3339),
|
||||||
|
"identity": map[string]any{
|
||||||
|
"id": "user-123",
|
||||||
|
"traits": map[string]any{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "User",
|
||||||
|
"role": "user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
mockKratos := new(MockKratosAdminService)
|
||||||
|
mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
|
||||||
|
{
|
||||||
|
ID: "current-sid",
|
||||||
|
Active: true,
|
||||||
|
AuthenticatedAt: now,
|
||||||
|
ExpiresAt: now.Add(24 * time.Hour),
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
h := &AuthHandler{
|
||||||
|
KratosAdmin: mockKratos,
|
||||||
|
AuditRepo: &mockAuditRepo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
app.Get("/api/v1/user/sessions", h.ListMySessions)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
|
||||||
|
req.Header.Set("Cookie", "ory_kratos_session=valid")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/146.0.0.0 Safari/537.36")
|
||||||
|
req.Header.Set("X-Forwarded-For", "203.0.113.25")
|
||||||
|
|
||||||
|
resp, err := app.Test(req, -1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Items []struct {
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
IsCurrent bool `json:"is_current"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
AppName string `json:"app_name"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, body.Items, 1) {
|
||||||
|
assert.Equal(t, "current-sid", body.Items[0].SessionID)
|
||||||
|
assert.True(t, body.Items[0].IsCurrent)
|
||||||
|
assert.Equal(t, "203.0.113.25", body.Items[0].IPAddress)
|
||||||
|
assert.Contains(t, body.Items[0].UserAgent, "Mozilla/5.0")
|
||||||
|
assert.Equal(t, "userfront", body.Items[0].ClientID)
|
||||||
|
assert.Equal(t, "UserFront", body.Items[0].AppName)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockKratos.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeleteMySession_Success(t *testing.T) {
|
func TestDeleteMySession_Success(t *testing.T) {
|
||||||
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
||||||
var hydraRevokeCalls int
|
var hydraRevokeCalls int
|
||||||
|
|||||||
200
userfront-e2e/tests/session-cross-browser-debug.spec.ts
Normal file
200
userfront-e2e/tests/session-cross-browser-debug.spec.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import { expect, test, type BrowserContext, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const USERFRONT_BASE_URL = process.env.USERFRONT_BASE_URL ?? 'https://sso-test.hmac.kr';
|
||||||
|
const ADMINFRONT_URL = process.env.ADMINFRONT_URL ?? 'http://localhost:5173';
|
||||||
|
const LOGIN_ID = process.env.E2E_LOGIN_ID ?? '';
|
||||||
|
const PASSWORD = process.env.E2E_PASSWORD ?? '';
|
||||||
|
|
||||||
|
type SessionApiResponse = {
|
||||||
|
items?: Array<{
|
||||||
|
session_id?: string;
|
||||||
|
client_id?: string;
|
||||||
|
app_name?: string;
|
||||||
|
is_current?: boolean;
|
||||||
|
user_agent?: string;
|
||||||
|
ip_address?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureCredentials(): void {
|
||||||
|
if (!LOGIN_ID || !PASSWORD) {
|
||||||
|
test.skip(true, 'E2E credentials are required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableFlutterAccessibility(page: Page): Promise<void> {
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const button = page.getByRole('button', { name: 'Enable accessibility' });
|
||||||
|
if (await button.count()) {
|
||||||
|
try {
|
||||||
|
await button.click({ force: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const placeholder = page.locator('flt-semantics-placeholder');
|
||||||
|
if (await placeholder.count()) {
|
||||||
|
await placeholder.first().click({ force: true });
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickPasswordTab(page: Page): Promise<void> {
|
||||||
|
await page.waitForTimeout(900);
|
||||||
|
const pane = page.locator('flt-glass-pane');
|
||||||
|
await pane.click({
|
||||||
|
position: { x: 522, y: 158 },
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(120);
|
||||||
|
await pane.click({
|
||||||
|
position: { x: 522, y: 158 },
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillAt(page: Page, x: number, y: number, value: string): Promise<void> {
|
||||||
|
const pane = page.locator('flt-glass-pane');
|
||||||
|
await pane.click({ position: { x, y }, force: true });
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
await page.keyboard.press('Control+A');
|
||||||
|
await page.keyboard.press('Backspace');
|
||||||
|
await page.keyboard.type(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginViaUserFront(page: Page): Promise<void> {
|
||||||
|
await page.waitForURL(/\/ko\/(signin|login)/, { timeout: 30_000 });
|
||||||
|
const loginIdInput = page.getByPlaceholder(/이메일 또는 휴대폰 번호|email|phone/i);
|
||||||
|
const passwordInput = page.getByPlaceholder(/비밀번호|password/i);
|
||||||
|
const submitButton = page.getByRole('button', { name: /로그인|Login/i });
|
||||||
|
|
||||||
|
if ((await loginIdInput.count()) >= 1 && (await passwordInput.count()) >= 1) {
|
||||||
|
await loginIdInput.first().fill(LOGIN_ID);
|
||||||
|
await passwordInput.first().fill(PASSWORD);
|
||||||
|
await submitButton.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await clickPasswordTab(page);
|
||||||
|
await fillAt(page, 640, 245, LOGIN_ID);
|
||||||
|
await fillAt(page, 640, 311, PASSWORD);
|
||||||
|
await page.locator('flt-glass-pane').click({
|
||||||
|
position: { x: 640, y: 381 },
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureConsentIfNeeded(page: Page): Promise<void> {
|
||||||
|
if (!/\/ko\/consent/.test(page.url())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowButton = page
|
||||||
|
.getByRole('button')
|
||||||
|
.filter({ hasText: /허용|동의|Accept|Allow/i })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (await allowButton.count()) {
|
||||||
|
await allowButton.click({ force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureUserSessionsOnReload(page: Page): Promise<SessionApiResponse> {
|
||||||
|
const responsePromise = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.request().method() === 'GET' &&
|
||||||
|
response.url().includes('/api/v1/user/sessions'),
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
const response = await responsePromise;
|
||||||
|
return (await response.json()) as SessionApiResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginUserFront(context: BrowserContext): Promise<Page> {
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(`${USERFRONT_BASE_URL}/ko/signin`, {
|
||||||
|
waitUntil: 'domcontentloaded',
|
||||||
|
});
|
||||||
|
await loginViaUserFront(page);
|
||||||
|
await expect(page).toHaveURL(/\/ko\/dashboard/, { timeout: 60_000 });
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginAdminFront(context: BrowserContext): Promise<Page> {
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(ADMINFRONT_URL, { waitUntil: 'domcontentloaded' });
|
||||||
|
const ssoButton = page.getByRole('button', { name: /SSO 계정으로 로그인|SSO/i });
|
||||||
|
if (await ssoButton.count()) {
|
||||||
|
await ssoButton.click({ force: true });
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
}
|
||||||
|
if (/\/login$/.test(page.url())) {
|
||||||
|
const authorizeUrl = await page.evaluate(() => {
|
||||||
|
const origin = window.location.origin;
|
||||||
|
const authority = 'https://sso-test.hmac.kr/oidc';
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: 'adminfront',
|
||||||
|
redirect_uri: `${origin}/auth/callback`,
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'openid offline_access profile email',
|
||||||
|
state: `pw-${Date.now()}`,
|
||||||
|
nonce: `pw-${Date.now()}`,
|
||||||
|
code_challenge: 'test-code-challenge-test-code-challenge-test',
|
||||||
|
code_challenge_method: 'plain',
|
||||||
|
});
|
||||||
|
return `${authority}/oauth2/auth?${params.toString()}`;
|
||||||
|
});
|
||||||
|
await page.goto(authorizeUrl, { waitUntil: 'domcontentloaded' });
|
||||||
|
}
|
||||||
|
await loginViaUserFront(page);
|
||||||
|
await ensureConsentIfNeeded(page);
|
||||||
|
await page.waitForURL(/localhost:5173|\/auth\/callback|\/dashboard|\/tenants/, {
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('cross-browser session debug', () => {
|
||||||
|
test('userfront session card should map adminfront session metadata across contexts', async ({
|
||||||
|
browser,
|
||||||
|
}, testInfo) => {
|
||||||
|
ensureCredentials();
|
||||||
|
|
||||||
|
const userfrontContext = await browser.newContext({ locale: 'ko-KR' });
|
||||||
|
const adminfrontContext = await browser.newContext({ locale: 'ko-KR' });
|
||||||
|
|
||||||
|
const userfrontPage = await loginUserFront(userfrontContext);
|
||||||
|
const adminfrontPage = await loginAdminFront(adminfrontContext);
|
||||||
|
|
||||||
|
const sessionsPayload = await captureUserSessionsOnReload(userfrontPage);
|
||||||
|
const items = sessionsPayload.items ?? [];
|
||||||
|
const adminfrontItems = items.filter((item) =>
|
||||||
|
(item.client_id ?? '').toLowerCase().includes('adminfront'),
|
||||||
|
);
|
||||||
|
const unknownCards = await userfrontPage.locator('text=세션 정보').allTextContents();
|
||||||
|
const adminFrontCards = await userfrontPage.locator('text=AdminFront').allTextContents();
|
||||||
|
|
||||||
|
await testInfo.attach('user-sessions.json', {
|
||||||
|
body: JSON.stringify(sessionsPayload, null, 2),
|
||||||
|
contentType: 'application/json',
|
||||||
|
});
|
||||||
|
await testInfo.attach('card-summary.json', {
|
||||||
|
body: JSON.stringify(
|
||||||
|
{
|
||||||
|
unknownCards,
|
||||||
|
adminFrontCards,
|
||||||
|
currentUrl: userfrontPage.url(),
|
||||||
|
adminfrontUrl: adminfrontPage.url(),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
contentType: 'application/json',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(adminfrontItems.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user