Skip to content

Commit c38a2c9

Browse files
authored
Merge ec61867 into efafeec
2 parents efafeec + ec61867 commit c38a2c9

File tree

10 files changed

+211
-10
lines changed

10 files changed

+211
-10
lines changed

.changeset/lemon-ligers-protect.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@firebase/firestore-types': minor
3+
'@firebase/firestore': minor
4+
'firebase': minor
5+
---
6+
7+
Add mockUserToken support for Firestore.

packages/firebase/index.d.ts

+84-1
Original file line numberDiff line numberDiff line change
@@ -8179,8 +8179,16 @@ declare namespace firebase.firestore {
81798179
*
81808180
* @param host the emulator host (ex: localhost).
81818181
* @param port the emulator port (ex: 9000).
8182+
* @param options.mockUserToken - the mock auth token to use for unit
8183+
* testing Security Rules.
81828184
*/
8183-
useEmulator(host: string, port: number): void;
8185+
useEmulator(
8186+
host: string,
8187+
port: number,
8188+
options?: {
8189+
mockUserToken?: EmulatorMockTokenOptions;
8190+
}
8191+
): void;
81848192

81858193
/**
81868194
* Attempts to enable persistent storage, if possible.
@@ -9976,6 +9984,81 @@ declare namespace firebase.firestore {
99769984
name: string;
99779985
stack?: string;
99789986
}
9987+
9988+
type FirebaseSignInProvider =
9989+
| 'custom'
9990+
| 'email'
9991+
| 'password'
9992+
| 'phone'
9993+
| 'anonymous'
9994+
| 'google.com'
9995+
| 'facebook.com'
9996+
| 'github.com'
9997+
| 'twitter.com'
9998+
| 'microsoft.com'
9999+
| 'apple.com';
10000+
10001+
interface FirebaseIdToken {
10002+
/** Always set to https://securetoken.google.com/PROJECT_ID */
10003+
iss: string;
10004+
10005+
/** Always set to PROJECT_ID */
10006+
aud: string;
10007+
10008+
/** The user's unique id */
10009+
sub: string;
10010+
10011+
/** The token issue time, in seconds since epoch */
10012+
iat: number;
10013+
10014+
/** The token expiry time, normally 'iat' + 3600 */
10015+
exp: number;
10016+
10017+
/** The user's unique id, must be equal to 'sub' */
10018+
user_id: string;
10019+
10020+
/** The time the user authenticated, normally 'iat' */
10021+
auth_time: number;
10022+
10023+
/** The sign in provider, only set when the provider is 'anonymous' */
10024+
provider_id?: 'anonymous';
10025+
10026+
/** The user's primary email */
10027+
email?: string;
10028+
10029+
/** The user's email verification status */
10030+
email_verified?: boolean;
10031+
10032+
/** The user's primary phone number */
10033+
phone_number?: string;
10034+
10035+
/** The user's display name */
10036+
name?: string;
10037+
10038+
/** The user's profile photo URL */
10039+
picture?: string;
10040+
10041+
/** Information on all identities linked to this user */
10042+
firebase: {
10043+
/** The primary sign-in provider */
10044+
sign_in_provider: FirebaseSignInProvider;
10045+
10046+
/** A map of providers to the user's list of unique identifiers from each provider */
10047+
identities?: { [provider in FirebaseSignInProvider]?: string[] };
10048+
};
10049+
10050+
/** Custom claims set by the developer */
10051+
[claim: string]: unknown;
10052+
10053+
// NO LONGER SUPPORTED. Use "sub" instead. (Not a jsdoc comment to avoid generating docs.)
10054+
uid?: never;
10055+
}
10056+
10057+
export type EmulatorMockTokenOptions = (
10058+
| { user_id: string }
10059+
| { sub: string }
10060+
) &
10061+
Partial<FirebaseIdToken>;
997910062
}
998010063

998110064
export default firebase;

packages/firestore-types/index.d.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { FirebaseApp, FirebaseNamespace } from '@firebase/app-types';
18+
import { EmulatorMockTokenOptions } from '@firebase/util';
1919

2020
export type DocumentData = { [field: string]: any };
2121

@@ -61,7 +61,13 @@ export class FirebaseFirestore {
6161

6262
settings(settings: Settings): void;
6363

64-
useEmulator(host: string, port: number): void;
64+
useEmulator(
65+
host: string,
66+
port: number,
67+
options?: {
68+
mockUserToken?: EmulatorMockTokenOptions;
69+
}
70+
): void;
6571

6672
enablePersistence(settings?: PersistenceSettings): Promise<void>;
6773

packages/firestore-types/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"index.d.ts"
1313
],
1414
"peerDependencies": {
15-
"@firebase/app-types": "0.x"
15+
"@firebase/app-types": "0.x",
16+
"@firebase/util": "1.x"
1617
},
1718
"repository": {
1819
"directory": "packages/firestore-types",

packages/firestore/externs.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"externs" : [
2+
"externs": [
33
"node_modules/@types/node/base.d.ts",
44
"node_modules/@types/node/globals.d.ts",
55
"node_modules/typescript/lib/lib.es5.d.ts",
@@ -26,6 +26,7 @@
2626
"packages/logger/dist/src/logger.d.ts",
2727
"packages/webchannel-wrapper/src/index.d.ts",
2828
"packages/util/dist/src/crypt.d.ts",
29+
"packages/util/dist/src/emulator.d.ts",
2930
"packages/util/dist/src/environment.d.ts",
3031
"packages/util/dist/src/compat.d.ts",
3132
"packages/firestore/export.ts",

packages/firestore/src/api/credentials.ts

+35
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,41 @@ export class EmptyCredentialsProvider implements CredentialsProvider {
135135
}
136136
}
137137

138+
/**
139+
* A CredentialsProvider that always returns a constant token. Used for
140+
* emulator token mocking.
141+
*/
142+
export class EmulatorCredentialsProvider implements CredentialsProvider {
143+
constructor(private token: Token) {}
144+
145+
/**
146+
* Stores the listener registered with setChangeListener()
147+
* This isn't actually necessary since the UID never changes, but we use this
148+
* to verify the listen contract is adhered to in tests.
149+
*/
150+
private changeListener: CredentialChangeListener | null = null;
151+
152+
getToken(): Promise<Token | null> {
153+
return Promise.resolve(this.token);
154+
}
155+
156+
invalidateToken(): void {}
157+
158+
setChangeListener(changeListener: CredentialChangeListener): void {
159+
debugAssert(
160+
!this.changeListener,
161+
'Can only call setChangeListener() once.'
162+
);
163+
this.changeListener = changeListener;
164+
// Fire with initial user.
165+
changeListener(this.token.user);
166+
}
167+
168+
removeChangeListener(): void {
169+
this.changeListener = null;
170+
}
171+
}
172+
138173
export class FirebaseCredentialsProvider implements CredentialsProvider {
139174
/**
140175
* The auth token listener registered with FirebaseApp, retained here so we

packages/firestore/src/api/database.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ import {
4444
WhereFilterOp as PublicWhereFilterOp,
4545
WriteBatch as PublicWriteBatch
4646
} from '@firebase/firestore-types';
47-
import { Compat, getModularInstance } from '@firebase/util';
47+
import {
48+
Compat,
49+
EmulatorMockTokenOptions,
50+
getModularInstance
51+
} from '@firebase/util';
4852

4953
import {
5054
LoadBundleTask,
@@ -223,8 +227,14 @@ export class Firestore
223227
this._delegate._setSettings(settingsLiteral);
224228
}
225229

226-
useEmulator(host: string, port: number): void {
227-
useFirestoreEmulator(this._delegate, host, port);
230+
useEmulator(
231+
host: string,
232+
port: number,
233+
options: {
234+
mockUserToken?: EmulatorMockTokenOptions;
235+
} = {}
236+
): void {
237+
useFirestoreEmulator(this._delegate, host, port, options);
228238
}
229239

230240
enableNetwork(): Promise<void> {

packages/firestore/src/lite/database.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@ import {
2424
} from '@firebase/app-exp';
2525
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
2626
import { Provider } from '@firebase/component';
27+
import { createMockUserToken, EmulatorMockTokenOptions } from '@firebase/util';
2728

2829
import {
2930
CredentialsProvider,
3031
EmptyCredentialsProvider,
32+
EmulatorCredentialsProvider,
3133
FirebaseCredentialsProvider,
32-
makeCredentialsProvider
34+
makeCredentialsProvider,
35+
OAuthToken
3336
} from '../api/credentials';
37+
import { User } from '../auth/user';
3438
import { DatabaseId } from '../core/database_info';
3539
import { Code, FirestoreError } from '../util/error';
3640
import { cast } from '../util/input_validation';
@@ -226,11 +230,16 @@ export function getFirestore(app: FirebaseApp = getApp()): FirebaseFirestore {
226230
* emulator.
227231
* @param host - the emulator host (ex: localhost).
228232
* @param port - the emulator port (ex: 9000).
233+
* @param options.mockUserToken - the mock auth token to use for unit testing
234+
* Security Rules.
229235
*/
230236
export function useFirestoreEmulator(
231237
firestore: FirebaseFirestore,
232238
host: string,
233-
port: number
239+
port: number,
240+
options: {
241+
mockUserToken?: EmulatorMockTokenOptions;
242+
} = {}
234243
): void {
235244
firestore = cast(firestore, FirebaseFirestore);
236245
const settings = firestore._getSettings();
@@ -247,6 +256,23 @@ export function useFirestoreEmulator(
247256
host: `${host}:${port}`,
248257
ssl: false
249258
});
259+
260+
if (options.mockUserToken) {
261+
// Let createMockUserToken validate first (catches common mistakes like
262+
// invalid field "uid" and missing field "sub" / "user_id".)
263+
const token = createMockUserToken(options.mockUserToken);
264+
const uid = options.mockUserToken.sub || options.mockUserToken.user_id;
265+
if (!uid) {
266+
throw new FirestoreError(
267+
Code.INVALID_ARGUMENT,
268+
"mockUserToken must contain 'sub' or 'user_id' field!"
269+
);
270+
}
271+
272+
firestore._credentials = new EmulatorCredentialsProvider(
273+
new OAuthToken(token, new User(uid))
274+
);
275+
}
250276
}
251277

252278
/**

packages/firestore/test/integration/api/validation.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,24 @@ apiDescribe('Validation:', (persistence: boolean) => {
157157
expect(() => db.useEmulator('localhost', 9000)).to.throw(errorMsg);
158158
}
159159
);
160+
161+
validationIt(persistence, 'useEmulator can set mockUserToken', () => {
162+
const db = newTestFirestore('test-project');
163+
// Verify that this doesn't throw.
164+
db.useEmulator('localhost', 9000, { mockUserToken: { sub: 'foo' } });
165+
});
166+
167+
validationIt(
168+
persistence,
169+
'throws if sub / user_id is missing in mockUserToken',
170+
async db => {
171+
const errorMsg = "mockUserToken must contain 'sub' or 'user_id' field!";
172+
173+
expect(() =>
174+
db.useEmulator('localhost', 9000, { mockUserToken: {} as any })
175+
).to.throw(errorMsg);
176+
}
177+
);
160178
});
161179

162180
describe('Firestore', () => {

packages/firestore/test/unit/api/database.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { expect } from 'chai';
1919

20+
import { EmulatorCredentialsProvider } from '../../../src/api/credentials';
2021
import {
2122
collectionReference,
2223
documentReference,
@@ -250,4 +251,17 @@ describe('Settings', () => {
250251
expect(db._delegate._getSettings().host).to.equal('localhost:9000');
251252
expect(db._delegate._getSettings().ssl).to.be.false;
252253
});
254+
255+
it('sets credentials based on mockUserToken', async () => {
256+
// Use a new instance of Firestore in order to configure settings.
257+
const db = newTestFirestore();
258+
const mockUserToken = { sub: 'foobar' };
259+
db.useEmulator('localhost', 9000, { mockUserToken });
260+
261+
const credentials = db._delegate._credentials;
262+
expect(credentials).to.be.instanceOf(EmulatorCredentialsProvider);
263+
const token = await credentials.getToken();
264+
expect(token!.type).to.eql('OAuth');
265+
expect(token!.user.uid).to.eql(mockUserToken.sub);
266+
});
253267
});

0 commit comments

Comments
 (0)