@@ -7,9 +7,13 @@ import { StatsD } from 'hot-shots';
77import { DocumentNode } from 'graphql' ;
88
99import { 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' ;
1112import { StrapiClient , StrapiClientEventResponse } from './strapi.client' ;
1213import { 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
1418jest . 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