Skip to content

Commit 14c744d

Browse files
committed
Add AppCheck public API (#23)
* Add AppCheck public API * Add AppCheck public api unit tests
1 parent 9b0351a commit 14c744d

File tree

10 files changed

+206
-0
lines changed

10 files changed

+206
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ node_modules/
1414
# Real key file should not be checked in
1515
test/resources/key.json
1616
test/resources/apikey.txt
17+
test/resources/appid.txt
1718

1819
# Release tarballs should not be checked in
1920
firebase-admin-*.tgz

etc/firebase-admin.api.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export function app(name?: string): app.App;
1515
// @public (undocumented)
1616
export namespace app {
1717
export interface App {
18+
// (undocumented)
19+
appCheck(): appCheck.AppCheck;
1820
// (undocumented)
1921
auth(): auth.Auth;
2022
// (undocumented)
@@ -41,6 +43,22 @@ export namespace app {
4143
}
4244
}
4345

46+
// @public
47+
export function appCheck(app?: app.App): appCheck.AppCheck;
48+
49+
// @public (undocumented)
50+
export namespace appCheck {
51+
export interface AppCheck {
52+
// (undocumented)
53+
app: app.App;
54+
createToken(appId: string): Promise<AppCheckToken>;
55+
}
56+
export interface AppCheckToken {
57+
token: string;
58+
ttlMillis: number;
59+
}
60+
}
61+
4462
// @public
4563
export interface AppOptions {
4664
credential?: credential.Credential;

src/firebase-app.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { InstanceId } from './instance-id/instance-id';
3535
import { ProjectManagement } from './project-management/project-management';
3636
import { SecurityRules } from './security-rules/security-rules';
3737
import { RemoteConfig } from './remote-config/remote-config';
38+
import { AppCheck } from './app-check/app-check';
3839

3940
import Credential = credential.Credential;
4041
import Database = database.Database;
@@ -318,6 +319,18 @@ export class FirebaseApp implements app.App {
318319
});
319320
}
320321

322+
/**
323+
* Returns the AppCheck service instance associated with this app.
324+
*
325+
* @return The AppCheck service instance of this app.
326+
*/
327+
public appCheck(): AppCheck {
328+
return this.ensureService_('appCheck', () => {
329+
const appCheckService: typeof AppCheck = require('./app-check/app-check').AppCheck;
330+
return new appCheckService(this);
331+
});
332+
}
333+
321334
/**
322335
* Returns the name of the FirebaseApp instance.
323336
*

src/firebase-namespace-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import { Agent } from 'http';
18+
import { appCheck } from './app-check/index';
1819
import { auth } from './auth/index';
1920
import { credential } from './credential/index';
2021
import { database } from './database/index';
@@ -222,6 +223,7 @@ export namespace app {
222223
*/
223224
options: AppOptions;
224225

226+
appCheck(): appCheck.AppCheck;
225227
auth(): auth.Auth;
226228
database(url?: string): database.Database;
227229
firestore(): firestore.Firestore;

src/firebase-namespace.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
export * from './credential/index';
1818
export * from './firebase-namespace-api';
19+
export * from './app-check/index';
1920
export * from './auth/index';
2021
export * from './database/index';
2122
export * from './firestore/index';

src/firebase-namespace.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { FirebaseApp } from './firebase-app';
2323
import { cert, refreshToken, applicationDefault } from './credential/credential';
2424
import { getApplicationDefault } from './credential/credential-internal';
2525

26+
import { appCheck } from './app-check/index';
2627
import { auth } from './auth/index';
2728
import { database } from './database/index';
2829
import { firestore } from './firestore/index';
@@ -38,6 +39,7 @@ import * as validator from './utils/validator';
3839
import { getSdkVersion } from './utils/index';
3940

4041
import App = app.App;
42+
import AppCheck = appCheck.AppCheck;
4143
import Auth = auth.Auth;
4244
import Database = database.Database;
4345
import Firestore = firestore.Firestore;
@@ -357,6 +359,18 @@ export class FirebaseNamespace {
357359
return Object.assign(fn, { RemoteConfig: remoteConfig });
358360
}
359361

362+
/**
363+
* Gets the `AppCheck` service namespace. The returned namespace can be used to get the
364+
* `AppCheck` service for the default app or an explicitly specified app.
365+
*/
366+
get appCheck(): FirebaseServiceNamespace<AppCheck> {
367+
const fn: FirebaseServiceNamespace<AppCheck> = (app?: App) => {
368+
return this.ensureApp(app).appCheck();
369+
};
370+
const appCheck = require('./app-check/app-check').AppCheck;
371+
return Object.assign(fn, { AppCheck: appCheck });
372+
}
373+
360374
// TODO: Change the return types to app.App in the following methods.
361375

362376
/**

test/integration/app-check.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*!
2+
* Copyright 2021 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as _ from 'lodash';
18+
import * as admin from '../../lib/index';
19+
import * as chai from 'chai';
20+
import * as chaiAsPromised from 'chai-as-promised';
21+
import fs = require('fs');
22+
import path = require('path');
23+
24+
// eslint-disable-next-line @typescript-eslint/no-var-requires
25+
const chalk = require('chalk');
26+
27+
chai.should();
28+
chai.use(chaiAsPromised);
29+
30+
const expect = chai.expect;
31+
32+
let appId: string;
33+
34+
describe('admin.appCheck', () => {
35+
before(async () => {
36+
try {
37+
appId = fs.readFileSync(path.join(__dirname, '../resources/appId.txt')).toString().trim();
38+
} catch (error) {
39+
console.log(chalk.yellow(
40+
'Unable to find an an App ID. Skipping tests that require a valid App ID.',
41+
error,
42+
));
43+
}
44+
});
45+
46+
describe('createToken', () => {
47+
it('should succeed with a vaild token', function() {
48+
if (!appId) {
49+
this.skip();
50+
}
51+
return admin.appCheck().createToken(appId as string)
52+
.then((token) => {
53+
expect(token).to.have.keys(['token', 'ttlMillis']);
54+
expect(token.token).to.be.a('string').and.to.not.be.empty;
55+
expect(token.ttlMillis).to.be.a('number');
56+
});
57+
});
58+
59+
it('should propagate API errors', () => {
60+
// rejects with invalid-argument when appId is incorrect
61+
return admin.appCheck().createToken('incorrect-app-id')
62+
.should.eventually.be.rejected.and.have.property('code', 'app-check/invalid-argument');
63+
});
64+
65+
const invalidAppIds = ['', null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop];
66+
invalidAppIds.forEach((invalidAppId) => {
67+
it(`should throw given an invalid appId: ${JSON.stringify(invalidAppId)}`, () => {
68+
expect(() => admin.appCheck().createToken(invalidAppId as any))
69+
.to.throw('appId` must be a non-empty string.');
70+
});
71+
});
72+
});
73+
});

test/unit/firebase-app.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { instanceId } from '../../src/instance-id/index';
4141
import { projectManagement } from '../../src/project-management/index';
4242
import { securityRules } from '../../src/security-rules/index';
4343
import { remoteConfig } from '../../src/remote-config/index';
44+
import { appCheck } from '../../src/app-check/index';
4445
import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error';
4546

4647
import Auth = auth.Auth;
@@ -53,6 +54,7 @@ import InstanceId = instanceId.InstanceId;
5354
import ProjectManagement = projectManagement.ProjectManagement;
5455
import SecurityRules = securityRules.SecurityRules;
5556
import RemoteConfig = remoteConfig.RemoteConfig;
57+
import AppCheck = appCheck.AppCheck;
5658

5759
chai.should();
5860
chai.use(sinonChai);
@@ -669,6 +671,32 @@ describe('FirebaseApp', () => {
669671
});
670672
});
671673

674+
describe('appCheck()', () => {
675+
it('should throw if the app has already been deleted', () => {
676+
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
677+
678+
return app.delete().then(() => {
679+
expect(() => {
680+
return app.appCheck();
681+
}).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`);
682+
});
683+
});
684+
685+
it('should return the AppCheck client', () => {
686+
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
687+
688+
const appCheck: AppCheck = app.appCheck();
689+
expect(appCheck).to.not.be.null;
690+
});
691+
692+
it('should return a cached version of AppCheck on subsequent calls', () => {
693+
const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName);
694+
const service1: AppCheck = app.appCheck();
695+
const service2: AppCheck = app.appCheck();
696+
expect(service1).to.equal(service2);
697+
});
698+
});
699+
672700
describe('INTERNAL.getToken()', () => {
673701

674702
it('throws a custom credential implementation which returns invalid access tokens', () => {

test/unit/firebase-namespace.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ import { instanceId } from '../../src/instance-id/index';
5656
import { projectManagement } from '../../src/project-management/index';
5757
import { securityRules } from '../../src/security-rules/index';
5858
import { remoteConfig } from '../../src/remote-config/index';
59+
import { appCheck } from '../../src/app-check/index';
5960

61+
import { AppCheck as AppCheckImpl } from '../../src/app-check/app-check';
6062
import { Auth as AuthImpl } from '../../src/auth/auth';
6163
import { InstanceId as InstanceIdImpl } from '../../src/instance-id/instance-id';
6264
import { MachineLearning as MachineLearningImpl } from '../../src/machine-learning/machine-learning';
@@ -67,6 +69,7 @@ import { SecurityRules as SecurityRulesImpl } from '../../src/security-rules/sec
6769
import { Storage as StorageImpl } from '../../src/storage/storage';
6870

6971
import App = app.App;
72+
import AppCheck = appCheck.AppCheck;
7073
import Auth = auth.Auth;
7174
import Database = database.Database;
7275
import Firestore = firestore.Firestore;
@@ -759,4 +762,42 @@ describe('FirebaseNamespace', () => {
759762
expect(service1).to.equal(service2);
760763
});
761764
});
765+
766+
describe('#appCheck()', () => {
767+
it('should throw when called before initializing an app', () => {
768+
expect(() => {
769+
firebaseNamespace.appCheck();
770+
}).to.throw(DEFAULT_APP_NOT_FOUND);
771+
});
772+
773+
it('should throw when default app is not initialized', () => {
774+
firebaseNamespace.initializeApp(mocks.appOptions, 'testApp');
775+
expect(() => {
776+
firebaseNamespace.appCheck();
777+
}).to.throw(DEFAULT_APP_NOT_FOUND);
778+
});
779+
780+
it('should return a valid namespace when the default app is initialized', () => {
781+
const app: App = firebaseNamespace.initializeApp(mocks.appOptions);
782+
const fac: AppCheck = firebaseNamespace.appCheck();
783+
expect(fac.app).to.be.deep.equal(app);
784+
});
785+
786+
it('should return a valid namespace when the named app is initialized', () => {
787+
const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp');
788+
const fac: AppCheck = firebaseNamespace.appCheck(app);
789+
expect(fac.app).to.be.deep.equal(app);
790+
});
791+
792+
it('should return a reference to AppCheck type', () => {
793+
expect(firebaseNamespace.appCheck.AppCheck).to.be.deep.equal(AppCheckImpl);
794+
});
795+
796+
it('should return a cached version of AppCheck on subsequent calls', () => {
797+
firebaseNamespace.initializeApp(mocks.appOptions);
798+
const service1: AppCheck = firebaseNamespace.appCheck();
799+
const service2: AppCheck = firebaseNamespace.appCheck();
800+
expect(service1).to.equal(service2);
801+
});
802+
});
762803
});

test/unit/firebase.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,21 @@ describe('Firebase', () => {
235235
});
236236
});
237237

238+
describe('#appCheck', () => {
239+
it('should throw if the app has not been initialized', () => {
240+
expect(() => {
241+
return firebaseAdmin.appCheck();
242+
}).to.throw('The default Firebase app does not exist.');
243+
});
244+
245+
it('should return the appCheck service', () => {
246+
firebaseAdmin.initializeApp(mocks.appOptions);
247+
expect(() => {
248+
return firebaseAdmin.appCheck();
249+
}).not.to.throw();
250+
});
251+
});
252+
238253
describe('#storage', () => {
239254
it('should throw if the app has not be initialized', () => {
240255
expect(() => {

0 commit comments

Comments
 (0)