Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/api_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8108,6 +8108,7 @@ def _handle_routes(db):
ep = db.execute("SELECT * FROM employer_profiles WHERE user_id = ?", [user_id]).fetchone()
user_data['worker_profile'] = row_to_dict(wp)
user_data['employer_profile'] = row_to_dict(ep)
user_data['is_new_user'] = is_new_google_user

return json_response(user_data, 200 if existing else 201)

Expand Down
29 changes: 29 additions & 0 deletions backend/test_deep_audit_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,35 @@ def _request_api(self, method, path, payload=None, token=""):
self.module.handle_request()
return parse_cgi_output(out.getvalue())

def test_google_auth_reports_whether_the_account_was_created(self):
os.environ["GOOGLE_CLIENT_ID"] = "google-client-under-test"
verified_identity = {
"aud": "google-client-under-test",
"iss": "https://accounts.google.com",
"email": "new-google-user@example.com",
"email_verified": "true",
"name": "New Google User",
"sub": "google-sub-under-test",
}
response = mock.MagicMock()
response.__enter__.return_value.read.return_value = json.dumps(verified_identity).encode("utf-8")

with (
mock.patch.object(self.module.urllib.request, "urlopen", return_value=response),
mock.patch.object(self.module, "send_welcome_email"),
):
created_status, created = self._request_api(
"POST", "/auth/google", {"credential": "new-google-credential"}
)
existing_status, existing = self._request_api(
"POST", "/auth/google", {"credential": "returning-google-credential"}
)

self.assertEqual(created_status, 201, created)
self.assertIs(created["is_new_user"], True)
self.assertEqual(existing_status, 200, existing)
self.assertIs(existing["is_new_user"], False)

def test_service_mutations_enforce_checkout_compatible_delivery_days(self):
token = "tok-service-delivery"
db = self.module.get_db()
Expand Down
12 changes: 9 additions & 3 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,16 @@ <h2>Common agent-delegated work</h2>
function handleGoogleCredential(response) {
api('/auth/google', { method: 'POST', body: { credential: response.credential } })
.then(data => {
const { token, ...user } = data;
const { token, is_new_user: isNewUser, ...user } = data;
saveSession(token, user);
toast('Welcome to GoHireHumans!', 'success');
trackRecommendedEvent('login', { method: 'google' });
if (isNewUser) {
trackEvent('signup_completed', { source: 'google_auth', method: 'google' });
trackRecommendedEvent('sign_up', { method: 'google', source: 'google_auth' });
toast('Account created! Welcome to GoHireHumans.', 'success');
} else {
trackRecommendedEvent('login', { method: 'google' });
toast('Welcome back!', 'success');
}
navigate('#/');
})
.catch(err => toast(err.message || 'Google sign-in failed', 'error'));
Expand Down
44 changes: 44 additions & 0 deletions frontend/tests/browser-regression.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,50 @@ test.describe('GoHireHumans public/browser regression suite', () => {
expect(call[2]).not.toHaveProperty('campaign');
});

test('Google auth distinguishes completed signup from returning login', async ({ page }) => {
await setupDeterministicLocalPage(page);
await page.goto('/', { waitUntil: 'domcontentloaded' });

const eventsFor = async isNewUser => page.evaluate(async newAccount => {
const events = [];
window.gtag = (...args) => events.push(args);
window.api = async path => {
if (path !== '/auth/google') throw new Error(`Unexpected API path: ${path}`);
return {
id: newAccount ? 701 : 702,
email: newAccount ? 'new-google@example.test' : 'returning-google@example.test',
name: newAccount ? 'New Google User' : 'Returning Google User',
token: 'google-auth-test-token',
is_new_user: newAccount,
};
};
window.saveSession = () => {};
window.toast = () => {};
window.navigate = () => {};
handleGoogleCredential({ credential: 'google-credential-under-test' });
for (let attempt = 0; attempt < 20 && events.length === 0; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 10));
}
return events
.filter(args => args[0] === 'event')
.map(args => ({ name: args[1], params: args[2] }));
}, isNewUser);

const newAccountEvents = await eventsFor(true);
expect(newAccountEvents.map(event => event.name)).toEqual(['signup_completed', 'sign_up']);
expect(newAccountEvents.find(event => event.name === 'signup_completed')?.params).toMatchObject({
ui_source: 'google_auth',
method: 'google',
});
expect(newAccountEvents.map(event => event.name)).not.toContain('login');

const returningEvents = await eventsFor(false);
expect(returningEvents.map(event => event.name)).toEqual(['login']);
expect(returningEvents[0]?.params).toMatchObject({ method: 'google' });
expect(returningEvents.map(event => event.name)).not.toContain('sign_up');
expect(returningEvents.map(event => event.name)).not.toContain('signup_completed');
});

test('application submission failure stays visible and retryable', async ({ page }) => {
await setupDeterministicLocalPage(page);
await page.goto('/', { waitUntil: 'domcontentloaded' });
Expand Down
Loading