forked from baron/baron-sso
Merge pull request 'temp-branch' (#464) from temp-branch into dev
Reviewed-on: baron/baron-sso#464
This commit is contained in:
@@ -19,6 +19,12 @@
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist", "node_modules", "tsconfig*.json"]
|
||||
"ignore": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"tsconfig*.json",
|
||||
"test-results",
|
||||
"playwright-report"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
261
adminfront/tests/users.spec.ts
Normal file
261
adminfront/tests/users.spec.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("User Management", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
id_token: "fake-id-token",
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
scope: "openid profile email",
|
||||
profile: {
|
||||
sub: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("oidc.state", "dummy");
|
||||
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
if (route.request().url().includes("/.well-known/openid-configuration")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
issuer: "http://localhost:5000/oidc",
|
||||
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
||||
token_endpoint: "http://localhost:5000/oidc/token",
|
||||
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
||||
jwks_uri: "http://localhost:5000/oidc/jwks",
|
||||
},
|
||||
});
|
||||
}
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
const url = route.request().url();
|
||||
const method = route.request().method();
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants") && method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "t-1",
|
||||
slug: "test-tenant",
|
||||
name: "Test Tenant",
|
||||
config: {
|
||||
userSchema: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/users/u-1") && method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "u-1",
|
||||
name: "John Doe",
|
||||
email: "john@test.com",
|
||||
loginId: "johndoe",
|
||||
tenantSlug: "test-tenant",
|
||||
role: "user",
|
||||
status: "active",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/users") && method === "POST") {
|
||||
// Parse request payload to simulate validation checks
|
||||
const postData = route.request().postDataJSON();
|
||||
if (postData && postData.loginId === "existing_user") {
|
||||
// Simulate a backend conflict error (409) for an existing loginId
|
||||
return route.fulfill({
|
||||
status: 409,
|
||||
json: {
|
||||
error: "이미 존재하는 로그인 ID 입니다.",
|
||||
},
|
||||
});
|
||||
}
|
||||
// Mock successful user creation
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "new-user-id",
|
||||
name: "New User",
|
||||
email: "newuser@test.com",
|
||||
loginId: postData?.loginId || "newuser123",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/users/u-1") && method === "PUT") {
|
||||
// Parse request payload
|
||||
const postData = route.request().postDataJSON();
|
||||
if (postData && postData.loginId === "existing_user") {
|
||||
// Simulate a backend conflict error (409) for an existing loginId
|
||||
return route.fulfill({
|
||||
status: 409,
|
||||
json: {
|
||||
error: "이미 존재하는 로그인 ID 입니다.",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Mock successful user update
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "u-1",
|
||||
name: "John Doe Updated",
|
||||
email: "john@test.com",
|
||||
loginId: postData?.loginId || "johndoe_updated",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/users") && method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "u-1",
|
||||
name: "John Doe",
|
||||
email: "john@test.com",
|
||||
loginId: "johndoe",
|
||||
role: "user",
|
||||
status: "active",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: { items: [], total: 0 } });
|
||||
});
|
||||
});
|
||||
|
||||
test("should successfully edit a user's Login ID", async ({ page }) => {
|
||||
await page.goto("/users/u-1");
|
||||
|
||||
// Wait for the form to load with the existing login ID
|
||||
const loginIdInput = page.locator('input[id="loginId"]');
|
||||
await expect(loginIdInput).toBeVisible();
|
||||
await expect(loginIdInput).toHaveValue("johndoe");
|
||||
|
||||
// Change the Login ID
|
||||
await loginIdInput.fill("johndoe_updated");
|
||||
|
||||
// Submit the form
|
||||
const saveButton = page.getByRole("button", {
|
||||
name: /변경사항 저장|Save/i,
|
||||
});
|
||||
await saveButton.click();
|
||||
|
||||
// Check for success message
|
||||
await expect(
|
||||
page.getByText(/사용자 정보가 수정되었습니다/i).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show conflict error when updating to an existing Login ID", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/u-1");
|
||||
|
||||
const loginIdInput = page.locator('input[id="loginId"]');
|
||||
await expect(loginIdInput).toBeVisible();
|
||||
|
||||
// Enter a login ID that triggers our mock conflict error
|
||||
await loginIdInput.fill("existing_user");
|
||||
|
||||
const saveButton = page.getByRole("button", {
|
||||
name: /변경사항 저장|Save/i,
|
||||
});
|
||||
await saveButton.click();
|
||||
|
||||
// Check for the specific conflict error message from the backend mock
|
||||
await expect(
|
||||
page.getByText(/이미 존재하는 로그인 ID 입니다/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should successfully create a new user with a Login ID", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/new");
|
||||
|
||||
// Ensure the page title is loaded
|
||||
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
|
||||
|
||||
// Fill required fields
|
||||
await page.locator('input[name="name"]').fill("New User");
|
||||
await page.locator('input[name="email"]').fill("newuser@test.com");
|
||||
|
||||
// Fill Login ID
|
||||
const loginIdInput = page.locator('input[name="loginId"]');
|
||||
await loginIdInput.fill("newuser123");
|
||||
|
||||
// Submit the form
|
||||
const createButton = page.getByRole("button", { name: /생성/i });
|
||||
await createButton.click();
|
||||
|
||||
// Assuming successful creation redirects back to the user list
|
||||
await expect(page).toHaveURL(/.*\/users$/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should show conflict error when creating with an existing Login ID", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/new");
|
||||
|
||||
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
|
||||
|
||||
// Fill required fields
|
||||
await page.locator('input[name="name"]').fill("New User");
|
||||
await page.locator('input[name="email"]').fill("newuser@test.com");
|
||||
|
||||
// Fill Login ID that triggers the mock conflict error
|
||||
const loginIdInput = page.locator('input[name="loginId"]');
|
||||
await loginIdInput.fill("existing_user");
|
||||
|
||||
// Submit the form
|
||||
const createButton = page.getByRole("button", { name: /생성/i });
|
||||
await createButton.click();
|
||||
|
||||
// Check for the specific conflict error message from the backend mock
|
||||
await expect(
|
||||
page.getByText(/이미 존재하는 로그인 ID 입니다/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -460,7 +460,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
} else {
|
||||
// If companyCode provided but not found, we should probably reject if we want strictness,
|
||||
// or just treat as GENERAL user. Given the risk "존재하지 않는 테넌트도 저장됨", we should reject.
|
||||
return errorJSON(c, fiber.StatusBadRequest, "Invalid company code.")
|
||||
return errorJSON(c, fiber.StatusBadRequest, "해당하는 가족사(테넌트)를 찾을 수 없습니다.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +473,8 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
normalizedPhone = "+" + normalizedPhone
|
||||
}
|
||||
|
||||
slog.Info("[Signup] Phone normalization", "raw", req.Phone, "normalized", normalizedPhone)
|
||||
|
||||
// IDP에 전달할 BrokerUser 스키마 구성
|
||||
attributes := map[string]interface{}{
|
||||
"department": req.Department,
|
||||
@@ -517,7 +519,8 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
return errorJSON(c, fiber.StatusConflict, "User already exists")
|
||||
}
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Failed to create user")
|
||||
// Include the actual error message in the response for debugging
|
||||
return errorJSON(c, fiber.StatusInternalServerError, fmt.Sprintf("Failed to create user: %v", err))
|
||||
}
|
||||
|
||||
// 4. Cleanup Redis
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestSignup_CompanyCodeValidation(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
var res map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&res)
|
||||
assert.Equal(t, "Invalid company code.", res["error"])
|
||||
assert.Equal(t, "해당하는 가족사(테넌트)를 찾을 수 없습니다.", res["error"])
|
||||
})
|
||||
|
||||
t.Run("Active Company Code", func(t *testing.T) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -45,7 +46,7 @@ func (r *tenantRepository) FindByID(ctx context.Context, id string) (*domain.Ten
|
||||
|
||||
func (r *tenantRepository) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||
var tenant domain.Tenant
|
||||
if err := r.db.WithContext(ctx).Preload("Domains").Where("slug = ?", slug).First(&tenant).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Preload("Domains").Where("slug = ?", strings.ToLower(slug)).First(&tenant).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tenant, nil
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist", "node_modules", "tsconfig*.json"]
|
||||
"ignore": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"tsconfig*.json",
|
||||
"test-results",
|
||||
"playwright-report"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
} from "../../components/ui/avatar";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { t } from "../../lib/i18n";
|
||||
import ProfileTenantSwitcher from "./ProfileTenantSwitcher";
|
||||
import { fetchMe } from "../auth/authApi";
|
||||
import ProfileTenantSwitcher from "./ProfileTenantSwitcher";
|
||||
|
||||
function ProfilePage() {
|
||||
const auth = useAuth();
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:userfront/i18n.dart';
|
||||
import '../../../core/i18n/locale_utils.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/ui/toast_service.dart';
|
||||
|
||||
class SignupScreen extends StatefulWidget {
|
||||
const SignupScreen({super.key});
|
||||
@@ -328,24 +329,29 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
} catch (e) {
|
||||
String eStr = e.toString().toLowerCase();
|
||||
setState(() {
|
||||
if (eStr.contains('uppercase')) {
|
||||
if (eStr.contains('password') && eStr.contains('uppercase')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.uppercase_required',
|
||||
);
|
||||
} else if (eStr.contains('lowercase')) {
|
||||
} else if (eStr.contains('password') && eStr.contains('lowercase')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.lowercase_required',
|
||||
);
|
||||
} else if (eStr.contains('digit') || eStr.contains('number')) {
|
||||
} else if (eStr.contains('password') &&
|
||||
(eStr.contains('digit') || eStr.contains('number'))) {
|
||||
_passwordError = tr('msg.userfront.signup.password.number_required');
|
||||
} else if (eStr.contains('symbol') || eStr.contains('special')) {
|
||||
} else if (eStr.contains('password') &&
|
||||
(eStr.contains('symbol') || eStr.contains('special'))) {
|
||||
_passwordError = tr('msg.userfront.signup.password.symbol_required');
|
||||
} else if (eStr.contains('length') || eStr.contains('12 characters')) {
|
||||
} else if (eStr.contains('password') &&
|
||||
(eStr.contains('length') || eStr.contains('12 characters'))) {
|
||||
_passwordError = tr('msg.userfront.signup.password.length_required');
|
||||
} else {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.failed',
|
||||
params: {'error': e.toString()},
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.signup.failed',
|
||||
params: {'error': e.toString().replaceFirst('Exception: ', '')},
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -354,6 +360,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
if (!mounted) return;
|
||||
ToastService.error(message);
|
||||
}
|
||||
|
||||
void _showSuccessDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -1488,8 +1499,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
.replaceAll('Exception: ', ''),
|
||||
);
|
||||
} finally {
|
||||
if (mounted)
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('중복 확인'),
|
||||
|
||||
Reference in New Issue
Block a user