Skip to content

Commit 3e9af2a

Browse files
author
Robert-Jan Huijsman
authored
Auth: change the UserRecord's timestamps from strings to Date, in accordance with our spec. (#114)
* Auth: change the UserRecord's timestamps from strings to Date, in accordance with our spec. * Auth: Update testing to test both onCreate and onDelete separately, to ensure they both use the correct dataConstructor. * Move deep clone of incoming payload to makeCloudFunction(), as it was intended to be.
1 parent 0c0b103 commit 3e9af2a

File tree

3 files changed

+96
-47
lines changed

3 files changed

+96
-47
lines changed

spec/providers/auth.spec.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ describe('AuthBuilder', () => {
4141
const cloudFunction = auth.user().onCreate(() => null);
4242
expect(cloudFunction.__trigger).to.deep.equal({
4343
eventTrigger: {
44-
eventType: 'providers/firebase.auth/eventTypes/user.create',
45-
resource: 'projects/project1',
44+
eventType: 'providers/firebase.auth/eventTypes/user.create',
45+
resource: 'projects/project1',
4646
},
4747
});
4848
});
@@ -53,16 +53,17 @@ describe('AuthBuilder', () => {
5353
const cloudFunction = auth.user().onDelete(handler);
5454
expect(cloudFunction.__trigger).to.deep.equal({
5555
eventTrigger: {
56-
eventType: 'providers/firebase.auth/eventTypes/user.delete',
57-
resource: 'projects/project1',
56+
eventType: 'providers/firebase.auth/eventTypes/user.delete',
57+
resource: 'projects/project1',
5858
},
5959
});
6060
});
6161
});
6262

6363
describe('#_dataConstructor', () => {
6464
it('should handle an event with the appropriate fields', () => {
65-
const cloudFunction = auth.user().onCreate((ev: Event<firebase.auth.UserRecord>) => ev.data);
65+
const cloudFunctionCreate = auth.user().onCreate((ev: Event<firebase.auth.UserRecord>) => ev.data);
66+
const cloudFunctionDelete = auth.user().onDelete((ev: Event<firebase.auth.UserRecord>) => ev.data);
6667

6768
// The event data delivered over the wire will be the JSON for a UserRecord:
6869
// https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data
@@ -93,7 +94,33 @@ describe('AuthBuilder', () => {
9394
},
9495
};
9596

96-
return expect(cloudFunction(event)).to.eventually.deep.equal(event.data);
97+
const expectedData = {
98+
uid: 'abcde12345',
99+
email: 'foo@bar.baz',
100+
emailVerified: false,
101+
displayName: 'My Display Name',
102+
photoURL: 'bar.baz/foo.jpg',
103+
disabled: false,
104+
metadata: {
105+
// Gotcha's:
106+
// - JS Date is, by default, local-time based, not UTC-based.
107+
// - JS Date's month is zero-based.
108+
createdAt: new Date(Date.UTC(2016, 11, 15, 19, 37, 37, 59)),
109+
lastSignedInAt: new Date(Date.UTC(2017, 0, 1)),
110+
},
111+
providerData: [{
112+
uid: 'g-abcde12345',
113+
email: 'foo@gmail.com',
114+
displayName: 'My Google Provider Display Name',
115+
photoURL: 'googleusercontent.com/foo.jpg',
116+
providerId: 'google.com',
117+
}],
118+
};
119+
120+
return Promise.all([
121+
expect(cloudFunctionCreate(event)).to.eventually.deep.equal(expectedData),
122+
expect(cloudFunctionDelete(event)).to.eventually.deep.equal(expectedData),
123+
]);
97124
});
98125

99126
// This isn't expected to happen in production, but if it does we should
@@ -102,9 +129,9 @@ describe('AuthBuilder', () => {
102129
const cloudFunction = auth.user().onCreate((ev: Event<firebase.auth.UserRecord>) => ev.data);
103130

104131
let event: Event<firebase.auth.UserRecord> = {
105-
data: {
132+
data: {
106133
uid: 'abcde12345',
107-
metadata: {
134+
metadata: {
108135
// TODO(inlined) We'll need to manually parse these!
109136
createdAt: new Date(),
110137
lastSignedInAt: new Date(),

src/cloud-functions.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface Event<T> {
2929
timestamp?: string;
3030
eventType?: string;
3131
resource?: string;
32-
params?: {[option: string]: any};
32+
params?: { [option: string]: any };
3333
data: T;
3434

3535
/** @internal */
@@ -76,19 +76,19 @@ export function makeCloudFunction<EventData>({
7676
}: MakeCloudFunctionArgs<EventData>): CloudFunction<EventData> {
7777
let cloudFunction: any = (event: Event<any>) => {
7878
return Promise.resolve(event)
79-
.then(before)
80-
.then(() => {
81-
let typedEvent: Event<EventData> = _.assign({}, event);
82-
typedEvent.data = dataConstructor(event);
83-
typedEvent.params = event.params || {};
84-
return handler(typedEvent);
85-
}).then(result => {
86-
if (after) { after(event); };
87-
return result;
88-
}, err => {
89-
if (after) { after(event); };
90-
return Promise.reject(err);
91-
});
79+
.then(before)
80+
.then(() => {
81+
let typedEvent: Event<EventData> = _.cloneDeep(event);
82+
typedEvent.data = dataConstructor(event);
83+
typedEvent.params = event.params || {};
84+
return handler(typedEvent);
85+
}).then(result => {
86+
if (after) { after(event); };
87+
return result;
88+
}, err => {
89+
if (after) { after(event); };
90+
return Promise.reject(err);
91+
});
9292
};
9393
cloudFunction.__trigger = {
9494
eventTrigger: {

src/providers/auth.ts

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
// The MIT License (MIT)
2-
//
3-
// Copyright (c) 2017 Firebase
4-
//
5-
// Permission is hereby granted, free of charge, to any person obtaining a copy
6-
// of this software and associated documentation files (the "Software"), to deal
7-
// in the Software without restriction, including without limitation the rights
8-
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9-
// copies of the Software, and to permit persons to whom the Software is
10-
// furnished to do so, subject to the following conditions:
11-
//
12-
// The above copyright notice and this permission notice shall be included in all
13-
// copies or substantial portions of the Software.
14-
//
15-
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16-
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17-
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18-
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19-
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20-
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
// SOFTWARE.
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2017 Firebase
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
2222

2323
import { makeCloudFunction, CloudFunction, Event } from '../cloud-functions';
2424
import * as firebase from 'firebase-admin';
@@ -33,6 +33,26 @@ export function user() {
3333

3434
/** Builder used to create Cloud Functions for Firebase Auth user lifecycle events. */
3535
export class UserBuilder {
36+
private static dataConstructor(raw: any): firebase.auth.UserRecord {
37+
// The UserRecord returned here is an interface. The firebase-admin/auth/user-record module
38+
// also has a class of the same name, which is one implementation of the interface. Here,
39+
// because our wire format already almost matches the UserRecord interface, we only use the
40+
// interface, no need to use the class.
41+
//
42+
// The one change we need to make to match the interface is to our incoming timestamps. The
43+
// interface requires them to be Date objects, while they raw payload has them as strings.
44+
if (raw.data.metadata) {
45+
let metadata = raw.data.metadata;
46+
if (metadata.lastSignedInAt && typeof metadata.lastSignedInAt === 'string') {
47+
metadata.lastSignedInAt = new Date(metadata.lastSignedInAt);
48+
}
49+
if (metadata.createdAt && typeof metadata.createdAt === 'string') {
50+
metadata.createdAt = new Date(metadata.createdAt);
51+
}
52+
}
53+
return raw.data;
54+
}
55+
3656
/** @internal */
3757
constructor(private resource: string) { }
3858

@@ -44,6 +64,7 @@ export class UserBuilder {
4464
provider, handler,
4565
resource: this.resource,
4666
eventType: 'user.create',
67+
dataConstructor: UserBuilder.dataConstructor,
4768
});
4869
}
4970

@@ -55,12 +76,13 @@ export class UserBuilder {
5576
provider, handler,
5677
resource: this.resource,
5778
eventType: 'user.delete',
79+
dataConstructor: UserBuilder.dataConstructor,
5880
});
5981
}
6082
}
6183

62-
/**
63-
* The UserRecord passed to Cloud Functions is the same UserRecord that is returned by the Firebase Admin
64-
* SDK.
65-
*/
84+
/**
85+
* The UserRecord passed to Cloud Functions is the same UserRecord that is returned by the Firebase Admin
86+
* SDK.
87+
*/
6688
export type UserRecord = firebase.auth.UserRecord;

0 commit comments

Comments
 (0)