Skip to content

Commit fa2d3e6

Browse files
committed
Add mockUserToken support for Firestore.
1 parent 6238b8b commit fa2d3e6

File tree

7 files changed

+190
-7
lines changed

7 files changed

+190
-7
lines changed

packages/app-types/index.d.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,84 @@ export interface VersionService {
121121
version: string;
122122
}
123123

124+
export type FirebaseSignInProvider =
125+
| 'custom'
126+
| 'email'
127+
| 'password'
128+
| 'phone'
129+
| 'anonymous'
130+
| 'google.com'
131+
| 'facebook.com'
132+
| 'github.com'
133+
| 'twitter.com'
134+
| 'microsoft.com'
135+
| 'apple.com';
136+
137+
export interface FirebaseIdToken {
138+
// Firebase Auth tokens contain snake_case claims following the JWT standard / convention.
139+
/* eslint-disable camelcase */
140+
141+
// Always set to https://securetoken.google.com/PROJECT_ID
142+
iss: string;
143+
144+
// Always set to PROJECT_ID
145+
aud: string;
146+
147+
// The user's unique id
148+
sub: string;
149+
150+
// The token issue time, in seconds since epoch
151+
iat: number;
152+
153+
// The token expiry time, normally 'iat' + 3600
154+
exp: number;
155+
156+
// The user's unique id, must be equal to 'sub'
157+
user_id: string;
158+
159+
// The time the user authenticated, normally 'iat'
160+
auth_time: number;
161+
162+
// The sign in provider, only set when the provider is 'anonymous'
163+
provider_id?: 'anonymous';
164+
165+
// The user's primary email
166+
email?: string;
167+
168+
// The user's email verification status
169+
email_verified?: boolean;
170+
171+
// The user's primary phone number
172+
phone_number?: string;
173+
174+
// The user's display name
175+
name?: string;
176+
177+
// The user's profile photo URL
178+
picture?: string;
179+
180+
// Information on all identities linked to this user
181+
firebase: {
182+
// The primary sign-in provider
183+
sign_in_provider: FirebaseSignInProvider;
184+
185+
// A map of providers to the user's list of unique identifiers from
186+
// each provider
187+
identities?: { [provider in FirebaseSignInProvider]?: string[] };
188+
};
189+
190+
// Custom claims set by the developer
191+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
192+
[claim: string]: any;
193+
194+
uid?: never; // Try to catch a common mistake of "uid" (should be "sub" instead).
195+
196+
/* eslint-enable camelcase */
197+
}
198+
199+
export type EmulatorMockTokenOptions = ({ user_id: string } | { sub: string }) &
200+
Partial<FirebaseIdToken>;
201+
124202
declare module '@firebase/component' {
125203
interface NameServiceMapping {
126204
'app': FirebaseApp;

packages/firestore-types/index.d.ts

Lines changed: 8 additions & 2 deletions
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/app-types';
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/src/api/credentials.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,38 @@ export class EmptyCredentialsProvider implements CredentialsProvider {
135135
}
136136
}
137137

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

packages/firestore/src/api/database.ts

Lines changed: 13 additions & 3 deletions
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

Lines changed: 24 additions & 2 deletions
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';
@@ -228,7 +232,10 @@ export function getFirestore(app: FirebaseApp = getApp()): FirebaseFirestore {
228232
export function useFirestoreEmulator(
229233
firestore: FirebaseFirestore,
230234
host: string,
231-
port: number
235+
port: number,
236+
options: {
237+
mockUserToken?: EmulatorMockTokenOptions;
238+
} = {}
232239
): void {
233240
firestore = cast(firestore, FirebaseFirestore);
234241
const settings = firestore._getSettings();
@@ -245,6 +252,21 @@ export function useFirestoreEmulator(
245252
host: `${host}:${port}`,
246253
ssl: false
247254
});
255+
256+
if (options.mockUserToken) {
257+
const uid = options.mockUserToken.sub || options.mockUserToken.user_id;
258+
if (!uid) {
259+
throw new FirestoreError(
260+
Code.INVALID_ARGUMENT,
261+
"mockUserToken must contain 'sub' or 'user_id' field!"
262+
);
263+
}
264+
265+
const token = createMockUserToken(options.mockUserToken);
266+
firestore._credentials = new EmulatorCredentialsProvider(
267+
new OAuthToken(token, new User(uid))
268+
);
269+
}
248270
}
249271

250272
/**

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

Lines changed: 18 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { createMockUserToken } from '@firebase/util';
1819
import { expect } from 'chai';
20+
import { EmulatorCredentialsProvider } from '../../../src/api/credentials';
1921

2022
import {
2123
collectionReference,
@@ -250,4 +252,19 @@ describe('Settings', () => {
250252
expect(db._delegate._getSettings().host).to.equal('localhost:9000');
251253
expect(db._delegate._getSettings().ssl).to.be.false;
252254
});
255+
256+
it('sets credentials based on mockUserToken', async () => {
257+
// Use a new instance of Firestore in order to configure settings.
258+
const db = newTestFirestore();
259+
const mockUserToken = { sub: 'foobar' };
260+
db.useEmulator('localhost', 9000, { mockUserToken });
261+
262+
expect(db._delegate._getSettings().host).to.equal('localhost:9000');
263+
expect(db._delegate._getSettings().ssl).to.be.false;
264+
const { credentials } = db._delegate._getSettings();
265+
expect(credentials).to.be.instanceOf(EmulatorCredentialsProvider);
266+
await expect(credentials.getToken()).to.eventually.be.eql(
267+
createMockUserToken(mockUserToken)
268+
);
269+
});
253270
});

0 commit comments

Comments
 (0)