Skip to content

Commit 09124e7

Browse files
committed
test(plugin-hono-server): reproduce the always-null /auth/me/localization answer
Widens the #14788 fixture so the tenant can configure `localization.timezone` and `localization.currency` rows (the endpoint reads all three keys in one `$in` query, so the double now answers whichever the fixture sets), and adds five cases for the resolved regional defaults. Measured against unchanged source: 4 failed | 9 passed. An authenticated caller configured with `Asia/Shanghai` / `CNY` is answered `currency: null, timezone: null` — the defect, reproduced here rather than inherited from the card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
1 parent c2a336c commit 09124e7

1 file changed

Lines changed: 67 additions & 3 deletions

File tree

‎packages/plugins/plugin-hono-server/src/current-user-endpoints-localization.test.ts‎

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ interface MountOptions {
6161
rules?: Row[] | null;
6262
/** Tenant-scoped `localization.locale` `sys_setting` row value (rung 3). */
6363
settingLocale?: string;
64+
/** Tenant-scoped `localization.timezone` `sys_setting` row value. */
65+
settingTimezone?: string;
66+
/** Tenant-scoped `localization.currency` `sys_setting` row value. */
67+
settingCurrency?: string;
6468
/** Whether a session resolves at all. */
6569
authenticated?: boolean;
6670
/**
@@ -73,7 +77,7 @@ interface MountOptions {
7377
failUserRead?: boolean;
7478
}
7579

76-
function mount({ storedLocale, rules = [LOCALE_SHAPE_RULE], settingLocale, authenticated = true, failUserRead = false }: MountOptions = {}) {
80+
function mount({ storedLocale, rules = [LOCALE_SHAPE_RULE], settingLocale, settingTimezone, settingCurrency, authenticated = true, failUserRead = false }: MountOptions = {}) {
7781
const reads: Array<{ object: string; opts: any }> = [];
7882
let sysUserReads = 0;
7983
const ql = {
@@ -83,8 +87,18 @@ function mount({ storedLocale, rules = [LOCALE_SHAPE_RULE], settingLocale, authe
8387
if (failUserRead && ++sysUserReads > 1) throw new Error('sys_user unavailable');
8488
return opts?.where?.id === USER ? [{ id: USER, email: 'lang@example.com', locale: storedLocale }] : [];
8589
}
86-
if (object === 'sys_setting' && settingLocale !== undefined) {
87-
return [{ namespace: 'localization', key: 'locale', value: settingLocale, scope: 'tenant' }];
90+
if (object === 'sys_setting') {
91+
// The endpoint reads all three `localization` keys in ONE `$in`
92+
// query, so the double answers whichever of them the fixture
93+
// configured — and nothing for the rest.
94+
const configured: Array<[string, string | undefined]> = [
95+
['locale', settingLocale],
96+
['timezone', settingTimezone],
97+
['currency', settingCurrency],
98+
];
99+
return configured
100+
.filter(([, value]) => value !== undefined)
101+
.map(([key, value]) => ({ namespace: 'localization', key, value, scope: 'tenant' }));
88102
}
89103
return [];
90104
},
@@ -215,3 +229,53 @@ describe('/auth/me/localization — the signed-in user\'s language, three rungs
215229
expect(reads.filter((r) => r.object === 'sys_user')).toEqual([]);
216230
});
217231
});
232+
233+
describe('/auth/me/localization — the regional defaults are RESOLVED, not always null (#15387)', () => {
234+
it('answers the tenant\'s configured currency and time zone', async () => {
235+
const { get } = mount({ settingTimezone: 'Asia/Shanghai', settingCurrency: 'CNY', settingLocale: 'zh-CN' });
236+
const { status, body } = await get();
237+
expect(status).toBe(200);
238+
// The WHOLE published shape, so a fourth key cannot appear unnoticed.
239+
expect(body).toEqual({ authenticated: true, currency: 'CNY', locale: 'zh-CN', timezone: 'Asia/Shanghai' });
240+
});
241+
242+
it('resolves them independently of which locale rung won — they are not a by-product of the language cascade', async () => {
243+
// Rung 1 answers the language, so the deployment cascade does NOT decide
244+
// `locale` here. Before this card that short-circuit was the only reason
245+
// the cascade was consulted at all, and `currency` / `timezone` came off
246+
// an ExecutionContext that never carried them.
247+
const { get } = mount({ storedLocale: 'ja-JP', settingLocale: 'zh-CN', settingTimezone: 'Europe/Paris', settingCurrency: 'eur' });
248+
const { body } = await get('de-DE');
249+
// `eur` lower-case: the cascade's own coercion upper-cases a 3-letter code.
250+
expect(body).toEqual({ authenticated: true, currency: 'EUR', locale: 'ja-JP', timezone: 'Europe/Paris' });
251+
});
252+
253+
it('a value the cascade refuses falls to the cascade\'s own answer — this surface adds no second parser', async () => {
254+
// `coerceCurrency` takes exactly three letters; `coerceTimeZone` takes an
255+
// `iana_time_zone` domain member. Neither refusal is re-implemented here:
256+
// the endpoint answers whatever the shared resolver answers.
257+
const { get } = mount({ settingTimezone: 'Middle/Earth', settingCurrency: 'euro' });
258+
const { body } = await get();
259+
expect(body.timezone).toBe('UTC');
260+
expect(body.currency).toBeNull();
261+
});
262+
263+
it('with nothing configured: the time-zone floor answers, and currency is the one value that stays null', async () => {
264+
// The asymmetry is the cascade's, and it is deliberate to pin: `timezone`
265+
// has a floor (`UTC`) so an authenticated caller can never see null for
266+
// it again, while `currency` has none — a deployment that configures no
267+
// currency has no reference currency, and inventing one would be a wrong
268+
// answer rather than a missing one.
269+
const { get } = mount({});
270+
const { body } = await get();
271+
expect(body).toEqual({ authenticated: true, currency: null, locale: 'en-US', timezone: 'UTC' });
272+
});
273+
274+
it('the unauthenticated answer stays localization-free', async () => {
275+
const { get, reads } = mount({ authenticated: false, settingTimezone: 'Asia/Shanghai', settingCurrency: 'CNY' });
276+
const { body } = await get();
277+
expect(body).toEqual({ authenticated: false });
278+
// Anti-vacuity: no settings read is issued for a caller with no session.
279+
expect(reads.filter((r) => r.object === 'sys_setting')).toEqual([]);
280+
});
281+
});

0 commit comments

Comments
 (0)