From e481ae282118aa1e67b056166b480c30c77dfb91 Mon Sep 17 00:00:00 2001 From: Lectom Date: Tue, 26 May 2026 11:30:00 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20fallback=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD.=20.env=EC=9C=A0=EC=B6=9C=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=EC=84=B1=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/sms_service_test.go | 2 +- devfront/tests/helpers/devfront-fixtures.ts | 8 +- docker/docker-compose.staging.template.yaml | 8 +- docker/staging_pull_compose.template.yaml | 8 +- docs/devfront_auth_flow_explanation.md | 2 +- ...oidc_redirect_mapping_validation_policy.md | 2 +- test/frontend_dev_bind_mount_policy_test.sh | 4 + ...erfront_loading_performance_policy_test.sh | 7 ++ .../tests/runtime-env-mobile.spec.ts | 97 +++++++++++++++++++ .../tests/session-cross-browser-debug.spec.ts | 4 +- userfront/lib/core/services/runtime_env.dart | 13 +-- userfront/pubspec.lock | 16 --- userfront/pubspec.yaml | 1 - userfront/scripts/dev-server.sh | 2 + .../test/login_challenge_resolver_test.dart | 6 +- userfront/test/oidc_redirect_guard_test.dart | 4 +- .../test/password_login_flow_policy_test.dart | 2 +- .../test/runtime_env_compile_time_test.dart | 43 ++++++++ 18 files changed, 177 insertions(+), 52 deletions(-) create mode 100644 userfront-e2e/tests/runtime-env-mobile.spec.ts create mode 100644 userfront/test/runtime_env_compile_time_test.dart diff --git a/backend/internal/service/sms_service_test.go b/backend/internal/service/sms_service_test.go index c9b2f1b9..5f90419a 100644 --- a/backend/internal/service/sms_service_test.go +++ b/backend/internal/service/sms_service_test.go @@ -14,7 +14,7 @@ func TestBuildNaverSmsRequest_UsesSMSForShortContent(t *testing.T) { } func TestBuildNaverSmsRequest_UsesLMSForLongContent(t *testing.T) { - content := "[Baron 로그인] 비밀번호 재설정 링크: http://sso-test.hmac.kr/api/v1/auth/password/reset/v/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + content := "[Baron 로그인] 비밀번호 재설정 링크: http://sso.example.test/api/v1/auth/password/reset/v/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" req := buildNaverSmsRequest("0262857755", "821012345678", content) if req.Type != "LMS" { diff --git a/devfront/tests/helpers/devfront-fixtures.ts b/devfront/tests/helpers/devfront-fixtures.ts index 3e1b0df5..349c9ece 100644 --- a/devfront/tests/helpers/devfront-fixtures.ts +++ b/devfront/tests/helpers/devfront-fixtures.ts @@ -159,12 +159,12 @@ export async function seedAuth(page: Page, role?: string) { const storageKeys = [ "user:http://localhost:5000/oidc:devfront", "user:http://localhost:5000/oidc/:devfront", - "user:https://sso-test.hmac.kr/oidc:devfront", - "user:https://sso-test.hmac.kr/oidc/:devfront", + "user:https://sso.example.test/oidc:devfront", + "user:https://sso.example.test/oidc/:devfront", "oidc.user:http://localhost:5000/oidc:devfront", "oidc.user:http://localhost:5000/oidc/:devfront", - "oidc.user:https://sso-test.hmac.kr/oidc:devfront", - "oidc.user:https://sso-test.hmac.kr/oidc/:devfront", + "oidc.user:https://sso.example.test/oidc:devfront", + "oidc.user:https://sso.example.test/oidc/:devfront", ]; for (const key of storageKeys) { diff --git a/docker/docker-compose.staging.template.yaml b/docker/docker-compose.staging.template.yaml index ef4f95ca..efa65c99 100644 --- a/docker/docker-compose.staging.template.yaml +++ b/docker/docker-compose.staging.template.yaml @@ -91,11 +91,9 @@ services: backend: condition: service_healthy command: > - /bin/sh -c "mkdir -p /usr/share/nginx/html/assets && - echo \"BACKEND_URL=${BACKEND_URL:-}\" >> /usr/share/nginx/html/assets/.env && - echo \"USERFRONT_URL=${USERFRONT_URL}\" >> /usr/share/nginx/html/assets/.env && - echo \"APP_ENV=stage\" >> /usr/share/nginx/html/assets/.env && - cp /usr/share/nginx/html/assets/.env /usr/share/nginx/html/.env && + /bin/sh -c "echo \"[userfront-runtime] BACKEND_URL configured: $${BACKEND_URL:+yes}\" && + echo \"[userfront-runtime] USERFRONT_URL configured: $${USERFRONT_URL:+yes}\" && + echo \"[userfront-runtime] APP_ENV=$${APP_ENV:-stage}\" && nginx -g 'daemon off;'" healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"] diff --git a/docker/staging_pull_compose.template.yaml b/docker/staging_pull_compose.template.yaml index 42a19202..5e7d405a 100644 --- a/docker/staging_pull_compose.template.yaml +++ b/docker/staging_pull_compose.template.yaml @@ -520,11 +520,9 @@ services: backend: condition: service_healthy command: > - /bin/sh -c "mkdir -p /usr/share/nginx/html/assets && - echo \"BACKEND_URL=$${BACKEND_URL}\" >> /usr/share/nginx/html/assets/.env && - echo \"USERFRONT_URL=$${USERFRONT_URL}\" >> /usr/share/nginx/html/assets/.env && - echo \"APP_ENV=$${APP_ENV}\" >> /usr/share/nginx/html/assets/.env && - cp /usr/share/nginx/html/assets/.env /usr/share/nginx/html/.env && + /bin/sh -c "echo \"[userfront-runtime] BACKEND_URL configured: $${BACKEND_URL:+yes}\" && + echo \"[userfront-runtime] USERFRONT_URL configured: $${USERFRONT_URL:+yes}\" && + echo \"[userfront-runtime] APP_ENV=$${APP_ENV}\" && nginx -g 'daemon off;'" healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"] diff --git a/docs/devfront_auth_flow_explanation.md b/docs/devfront_auth_flow_explanation.md index 46b86dff..12e58ed8 100644 --- a/docs/devfront_auth_flow_explanation.md +++ b/docs/devfront_auth_flow_explanation.md @@ -82,7 +82,7 @@ hydra clients create ### 발생 원인 (Problem) * `devfront`가 로그인 성공 후 사용자 정보를 가져오기 위해 백엔드 API인 `/api/v1/user/me`를 호출했습니다. * 이 API는 백엔드 세션 쿠키를 기반으로 동작하도록 설계되어 있었습니다. -* 하지만 브라우저 보안 정책(SameSite/Cross-Domain)으로 인해 `localhost`에서 보낸 요청에는 `sso-test.hmac.kr` 도메인의 쿠키가 포함되지 않았습니다. +* 하지만 브라우저 보안 정책(SameSite/Cross-Domain)으로 인해 `localhost`에서 보낸 요청에는 `sso.example.test` 도메인의 쿠키가 포함되지 않았습니다. * **결과**: 백엔드는 401 Unauthorized를 반환 -> `devfront`는 401을 받으면 다시 로그인을 시도하도록 구현되어 있어 무한 루프가 발생했습니다. ### 해결 방법 (Solution) diff --git a/docs/oidc_redirect_mapping_validation_policy.md b/docs/oidc_redirect_mapping_validation_policy.md index c31a73d6..28d8c23a 100644 --- a/docs/oidc_redirect_mapping_validation_policy.md +++ b/docs/oidc_redirect_mapping_validation_policy.md @@ -17,7 +17,7 @@ 3. 매핑 체인이 없거나 규칙이 누락된 경우는 실패(`unmapped_fail`)로 간주합니다. ## 용어 정의 -- Public URL: 브라우저에서 접근하는 URL. 예: `https://sso-test.hmac.kr/oidc/oauth2/auth` +- Public URL: 브라우저에서 접근하는 URL. 예: `https://sso.example.test/oidc/oauth2/auth` - Internal URL: 컨테이너 내부 통신 URL. 예: `http://hydra:4444/oauth2/auth` - Mapping Chain: Public 요청이 Gateway/Oathkeeper 규칙을 통해 Internal URL로 전달되는 경로 diff --git a/test/frontend_dev_bind_mount_policy_test.sh b/test/frontend_dev_bind_mount_policy_test.sh index 139e1617..9df97d6c 100644 --- a/test/frontend_dev_bind_mount_policy_test.sh +++ b/test/frontend_dev_bind_mount_policy_test.sh @@ -40,10 +40,14 @@ grep -Fq -- "AS dev" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must grep -Fq -- "AS production" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must keep an explicit production target" grep -Fq -- "flutter run" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must use flutter run" grep -Fq -- "--wasm" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep WebAssembly enabled" +grep -Fq -- "--dart-define=BACKEND_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass backend URL through dart-define" grep -Fq -- "--dart-define=CLIENT_LOG_DEBUG=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass client log debug mode through dart-define" grep -Fq -- "--dart-define=APP_ENV=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass app env through dart-define" +grep -Fq -- "--dart-define=USERFRONT_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass userfront URL through dart-define" grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must accept optional Flutter run flags" assert_contains 'CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}' +assert_contains 'BACKEND_URL=${BACKEND_URL:-}' +assert_contains 'USERFRONT_URL=${USERFRONT_URL}' assert_contains 'USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}' if grep -Fq -- "--debug" "$USERFRONT_DEV_SERVER"; then fail "make dev must not hard-code Flutter debug mode in the userfront dev server" diff --git a/test/userfront_loading_performance_policy_test.sh b/test/userfront_loading_performance_policy_test.sh index 39380591..d1590210 100644 --- a/test/userfront_loading_performance_policy_test.sh +++ b/test/userfront_loading_performance_policy_test.sh @@ -46,6 +46,13 @@ if rg -n "gzip|gzipSync|\\.gz" userfront/nginx.conf userfront/scripts/optimize-w fail "userfront web compression must be managed as brotli-only" fi rg -q "Cache-Control.*no-cache" userfront/nginx.conf || fail "HTML/app shell must use no-cache revalidation" +if rg -n "assets/\\.env|/\\.env|runtimeEnvBody|dotenv\\.load" userfront/lib userfront/nginx.conf userfront-e2e/scripts/serve-userfront-build.mjs; then + fail "userfront must not request, load, or serve public .env assets to browsers" +fi +if rg -n "/usr/share/nginx/html/.+\\.env|assets/\\.env|cp .+\\.env" docker/docker-compose.staging.template.yaml docker/staging_pull_compose.template.yaml; then + fail "userfront deployment must not write runtime .env into the public static document root" +fi +rg -q "\\[userfront-runtime\\] BACKEND_URL configured" docker/docker-compose.staging.template.yaml docker/staging_pull_compose.template.yaml || fail "userfront runtime config presence must be logged server-side only" rg -q "Cache-Control.*immutable" userfront/nginx.conf || fail "versioned static assets must use immutable cache" tmp_dir="$(mktemp -d)" diff --git a/userfront-e2e/tests/runtime-env-mobile.spec.ts b/userfront-e2e/tests/runtime-env-mobile.spec.ts new file mode 100644 index 00000000..2c97913f --- /dev/null +++ b/userfront-e2e/tests/runtime-env-mobile.spec.ts @@ -0,0 +1,97 @@ +import { expect, test, type Page } from '@playwright/test'; + +type SigninCase = { + path: '/ko/signin' | '/en/signin'; + theme: 'light' | 'dark'; +}; + +const signinCases: SigninCase[] = [ + { path: '/ko/signin', theme: 'light' }, + { path: '/ko/signin', theme: 'dark' }, + { path: '/en/signin', theme: 'light' }, + { path: '/en/signin', theme: 'dark' }, +]; + +async function mockPublicApis(page: Page): Promise { + await page.route('**/api/v1/**', async (route) => { + const requestUrl = new URL(route.request().url()); + if (requestUrl.pathname.endsWith('/api/v1/user/me')) { + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: 'unauthorized' }), + }); + return; + } + + if (requestUrl.pathname.endsWith('/api/v1/auth/tenant-info')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + }); +} + +async function expectFlutterCanvasRendered(page: Page): Promise { + const canvas = page.locator('canvas').first(); + await expect(canvas).toBeVisible({ timeout: 30_000 }); + const box = await canvas.boundingBox(); + expect(box?.width ?? 0).toBeGreaterThan(100); + expect(box?.height ?? 0).toBeGreaterThan(100); +} + +async function seedAuthTheme(page: Page, theme: SigninCase['theme']): Promise { + await page.addInitScript((themeValue) => { + window.localStorage.setItem('userfront_auth_theme', themeValue); + window.localStorage.setItem('flutter.userfront_auth_theme', themeValue); + }, theme); +} + +test.describe('UserFront signin runtime matrix', () => { + test.beforeEach(async ({ page }) => { + await mockPublicApis(page); + }); + + for (const entry of signinCases) { + test(`${entry.path} renders in ${entry.theme} theme`, async ({ page }) => { + await seedAuthTheme(page, entry.theme); + await page.goto(entry.path); + await expect(page).toHaveURL(new RegExp(`${entry.path}(?:\\?.*)?$`)); + await expectFlutterCanvasRendered(page); + }); + } + + test('signin uses configured BACKEND_URL for public API requests', async ({ + page, + }) => { + const expectedBackendOrigin = process.env.EXPECTED_BACKEND_ORIGIN; + test.skip(!expectedBackendOrigin, 'set EXPECTED_BACKEND_ORIGIN'); + + const requestedApiOrigins = new Set(); + page.on('request', (request) => { + const requestUrl = new URL(request.url()); + if (requestUrl.pathname.startsWith('/api/v1/')) { + requestedApiOrigins.add(requestUrl.origin); + } + }); + + for (const entry of signinCases) { + await seedAuthTheme(page, entry.theme); + await page.goto(entry.path); + await expectFlutterCanvasRendered(page); + await expect + .poll(() => [...requestedApiOrigins], { timeout: 30_000 }) + .toContain(expectedBackendOrigin); + expect(requestedApiOrigins).not.toContain('https://sso.example.test'); + } + }); +}); diff --git a/userfront-e2e/tests/session-cross-browser-debug.spec.ts b/userfront-e2e/tests/session-cross-browser-debug.spec.ts index b22a025a..9c07e4d1 100644 --- a/userfront-e2e/tests/session-cross-browser-debug.spec.ts +++ b/userfront-e2e/tests/session-cross-browser-debug.spec.ts @@ -1,6 +1,6 @@ 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 USERFRONT_BASE_URL = process.env.USERFRONT_BASE_URL ?? 'https://sso.example.test'; const ADMINFRONT_URL = process.env.ADMINFRONT_URL ?? 'http://localhost:5173'; const LOGIN_ID = process.env.E2E_LOGIN_ID ?? ''; const PASSWORD = process.env.E2E_PASSWORD ?? ''; @@ -134,7 +134,7 @@ async function loginAdminFront(context: BrowserContext): Promise { if (/\/login$/.test(page.url())) { const authorizeUrl = await page.evaluate(() => { const origin = window.location.origin; - const authority = 'https://sso-test.hmac.kr/oidc'; + const authority = `${USERFRONT_BASE_URL}/oidc`; const params = new URLSearchParams({ client_id: 'adminfront', redirect_uri: `${origin}/auth/callback`, diff --git a/userfront/lib/core/services/runtime_env.dart b/userfront/lib/core/services/runtime_env.dart index 8e817a68..1ca381be 100644 --- a/userfront/lib/core/services/runtime_env.dart +++ b/userfront/lib/core/services/runtime_env.dart @@ -1,9 +1,9 @@ -import 'package:flutter_dotenv/flutter_dotenv.dart'; - const _compileTimeEnv = { 'APP_ENV': String.fromEnvironment('APP_ENV'), + 'BACKEND_URL': String.fromEnvironment('BACKEND_URL'), 'CLIENT_LOG_DEBUG': String.fromEnvironment('CLIENT_LOG_DEBUG'), 'USERFRONT_DEBUG_LOG': String.fromEnvironment('USERFRONT_DEBUG_LOG'), + 'USERFRONT_URL': String.fromEnvironment('USERFRONT_URL'), }; String runtimeOriginFallback() { @@ -13,17 +13,10 @@ String runtimeOriginFallback() { return origin; } } catch (_) {} - return 'https://sso-test.hmac.kr'; + return ''; } String envOrDefault(String key, String fallback) { - if (dotenv.isInitialized) { - final value = dotenv.env[key]; - if (value != null && value.trim().isNotEmpty) { - return value; - } - } - final compileTimeValue = _compileTimeEnv[key]; if (compileTimeValue != null && compileTimeValue.trim().isNotEmpty) { return compileTimeValue; diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock index 238c821f..1b146ee3 100644 --- a/userfront/pubspec.lock +++ b/userfront/pubspec.lock @@ -150,14 +150,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_dotenv: - dependency: "direct main" - description: - name: flutter_dotenv - sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4 - url: "https://pub.dev" - source: hosted - version: "6.0.0" flutter_driver: dependency: transitive description: flutter @@ -276,14 +268,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" leak_tracker: dependency: transitive description: diff --git a/userfront/pubspec.yaml b/userfront/pubspec.yaml index 2aa16432..f9fee9ca 100644 --- a/userfront/pubspec.yaml +++ b/userfront/pubspec.yaml @@ -39,7 +39,6 @@ dependencies: flutter_riverpod: ^3.0.3 go_router: ^17.0.1 http: ^1.6.0 - flutter_dotenv: ^6.0.0 flutter_svg: ^2.2.1 url_launcher: ^6.3.2 logging: ^1.2.0 diff --git a/userfront/scripts/dev-server.sh b/userfront/scripts/dev-server.sh index e3767470..2e1691e1 100644 --- a/userfront/scripts/dev-server.sh +++ b/userfront/scripts/dev-server.sh @@ -10,8 +10,10 @@ set -- flutter run \ --web-hostname 0.0.0.0 \ --web-port "${USERFRONT_INTERNAL_PORT:-5000}" \ --wasm \ + --dart-define=BACKEND_URL="${BACKEND_URL:-}" \ --dart-define=CLIENT_LOG_DEBUG="${CLIENT_LOG_DEBUG:-false}" \ --dart-define=APP_ENV="${APP_ENV:-dev}" \ + --dart-define=USERFRONT_URL="${USERFRONT_URL:-}" \ ${USERFRONT_FLUTTER_RUN_FLAGS:-} \ --no-web-resources-cdn diff --git a/userfront/test/login_challenge_resolver_test.dart b/userfront/test/login_challenge_resolver_test.dart index fa9603cb..b379af80 100644 --- a/userfront/test/login_challenge_resolver_test.dart +++ b/userfront/test/login_challenge_resolver_test.dart @@ -8,7 +8,7 @@ void main() { widgetLoginChallenge: 'widget-challenge', uri: Uri.parse('/ko/login'), rawSearch: '?login_challenge=raw-search', - rawHref: 'https://sso-test.hmac.kr/ko/login?login_challenge=raw-href', + rawHref: 'https://sso.example.test/ko/login?login_challenge=raw-href', ); expect(resolved.value, 'widget-challenge'); @@ -46,7 +46,7 @@ void main() { uri: Uri.parse('/ko/login'), rawSearch: '', rawHref: - 'https://sso-test.hmac.kr/ko/login?a=1&login_challenge=raw-href-value#fragment', + 'https://sso.example.test/ko/login?a=1&login_challenge=raw-href-value#fragment', ); expect(resolved.value, 'raw-href-value'); @@ -59,7 +59,7 @@ void main() { widgetLoginChallenge: null, uri: Uri.parse('/ko/login'), rawSearch: '', - rawHref: 'https://sso-test.hmac.kr/ko/login?x=1', + rawHref: 'https://sso.example.test/ko/login?x=1', ); expect(resolved.value, isNull); diff --git a/userfront/test/oidc_redirect_guard_test.dart b/userfront/test/oidc_redirect_guard_test.dart index 10cb6a0c..0ea4c88f 100644 --- a/userfront/test/oidc_redirect_guard_test.dart +++ b/userfront/test/oidc_redirect_guard_test.dart @@ -5,12 +5,12 @@ void main() { group('oidc_redirect_guard', () { test('http/https 절대 URL만 허용', () { final ok = validateOidcRedirectTarget( - 'https://sso-test.hmac.kr/oidc/oauth2/auth?client_id=devfront&login_verifier=abc&state=xyz&code_challenge=ccc&code_challenge_method=S256&response_type=code&scope=openid%20profile&redirect_uri=http%3A%2F%2Flocalhost%3A5174%2Fcallback', + 'https://sso.example.test/oidc/oauth2/auth?client_id=devfront&login_verifier=abc&state=xyz&code_challenge=ccc&code_challenge_method=S256&response_type=code&scope=openid%20profile&redirect_uri=http%3A%2F%2Flocalhost%3A5174%2Fcallback', ); expect(ok.isValid, isTrue); expect(ok.reason, 'ok'); expect(ok.scheme, 'https'); - expect(ok.host, 'sso-test.hmac.kr'); + expect(ok.host, 'sso.example.test'); expect(ok.path, '/oidc/oauth2/auth'); expect(ok.isOidcAuthPath, isTrue); expect(ok.queryParamCount, 8); diff --git a/userfront/test/password_login_flow_policy_test.dart b/userfront/test/password_login_flow_policy_test.dart index 426c5eba..3ef2af86 100644 --- a/userfront/test/password_login_flow_policy_test.dart +++ b/userfront/test/password_login_flow_policy_test.dart @@ -7,7 +7,7 @@ void main() { final action = decidePasswordLoginNextAction( hasLoginChallenge: true, redirectTo: - 'https://sso-test.hmac.kr/oidc/oauth2/auth?login_verifier=a', + 'https://sso.example.test/oidc/oauth2/auth?login_verifier=a', jwt: 'jwt-token', ); diff --git a/userfront/test/runtime_env_compile_time_test.dart b/userfront/test/runtime_env_compile_time_test.dart new file mode 100644 index 00000000..84a2bc6d --- /dev/null +++ b/userfront/test/runtime_env_compile_time_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:userfront/core/services/runtime_env.dart'; + +const _expectedBackendUrl = String.fromEnvironment('BACKEND_URL'); +const _expectedUserfrontUrl = String.fromEnvironment('USERFRONT_URL'); + +void main() { + group('runtime env compile-time defines', () { + test('runtime fallback is empty outside a browser origin', () { + expect(runtimeOriginFallback(), isEmpty); + }); + + test('BACKEND_URL dart-define overrides runtime origin fallback when set', () { + if (_expectedBackendUrl.isEmpty) { + expect(runtimeBackendUrl(), runtimeOriginFallback()); + return; + } + + expect(runtimeBackendUrl(), sanitizedUrl(_expectedBackendUrl)); + }); + + test( + 'USERFRONT_URL dart-define overrides runtime origin fallback when set', + () { + if (_expectedUserfrontUrl.isEmpty) { + expect(runtimeUserfrontUrl(), runtimeOriginFallback()); + return; + } + + expect(runtimeUserfrontUrl(), sanitizedUrl(_expectedUserfrontUrl)); + }, + ); + + test('dart-define URLs are sanitized', () { + if (_expectedBackendUrl.isEmpty || _expectedUserfrontUrl.isEmpty) { + return; + } + + expect(runtimeBackendUrl(), isNot(endsWith('/'))); + expect(runtimeUserfrontUrl(), isNot(endsWith('/'))); + }); + }); +}