diff --git a/src/token-service/metadata-token-service.test.ts b/src/token-service/metadata-token-service.test.ts new file mode 100644 index 00000000..e46151cc --- /dev/null +++ b/src/token-service/metadata-token-service.test.ts @@ -0,0 +1,190 @@ +import axios from 'axios' +import {MetadataTokenService} from './metadata-token-service' +import Mock = jest.Mock; + +describe('metadata-token-service', () => { + + let oldGet = axios.get; + + beforeEach(() => { + axios.get = jest.fn(); + jest.useFakeTimers(); + }); + + afterEach(() => { + axios.get = oldGet; + jest.useRealTimers(); + }); + + it('simple scenario', async () => { + + let metadataTokenService = new MetadataTokenService(); + + // set token + (axios.get as Mock).mockReturnValue({ + status: 200, + data: { + access_token: '123' + } + }); + + // first time + const t1 = await metadataTokenService.getToken(); + + expect(t1).toBe('123'); + expect((axios.get as Mock).mock.calls).toHaveLength(1); + + // second time - no extra token request + const t2 = await metadataTokenService.getToken(); + + expect(t2).toBe('123'); + expect((axios.get as Mock).mock.calls).toHaveLength(1); + }); + + it('provider works over few hours, so token expected to get updated every hour', async () => { + + let metadataTokenService = new MetadataTokenService(); + + // set token + (axios.get as Mock).mockReturnValue({ + status: 200, + data: { + access_token: '123' + } + }); + + // first time + let t = await metadataTokenService.getToken(); + expect(t).toBe('123'); + expect((axios.get as Mock).mock.calls).toHaveLength(1); + + // change token + (axios.get as Mock).mockReturnValue({ + status: 200, + data: { + access_token: '456' + } + }); + + // after 30 minutes + jest.advanceTimersByTime(30 * 60 * 1000); + + t = await metadataTokenService.getToken(); + expect(t).toBe('123'); + expect((axios.get as Mock).mock.calls).toHaveLength(1); + + // after 1 hour + jest.advanceTimersByTime(30 * 60 * 1000); + + t = await metadataTokenService.getToken(); + expect(t).toBe('456'); + expect((axios.get as Mock).mock.calls).toHaveLength(2); + + // change token + (axios.get as Mock).mockReturnValue({ + status: 200, + data: { + access_token: '789' + } + }); + + // after 1 hour 30 minutes + jest.advanceTimersByTime(30 * 60 * 1000); + + t = await metadataTokenService.getToken(); + expect(t).toBe('456'); + expect((axios.get as Mock).mock.calls).toHaveLength(2); + + // after 2 hours + jest.advanceTimersByTime(30 * 60 * 1000); + + t = await metadataTokenService.getToken(); + expect(t).toBe('789'); + expect((axios.get as Mock).mock.calls).toHaveLength(3); + + // after 2 hours 30 minutes + jest.advanceTimersByTime(30 * 60 * 1000); + + t = await metadataTokenService.getToken(); + expect(t).toBe('789'); + expect((axios.get as Mock).mock.calls).toHaveLength(3); + + // after 2 hours 59 minutes + jest.advanceTimersByTime(29 * 60 * 1000); + + t = await metadataTokenService.getToken(); + expect(t).toBe('789'); + expect((axios.get as Mock).mock.calls).toHaveLength(3); + }); + + it('Iam always returns an error', async () => { + + let metadataTokenService = new MetadataTokenService(); + + // return an error + (axios.get as Mock).mockReturnValue({ + status: 400, + }); + + await expect(() => metadataTokenService.getToken()).rejects.toThrow(); + }); + + it('Iam occasionally returns an error', async () => { + + let metadataTokenService = new MetadataTokenService(); + + // return token on 4th attempt - tests initialize() + const nextResp = (function*() { + + for (let i = 0; i < 3; i++) { + yield { + status: 400, + }; + } + + return { + status: 200, + data: { + access_token: '123' + } + }; + })(); + + (axios.get as Mock).mockImplementation(() => nextResp.next().value); + + // first time - return token, even if it was returned only on 4th attempt + let t = await metadataTokenService.getToken(); + expect(t).toBe('123'); + expect((axios.get as Mock).mock.calls).toHaveLength(4); + + // after 1 hour, return on an error use old token and make only one attempt to get token + (axios.get as Mock).mockReturnValue({ + status: 400, + }); + + // return an error + (axios.get as Mock).mockReturnValue({ + status: 400, + }); + + // after 1 hour, + jest.advanceTimersByTime(60 * 60 * 1000); + + // Iam returns an error on 1st attempt, so we use the old token + t = await metadataTokenService.getToken(); + expect(t).toBe('123'); + expect((axios.get as Mock).mock.calls).toHaveLength(5); + + // on next attempt we receive new token, and use this one + (axios.get as Mock).mockReturnValue({ + status: 200, + data: { + access_token: '456' + } + }); + + t = await metadataTokenService.getToken(); + expect(t).toBe('456'); + expect((axios.get as Mock).mock.calls).toHaveLength(6); + }); +}); \ No newline at end of file diff --git a/src/token-service/metadata-token-service.ts b/src/token-service/metadata-token-service.ts index 03a83966..e51eaa60 100644 --- a/src/token-service/metadata-token-service.ts +++ b/src/token-service/metadata-token-service.ts @@ -11,10 +11,13 @@ const DEFAULT_OPTIONS: Options = { }, }; +const TOKEN_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour ~ 10% of 12 hours as recommended in Iam documentation + export class MetadataTokenService implements TokenService { private readonly url: string; private readonly opts: Options; private token?: string; + private lastFetch = 0; constructor(url: string = DEFAULT_URL, options: Options = DEFAULT_OPTIONS) { this.url = url; @@ -22,17 +25,18 @@ export class MetadataTokenService implements TokenService { } async getToken(): Promise { - if (!this.token) { - await this.initialize(); - if (!this.token) { - throw new Error('Token is empty after MetadataTokenService.initialize'); + if (!this.token) { + await this.initialize(); // may throw error, so there is no need to check for !this.token + } else if ((Date.now() - this.lastFetch) >= TOKEN_UPDATE_PERIOD_MS) { // then time to update token + try { + this.token = await this.fetchToken(); + } catch { + // nothing - use old token } - - return this.token; } - return this.token; + return this.token as string; } private async fetchToken(): Promise { @@ -42,6 +46,8 @@ export class MetadataTokenService implements TokenService { throw new Error(`failed to fetch token from metadata service: ${res.status} ${res.statusText}`); } + this.lastFetch = Date.now(); + return res.data.access_token; } @@ -67,12 +73,5 @@ export class MetadataTokenService implements TokenService { `failed to fetch token from metadata service: ${lastError}`, ); } - setTimeout(async () => { - try { - this.token = await this.fetchToken(); - } catch { - // TBD - } - }, 30_000); } }