-
Notifications
You must be signed in to change notification settings - Fork 4
[HYPER-186] show ToS/privacy checkbox for new users during login #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
c8378e3
feat: show tos checkbox if new user when login hint provided
Kzoeps f6cbfb4
feat: show tos checkbox if new user when email entered on auth service
Kzoeps 3d1869e
fix: change to post route for new user check
Kzoeps a5eccd1
fix: check for tos acceptance on verify otp
Kzoeps cb78a08
fix(auth): require literal true for ToS acceptance
aspiers caaf042
fix(auth): decouple new-user DID lookup from OTP send
aspiers 6717242
tests: add features for tos test
Kzoeps a414d23
fix(auth-service): make isNewUser optional on renderLoginPage opts
aspiers c494eb5
test(e2e): add step definitions for ToS acceptance scenarios
aspiers 9bc8339
docs(e2e): clarify ToS step verifies client-side guard, not server hook
aspiers 0c7b975
test(e2e): restore @email tag on returning-user OTP scenario
aspiers b886d4a
test(e2e): accept ToS in new-user OAuth sign-up flow helpers
aspiers f295508
test(e2e): accept ToS in incorrect-OTP steps and restore picker step
aspiers 706bb07
test(e2e): accept ToS in client-branding new-user handle step
aspiers 27b7aa5
fix(auth-service): pass tosAccepted=true when verifying recovery OTP
aspiers 471658e
fix(auth-service): gate ToS bypass on submitted email being a backup …
aspiers 528d68e
test(e2e): wait for ToS reveal before accepting (fix race)
aspiers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| /** | ||
| * Step definitions for the Terms-of-Service acceptance enforcement | ||
| * scenarios in features/security.feature and features/passwordless-authentication.feature. | ||
| * | ||
| * Server-side enforcement lives in packages/auth-service/src/better-auth.ts | ||
| * (enforceTosAcceptance, wired as a hooks.before on better-auth). The unit | ||
| * tests in better-auth-otp.test.ts cover that helper in isolation; these | ||
| * E2E steps exercise it end-to-end against the deployed auth service: | ||
| * | ||
| * - "new user without tosAccepted" → direct HTTP against | ||
| * /api/auth/email-otp/send-verification-otp + /api/auth/sign-in/email-otp, | ||
| * asserting a 400 with the expected error message. | ||
| * | ||
| * - "returning user without tosAccepted" → creates a real account via | ||
| * the browser-driven sign-up flow, then drives a fresh sign-in via | ||
| * direct HTTP with no tosAccepted flag, asserting success. | ||
| * | ||
| * - "OTP form does not show a ToS checkbox" → DOM assertion that | ||
| * #tos-field stays hidden on the login page for the returning-user | ||
| * case (the email step has already left the world on the OTP form). | ||
| */ | ||
|
|
||
| import { Then, When } from '@cucumber/cucumber' | ||
| import { expect } from '@playwright/test' | ||
| import { testEnv } from '../support/env.js' | ||
| import type { EpdsWorld } from '../support/world.js' | ||
| import { getPage } from '../support/utils.js' | ||
| import { clearMailpit, extractOtp, waitForEmail } from '../support/mailpit.js' | ||
|
|
||
| const AUTH_BASE = () => `${testEnv.authUrl}/api/auth` | ||
|
|
||
| async function sendVerificationOtp(email: string): Promise<void> { | ||
| const res = await fetch(`${AUTH_BASE()}/email-otp/send-verification-otp`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ email, type: 'sign-in' }), | ||
| }) | ||
| if (!res.ok) { | ||
| const text = await res.text() | ||
| throw new Error( | ||
| `send-verification-otp failed: ${res.status} ${text.slice(0, 200)}`, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| async function signInWithOtp( | ||
| world: EpdsWorld, | ||
| email: string, | ||
| otp: string, | ||
| includeTosAccepted: boolean, | ||
| ): Promise<void> { | ||
| const body: Record<string, unknown> = { email, otp } | ||
| if (includeTosAccepted) body.tosAccepted = true | ||
|
|
||
| const res = await fetch(`${AUTH_BASE()}/sign-in/email-otp`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(body), | ||
| }) | ||
| world.lastHttpStatus = res.status | ||
| const text = await res.text() | ||
| try { | ||
| world.lastHttpJson = JSON.parse(text) as Record<string, unknown> | ||
| } catch { | ||
| world.lastHttpJson = { body: text } | ||
| } | ||
| } | ||
|
|
||
| When( | ||
| 'a new user submits a valid OTP code without the tosAccepted flag', | ||
| async function (this: EpdsWorld) { | ||
| if (!testEnv.mailpitPass) return 'pending' | ||
| const email = `tos-new-${Date.now()}@example.com` | ||
| this.testEmail = email | ||
|
|
||
| await clearMailpit(email) | ||
| await sendVerificationOtp(email) | ||
| const message = await waitForEmail(`to:${email}`) | ||
| const otp = await extractOtp(message.ID) | ||
|
|
||
| await signInWithOtp(this, email, otp, false) | ||
| }, | ||
| ) | ||
|
|
||
| When( | ||
| 'the returning user submits a valid OTP code without the tosAccepted flag', | ||
| async function (this: EpdsWorld) { | ||
| if (!testEnv.mailpitPass) return 'pending' | ||
| if (!this.testEmail) { | ||
| throw new Error( | ||
| 'No testEmail — "a returning user has a PDS account" Given must run first', | ||
| ) | ||
| } | ||
| await clearMailpit(this.testEmail) | ||
| await sendVerificationOtp(this.testEmail) | ||
| const message = await waitForEmail(`to:${this.testEmail}`) | ||
| const otp = await extractOtp(message.ID) | ||
|
|
||
| await signInWithOtp(this, this.testEmail, otp, false) | ||
| }, | ||
| ) | ||
|
|
||
| Then('the auth service returns a 400 Bad Request', function (this: EpdsWorld) { | ||
| expect(this.lastHttpStatus).toBe(400) | ||
| }) | ||
|
|
||
| Then( | ||
| 'the error message is {string}', | ||
| function (this: EpdsWorld, expected: string) { | ||
| const body = this.lastHttpJson ?? {} | ||
| const message = (body as { message?: string }).message | ||
| expect(message).toBe(expected) | ||
| }, | ||
| ) | ||
|
|
||
| Then('the sign-in succeeds', function (this: EpdsWorld) { | ||
| expect(this.lastHttpStatus).toBe(200) | ||
| }) | ||
|
|
||
| Then( | ||
| 'the OTP form does not show a Terms of Service checkbox', | ||
| async function (this: EpdsWorld) { | ||
| const page = getPage(this) | ||
| // The server renders #tos-field into the DOM unconditionally but | ||
| // sets inline display:none for non-new-users; the client-side | ||
| // isNewUser check flips it to block only when isNewUser === true. | ||
| // Assert it's hidden (either absent or display:none). | ||
| const tosField = page.locator('#tos-field') | ||
| await expect(tosField).toBeHidden() | ||
| }, | ||
| ) | ||
|
|
||
| Then( | ||
| 'the OTP form shows a Terms of Service checkbox', | ||
| async function (this: EpdsWorld) { | ||
| const page = getPage(this) | ||
| // Client-side JS flips #tos-field from display:none to display:block | ||
| // once checkIsNewUser confirms isNewUser === true. Wait up to 10s for | ||
| // the Path B new-user-check POST to resolve. | ||
| await expect(page.locator('#tos-field')).toBeVisible({ timeout: 10_000 }) | ||
| await expect(page.locator('#tos-accept')).toBeVisible() | ||
| }, | ||
| ) | ||
|
|
||
| When( | ||
| 'the user submits the OTP code without accepting the Terms of Service', | ||
| async function (this: EpdsWorld) { | ||
| if (!testEnv.mailpitPass) return 'pending' | ||
| const page = getPage(this) | ||
|
|
||
| // The preceding "the user requests an OTP for {string}" step doesn't | ||
| // populate world.otpCode, so fetch it here from the input that was | ||
| // used on the login page — mailpit is keyed by recipient. | ||
| const email = await page.inputValue('#email') | ||
| if (!email) throw new Error('Email input is empty — cannot locate OTP') | ||
| const message = await waitForEmail(`to:${email}`) | ||
| const otp = await extractOtp(message.ID) | ||
| this.otpCode = otp | ||
|
|
||
| // Deliberately leave #tos-accept unchecked. This scenario verifies the | ||
| // *client-side* guard: by the time we click Verify, #tos-field is | ||
| // visible (the prior step waited for it) so isNewUser === true, and the | ||
| // submit handler short-circuits with showError(...) before any fetch is | ||
| // issued. The "sign-in is not completed" assertion below confirms the UI | ||
| // stayed put. The server-side enforceTosAcceptance hook is covered | ||
| // separately by the direct-HTTP scenario in features/security.feature. | ||
| // | ||
| // Strip the HTML5 `required` attribute so the browser's native form | ||
| // validation doesn't block submission first — we want the JS guard to | ||
| // be the thing that fires. | ||
| await page.evaluate(() => { | ||
| document.getElementById('tos-accept')?.removeAttribute('required') | ||
| }) | ||
| await page.fill('#code', otp) | ||
| await page.click('#form-verify-otp .btn-primary') | ||
| }, | ||
| ) | ||
|
|
||
| When('the user accepts the Terms of Service', async function (this: EpdsWorld) { | ||
| const page = getPage(this) | ||
| await page.check('#tos-accept') | ||
| }) | ||
|
|
||
| When( | ||
| 'the user enters the OTP code from the email', | ||
| async function (this: EpdsWorld) { | ||
| if (!testEnv.mailpitPass) return 'pending' | ||
| const page = getPage(this) | ||
| const email = await page.inputValue('#email') | ||
| if (!email) throw new Error('Email input is empty — cannot locate OTP') | ||
| const message = await waitForEmail(`to:${email}`) | ||
| const otp = await extractOtp(message.ID) | ||
| this.otpCode = otp | ||
| await page.fill('#code', otp) | ||
| await page.click('#form-verify-otp .btn-primary') | ||
| }, | ||
| ) | ||
|
|
||
| Then('the sign-in is not completed', async function (this: EpdsWorld) { | ||
| const page = getPage(this) | ||
| // Stay on the login page — never reach /welcome or /auth/choose-handle. | ||
| // Give the browser a moment to process any navigation that might occur. | ||
| await page.waitForTimeout(1500) | ||
| const url = page.url() | ||
| expect(url).not.toMatch(/\/welcome/) | ||
| expect(url).not.toMatch(/\/auth\/choose-handle/) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.