Skip to content

Commit e8c508d

Browse files
authored
Merge pull request #19323 from mozilla/fxa-12253
feat(l10n): Update backend to return localized strapi data
2 parents 734176a + dbb0e7e commit e8c508d

10 files changed

Lines changed: 1203 additions & 797 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ secrets.json
3737
.es5
3838
.es5cache
3939
/artifacts
40+
**/artifacts
41+
**/.nx/cache/**/artifacts
4042
.last-audit
4143
.eslintcache
4244
storybooks-publish
@@ -60,6 +62,7 @@ coverage.html
6062
**/coverage
6163
**/.nyc_output
6264
test-results.xml
65+
**/artifacts/tests/**/*.xml
6366

6467
# Local configuration
6568
/_dev/firebase/.config

libs/shared/cms/src/lib/relying-party-configuration.manager.spec.ts

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import { StatsD } from 'hot-shots';
77
import { DocumentNode } from 'graphql';
88

99
import { StatsDService } from '@fxa/shared/metrics/statsd';
10-
import { relyingPartyQuery, RelyingPartyConfigurationManager } from '../../src';
10+
import { relyingPartyQuery } from '../../src';
11+
import { RelyingPartyConfigurationManager } from './relying-party-configuration.manager';
1112
import { StrapiClient, StrapiClientEventResponse } from './strapi.client';
1213
import { RelyingPartyQueryFactory } from './queries/relying-party/factories';
14+
import { MockFirestoreProvider } from '@fxa/shared/db/firestore';
15+
import { MockStrapiClientConfigProvider } from './strapi.client.config';
16+
import { LOGGER_PROVIDER } from '@fxa/shared/log';
1317

1418
jest.mock('@type-cacheable/core', () => {
1519
const noopDecorator =
@@ -55,6 +59,7 @@ describe('RelyingPartyConfigurationManager', () => {
5559
let relyingPartyConfigurationManager: RelyingPartyConfigurationManager;
5660
let mockStrapiClient: jest.Mocked<StrapiClient>;
5761
let mockStatsd: jest.Mocked<StatsD>;
62+
let mockWinstonLogger: any;
5863

5964
beforeEach(async () => {
6065
mockStatsd = {
@@ -70,17 +75,30 @@ describe('RelyingPartyConfigurationManager', () => {
7075
invalidateQueryCache: jest.fn(),
7176
} as any;
7277

78+
mockWinstonLogger = {
79+
error: jest.fn(),
80+
warn: jest.fn(),
81+
info: jest.fn(),
82+
debug: jest.fn(),
83+
} as any;
84+
7385
const module = await Test.createTestingModule({
7486
providers: [
7587
RelyingPartyConfigurationManager,
88+
MockStrapiClientConfigProvider,
89+
MockFirestoreProvider,
7690
{ provide: StatsDService, useValue: mockStatsd },
7791
{ provide: StrapiClient, useValue: mockStrapiClient },
92+
{ provide: LOGGER_PROVIDER, useValue: mockWinstonLogger },
7893
],
7994
}).compile();
8095

8196
relyingPartyConfigurationManager = module.get(
8297
RelyingPartyConfigurationManager
8398
);
99+
100+
// Get the mock StatsD service from the module
101+
mockStatsd = module.get(StatsDService);
84102
});
85103

86104
afterEach(() => {
@@ -156,7 +174,8 @@ describe('RelyingPartyConfigurationManager', () => {
156174
describe('fetchCMSData', () => {
157175
it('should call StrapiClient.query with correct arguments and return data', async () => {
158176
const mockData = RelyingPartyQueryFactory();
159-
const { clientId, entrypoint } = mockData.relyingParties[0];
177+
const clientId = 'test-client-id';
178+
const entrypoint = 'test-entrypoint';
160179

161180
mockStrapiClient.query.mockResolvedValue(mockData);
162181

@@ -186,7 +205,7 @@ describe('RelyingPartyConfigurationManager', () => {
186205
});
187206

188207
describe('invalidateCache', () => {
189-
it('should call StrapiClient.invalidateCache', async () => {
208+
it('should call StrapiClient.invalidateQueryCache with correct parameters', async () => {
190209
const clientId = 'test-client-id';
191210
const entrypoint = 'test-entrypoint';
192211

@@ -200,4 +219,153 @@ describe('RelyingPartyConfigurationManager', () => {
200219
);
201220
});
202221
});
222+
223+
describe('getFtlContent', () => {
224+
// Mock global fetch for these tests
225+
const mockFetch = jest.fn();
226+
global.fetch = mockFetch;
227+
228+
beforeEach(() => {
229+
mockFetch.mockClear();
230+
});
231+
232+
it('should fetch FTL content from URL successfully', async () => {
233+
const locale = 'en';
234+
const config = {
235+
cmsl10n: {
236+
ftlUrl: {
237+
template: 'https://example.com/locales/{locale}/cms.ftl',
238+
timeout: 5000,
239+
},
240+
},
241+
};
242+
const ftlContent = 'test FTL content';
243+
244+
mockFetch.mockResolvedValue({
245+
ok: true,
246+
text: () => Promise.resolve(ftlContent),
247+
});
248+
249+
const result = await relyingPartyConfigurationManager.getFtlContent(locale, config);
250+
251+
expect(result).toBe(ftlContent);
252+
expect(mockFetch).toHaveBeenCalledWith(
253+
'https://example.com/locales/en/cms.ftl',
254+
{
255+
headers: { Accept: 'text/plain' },
256+
signal: expect.any(AbortSignal),
257+
}
258+
);
259+
});
260+
261+
it('should return empty string when URL is not configured', async () => {
262+
const locale = 'en';
263+
const config = {
264+
cmsl10n: {
265+
ftlUrl: {
266+
template: '',
267+
timeout: 5000,
268+
},
269+
},
270+
};
271+
272+
const result = await relyingPartyConfigurationManager.getFtlContent(locale, config);
273+
274+
expect(result).toBe('');
275+
expect(mockFetch).not.toHaveBeenCalled();
276+
});
277+
278+
it('should return empty string when fetch returns 404', async () => {
279+
const locale = 'fr';
280+
const config = {
281+
cmsl10n: {
282+
ftlUrl: {
283+
template: 'https://example.com/locales/{locale}/cms.ftl',
284+
timeout: 5000,
285+
},
286+
},
287+
};
288+
289+
mockFetch.mockResolvedValue({
290+
ok: false,
291+
status: 404,
292+
});
293+
294+
const result = await relyingPartyConfigurationManager.getFtlContent(locale, config);
295+
296+
expect(result).toBe('');
297+
});
298+
299+
it('should handle fetch errors and emit metrics', async () => {
300+
const locale = 'en';
301+
const config = {
302+
cmsl10n: {
303+
ftlUrl: {
304+
template: 'https://example.com/locales/{locale}/cms.ftl',
305+
timeout: 5000,
306+
},
307+
},
308+
};
309+
310+
mockFetch.mockRejectedValue(new Error('Network error'));
311+
312+
await expect(
313+
relyingPartyConfigurationManager.getFtlContent(locale, config)
314+
).rejects.toThrow('Network error');
315+
316+
// Verify metrics are emitted for errors
317+
expect(mockStatsd.timing).toHaveBeenCalledWith(
318+
'cms_ftl_request',
319+
expect.any(Number),
320+
undefined,
321+
{
322+
method: 'getFtlContent',
323+
error: 'true',
324+
cache: 'false',
325+
locale,
326+
}
327+
);
328+
});
329+
});
330+
331+
describe('invalidateFtlCache', () => {
332+
it('should exist as a method and complete without error', async () => {
333+
const locale = 'en';
334+
335+
// The method exists and can be called, but the actual cache clearing
336+
// is handled by the @CacheClear decorators
337+
await expect(
338+
relyingPartyConfigurationManager.invalidateFtlCache(locale)
339+
).resolves.toBeUndefined();
340+
});
341+
});
342+
343+
describe('integration with StrapiClient events', () => {
344+
it('should handle FTL-related response events in metrics', () => {
345+
const eventHandler = mockStrapiClient.on.mock.calls[0][1];
346+
347+
const ftlResponse: StrapiClientEventResponse = {
348+
method: 'cacheFtl',
349+
requestStartTime: 1000,
350+
requestEndTime: 1100,
351+
elapsed: 100,
352+
cache: true,
353+
cacheType: 'memory',
354+
};
355+
356+
eventHandler(ftlResponse);
357+
358+
expect(mockStatsd.timing).toHaveBeenCalledWith(
359+
'cms_accounts_request',
360+
100,
361+
undefined,
362+
{
363+
method: 'cacheFtl',
364+
error: 'false',
365+
cache: 'true',
366+
cacheType: 'memory',
367+
}
368+
);
369+
});
370+
});
203371
});

0 commit comments

Comments
 (0)