Skip to content

Commit f9df471

Browse files
Alexey ZorkaltsevAlexey Zorkaltsev
authored andcommitted
bug: fix metadata-token-service.ts
1 parent 7ac6ff1 commit f9df471

2 files changed

Lines changed: 203 additions & 14 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import axios from 'axios'
2+
import {MetadataTokenService} from './metadata-token-service'
3+
import Mock = jest.Mock;
4+
5+
describe('metadata-token-service', () => {
6+
7+
let oldGet = axios.get;
8+
9+
beforeEach(() => {
10+
axios.get = jest.fn();
11+
jest.useFakeTimers();
12+
});
13+
14+
afterEach(() => {
15+
axios.get = oldGet;
16+
jest.useRealTimers();
17+
});
18+
19+
it('simple scenario', async () => {
20+
21+
let metadataTokenService = new MetadataTokenService();
22+
23+
// set token
24+
(axios.get as Mock).mockReturnValue({
25+
status: 200,
26+
data: {
27+
access_token: '123'
28+
}
29+
});
30+
31+
// first time
32+
const t1 = await metadataTokenService.getToken();
33+
34+
expect(t1).toBe('123');
35+
expect((axios.get as Mock).mock.calls).toHaveLength(1);
36+
37+
// second time - no extra token request
38+
const t2 = await metadataTokenService.getToken();
39+
40+
expect(t2).toBe('123');
41+
expect((axios.get as Mock).mock.calls).toHaveLength(1);
42+
});
43+
44+
it('provider works over few hours, so token expected to get updated every hour', async () => {
45+
46+
let metadataTokenService = new MetadataTokenService();
47+
48+
// set token
49+
(axios.get as Mock).mockReturnValue({
50+
status: 200,
51+
data: {
52+
access_token: '123'
53+
}
54+
});
55+
56+
// first time
57+
let t = await metadataTokenService.getToken();
58+
expect(t).toBe('123');
59+
expect((axios.get as Mock).mock.calls).toHaveLength(1);
60+
61+
// change token
62+
(axios.get as Mock).mockReturnValue({
63+
status: 200,
64+
data: {
65+
access_token: '456'
66+
}
67+
});
68+
69+
// after 30 minutes
70+
jest.advanceTimersByTime(30 * 60 * 1000);
71+
72+
t = await metadataTokenService.getToken();
73+
expect(t).toBe('123');
74+
expect((axios.get as Mock).mock.calls).toHaveLength(1);
75+
76+
// after 1 hour
77+
jest.advanceTimersByTime(30 * 60 * 1000);
78+
79+
t = await metadataTokenService.getToken();
80+
expect(t).toBe('456');
81+
expect((axios.get as Mock).mock.calls).toHaveLength(2);
82+
83+
// change token
84+
(axios.get as Mock).mockReturnValue({
85+
status: 200,
86+
data: {
87+
access_token: '789'
88+
}
89+
});
90+
91+
// after 1 hour 30 minutes
92+
jest.advanceTimersByTime(30 * 60 * 1000);
93+
94+
t = await metadataTokenService.getToken();
95+
expect(t).toBe('456');
96+
expect((axios.get as Mock).mock.calls).toHaveLength(2);
97+
98+
// after 2 hours
99+
jest.advanceTimersByTime(30 * 60 * 1000);
100+
101+
t = await metadataTokenService.getToken();
102+
expect(t).toBe('789');
103+
expect((axios.get as Mock).mock.calls).toHaveLength(3);
104+
105+
// after 2 hours 30 minutes
106+
jest.advanceTimersByTime(30 * 60 * 1000);
107+
108+
t = await metadataTokenService.getToken();
109+
expect(t).toBe('789');
110+
expect((axios.get as Mock).mock.calls).toHaveLength(3);
111+
112+
// after 2 hours 59 minutes
113+
jest.advanceTimersByTime(29 * 60 * 1000);
114+
115+
t = await metadataTokenService.getToken();
116+
expect(t).toBe('789');
117+
expect((axios.get as Mock).mock.calls).toHaveLength(3);
118+
});
119+
120+
it('Iam always returns an error', async () => {
121+
122+
let metadataTokenService = new MetadataTokenService();
123+
124+
// return an error
125+
(axios.get as Mock).mockReturnValue({
126+
status: 400,
127+
});
128+
129+
await expect(() => metadataTokenService.getToken()).rejects.toThrow();
130+
});
131+
132+
it('Iam occasionally returns an error', async () => {
133+
134+
let metadataTokenService = new MetadataTokenService();
135+
136+
// return token on 4th attempt - tests initialize()
137+
const nextResp = (function*() {
138+
139+
for (let i = 0; i < 3; i++) {
140+
yield {
141+
status: 400,
142+
};
143+
}
144+
145+
return {
146+
status: 200,
147+
data: {
148+
access_token: '123'
149+
}
150+
};
151+
})();
152+
153+
(axios.get as Mock).mockImplementation(() => nextResp.next().value);
154+
155+
// first time - return token, even if it was returned only on 4th attempt
156+
let t = await metadataTokenService.getToken();
157+
expect(t).toBe('123');
158+
expect((axios.get as Mock).mock.calls).toHaveLength(4);
159+
160+
// after 1 hour, return on an error use old token and make only one attempt to get token
161+
(axios.get as Mock).mockReturnValue({
162+
status: 400,
163+
});
164+
165+
// return an error
166+
(axios.get as Mock).mockReturnValue({
167+
status: 400,
168+
});
169+
170+
// after 1 hour,
171+
jest.advanceTimersByTime(60 * 60 * 1000);
172+
173+
// Iam returns an error on 1st attempt, so we use the old token
174+
t = await metadataTokenService.getToken();
175+
expect(t).toBe('123');
176+
expect((axios.get as Mock).mock.calls).toHaveLength(5);
177+
178+
// on next attempt we receive new token, and use this one
179+
(axios.get as Mock).mockReturnValue({
180+
status: 200,
181+
data: {
182+
access_token: '456'
183+
}
184+
});
185+
186+
t = await metadataTokenService.getToken();
187+
expect(t).toBe('456');
188+
expect((axios.get as Mock).mock.calls).toHaveLength(6);
189+
});
190+
});

src/token-service/metadata-token-service.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,32 @@ const DEFAULT_OPTIONS: Options = {
1111
},
1212
};
1313

14+
const TOKEN_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour ~ 10% of 12 hours as recommended in Iam documentation
15+
1416
export class MetadataTokenService implements TokenService {
1517
private readonly url: string;
1618
private readonly opts: Options;
1719
private token?: string;
20+
private lastFetch = 0;
1821

1922
constructor(url: string = DEFAULT_URL, options: Options = DEFAULT_OPTIONS) {
2023
this.url = url;
2124
this.opts = options;
2225
}
2326

2427
async getToken(): Promise<string> {
25-
if (!this.token) {
26-
await this.initialize();
2728

28-
if (!this.token) {
29-
throw new Error('Token is empty after MetadataTokenService.initialize');
29+
if (!this.token) {
30+
await this.initialize(); // may throw error, so there is no need to check for !this.token
31+
} else if ((Date.now() - this.lastFetch) >= TOKEN_UPDATE_PERIOD_MS) { // then time to update token
32+
try {
33+
this.token = await this.fetchToken();
34+
} catch {
35+
// nothing - use old token
3036
}
31-
32-
return this.token;
3337
}
3438

35-
return this.token;
39+
return this.token as string;
3640
}
3741

3842
private async fetchToken(): Promise<string> {
@@ -42,6 +46,8 @@ export class MetadataTokenService implements TokenService {
4246
throw new Error(`failed to fetch token from metadata service: ${res.status} ${res.statusText}`);
4347
}
4448

49+
this.lastFetch = Date.now();
50+
4551
return res.data.access_token;
4652
}
4753

@@ -67,12 +73,5 @@ export class MetadataTokenService implements TokenService {
6773
`failed to fetch token from metadata service: ${lastError}`,
6874
);
6975
}
70-
setTimeout(async () => {
71-
try {
72-
this.token = await this.fetchToken();
73-
} catch {
74-
// TBD
75-
}
76-
}, 30_000);
7776
}
7877
}

0 commit comments

Comments
 (0)