Skip to content

Commit

Permalink
sync: Test for onboarding after GitHub OAuth (2105ba3)
Browse files Browse the repository at this point in the history
  • Loading branch information
abetoots committed Aug 25, 2024
1 parent 05046b8 commit 392e4c9
Show file tree
Hide file tree
Showing 14 changed files with 318 additions and 41 deletions.
14 changes: 13 additions & 1 deletion apps/web/app/components/ui/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,34 @@ const childrenSizeClassName = {
* Alternatively, if you're not ok with the icon being to the left of the text,
* you need to wrap the icon and text in a common parent and set the parent to
* display "flex" (or "inline-flex") with "items-center" and a reasonable gap.
*
* Pass `title` prop to the `Icon` component to get `<title>` element rendered
* in the SVG container, providing this way for accessibility.
*/
export function Icon({
name,
size = 'font',
className,
title,
children,
...props
}: SVGProps<SVGSVGElement> & {
name: IconName
size?: Size
title?: string
}) {
if (children) {
return (
<span
className={`inline-flex items-center ${childrenSizeClassName[size]}`}
>
<Icon name={name} size={size} className={className} {...props} />
<Icon
name={name}
size={size}
className={className}
title={title}
{...props}
/>
{children}
</span>
)
Expand All @@ -59,6 +70,7 @@ export function Icon({
{...props}
className={cn(sizeClassName[size], 'inline self-center', className)}
>
{title ? <title>{title}</title> : null}
<use href={`${href}#${name}`} />
</svg>
)
Expand Down
25 changes: 19 additions & 6 deletions apps/web/app/components/ui/status-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,31 @@ export const StatusButton = React.forwardRef<
})
const companion = {
pending: delayedPending ? (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="update" className="animate-spin" />
<div
role="status"
className="inline-flex h-6 w-6 items-center justify-center"
>
<Icon name="update" className="animate-spin" title="loading" />
</div>
) : null,
success: (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="check" />
<div
role="status"
className="inline-flex h-6 w-6 items-center justify-center"
>
<Icon name="check" title="success" />
</div>
),
error: (
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-destructive">
<Icon name="cross-1" className="text-destructive-foreground" />
<div
role="status"
className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-destructive"
>
<Icon
name="cross-1"
className="text-destructive-foreground"
title="error"
/>
</div>
),
idle: null,
Expand Down
15 changes: 11 additions & 4 deletions apps/web/app/routes/_auth+/auth.$provider.callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { ProviderNameSchema, providerLabels } from '#app/utils/connections.tsx'
import { prisma } from '#app/utils/db.server.ts'
import { ensurePrimary } from '#app/utils/litefs.server.ts'
import { combineHeaders } from '#app/utils/misc.tsx'
import {
normalizeEmail,
normalizeUsername,
} from '#app/utils/providers/provider.ts'
import {
destroyRedirectToHeader,
getRedirectCookieValue,
Expand Down Expand Up @@ -35,8 +39,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
const authResult = await authenticator
.authenticate(providerName, request, { throwOnError: true })
.then(
(data) => ({ success: true, data }) as const,
(error) => ({ success: false, error }) as const,
data => ({ success: true, data }) as const,
error => ({ success: false, error }) as const,
)

if (!authResult.success) {
Expand Down Expand Up @@ -140,8 +144,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
verifySession.set(onboardingEmailSessionKey, profile.email)
verifySession.set(prefilledProfileKey, {
...profile,
email: profile.email.toLowerCase(),
username: profile.username?.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
email: normalizeEmail(profile.email),
username:
typeof profile.username === 'string'
? normalizeUsername(profile.username)
: undefined,
})
verifySession.set(providerIdKey, profile.id)
const onboardingRedirect = [
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/routes/_auth+/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
checkHoneypot(formData)
const submission = await parseWithZod(formData, {
schema: (intent) =>
schema: intent =>
SignupFormSchema.superRefine(async (data, ctx) => {
const existingUser = await prisma.user.findUnique({
where: { username: data.username },
Expand All @@ -83,7 +83,7 @@ export async function action({ request }: ActionFunctionArgs) {
})
return
}
}).transform(async (data) => {
}).transform(async data => {
if (intent !== null) return { ...data, session: null }

const session = await signup({ ...data, email })
Expand Down Expand Up @@ -129,7 +129,7 @@ export const meta: MetaFunction = () => {
return [{ title: 'Setup Epic Notes Account' }]
}

export default function SignupRoute() {
export default function OnboardingRoute() {
const data = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()
const isPending = useIsPending()
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/routes/_auth+/onboarding_.$provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
})
return
}
}).transform(async (data) => {
}).transform(async data => {
const session = await signupWithConnection({
...data,
email,
Expand Down Expand Up @@ -178,7 +178,7 @@ export const meta: MetaFunction = () => {
return [{ title: 'Setup Epic Notes Account' }]
}

export default function SignupRoute() {
export default function OnboardingProviderRoute() {
const data = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()
const isPending = useIsPending()
Expand Down
3 changes: 3 additions & 0 deletions apps/web/app/utils/providers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const MOCK_CODE_GITHUB = 'MOCK_CODE_GITHUB_KODY'

export const MOCK_CODE_GITHUB_HEADER = 'x-mock-code-github'
11 changes: 8 additions & 3 deletions apps/web/app/utils/providers/github.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { z } from 'zod'
import { cache, cachified } from '../cache.server.ts'
import { connectionSessionStorage } from '../connections.server.ts'
import { type Timings } from '../timing.server.ts'
import { MOCK_CODE_GITHUB_HEADER, MOCK_CODE_GITHUB } from './constants.ts'
import { type AuthProvider } from './provider.ts'

const GitHubUserSchema = z.object({ login: z.string() })
Expand All @@ -19,8 +20,9 @@ const GitHubUserParseResult = z
}),
)

const shouldMock = process.env.GITHUB_CLIENT_ID?.startsWith('MOCK_')

const shouldMock =
process.env.GITHUB_CLIENT_ID?.startsWith('MOCK_') ||
process.env.NODE_ENV === 'test'
export class GitHubProvider implements AuthProvider {
getAuthStrategy() {
return new GitHubStrategy(
Expand Down Expand Up @@ -87,7 +89,10 @@ export class GitHubProvider implements AuthProvider {
)
const state = cuid()
connectionSession.set('oauth2:state', state)
const code = 'MOCK_CODE_GITHUB_KODY'
// allows us to inject a code when running e2e tests,
// but falls back to a pre-defined 🐨 constant
const code =
request.headers.get(MOCK_CODE_GITHUB_HEADER) || MOCK_CODE_GITHUB
const searchParams = new URLSearchParams({ code, state })
throw redirect(`/auth/github/callback?${searchParams}`, {
headers: {
Expand Down
5 changes: 5 additions & 0 deletions apps/web/app/utils/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ export interface AuthProvider {
link?: string | null
}>
}

export const normalizeEmail = (s: string) => s.toLowerCase()

export const normalizeUsername = (s: string) =>
s.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()
11 changes: 7 additions & 4 deletions apps/web/app/utils/user-validation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { z } from 'zod'

export const USERNAME_MIN_LENGTH = 3
export const USERNAME_MAX_LENGTH = 20

export const UsernameSchema = z
.string({ required_error: 'Username is required' })
.min(3, { message: 'Username is too short' })
.max(20, { message: 'Username is too long' })
.min(USERNAME_MIN_LENGTH, { message: 'Username is too short' })
.max(USERNAME_MAX_LENGTH, { message: 'Username is too long' })
.regex(/^[a-zA-Z0-9_]+$/, {
message: 'Username can only include letters, numbers, and underscores',
})
// users can type the username in any case, but we store it in lowercase
.transform((value) => value.toLowerCase())
.transform(value => value.toLowerCase())

export const PasswordSchema = z
.string({ required_error: 'Password is required' })
Expand All @@ -24,7 +27,7 @@ export const EmailSchema = z
.min(3, { message: 'Email is too short' })
.max(100, { message: 'Email is too long' })
// users can type the email in any case, but we store it in lowercase
.transform((value) => value.toLowerCase())
.transform(value => value.toLowerCase())

export const PasswordAndConfirmPasswordSchema = z
.object({ password: PasswordSchema, confirmPassword: PasswordSchema })
Expand Down
1 change: 1 addition & 0 deletions apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default defineConfig({
stderr: 'pipe',
env: {
PORT,
NODE_ENV: 'test',
},
},
})
7 changes: 4 additions & 3 deletions apps/web/prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { faker } from '@faker-js/faker'
import { promiseHash } from 'remix-utils/promise'
import { prisma } from '#app/utils/db.server.ts'
import { MOCK_CODE_GITHUB } from '#app/utils/providers/constants'
import {
cleanupDb,
createPassword,
Expand Down Expand Up @@ -97,7 +98,7 @@ async function seed() {
},
},
})
.catch((e) => {
.catch(e => {
console.error('Error creating a user:', e)
return null
})
Expand Down Expand Up @@ -139,7 +140,7 @@ async function seed() {
}),
})

const githubUser = await insertGitHubUser('MOCK_CODE_GITHUB_KODY')
const githubUser = await insertGitHubUser(MOCK_CODE_GITHUB)

await prisma.user.create({
select: { id: true },
Expand Down Expand Up @@ -260,7 +261,7 @@ async function seed() {
}

seed()
.catch((e) => {
.catch(e => {
console.error(e)
process.exit(1)
})
Expand Down
Loading

0 comments on commit 392e4c9

Please sign in to comment.