Skip to content

Commit 70bc0ff

Browse files
committed
Port 3P token API to exp
1 parent 3ea1e1f commit 70bc0ff

File tree

10 files changed

+455
-61
lines changed

10 files changed

+455
-61
lines changed

common/api-review/app-check-exp.api.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
```ts
66

77
import { FirebaseApp } from '@firebase/app-exp';
8+
import { PartialObserver } from '@firebase/util';
9+
import { Unsubscribe } from '@firebase/util';
810

911
// @public
1012
export interface AppCheck {
@@ -30,6 +32,14 @@ export interface AppCheckToken {
3032
readonly token: string;
3133
}
3234

35+
// @public
36+
export type AppCheckTokenListener = (token: AppCheckTokenResult) => void;
37+
38+
// @public
39+
export interface AppCheckTokenResult {
40+
readonly token: string;
41+
}
42+
3343
// Warning: (ae-forgotten-export) The symbol "AppCheckProvider" needs to be exported by the entry point index.d.ts
3444
//
3545
// @public
@@ -48,9 +58,18 @@ export interface CustomProviderOptions {
4858
getToken: () => Promise<AppCheckToken>;
4959
}
5060

61+
// @public
62+
export function getToken(appCheckInstance: AppCheck, forceRefresh?: boolean): Promise<AppCheckTokenResult>;
63+
5164
// @public
5265
export function initializeAppCheck(app: FirebaseApp | undefined, options: AppCheckOptions): AppCheck;
5366

67+
// @public
68+
export function onTokenChanged(appCheckInstance: AppCheck, observer: PartialObserver<AppCheckTokenResult>): Unsubscribe;
69+
70+
// @public
71+
export function onTokenChanged(appCheckInstance: AppCheck, onNext: (tokenResult: AppCheckTokenResult) => void, onError?: (error: Error) => void, onCompletion?: () => void): Unsubscribe;
72+
5473
// @public
5574
export class ReCaptchaV3Provider implements AppCheckProvider {
5675
constructor(_siteKey: string);
@@ -61,7 +80,7 @@ export class ReCaptchaV3Provider implements AppCheckProvider {
6180
}
6281

6382
// @public
64-
export function setTokenAutoRefreshEnabled(app: FirebaseApp, isTokenAutoRefreshEnabled: boolean): void;
83+
export function setTokenAutoRefreshEnabled(appCheckInstance: AppCheck, isTokenAutoRefreshEnabled: boolean): void;
6584

6685

6786
// (No @packageDocumentation comment for this package)

packages-exp/app-check-exp/src/api.test.ts

+185-5
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,29 @@
1616
*/
1717
import '../test/setup';
1818
import { expect } from 'chai';
19-
import { stub } from 'sinon';
20-
import { setTokenAutoRefreshEnabled, initializeAppCheck } from './api';
21-
import { FAKE_SITE_KEY, getFullApp, getFakeApp } from '../test/util';
22-
import { getState } from './state';
19+
import { match, spy, stub } from 'sinon';
20+
import {
21+
setTokenAutoRefreshEnabled,
22+
initializeAppCheck,
23+
getToken,
24+
onTokenChanged
25+
} from './api';
26+
import {
27+
FAKE_SITE_KEY,
28+
getFullApp,
29+
getFakeApp,
30+
getFakeGreCAPTCHA,
31+
getFakeAppCheck,
32+
getFakePlatformLoggingProvider,
33+
removegreCAPTCHAScriptsOnPage
34+
} from '../test/util';
35+
import { clearState, getState } from './state';
2336
import * as reCAPTCHA from './recaptcha';
37+
import * as util from './util';
38+
import * as logger from './logger';
39+
import * as client from './client';
40+
import * as storage from './storage';
41+
import * as internalApi from './internal-api';
2442
import { deleteApp, FirebaseApp } from '@firebase/app-exp';
2543
import { ReCaptchaV3Provider } from './providers';
2644

@@ -29,9 +47,12 @@ describe('api', () => {
2947

3048
beforeEach(() => {
3149
app = getFullApp();
50+
stub(util, 'getRecaptcha').returns(getFakeGreCAPTCHA());
3251
});
3352

3453
afterEach(() => {
54+
clearState();
55+
removegreCAPTCHAScriptsOnPage();
3556
return deleteApp(app);
3657
});
3758

@@ -88,8 +109,167 @@ describe('api', () => {
88109
describe('setTokenAutoRefreshEnabled()', () => {
89110
it('sets isTokenAutoRefreshEnabled correctly', () => {
90111
const app = getFakeApp({ automaticDataCollectionEnabled: false });
91-
setTokenAutoRefreshEnabled(app, true);
112+
const appCheck = getFakeAppCheck(app);
113+
setTokenAutoRefreshEnabled(appCheck, true);
92114
expect(getState(app).isTokenAutoRefreshEnabled).to.equal(true);
93115
});
94116
});
117+
describe('getToken()', () => {
118+
it('getToken() calls the internal getToken() function', async () => {
119+
const app = getFakeApp({ automaticDataCollectionEnabled: true });
120+
const appCheck = getFakeAppCheck(app);
121+
const internalGetToken = stub(internalApi, 'getToken').resolves({
122+
token: 'a-token-string'
123+
});
124+
await getToken(appCheck, true);
125+
expect(internalGetToken).to.be.calledWith(
126+
appCheck.app,
127+
match.any, // platformLoggerProvider
128+
true
129+
);
130+
});
131+
it('getToken() throws errors returned with token', async () => {
132+
const app = getFakeApp({ automaticDataCollectionEnabled: true });
133+
const appCheck = getFakeAppCheck(app);
134+
// If getToken() errors, it returns a dummy token with an error field
135+
// instead of throwing.
136+
stub(internalApi, 'getToken').resolves({
137+
token: 'a-dummy-token',
138+
error: Error('there was an error')
139+
});
140+
await expect(getToken(appCheck, true)).to.be.rejectedWith(
141+
'there was an error'
142+
);
143+
});
144+
});
145+
describe('onTokenChanged()', () => {
146+
it('Listeners work when using top-level parameters pattern', async () => {
147+
const appCheck = initializeAppCheck(app, {
148+
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
149+
isTokenAutoRefreshEnabled: true
150+
});
151+
const fakeRecaptchaToken = 'fake-recaptcha-token';
152+
const fakeRecaptchaAppCheckToken = {
153+
token: 'fake-recaptcha-app-check-token',
154+
expireTimeMillis: 123,
155+
issuedAtTimeMillis: 0
156+
};
157+
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
158+
stub(client, 'exchangeToken').returns(
159+
Promise.resolve(fakeRecaptchaAppCheckToken)
160+
);
161+
stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined));
162+
163+
const listener1 = (): void => {
164+
throw new Error();
165+
};
166+
const listener2 = spy();
167+
168+
const errorFn1 = spy();
169+
const errorFn2 = spy();
170+
171+
const unsubscribe1 = onTokenChanged(appCheck, listener1, errorFn1);
172+
const unsubscribe2 = onTokenChanged(appCheck, listener2, errorFn2);
173+
174+
expect(getState(app).tokenObservers.length).to.equal(2);
175+
176+
await internalApi.getToken(
177+
appCheck.app,
178+
getFakePlatformLoggingProvider()
179+
);
180+
181+
expect(listener2).to.be.calledWith({
182+
token: fakeRecaptchaAppCheckToken.token
183+
});
184+
// onError should not be called on listener errors.
185+
expect(errorFn1).to.not.be.called;
186+
expect(errorFn2).to.not.be.called;
187+
unsubscribe1();
188+
unsubscribe2();
189+
expect(getState(app).tokenObservers.length).to.equal(0);
190+
});
191+
192+
it('Listeners work when using Observer pattern', async () => {
193+
const appCheck = initializeAppCheck(app, {
194+
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
195+
isTokenAutoRefreshEnabled: true
196+
});
197+
const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();
198+
const fakeRecaptchaToken = 'fake-recaptcha-token';
199+
const fakeRecaptchaAppCheckToken = {
200+
token: 'fake-recaptcha-app-check-token',
201+
expireTimeMillis: 123,
202+
issuedAtTimeMillis: 0
203+
};
204+
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
205+
stub(client, 'exchangeToken').returns(
206+
Promise.resolve(fakeRecaptchaAppCheckToken)
207+
);
208+
stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined));
209+
210+
const listener1 = (): void => {
211+
throw new Error();
212+
};
213+
const listener2 = spy();
214+
215+
const errorFn1 = spy();
216+
const errorFn2 = spy();
217+
218+
/**
219+
* Reverse the order of adding the failed and successful handler, for extra
220+
* testing.
221+
*/
222+
const unsubscribe2 = onTokenChanged(appCheck, {
223+
next: listener2,
224+
error: errorFn2
225+
});
226+
const unsubscribe1 = onTokenChanged(appCheck, {
227+
next: listener1,
228+
error: errorFn1
229+
});
230+
231+
expect(getState(app).tokenObservers.length).to.equal(2);
232+
233+
await internalApi.getToken(appCheck.app, fakePlatformLoggingProvider);
234+
235+
expect(listener2).to.be.calledWith({
236+
token: fakeRecaptchaAppCheckToken.token
237+
});
238+
// onError should not be called on listener errors.
239+
expect(errorFn1).to.not.be.called;
240+
expect(errorFn2).to.not.be.called;
241+
unsubscribe1();
242+
unsubscribe2();
243+
expect(getState(app).tokenObservers.length).to.equal(0);
244+
});
245+
246+
it('onError() catches token errors', async () => {
247+
stub(logger.logger, 'error');
248+
const appCheck = initializeAppCheck(app, {
249+
provider: new ReCaptchaV3Provider(FAKE_SITE_KEY),
250+
isTokenAutoRefreshEnabled: false
251+
});
252+
const fakePlatformLoggingProvider = getFakePlatformLoggingProvider();
253+
const fakeRecaptchaToken = 'fake-recaptcha-token';
254+
stub(reCAPTCHA, 'getToken').returns(Promise.resolve(fakeRecaptchaToken));
255+
stub(client, 'exchangeToken').rejects('exchange error');
256+
stub(storage, 'writeTokenToStorage').returns(Promise.resolve(undefined));
257+
258+
const listener1 = spy();
259+
260+
const errorFn1 = spy();
261+
262+
const unsubscribe1 = onTokenChanged(appCheck, listener1, errorFn1);
263+
264+
await internalApi.getToken(app, fakePlatformLoggingProvider);
265+
266+
expect(getState(app).tokenObservers.length).to.equal(1);
267+
268+
expect(errorFn1).to.be.calledOnce;
269+
expect(errorFn1.args[0][0].name).to.include('exchange error');
270+
271+
unsubscribe1();
272+
expect(getState(app).tokenObservers.length).to.equal(0);
273+
});
274+
});
95275
});

0 commit comments

Comments
 (0)