|
| 1 | +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * i18n wire-dialect proof (#3636) — the SDK's i18n calls resolve against a |
| 5 | + * REAL router, not a pinned URL string. |
| 6 | + * |
| 7 | + * The URL pins in `client.test.ts` say what the client sends. They cannot say |
| 8 | + * whether anything answers: that is exactly how `i18n.getTranslations` and |
| 9 | + * `i18n.getFieldLabels` shipped a `?locale=` dialect no server has ever |
| 10 | + * mounted, passing every client-side test while 404-ing in production. |
| 11 | + * |
| 12 | + * So this suite mounts the two route patterns every serving surface declares — |
| 13 | + * `service-i18n`'s autonomous mounts, the dispatcher's HTTP mounts, and the |
| 14 | + * `plugin-rest-api.zod.ts` contract all agree on them — on a real Hono server, |
| 15 | + * and drives the real client at it. The old dialect is asserted DEAD in the |
| 16 | + * same suite, so a revert cannot pass quietly. |
| 17 | + * |
| 18 | + * service-i18n itself is deliberately not imported: it is not a dependency of |
| 19 | + * `@objectstack/client` and must not become one (the tranche-1 no-package-edge |
| 20 | + * lesson). The route PATTERNS are the contract under test, and they are |
| 21 | + * identical on both surfaces. |
| 22 | + */ |
| 23 | + |
| 24 | +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; |
| 25 | +import { LiteKernel } from '@objectstack/core'; |
| 26 | +import { HonoServerPlugin } from '@objectstack/plugin-hono-server'; |
| 27 | +import type { IHttpServer, IHttpRequest, IHttpResponse } from '@objectstack/spec/contracts'; |
| 28 | +import { ObjectStackClient } from './index'; |
| 29 | + |
| 30 | +describe('i18n SDK calls hit a real router (#3636)', () => { |
| 31 | + let baseUrl: string; |
| 32 | + let kernel: LiteKernel; |
| 33 | + let client: ObjectStackClient; |
| 34 | + |
| 35 | + beforeAll(async () => { |
| 36 | + kernel = new LiteKernel(); |
| 37 | + const hono = new HonoServerPlugin({ port: 0 }); |
| 38 | + kernel.use(hono); |
| 39 | + await kernel.bootstrap(); |
| 40 | + |
| 41 | + // The same three patterns I18nServicePlugin.registerI18nRoutes mounts, |
| 42 | + // answering with the same `{ success: true, data }` envelope it now emits |
| 43 | + // — see packages/services/service-i18n/src/i18n-route-ledger.ts. |
| 44 | + const server = kernel.getService<IHttpServer>('http-server'); |
| 45 | + server.get('/api/v1/i18n/locales', async (_req: IHttpRequest, res: IHttpResponse) => { |
| 46 | + res.json({ success: true, data: { locales: [{ code: 'zh-CN', label: 'zh-CN', isDefault: true }] } }); |
| 47 | + }); |
| 48 | + server.get('/api/v1/i18n/translations/:locale', async (req: IHttpRequest, res: IHttpResponse) => { |
| 49 | + res.json({ success: true, data: { locale: req.params.locale, translations: { hello: '你好' } } }); |
| 50 | + }); |
| 51 | + server.get('/api/v1/i18n/labels/:object/:locale', async (req: IHttpRequest, res: IHttpResponse) => { |
| 52 | + res.json({ success: true, data: { object: req.params.object, locale: req.params.locale, labels: { name: '名称' } } }); |
| 53 | + }); |
| 54 | + |
| 55 | + baseUrl = `http://localhost:${(server as unknown as { getPort(): number }).getPort()}`; |
| 56 | + client = new ObjectStackClient({ baseUrl }); |
| 57 | + }); |
| 58 | + |
| 59 | + afterAll(async () => { |
| 60 | + await kernel?.shutdown(); |
| 61 | + }); |
| 62 | + |
| 63 | + it('getTranslations resolves — the locale arrives as a path param', async () => { |
| 64 | + const res = await client.i18n.getTranslations('zh-CN'); |
| 65 | + expect(res.locale).toBe('zh-CN'); |
| 66 | + expect(res.translations).toEqual({ hello: '你好' }); |
| 67 | + }); |
| 68 | + |
| 69 | + it('getFieldLabels resolves — object AND locale arrive as path params', async () => { |
| 70 | + const res = await client.i18n.getFieldLabels('customer', 'zh-CN'); |
| 71 | + expect(res.object).toBe('customer'); |
| 72 | + expect(res.locale).toBe('zh-CN'); |
| 73 | + expect(res.labels).toEqual({ name: '名称' }); |
| 74 | + }); |
| 75 | + |
| 76 | + it('getLocales resolves', async () => { |
| 77 | + const res = await client.i18n.getLocales(); |
| 78 | + expect(res.locales).toHaveLength(1); |
| 79 | + }); |
| 80 | + |
| 81 | + it('the abandoned query dialect is dead on the wire — 404, both shapes', async () => { |
| 82 | + // This is what the client sent before #3636. Nothing routes it. |
| 83 | + for (const dead of [ |
| 84 | + '/api/v1/i18n/translations?locale=zh-CN', |
| 85 | + '/api/v1/i18n/labels/customer?locale=zh-CN', |
| 86 | + ]) { |
| 87 | + const res = await fetch(`${baseUrl}${dead}`); |
| 88 | + expect(res.status, `${dead} should not resolve on any surface`).toBe(404); |
| 89 | + } |
| 90 | + }); |
| 91 | +}); |
0 commit comments