forked from baron/baron-sso
CI test 업데이트
This commit is contained in:
@@ -146,7 +146,8 @@ jobs:
|
||||
|
||||
- name: Upload backend failure report artifact
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: backend-test-failure-report
|
||||
path: |
|
||||
@@ -168,94 +169,31 @@ jobs:
|
||||
channel: "stable"
|
||||
cache: true
|
||||
|
||||
- name: Ensure browser for Flutter web tests
|
||||
run: |
|
||||
has_runnable_browser=false
|
||||
for candidate in google-chrome google-chrome-stable chromium chromium-browser; do
|
||||
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" --version >/dev/null 2>&1; then
|
||||
has_runnable_browser=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$has_runnable_browser" = true ]; then
|
||||
echo "Runnable Chrome/Chromium already installed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y chromium || sudo apt-get install -y chromium-browser
|
||||
else
|
||||
apt-get update
|
||||
apt-get install -y chromium || apt-get install -y chromium-browser
|
||||
fi
|
||||
|
||||
for candidate in google-chrome google-chrome-stable chromium chromium-browser; do
|
||||
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" --version >/dev/null 2>&1; then
|
||||
echo "Installed browser: $candidate"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "No runnable Chrome/Chromium found after install."
|
||||
exit 1
|
||||
|
||||
- name: Run userfront tests
|
||||
run: |
|
||||
cd userfront
|
||||
if [ -d test ]; then
|
||||
CHROME_BIN=""
|
||||
for candidate in google-chrome google-chrome-stable chromium chromium-browser; do
|
||||
if command -v "$candidate" >/dev/null 2>&1 && "$candidate" --version >/dev/null 2>&1; then
|
||||
CHROME_BIN="$(command -v "$candidate")"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$CHROME_BIN" ]; then
|
||||
echo "Chrome/Chromium not found for web tests."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using browser: $CHROME_BIN"
|
||||
export CHROME_EXECUTABLE="$CHROME_BIN"
|
||||
mkdir -p ../reports
|
||||
set +e
|
||||
flutter test 2>&1 | tee ../reports/userfront-test-vm.log
|
||||
vm_test_exit_code=${PIPESTATUS[0]}
|
||||
|
||||
web_test_exit_code=0
|
||||
if [ "$vm_test_exit_code" -eq 0 ]; then
|
||||
flutter test --platform chrome test/locale_storage_platform_test.dart 2>&1 | tee ../reports/userfront-test-web.log
|
||||
web_test_exit_code=${PIPESTATUS[0]}
|
||||
fi
|
||||
flutter test 2>&1 | tee ../reports/userfront-test.log
|
||||
test_exit_code=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
if [ "$vm_test_exit_code" -ne 0 ] || [ "$web_test_exit_code" -ne 0 ]; then
|
||||
if [ "$test_exit_code" -ne 0 ]; then
|
||||
{
|
||||
echo "# Userfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`userfront-tests\`"
|
||||
echo "- Browser: \`$CHROME_BIN\`"
|
||||
echo "- VM Test Exit Code: \`$vm_test_exit_code\`"
|
||||
echo "- Web Test Exit Code: \`$web_test_exit_code\`"
|
||||
echo "- Exit Code: \`$test_exit_code\`"
|
||||
echo
|
||||
echo "## Commands"
|
||||
echo "1. \`flutter test\`"
|
||||
echo "2. \`flutter test --platform chrome test/locale_storage_platform_test.dart\`"
|
||||
echo "## Command"
|
||||
echo "\`flutter test\`"
|
||||
echo
|
||||
if [ -f ../reports/userfront-test-vm.log ]; then
|
||||
echo "## VM Test Log Tail (last 200 lines)"
|
||||
if [ -f ../reports/userfront-test.log ]; then
|
||||
echo "## Test Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 ../reports/userfront-test-vm.log
|
||||
echo '```'
|
||||
echo
|
||||
fi
|
||||
if [ -f ../reports/userfront-test-web.log ]; then
|
||||
echo "## Web Test Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 ../reports/userfront-test-web.log
|
||||
tail -n 200 ../reports/userfront-test.log
|
||||
echo '```'
|
||||
fi
|
||||
} > ../reports/userfront-test-failure-report.md
|
||||
@@ -265,6 +203,29 @@ jobs:
|
||||
echo "No userfront tests: skipping (test/ directory not found)."
|
||||
fi
|
||||
|
||||
- name: Ensure userfront failure report exists
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
mkdir -p reports
|
||||
if [ -f reports/userfront-test-failure-report.md ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Userfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`userfront-tests\`"
|
||||
echo "- Reason: \`Job failed before detailed report generation\`"
|
||||
echo
|
||||
if [ -f reports/userfront-test.log ]; then
|
||||
echo "## Test Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/userfront-test.log
|
||||
echo '```'
|
||||
fi
|
||||
} > reports/userfront-test-failure-report.md
|
||||
|
||||
- name: Publish userfront failure summary
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
@@ -274,13 +235,13 @@ jobs:
|
||||
|
||||
- name: Upload userfront failure report artifact
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: userfront-test-failure-report
|
||||
path: |
|
||||
reports/userfront-test-failure-report.md
|
||||
reports/userfront-test-vm.log
|
||||
reports/userfront-test-web.log
|
||||
reports/userfront-test.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
adminfront-tests:
|
||||
@@ -300,13 +261,62 @@ jobs:
|
||||
|
||||
- name: Install adminfront dependencies
|
||||
run: |
|
||||
mkdir -p reports
|
||||
set +e
|
||||
cd adminfront
|
||||
npm ci
|
||||
npm ci 2>&1 | tee ../reports/adminfront-install.log
|
||||
install_exit_code=${PIPESTATUS[0]}
|
||||
cd ..
|
||||
set -e
|
||||
|
||||
if [ "$install_exit_code" -ne 0 ]; then
|
||||
{
|
||||
echo "# Adminfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`adminfront-tests\`"
|
||||
echo "- Reason: \`Dependency install failed\`"
|
||||
echo "- Exit Code: \`$install_exit_code\`"
|
||||
echo
|
||||
echo "## Command"
|
||||
echo "\`cd adminfront && npm ci\`"
|
||||
echo
|
||||
echo "## Install Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/adminfront-install.log
|
||||
echo '```'
|
||||
} > reports/adminfront-test-failure-report.md
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Provision browsers for adminfront tests
|
||||
run: |
|
||||
set +e
|
||||
cd adminfront
|
||||
npx playwright install --with-deps
|
||||
npx playwright install --with-deps 2>&1 | tee ../reports/adminfront-provision.log
|
||||
provision_exit_code=${PIPESTATUS[0]}
|
||||
cd ..
|
||||
set -e
|
||||
|
||||
if [ "$provision_exit_code" -ne 0 ]; then
|
||||
{
|
||||
echo "# Adminfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`adminfront-tests\`"
|
||||
echo "- Reason: \`Browser provisioning failed\`"
|
||||
echo "- Exit Code: \`$provision_exit_code\`"
|
||||
echo
|
||||
echo "## Command"
|
||||
echo "\`cd adminfront && npx playwright install --with-deps\`"
|
||||
echo
|
||||
echo "## Provision Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/adminfront-provision.log
|
||||
echo '```'
|
||||
} > reports/adminfront-test-failure-report.md
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run adminfront tests
|
||||
run: |
|
||||
@@ -341,6 +351,43 @@ jobs:
|
||||
|
||||
exit "$test_exit_code"
|
||||
|
||||
- name: Ensure adminfront failure report exists
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
mkdir -p reports
|
||||
if [ -f reports/adminfront-test-failure-report.md ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Adminfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`adminfront-tests\`"
|
||||
echo "- Reason: \`Job failed before detailed report generation\`"
|
||||
echo
|
||||
if [ -f reports/adminfront-install.log ]; then
|
||||
echo "## Install Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/adminfront-install.log
|
||||
echo '```'
|
||||
echo
|
||||
fi
|
||||
if [ -f reports/adminfront-provision.log ]; then
|
||||
echo "## Provision Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/adminfront-provision.log
|
||||
echo '```'
|
||||
echo
|
||||
fi
|
||||
if [ -f reports/adminfront-test.log ]; then
|
||||
echo "## Test Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/adminfront-test.log
|
||||
echo '```'
|
||||
fi
|
||||
} > reports/adminfront-test-failure-report.md
|
||||
|
||||
- name: Publish adminfront failure summary
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
@@ -350,11 +397,14 @@ jobs:
|
||||
|
||||
- name: Upload adminfront failure report artifact
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: adminfront-test-failure-report
|
||||
path: |
|
||||
reports/adminfront-test-failure-report.md
|
||||
reports/adminfront-install.log
|
||||
reports/adminfront-provision.log
|
||||
reports/adminfront-test.log
|
||||
adminfront/playwright-report
|
||||
adminfront/test-results
|
||||
@@ -377,13 +427,62 @@ jobs:
|
||||
|
||||
- name: Install devfront dependencies
|
||||
run: |
|
||||
mkdir -p reports
|
||||
set +e
|
||||
cd devfront
|
||||
npm ci
|
||||
npm ci 2>&1 | tee ../reports/devfront-install.log
|
||||
install_exit_code=${PIPESTATUS[0]}
|
||||
cd ..
|
||||
set -e
|
||||
|
||||
if [ "$install_exit_code" -ne 0 ]; then
|
||||
{
|
||||
echo "# Devfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`devfront-tests\`"
|
||||
echo "- Reason: \`Dependency install failed\`"
|
||||
echo "- Exit Code: \`$install_exit_code\`"
|
||||
echo
|
||||
echo "## Command"
|
||||
echo "\`cd devfront && npm ci\`"
|
||||
echo
|
||||
echo "## Install Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/devfront-install.log
|
||||
echo '```'
|
||||
} > reports/devfront-test-failure-report.md
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Provision browsers for devfront tests
|
||||
run: |
|
||||
set +e
|
||||
cd devfront
|
||||
npx playwright install --with-deps
|
||||
npx playwright install --with-deps 2>&1 | tee ../reports/devfront-provision.log
|
||||
provision_exit_code=${PIPESTATUS[0]}
|
||||
cd ..
|
||||
set -e
|
||||
|
||||
if [ "$provision_exit_code" -ne 0 ]; then
|
||||
{
|
||||
echo "# Devfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`devfront-tests\`"
|
||||
echo "- Reason: \`Browser provisioning failed\`"
|
||||
echo "- Exit Code: \`$provision_exit_code\`"
|
||||
echo
|
||||
echo "## Command"
|
||||
echo "\`cd devfront && npx playwright install --with-deps\`"
|
||||
echo
|
||||
echo "## Provision Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/devfront-provision.log
|
||||
echo '```'
|
||||
} > reports/devfront-test-failure-report.md
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run devfront tests
|
||||
run: |
|
||||
@@ -418,6 +517,43 @@ jobs:
|
||||
|
||||
exit "$test_exit_code"
|
||||
|
||||
- name: Ensure devfront failure report exists
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
mkdir -p reports
|
||||
if [ -f reports/devfront-test-failure-report.md ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo "# Devfront Test Failure Report"
|
||||
echo
|
||||
echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`"
|
||||
echo "- Job: \`devfront-tests\`"
|
||||
echo "- Reason: \`Job failed before detailed report generation\`"
|
||||
echo
|
||||
if [ -f reports/devfront-install.log ]; then
|
||||
echo "## Install Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/devfront-install.log
|
||||
echo '```'
|
||||
echo
|
||||
fi
|
||||
if [ -f reports/devfront-provision.log ]; then
|
||||
echo "## Provision Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/devfront-provision.log
|
||||
echo '```'
|
||||
echo
|
||||
fi
|
||||
if [ -f reports/devfront-test.log ]; then
|
||||
echo "## Test Log Tail (last 200 lines)"
|
||||
echo '```text'
|
||||
tail -n 200 reports/devfront-test.log
|
||||
echo '```'
|
||||
fi
|
||||
} > reports/devfront-test-failure-report.md
|
||||
|
||||
- name: Publish devfront failure summary
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
@@ -427,11 +563,14 @@ jobs:
|
||||
|
||||
- name: Upload devfront failure report artifact
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: devfront-test-failure-report
|
||||
path: |
|
||||
reports/devfront-test-failure-report.md
|
||||
reports/devfront-install.log
|
||||
reports/devfront-provision.log
|
||||
reports/devfront-test.log
|
||||
devfront/playwright-report
|
||||
devfront/test-results
|
||||
|
||||
14
README.md
14
README.md
@@ -325,8 +325,8 @@ KETO_WRITE_URL = "http://keto:4467"
|
||||
|
||||
### 프런트 테스트 브라우저 프리비저닝 정책
|
||||
- `userfront-tests`
|
||||
- 실행 가능한 Chrome/Chromium을 탐지하고, 없으면 설치 후 테스트 실행
|
||||
- `CHROME_EXECUTABLE` 지정 후 웹 테스트를 실행
|
||||
- `flutter test`(VM)만 실행
|
||||
- `locale_storage` 정책 테스트는 엔진 단위로 통합되어 별도 브라우저 실행이 필요하지 않음
|
||||
- `adminfront-tests`, `devfront-tests`
|
||||
- Playwright 기반 테스트
|
||||
- `npx playwright install --with-deps`로 브라우저/OS 의존성을 사전 설치
|
||||
@@ -340,11 +340,11 @@ KETO_WRITE_URL = "http://keto:4467"
|
||||
- `adminfront-test-failure-report`
|
||||
- `devfront-test-failure-report`
|
||||
|
||||
### userfront의 `skipped` 테스트 안내
|
||||
- `locale_storage_platform_test.dart`는 웹 전용 테스트(`kIsWeb`)를 포함합니다.
|
||||
- 따라서 일반 `flutter test`(VM)에서는 일부 테스트가 `skipped` 되는 것이 정상입니다.
|
||||
- 실제 웹 실행 검증은 별도 단계에서 수행합니다.
|
||||
- `flutter test --platform chrome test/locale_storage_platform_test.dart`
|
||||
### userfront `locale_storage` 테스트 정책
|
||||
- `locale_storage_platform_test.dart`는 `LocaleStorageEngine` 기반 정책 테스트로 통합되었습니다.
|
||||
- 일반 `flutter test`(VM) 실행에 포함되며, 브라우저 전용 `kIsWeb` 케이스를 사용하지 않습니다.
|
||||
- 단일 파일만 확인하려면 다음 명령을 사용합니다.
|
||||
- `flutter test test/locale_storage_platform_test.dart`
|
||||
|
||||
### 로컬 개발 (Manual)
|
||||
Docker 없이는 개발할 수 없지만 Backend 및 [user/admin/dev]Front 코드는 개발모드로 수정하며 개발가능.
|
||||
|
||||
@@ -27,6 +27,10 @@ test("user create and delete flow", async ({ page }) => {
|
||||
const users: UserSummary[] = [];
|
||||
let idSeq = 1;
|
||||
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("admin_session", "playwright-admin-session");
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users**", async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
@@ -109,8 +113,14 @@ test("user create and delete flow", async ({ page }) => {
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page).toHaveURL(/\/users$/);
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "사용자 관리" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "사용자 추가" }).click();
|
||||
const addUserLink = page.getByRole("link", { name: "사용자 추가" });
|
||||
await expect(addUserLink).toBeVisible();
|
||||
await addUserLink.click();
|
||||
await expect(page).toHaveURL(/\/users\/new$/);
|
||||
|
||||
const uniqueEmail = `playwright-${Date.now()}@example.com`;
|
||||
|
||||
@@ -1,7 +1,68 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("clients page loads correctly", async ({ page }) => {
|
||||
const nowInSeconds = Math.floor(Date.now() / 1000);
|
||||
|
||||
await page.addInitScript((issuedAt) => {
|
||||
const mockOidcUser = {
|
||||
id_token: "playwright-id-token",
|
||||
session_state: "playwright-session",
|
||||
access_token: "playwright-access-token",
|
||||
refresh_token: "playwright-refresh-token",
|
||||
token_type: "Bearer",
|
||||
scope: "openid profile email",
|
||||
profile: {
|
||||
sub: "playwright-user",
|
||||
email: "playwright@example.com",
|
||||
name: "Playwright User",
|
||||
},
|
||||
expires_at: issuedAt + 3600,
|
||||
};
|
||||
|
||||
// oidc-client-ts storage key format: oidc.user:{authority}:{client_id}
|
||||
window.localStorage.setItem(
|
||||
"oidc.user:http://localhost:5000/oidc:devfront",
|
||||
JSON.stringify(mockOidcUser),
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
"oidc.user:http://localhost:5000/oidc/:devfront",
|
||||
JSON.stringify(mockOidcUser),
|
||||
);
|
||||
}, nowInSeconds);
|
||||
|
||||
await page.route("**/api/v1/dev/clients**", async (route) => {
|
||||
if (route.request().method() !== "GET") {
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "Not found" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
id: "client-playwright",
|
||||
name: "Playwright Client",
|
||||
type: "confidential",
|
||||
status: "active",
|
||||
createdAt: new Date().toISOString(),
|
||||
redirectUris: ["http://localhost:5174/callback"],
|
||||
scopes: ["openid", "profile", "email"],
|
||||
},
|
||||
],
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/clients");
|
||||
await expect(page).toHaveURL(/\/clients$/);
|
||||
|
||||
// 타이틀 확인
|
||||
await expect(page).toHaveTitle(/바론 개발자 서비스/);
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
| `userfront/test/locale_registry_test.dart` | `extractSupportedLocaleCodesFromAssets excludes template and invalid` | i18n 로케일 해석/정규화 규칙 검증 |
|
||||
| `userfront/test/locale_registry_test.dart` | `fallback locale prefers en when available` | fallback/복구 경로 검증 |
|
||||
| `userfront/test/locale_registry_test.dart` | `fallback locale uses first sorted code when en is absent` | fallback/복구 경로 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `legacy key에서 locale로 마이그레이션 (웹)` | i18n 로케일 해석/정규화 규칙 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `localStorage write/read (웹)` | 브라우저 스토리지 저장/복원 정책 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `localStorage 접근이 차단되면 sessionStorage로 fallback (웹)` | fallback/복구 경로 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `localStorage 접근이 차단되면 메모리 fallback (웹)` | fallback/복구 경로 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `기본 모드에서는 local 우선으로 저장/조회한다` | locale 저장 우선순위 정책 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `legacy key를 읽으면 current key로 마이그레이션한다` | legacy migration 회귀 방지 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `localStorage가 차단되면 sessionStorage로 fallback 한다` | fallback/복구 경로 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `local/session 모두 차단되면 memory fallback 한다` | fallback/복구 경로 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `sessionOnly 모드에서는 session + memory만 사용한다` | 테스트 강제 모드 정책 검증 |
|
||||
| `userfront/test/locale_storage_platform_test.dart` | `memoryOnly 모드에서는 memory만 사용한다` | 테스트 강제 모드 정책 검증 |
|
||||
| `userfront/test/locale_utils_test.dart` | `buildLocalizedPath applies locale` | i18n 로케일 해석/정규화 규칙 검증 |
|
||||
| `userfront/test/locale_utils_test.dart` | `buildLocalizedPath keeps unknown 2-letter prefix as path` | 리다이렉트/쿼리 보존 규칙 검증 |
|
||||
| `userfront/test/locale_utils_test.dart` | `buildLocalizedPath preserves fragment` | 리다이렉트/쿼리 보존 규칙 검증 |
|
||||
@@ -49,6 +51,7 @@
|
||||
| `userfront/test/password_login_flow_policy_test.dart` | `redirectTo가 있으면 OIDC redirect를 우선한다` | 로그인 분기/라우팅 규칙 검증 |
|
||||
| `userfront/test/router_redirect_widget_test.dart` | `/login: login_challenge와 redirect_uri를 전달` | 리다이렉트/쿼리 보존 규칙 검증 |
|
||||
| `userfront/test/router_redirect_widget_test.dart` | `로그인 상태: profile 접근 시 signin으로 리다이렉트하지 않음` | 로그인 분기/라우팅 규칙 검증 |
|
||||
| `userfront/test/router_redirect_widget_test.dart` | `로그인 후 같은 브라우저 새 창/팝업에서도 세션이 유지된다` | 로그인 세션 지속성(동일 브라우저) 검증 |
|
||||
| `userfront/test/router_redirect_widget_test.dart` | `비로그인: redirect_uri/login_challenge가 signin으로 전달` | 리다이렉트/쿼리 보존 규칙 검증 |
|
||||
| `userfront/test/router_redirect_widget_test.dart` | `비로그인: redirect_uri가 없으면 redirect_url을 전달` | 리다이렉트/쿼리 보존 규칙 검증 |
|
||||
| `userfront/test/widget_test.dart` | `BaronSSOApp builds` | 기본 앱 렌더링 스모크 검증 |
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
## 완료 기준(DoD)
|
||||
- 정책 변경 시 수정 포인트가 공통 모듈 중심으로 줄어듭니다.
|
||||
- 기존 locale 저장/조회 동작과 migration 동작이 유지됩니다.
|
||||
- 웹 테스트가 안정적으로 통과하며 fallback/migration 회귀 케이스가 포함됩니다.
|
||||
- VM 기반 테스트가 안정적으로 통과하며 fallback/migration 회귀 케이스가 포함됩니다.
|
||||
|
||||
## 구현 시 주의사항
|
||||
- 외부에서 사용하는 public API 시그니처는 가능한 유지합니다.
|
||||
@@ -83,10 +83,12 @@
|
||||
- lint: `inputs.run_lint`
|
||||
- backend-tests: `inputs.run_backend_tests`
|
||||
- userfront-tests: `inputs.run_userfront_tests`
|
||||
- userfront-tests에 웹 테스트 실행 유지
|
||||
- `flutter test --platform chrome test/locale_storage_platform_test.dart`
|
||||
- userfront-tests 정책 정리
|
||||
- `flutter test` 단일 실행으로 운영
|
||||
- `locale_storage` 정책 검증은 VM 테스트(`locale_storage_platform_test.dart`)로 통합
|
||||
- 브라우저 설치/`--platform chrome` 단계 제거
|
||||
|
||||
### 검증 결과
|
||||
- `cd userfront && flutter analyze --no-fatal-warnings --no-fatal-infos` 통과
|
||||
- `cd userfront && flutter test` 통과
|
||||
- `cd userfront && CHROME_EXECUTABLE=<browser> flutter test --platform chrome test/locale_storage_platform_test.dart` 통과
|
||||
- `cd userfront && flutter test test/locale_storage_platform_test.dart` 통과
|
||||
|
||||
244
userfront/lib/core/i18n/locale_storage_engine.dart
Normal file
244
userfront/lib/core/i18n/locale_storage_engine.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'locale_storage_backend.dart';
|
||||
import 'locale_storage_policy.dart';
|
||||
|
||||
enum _StorageTarget { local, session, memory }
|
||||
|
||||
abstract interface class LocaleStorageTarget {
|
||||
String? read(String key);
|
||||
|
||||
bool write(String key, String value);
|
||||
|
||||
bool remove(String key);
|
||||
|
||||
void clear();
|
||||
}
|
||||
|
||||
class LocaleStorageNoopTarget implements LocaleStorageTarget {
|
||||
const LocaleStorageNoopTarget();
|
||||
|
||||
@override
|
||||
String? read(String key) => null;
|
||||
|
||||
@override
|
||||
bool write(String key, String value) => false;
|
||||
|
||||
@override
|
||||
bool remove(String key) => false;
|
||||
|
||||
@override
|
||||
void clear() {}
|
||||
}
|
||||
|
||||
class LocaleStorageCallbackTarget implements LocaleStorageTarget {
|
||||
LocaleStorageCallbackTarget({
|
||||
required this.readCallback,
|
||||
required this.writeCallback,
|
||||
required this.removeCallback,
|
||||
required this.clearCallback,
|
||||
});
|
||||
|
||||
final String? Function(String key) readCallback;
|
||||
final void Function(String key, String value) writeCallback;
|
||||
final void Function(String key) removeCallback;
|
||||
final void Function() clearCallback;
|
||||
|
||||
@override
|
||||
String? read(String key) => readCallback(key);
|
||||
|
||||
@override
|
||||
bool write(String key, String value) {
|
||||
writeCallback(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool remove(String key) {
|
||||
removeCallback(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() => clearCallback();
|
||||
}
|
||||
|
||||
class LocaleStorageEngine implements LocaleStorageBackend {
|
||||
LocaleStorageEngine({
|
||||
required LocaleStorageTarget localTarget,
|
||||
required LocaleStorageTarget sessionTarget,
|
||||
}) : _localTarget = localTarget,
|
||||
_sessionTarget = sessionTarget;
|
||||
|
||||
final LocaleStorageTarget _localTarget;
|
||||
final LocaleStorageTarget _sessionTarget;
|
||||
final Map<String, String> _memory = {};
|
||||
LocaleStorageTestMode _mode = LocaleStorageTestMode.normal;
|
||||
|
||||
List<_StorageTarget> _fallbackTargets() {
|
||||
switch (_mode) {
|
||||
case LocaleStorageTestMode.normal:
|
||||
return [
|
||||
_StorageTarget.local,
|
||||
_StorageTarget.session,
|
||||
_StorageTarget.memory,
|
||||
];
|
||||
case LocaleStorageTestMode.sessionOnly:
|
||||
return [_StorageTarget.session, _StorageTarget.memory];
|
||||
case LocaleStorageTestMode.memoryOnly:
|
||||
return [_StorageTarget.memory];
|
||||
}
|
||||
}
|
||||
|
||||
String? _safeReadTarget(_StorageTarget target, String key) {
|
||||
try {
|
||||
switch (target) {
|
||||
case _StorageTarget.local:
|
||||
return _localTarget.read(key);
|
||||
case _StorageTarget.session:
|
||||
return _sessionTarget.read(key);
|
||||
case _StorageTarget.memory:
|
||||
return _memory[key];
|
||||
}
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _safeWriteTarget(_StorageTarget target, String key, String value) {
|
||||
try {
|
||||
switch (target) {
|
||||
case _StorageTarget.local:
|
||||
return _localTarget.write(key, value);
|
||||
case _StorageTarget.session:
|
||||
return _sessionTarget.write(key, value);
|
||||
case _StorageTarget.memory:
|
||||
_memory[key] = value;
|
||||
return true;
|
||||
}
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _safeRemoveTarget(_StorageTarget target, String key) {
|
||||
try {
|
||||
switch (target) {
|
||||
case _StorageTarget.local:
|
||||
return _localTarget.remove(key);
|
||||
case _StorageTarget.session:
|
||||
return _sessionTarget.remove(key);
|
||||
case _StorageTarget.memory:
|
||||
_memory.remove(key);
|
||||
return true;
|
||||
}
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _safeClearTarget(_StorageTarget target) {
|
||||
try {
|
||||
switch (target) {
|
||||
case _StorageTarget.local:
|
||||
_localTarget.clear();
|
||||
case _StorageTarget.session:
|
||||
_sessionTarget.clear();
|
||||
case _StorageTarget.memory:
|
||||
_memory.clear();
|
||||
}
|
||||
} catch (_) {
|
||||
// 테스트 정리 단계에서는 clear 예외를 무시합니다.
|
||||
}
|
||||
}
|
||||
|
||||
String? _readByKey(String key) {
|
||||
for (final target in _fallbackTargets()) {
|
||||
final value = _safeReadTarget(target, key);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _writeByKey(String key, String value) {
|
||||
for (final target in _fallbackTargets()) {
|
||||
if (_safeWriteTarget(target, key, value)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _removeEverywhere(String key) {
|
||||
_safeRemoveTarget(_StorageTarget.local, key);
|
||||
_safeRemoveTarget(_StorageTarget.session, key);
|
||||
_memory.remove(key);
|
||||
}
|
||||
|
||||
@override
|
||||
String? read() {
|
||||
final current = _readByKey(LocaleStoragePolicy.currentKey);
|
||||
if (LocaleStoragePolicy.hasValue(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
final legacy = _readByKey(LocaleStoragePolicy.legacyKey);
|
||||
if (LocaleStoragePolicy.shouldMigrateLegacy(
|
||||
current: current,
|
||||
legacy: legacy,
|
||||
)) {
|
||||
_writeByKey(LocaleStoragePolicy.currentKey, legacy!);
|
||||
_removeEverywhere(LocaleStoragePolicy.legacyKey);
|
||||
return legacy;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(String locale) {
|
||||
_writeByKey(LocaleStoragePolicy.currentKey, locale);
|
||||
}
|
||||
|
||||
@override
|
||||
void setTestMode(LocaleStorageTestMode mode) {
|
||||
_mode = mode;
|
||||
}
|
||||
|
||||
@override
|
||||
void clearForTests() {
|
||||
_safeClearTarget(_StorageTarget.local);
|
||||
_safeClearTarget(_StorageTarget.session);
|
||||
_memory.clear();
|
||||
_mode = LocaleStorageTestMode.normal;
|
||||
}
|
||||
|
||||
@override
|
||||
void seedLegacyForTests(String locale) {
|
||||
_writeByKey(LocaleStoragePolicy.legacyKey, locale);
|
||||
}
|
||||
|
||||
@override
|
||||
LocaleStorageDebugState debugStateForTests() {
|
||||
return LocaleStorageDebugState(
|
||||
mode: _mode,
|
||||
localCurrent: _safeReadTarget(
|
||||
_StorageTarget.local,
|
||||
LocaleStoragePolicy.currentKey,
|
||||
),
|
||||
localLegacy: _safeReadTarget(
|
||||
_StorageTarget.local,
|
||||
LocaleStoragePolicy.legacyKey,
|
||||
),
|
||||
sessionCurrent: _safeReadTarget(
|
||||
_StorageTarget.session,
|
||||
LocaleStoragePolicy.currentKey,
|
||||
),
|
||||
sessionLegacy: _safeReadTarget(
|
||||
_StorageTarget.session,
|
||||
LocaleStoragePolicy.legacyKey,
|
||||
),
|
||||
memoryCurrent: _memory[LocaleStoragePolicy.currentKey],
|
||||
memoryLegacy: _memory[LocaleStoragePolicy.legacyKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,7 @@
|
||||
import 'locale_storage_backend.dart';
|
||||
import 'locale_storage_policy.dart';
|
||||
import 'locale_storage_engine.dart';
|
||||
|
||||
class LocaleStorageImpl implements LocaleStorageBackend {
|
||||
final Map<String, String> _memory = {};
|
||||
LocaleStorageTestMode _mode = LocaleStorageTestMode.normal;
|
||||
|
||||
@override
|
||||
String? read() {
|
||||
final current = _memory[LocaleStoragePolicy.currentKey];
|
||||
if (LocaleStoragePolicy.hasValue(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
final legacy = _memory[LocaleStoragePolicy.legacyKey];
|
||||
if (LocaleStoragePolicy.shouldMigrateLegacy(
|
||||
current: current,
|
||||
legacy: legacy,
|
||||
)) {
|
||||
_memory[LocaleStoragePolicy.currentKey] = legacy!;
|
||||
_memory.remove(LocaleStoragePolicy.legacyKey);
|
||||
return legacy;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(String locale) {
|
||||
_memory[LocaleStoragePolicy.currentKey] = locale;
|
||||
}
|
||||
|
||||
@override
|
||||
void setTestMode(LocaleStorageTestMode mode) {
|
||||
_mode = mode;
|
||||
}
|
||||
|
||||
@override
|
||||
void clearForTests() {
|
||||
_memory.clear();
|
||||
_mode = LocaleStorageTestMode.normal;
|
||||
}
|
||||
|
||||
@override
|
||||
void seedLegacyForTests(String locale) {
|
||||
_memory[LocaleStoragePolicy.legacyKey] = locale;
|
||||
}
|
||||
|
||||
@override
|
||||
LocaleStorageDebugState debugStateForTests() {
|
||||
return LocaleStorageDebugState(
|
||||
mode: _mode,
|
||||
memoryCurrent: _memory[LocaleStoragePolicy.currentKey],
|
||||
memoryLegacy: _memory[LocaleStoragePolicy.legacyKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final LocaleStorageBackend localeStorage = LocaleStorageImpl();
|
||||
final LocaleStorageBackend localeStorage = LocaleStorageEngine(
|
||||
localTarget: const LocaleStorageNoopTarget(),
|
||||
sessionTarget: const LocaleStorageNoopTarget(),
|
||||
);
|
||||
|
||||
@@ -3,198 +3,20 @@
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
import 'locale_storage_backend.dart';
|
||||
import 'locale_storage_policy.dart';
|
||||
import 'locale_storage_engine.dart';
|
||||
|
||||
enum _StorageTarget { local, session, memory }
|
||||
|
||||
class LocaleStorageImpl implements LocaleStorageBackend {
|
||||
static final Map<String, String> _memory = {};
|
||||
static LocaleStorageTestMode _mode = LocaleStorageTestMode.normal;
|
||||
|
||||
List<_StorageTarget> _fallbackTargets() {
|
||||
switch (_mode) {
|
||||
case LocaleStorageTestMode.normal:
|
||||
return [
|
||||
_StorageTarget.local,
|
||||
_StorageTarget.session,
|
||||
_StorageTarget.memory,
|
||||
];
|
||||
case LocaleStorageTestMode.sessionOnly:
|
||||
return [_StorageTarget.session, _StorageTarget.memory];
|
||||
case LocaleStorageTestMode.memoryOnly:
|
||||
return [_StorageTarget.memory];
|
||||
}
|
||||
}
|
||||
|
||||
String? _safeReadLocal(String key) {
|
||||
try {
|
||||
return web.window.localStorage.getItem(key);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _safeReadSession(String key) {
|
||||
try {
|
||||
return web.window.sessionStorage.getItem(key);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _safeWriteLocal(String key, String value) {
|
||||
try {
|
||||
web.window.localStorage.setItem(key, value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _safeWriteSession(String key, String value) {
|
||||
try {
|
||||
web.window.sessionStorage.setItem(key, value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _safeRemoveLocal(String key) {
|
||||
try {
|
||||
web.window.localStorage.removeItem(key);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _safeRemoveSession(String key) {
|
||||
try {
|
||||
web.window.sessionStorage.removeItem(key);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _safeClearLocal() {
|
||||
try {
|
||||
web.window.localStorage.clear();
|
||||
} catch (_) {
|
||||
// 스토리지 접근이 차단된 경우는 테스트 정리에서 무시합니다.
|
||||
}
|
||||
}
|
||||
|
||||
void _safeClearSession() {
|
||||
try {
|
||||
web.window.sessionStorage.clear();
|
||||
} catch (_) {
|
||||
// 스토리지 접근이 차단된 경우는 테스트 정리에서 무시합니다.
|
||||
}
|
||||
}
|
||||
|
||||
String? _readFromTarget(_StorageTarget target, String key) {
|
||||
switch (target) {
|
||||
case _StorageTarget.local:
|
||||
return _safeReadLocal(key);
|
||||
case _StorageTarget.session:
|
||||
return _safeReadSession(key);
|
||||
case _StorageTarget.memory:
|
||||
return _memory[key];
|
||||
}
|
||||
}
|
||||
|
||||
bool _writeToTarget(_StorageTarget target, String key, String value) {
|
||||
switch (target) {
|
||||
case _StorageTarget.local:
|
||||
return _safeWriteLocal(key, value);
|
||||
case _StorageTarget.session:
|
||||
return _safeWriteSession(key, value);
|
||||
case _StorageTarget.memory:
|
||||
_memory[key] = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String? _readByKey(String key) {
|
||||
for (final target in _fallbackTargets()) {
|
||||
final value = _readFromTarget(target, key);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _writeByKey(String key, String value) {
|
||||
for (final target in _fallbackTargets()) {
|
||||
if (_writeToTarget(target, key, value)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _removeEverywhere(String key) {
|
||||
_safeRemoveLocal(key);
|
||||
_safeRemoveSession(key);
|
||||
_memory.remove(key);
|
||||
}
|
||||
|
||||
@override
|
||||
String? read() {
|
||||
final current = _readByKey(LocaleStoragePolicy.currentKey);
|
||||
if (LocaleStoragePolicy.hasValue(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
final legacy = _readByKey(LocaleStoragePolicy.legacyKey);
|
||||
if (LocaleStoragePolicy.shouldMigrateLegacy(
|
||||
current: current,
|
||||
legacy: legacy,
|
||||
)) {
|
||||
_writeByKey(LocaleStoragePolicy.currentKey, legacy!);
|
||||
_removeEverywhere(LocaleStoragePolicy.legacyKey);
|
||||
return legacy;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(String locale) {
|
||||
_writeByKey(LocaleStoragePolicy.currentKey, locale);
|
||||
}
|
||||
|
||||
@override
|
||||
void setTestMode(LocaleStorageTestMode mode) {
|
||||
_mode = mode;
|
||||
}
|
||||
|
||||
@override
|
||||
void clearForTests() {
|
||||
_safeClearLocal();
|
||||
_safeClearSession();
|
||||
_memory.clear();
|
||||
_mode = LocaleStorageTestMode.normal;
|
||||
}
|
||||
|
||||
@override
|
||||
void seedLegacyForTests(String locale) {
|
||||
_writeByKey(LocaleStoragePolicy.legacyKey, locale);
|
||||
}
|
||||
|
||||
@override
|
||||
LocaleStorageDebugState debugStateForTests() {
|
||||
return LocaleStorageDebugState(
|
||||
mode: _mode,
|
||||
localCurrent: _safeReadLocal(LocaleStoragePolicy.currentKey),
|
||||
localLegacy: _safeReadLocal(LocaleStoragePolicy.legacyKey),
|
||||
sessionCurrent: _safeReadSession(LocaleStoragePolicy.currentKey),
|
||||
sessionLegacy: _safeReadSession(LocaleStoragePolicy.legacyKey),
|
||||
memoryCurrent: _memory[LocaleStoragePolicy.currentKey],
|
||||
memoryLegacy: _memory[LocaleStoragePolicy.legacyKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final LocaleStorageBackend localeStorage = LocaleStorageImpl();
|
||||
final LocaleStorageBackend localeStorage = LocaleStorageEngine(
|
||||
localTarget: LocaleStorageCallbackTarget(
|
||||
readCallback: (key) => web.window.localStorage.getItem(key),
|
||||
writeCallback: (key, value) => web.window.localStorage.setItem(key, value),
|
||||
removeCallback: (key) => web.window.localStorage.removeItem(key),
|
||||
clearCallback: () => web.window.localStorage.clear(),
|
||||
),
|
||||
sessionTarget: LocaleStorageCallbackTarget(
|
||||
readCallback: (key) => web.window.sessionStorage.getItem(key),
|
||||
writeCallback: (key, value) =>
|
||||
web.window.sessionStorage.setItem(key, value),
|
||||
removeCallback: (key) => web.window.sessionStorage.removeItem(key),
|
||||
clearCallback: () => web.window.sessionStorage.clear(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,75 +1,138 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage_backend.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage_engine.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage_policy.dart';
|
||||
|
||||
class _FakeTarget implements LocaleStorageTarget {
|
||||
_FakeTarget();
|
||||
|
||||
final Map<String, String> store = {};
|
||||
bool throwOnRead = false;
|
||||
bool throwOnWrite = false;
|
||||
bool throwOnRemove = false;
|
||||
bool throwOnClear = false;
|
||||
|
||||
@override
|
||||
String? read(String key) {
|
||||
if (throwOnRead) {
|
||||
throw StateError('read blocked');
|
||||
}
|
||||
return store[key];
|
||||
}
|
||||
|
||||
@override
|
||||
bool write(String key, String value) {
|
||||
if (throwOnWrite) {
|
||||
throw StateError('write blocked');
|
||||
}
|
||||
store[key] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool remove(String key) {
|
||||
if (throwOnRemove) {
|
||||
throw StateError('remove blocked');
|
||||
}
|
||||
store.remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
if (throwOnClear) {
|
||||
throw StateError('clear blocked');
|
||||
}
|
||||
store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late _FakeTarget localTarget;
|
||||
late _FakeTarget sessionTarget;
|
||||
late LocaleStorageEngine engine;
|
||||
|
||||
setUp(() {
|
||||
LocaleStorage.setTestModeForTests(LocaleStorageTestMode.normal);
|
||||
LocaleStorage.clearForTests();
|
||||
localTarget = _FakeTarget();
|
||||
sessionTarget = _FakeTarget();
|
||||
engine = LocaleStorageEngine(
|
||||
localTarget: localTarget,
|
||||
sessionTarget: sessionTarget,
|
||||
);
|
||||
engine.clearForTests();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
LocaleStorage.setTestModeForTests(LocaleStorageTestMode.normal);
|
||||
LocaleStorage.clearForTests();
|
||||
});
|
||||
test('기본 모드에서는 local 우선으로 저장/조회한다', () {
|
||||
engine.write('ko');
|
||||
expect(engine.read(), 'ko');
|
||||
|
||||
test('localStorage write/read (웹)', () {
|
||||
if (!kIsWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocaleStorage.write('ko');
|
||||
expect(LocaleStorage.read(), 'ko');
|
||||
|
||||
final state = LocaleStorage.debugStateForTests();
|
||||
final state = engine.debugStateForTests();
|
||||
expect(state.localCurrent, 'ko');
|
||||
expect(state.sessionCurrent, isNull);
|
||||
expect(state.memoryCurrent, isNull);
|
||||
}, skip: !kIsWeb);
|
||||
});
|
||||
|
||||
test('legacy key에서 locale로 마이그레이션 (웹)', () {
|
||||
if (!kIsWeb) {
|
||||
return;
|
||||
}
|
||||
test('legacy key를 읽으면 current key로 마이그레이션한다', () {
|
||||
localTarget.store[LocaleStoragePolicy.legacyKey] = 'en';
|
||||
|
||||
LocaleStorage.seedLegacyForTests('en');
|
||||
expect(LocaleStorage.read(), 'en');
|
||||
expect(engine.read(), 'en');
|
||||
|
||||
final state = LocaleStorage.debugStateForTests();
|
||||
final state = engine.debugStateForTests();
|
||||
expect(state.localCurrent, 'en');
|
||||
expect(state.localLegacy, isNull);
|
||||
}, skip: !kIsWeb);
|
||||
});
|
||||
|
||||
test('localStorage 접근이 차단되면 메모리 fallback (웹)', () {
|
||||
if (!kIsWeb) {
|
||||
return;
|
||||
}
|
||||
test('localStorage가 차단되면 sessionStorage로 fallback 한다', () {
|
||||
localTarget
|
||||
..throwOnRead = true
|
||||
..throwOnWrite = true
|
||||
..throwOnRemove = true;
|
||||
|
||||
LocaleStorage.forceMemoryStorageForTests(true);
|
||||
engine.write('ko');
|
||||
expect(engine.read(), 'ko');
|
||||
|
||||
LocaleStorage.write('en');
|
||||
expect(LocaleStorage.read(), 'en');
|
||||
|
||||
final state = LocaleStorage.debugStateForTests();
|
||||
expect(state.localCurrent, isNull);
|
||||
expect(state.sessionCurrent, isNull);
|
||||
expect(state.memoryCurrent, 'en');
|
||||
}, skip: !kIsWeb);
|
||||
|
||||
test('localStorage 접근이 차단되면 sessionStorage로 fallback (웹)', () {
|
||||
if (!kIsWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocaleStorage.forceSessionStorageForTests(true);
|
||||
|
||||
LocaleStorage.write('ko');
|
||||
expect(LocaleStorage.read(), 'ko');
|
||||
|
||||
final state = LocaleStorage.debugStateForTests();
|
||||
final state = engine.debugStateForTests();
|
||||
expect(state.localCurrent, isNull);
|
||||
expect(state.sessionCurrent, 'ko');
|
||||
expect(state.memoryCurrent, isNull);
|
||||
}, skip: !kIsWeb);
|
||||
});
|
||||
|
||||
test('local/session 모두 차단되면 memory fallback 한다', () {
|
||||
localTarget
|
||||
..throwOnRead = true
|
||||
..throwOnWrite = true
|
||||
..throwOnRemove = true;
|
||||
sessionTarget
|
||||
..throwOnRead = true
|
||||
..throwOnWrite = true
|
||||
..throwOnRemove = true;
|
||||
|
||||
engine.write('en');
|
||||
expect(engine.read(), 'en');
|
||||
|
||||
final state = engine.debugStateForTests();
|
||||
expect(state.localCurrent, isNull);
|
||||
expect(state.sessionCurrent, isNull);
|
||||
expect(state.memoryCurrent, 'en');
|
||||
});
|
||||
|
||||
test('sessionOnly 모드에서는 session + memory만 사용한다', () {
|
||||
engine.setTestMode(LocaleStorageTestMode.sessionOnly);
|
||||
engine.write('ko');
|
||||
|
||||
final state = engine.debugStateForTests();
|
||||
expect(state.localCurrent, isNull);
|
||||
expect(state.sessionCurrent, 'ko');
|
||||
expect(state.memoryCurrent, isNull);
|
||||
});
|
||||
|
||||
test('memoryOnly 모드에서는 memory만 사용한다', () {
|
||||
engine.setTestMode(LocaleStorageTestMode.memoryOnly);
|
||||
engine.write('en');
|
||||
|
||||
final state = engine.debugStateForTests();
|
||||
expect(state.localCurrent, isNull);
|
||||
expect(state.sessionCurrent, isNull);
|
||||
expect(state.memoryCurrent, 'en');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,4 +152,25 @@ void main() {
|
||||
expect(find.text('profile-page'), findsOneWidget);
|
||||
expect(find.textContaining('signin|'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('로그인 후 같은 브라우저 새 창/팝업에서도 세션이 유지된다', (tester) async {
|
||||
await tester.pumpWidget(_buildTestApp('/en/signin'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('signin|'), findsOneWidget);
|
||||
|
||||
AuthTokenStore.setToken('persisted-token', provider: 'ory');
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
'/en/profile?redirect_uri=https%3A%2F%2Frp.example.com%2Fcb',
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('profile-page'), findsOneWidget);
|
||||
expect(find.textContaining('signin|'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user