Skip to content

Commit 44a40de

Browse files
tyler-daneCopilot
andauthored
Feat/1156-analytics (#1166)
* feat(backend): include email in authentication responses * feat(web): add unit tests for LoginView component with PostHog integration * refactor(web): remove donation link from LoginView component * refactor(web): add NotOnTheWaitlist and OnTheWaitlist components to LoginView * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d21456b commit 44a40de

File tree

8 files changed

+295
-77
lines changed

8 files changed

+295
-77
lines changed

packages/backend/src/auth/controllers/auth.controller.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class AuthController {
106106
const { authMethod, user } = await compassAuthService.determineAuthMethod(
107107
gUser.sub,
108108
);
109-
const { cUserId } =
109+
const { cUserId, email } =
110110
authMethod === "login"
111111
? await this.login(
112112
user as WithId<Schema_User>,
@@ -121,6 +121,7 @@ class AuthController {
121121
const result: Result_Auth_Compass = {
122122
cUserId,
123123
isNewUser: authMethod === "signup",
124+
email,
124125
};
125126

126127
res.promise(result);
@@ -165,7 +166,7 @@ class AuthController {
165166

166167
await userService.saveTimeFor("lastLoggedInAt", cUserId);
167168

168-
return { cUserId };
169+
return { cUserId, email: user.email };
169170
};
170171

171172
revokeSessionsByUser = async (
@@ -185,9 +186,9 @@ class AuthController {
185186
};
186187

187188
signup = async (gUser: TokenPayload, gRefreshToken: string) => {
188-
const userId = await userService.initUserData(gUser, gRefreshToken);
189+
const user = await userService.initUserData(gUser, gRefreshToken);
189190

190-
return { cUserId: userId };
191+
return { cUserId: user.userId, email: user.email };
191192
};
192193
}
193194

packages/backend/src/user/services/user.service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe("UserService", () => {
9696

9797
EmailDriver.mockEmailServiceResponse();
9898

99-
const userId = await userService.initUserData(
99+
const { userId } = await userService.initUserData(
100100
gUser,
101101
faker.internet.jwt(),
102102
);

packages/backend/src/user/services/user.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class UserService {
117117

118118
initUserData = async (gUser: TokenPayload, gRefreshToken: string) => {
119119
const cUser = await this.createUser(gUser, gRefreshToken);
120-
const { userId } = cUser;
120+
const { userId, email } = cUser;
121121

122122
if (isMissingUserTagId()) {
123123
logger.warn(
@@ -135,7 +135,7 @@ class UserService {
135135

136136
await eventService.createDefaultSomedays(userId);
137137

138-
return userId;
138+
return { userId, email };
139139
};
140140

141141
saveTimeFor = async (label: "lastLoggedInAt", userId: string) => {

packages/core/src/types/auth.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Result_Auth_Compass {
55
cUserId?: string;
66
error?: BaseError;
77
isNewUser?: boolean;
8+
email?: string;
89
}
910

1011
export interface Result_VerifyGToken {
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { usePostHog } from "posthog-js/react";
2+
import { act } from "react";
3+
import "@testing-library/jest-dom";
4+
import { screen, waitFor } from "@testing-library/react";
5+
import userEvent from "@testing-library/user-event";
6+
import { render } from "@web/__tests__/__mocks__/mock.render";
7+
import { AuthApi } from "@web/common/apis/auth.api";
8+
import { LoginView } from "./Login";
9+
10+
// Mock PostHog
11+
jest.mock("posthog-js/react");
12+
const mockUsePostHog = usePostHog as jest.MockedFunction<typeof usePostHog>;
13+
14+
// Mock AuthApi
15+
jest.mock("@web/common/apis/auth.api");
16+
const mockAuthApi = AuthApi as jest.Mocked<typeof AuthApi>;
17+
18+
// Mock useGoogleLogin hook
19+
jest.mock("@web/components/oauth/google/useGoogleLogin", () => ({
20+
useGoogleLogin: jest.fn(),
21+
}));
22+
23+
// Mock useAuthCheck hook
24+
jest.mock("@web/auth/useAuthCheck", () => ({
25+
useAuthCheck: () => ({
26+
isAuthenticated: false,
27+
isCheckingAuth: false,
28+
isGoogleTokenActive: false,
29+
isSessionActive: false,
30+
}),
31+
}));
32+
33+
// Mock useNavigate
34+
const mockNavigate = jest.fn();
35+
jest.mock("react-router-dom", () => ({
36+
...jest.requireActual("react-router-dom"),
37+
useNavigate: () => mockNavigate,
38+
}));
39+
40+
// Mock WaitlistApi
41+
jest.mock("@web/common/apis/waitlist.api", () => ({
42+
WaitlistApi: {
43+
getWaitlistStatus: jest.fn(),
44+
},
45+
}));
46+
47+
describe("LoginView", () => {
48+
const mockIdentify = jest.fn();
49+
const mockLogin = jest.fn();
50+
51+
beforeEach(() => {
52+
jest.clearAllMocks();
53+
54+
// Default mock implementations
55+
mockUsePostHog.mockReturnValue({
56+
identify: mockIdentify,
57+
} as any);
58+
59+
mockAuthApi.loginOrSignup.mockResolvedValue({
60+
cUserId: "test-user-id",
61+
isNewUser: false,
62+
email: "test@example.com",
63+
});
64+
65+
// Mock useGoogleLogin hook
66+
const {
67+
useGoogleLogin,
68+
} = require("@web/components/oauth/google/useGoogleLogin");
69+
useGoogleLogin.mockReturnValue({
70+
login: mockLogin,
71+
data: null,
72+
loading: false,
73+
});
74+
});
75+
76+
describe("PostHog Integration", () => {
77+
it("should call posthog.identify with email after successful authentication", async () => {
78+
const testEmail = "test@example.com";
79+
mockAuthApi.loginOrSignup.mockResolvedValue({
80+
cUserId: "user123",
81+
isNewUser: false,
82+
email: testEmail,
83+
});
84+
85+
render(<LoginView />);
86+
87+
// Simulate successful Google login
88+
const {
89+
useGoogleLogin,
90+
} = require("@web/components/oauth/google/useGoogleLogin");
91+
const mockUseGoogleLogin = useGoogleLogin as jest.Mock;
92+
93+
// Get the onSuccess callback from the mocked hook
94+
const onSuccessCallback = mockUseGoogleLogin.mock.calls[0][0].onSuccess;
95+
96+
// Call the onSuccess callback with a mock OAuth code
97+
await onSuccessCallback("mock-oauth-code");
98+
99+
// Verify PostHog identify was called with correct parameters
100+
expect(mockIdentify).toHaveBeenCalledWith(testEmail, {
101+
email: testEmail,
102+
});
103+
expect(mockAuthApi.loginOrSignup).toHaveBeenCalledWith("mock-oauth-code");
104+
});
105+
106+
it("should call posthog.identify for new user signup", async () => {
107+
const testEmail = "newuser@example.com";
108+
mockAuthApi.loginOrSignup.mockResolvedValue({
109+
cUserId: "new-user-id",
110+
isNewUser: true,
111+
email: testEmail,
112+
});
113+
114+
render(<LoginView />);
115+
116+
const {
117+
useGoogleLogin,
118+
} = require("@web/components/oauth/google/useGoogleLogin");
119+
const mockUseGoogleLogin = useGoogleLogin as jest.Mock;
120+
const onSuccessCallback = mockUseGoogleLogin.mock.calls[0][0].onSuccess;
121+
122+
await onSuccessCallback("mock-oauth-code");
123+
124+
// Verify PostHog identify was called for new user
125+
expect(mockIdentify).toHaveBeenCalledWith(testEmail, {
126+
email: testEmail,
127+
});
128+
});
129+
});
130+
131+
describe("Authentication Flow", () => {
132+
it("should call AuthApi.loginOrSignup with OAuth code", async () => {
133+
render(<LoginView />);
134+
135+
const {
136+
useGoogleLogin,
137+
} = require("@web/components/oauth/google/useGoogleLogin");
138+
const mockUseGoogleLogin = useGoogleLogin as jest.Mock;
139+
const onSuccessCallback = mockUseGoogleLogin.mock.calls[0][0].onSuccess;
140+
141+
await onSuccessCallback("test-oauth-code");
142+
143+
expect(mockAuthApi.loginOrSignup).toHaveBeenCalledWith("test-oauth-code");
144+
});
145+
146+
it("should log authentication errors", async () => {
147+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
148+
mockAuthApi.loginOrSignup.mockRejectedValue(new Error("Auth failed"));
149+
150+
render(<LoginView />);
151+
152+
const {
153+
useGoogleLogin,
154+
} = require("@web/components/oauth/google/useGoogleLogin");
155+
const mockUseGoogleLogin = useGoogleLogin as jest.Mock;
156+
const onErrorCallback = mockUseGoogleLogin.mock.calls[0][0].onError;
157+
158+
// Simulate error
159+
onErrorCallback(new Error("Auth failed"));
160+
161+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(Error));
162+
163+
consoleErrorSpy.mockRestore();
164+
});
165+
});
166+
167+
describe("Waitlist Flow", () => {
168+
it("should handle waitlist status check", async () => {
169+
// Mock window.location to not be localhost
170+
const originalLocation = window.location;
171+
delete (window as any).location;
172+
window.location = {
173+
...originalLocation,
174+
hostname: "example.com",
175+
} as any;
176+
177+
const { WaitlistApi } = require("@web/common/apis/waitlist.api");
178+
WaitlistApi.getWaitlistStatus.mockResolvedValue({
179+
isOnWaitlist: true,
180+
isInvited: true,
181+
isActive: true,
182+
});
183+
184+
render(<LoginView />);
185+
186+
const emailInput = screen.getByPlaceholderText("Enter your email");
187+
const submitButton = screen.getByText("Check Waitlist Status");
188+
189+
await act(async () => {
190+
await userEvent.type(emailInput, "test@example.com");
191+
});
192+
await act(async () => {
193+
await userEvent.click(submitButton);
194+
});
195+
196+
await waitFor(() => {
197+
expect(WaitlistApi.getWaitlistStatus).toHaveBeenCalledWith(
198+
"test@example.com",
199+
);
200+
});
201+
202+
// Restore original location
203+
window.location = originalLocation;
204+
});
205+
});
206+
});

0 commit comments

Comments
 (0)