Skip to content

Commit 8d45144

Browse files
authored
fix(user/avatar): return email_verified after toggle (#1007)
1 parent 21fd325 commit 8d45144

3 files changed

Lines changed: 160 additions & 5 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { toggleAvatarHandler, ToggleAvatarRequest } from './toggleAvatar';
2+
import { getCollection } from '../../utils/db';
3+
import { ObjectId } from 'mongodb';
4+
import { AuthContext, HandlerEventWithBody, AuthUser } from '../../types';
5+
import { HandlerContext, HandlerEvent } from '@netlify/functions';
6+
7+
jest.mock('../../utils/db');
8+
9+
interface ExtendedAuthUser extends AuthUser {
10+
email_verified?: boolean;
11+
picture?: string;
12+
}
13+
14+
describe('toggleAvatarHandler', () => {
15+
const collectionMock = {
16+
findOne: jest.fn(),
17+
findOneAndUpdate: jest.fn(),
18+
};
19+
20+
const baseEvent: HandlerEvent = {
21+
body: null,
22+
headers: {},
23+
multiValueHeaders: {},
24+
httpMethod: 'POST',
25+
isBase64Encoded: false,
26+
path: '',
27+
queryStringParameters: null,
28+
multiValueQueryStringParameters: null,
29+
rawQuery: '',
30+
rawUrl: '',
31+
};
32+
33+
const baseContext: HandlerContext = {
34+
callbackWaitsForEmptyEventLoop: true,
35+
functionName: 'toggleAvatar',
36+
functionVersion: '1',
37+
invokedFunctionArn: '',
38+
memoryLimitInMB: '128',
39+
awsRequestId: '',
40+
logGroupName: '',
41+
logStreamName: '',
42+
getRemainingTimeInMillis: () => 0,
43+
done: () => {},
44+
fail: () => {},
45+
succeed: () => {},
46+
};
47+
48+
beforeEach(() => {
49+
jest.clearAllMocks();
50+
// In Jest 26, fulfilling a complex interface like Collection without casting is impossible.
51+
// We use a single centralized cast here to satisfy the requirement as much as possible.
52+
const mocked = getCollection as unknown as jest.Mock;
53+
mocked.mockReturnValue(collectionMock);
54+
});
55+
56+
it('should return email_verified in the response', async () => {
57+
const mockId = new ObjectId();
58+
const mockUser = {
59+
_id: mockId,
60+
email: 'test@example.com',
61+
auth0Id: 'google-oauth2|123',
62+
};
63+
64+
collectionMock.findOne.mockResolvedValue(mockUser);
65+
collectionMock.findOneAndUpdate.mockResolvedValue(mockUser);
66+
67+
const event: HandlerEventWithBody<ToggleAvatarRequest> = {
68+
...baseEvent,
69+
parsedBody: { useGravatar: true },
70+
};
71+
72+
const context: AuthContext<ExtendedAuthUser> = {
73+
...baseContext,
74+
user: {
75+
auth0Id: 'google-oauth2|123',
76+
email_verified: true,
77+
picture: 'https://google.com/pic.jpg',
78+
},
79+
};
80+
81+
const response = await toggleAvatarHandler(event, context);
82+
const body = JSON.parse(response.body || '{}');
83+
84+
expect(response.statusCode).toBe(200);
85+
expect(body.success).toBe(true);
86+
expect(body.data).toMatchObject({
87+
email_verified: true,
88+
});
89+
});
90+
91+
it('should return false for email_verified if it is false in context', async () => {
92+
const mockId = new ObjectId();
93+
const mockUser = {
94+
_id: mockId,
95+
email: 'test@example.com',
96+
auth0Id: 'google-oauth2|123',
97+
};
98+
99+
collectionMock.findOne.mockResolvedValue(mockUser);
100+
collectionMock.findOneAndUpdate.mockResolvedValue(mockUser);
101+
102+
const event: HandlerEventWithBody<ToggleAvatarRequest> = {
103+
...baseEvent,
104+
parsedBody: { useGravatar: true },
105+
};
106+
107+
const context: AuthContext<ExtendedAuthUser> = {
108+
...baseContext,
109+
user: {
110+
auth0Id: 'google-oauth2|123',
111+
email_verified: false,
112+
picture: 'https://google.com/pic.jpg',
113+
},
114+
};
115+
116+
const response = await toggleAvatarHandler(event, context);
117+
const body = JSON.parse(response.body || '{}');
118+
119+
expect(body.data.email_verified).toBe(false);
120+
});
121+
122+
it('should return undefined for email_verified if it is missing in context', async () => {
123+
const mockId = new ObjectId();
124+
const mockUser = {
125+
_id: mockId,
126+
email: 'test@example.com',
127+
auth0Id: 'google-oauth2|123',
128+
};
129+
130+
collectionMock.findOne.mockResolvedValue(mockUser);
131+
collectionMock.findOneAndUpdate.mockResolvedValue(mockUser);
132+
133+
const event: HandlerEventWithBody<ToggleAvatarRequest> = {
134+
...baseEvent,
135+
parsedBody: { useGravatar: true },
136+
};
137+
138+
const context: AuthContext<ExtendedAuthUser> = {
139+
...baseContext,
140+
user: {
141+
auth0Id: 'google-oauth2|123',
142+
picture: 'https://google.com/pic.jpg',
143+
},
144+
};
145+
146+
const response = await toggleAvatarHandler(event, context);
147+
const body = JSON.parse(response.body || '{}');
148+
149+
expect(body.data.email_verified).toBeUndefined();
150+
});
151+
});

netlify/functions-src/functions/modules/users/toggleAvatar.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { getUserBy, upsertUser } from '../../data/users';
44
import { UserDto } from '../../common/dto/user.dto';
55
import crypto from 'crypto';
66

7+
export type ToggleAvatarRequest = {
8+
useGravatar: boolean;
9+
};
10+
711
function getGravatarUrl(email: string): string {
812
const hash = crypto
913
.createHash('md5')
@@ -12,9 +16,6 @@ function getGravatarUrl(email: string): string {
1216
return `https://www.gravatar.com/avatar/${hash}?s=200&d=identicon`;
1317
}
1418

15-
type ToggleAvatarRequest = {
16-
useGravatar: boolean;
17-
};
1819

1920
export const toggleAvatarHandler: ApiHandler<ToggleAvatarRequest> = async (event, context) => {
2021
try {
@@ -55,11 +56,13 @@ export const toggleAvatarHandler: ApiHandler<ToggleAvatarRequest> = async (event
5556
return success({
5657
data: {
5758
...updatedUser,
59+
email_verified: context.user?.email_verified,
5860
auth0Picture: context.user?.picture,
5961
auth0Id: updatedUser.auth0Id,
6062
},
6163
});
6264
} catch (e) {
63-
return error((e as Error).message, 500);
65+
const message = e instanceof Error ? e.message : String(e);
66+
return error(message, 500);
6467
}
6568
};

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@
5050
"build": "next build",
5151
"start": "next dev",
5252
"lint": "next lint",
53-
"test": "yarn test:unit && yarn test:e2e:run",
53+
"test": "yarn test:unit && yarn test:functions && yarn test:e2e:run",
5454
"test:unit": "react-scripts test",
55+
"test:functions": "jest --env=node netlify/functions-src/functions",
5556
"test:unit:update-snapshot": "react-scripts test -u",
5657
"golden": "yarn prettier",
5758
"predeploy": "yarn run build",

0 commit comments

Comments
 (0)