fix: add public profiles tests

This commit is contained in:
David Nguyen
2025-02-19 16:07:04 +11:00
parent 5ce2bae39d
commit a319ea0f5e
26 changed files with 187 additions and 41 deletions

View File

@@ -60,7 +60,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { user } = useSession(); const { user, refreshSession } = useSession();
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
const [isPublicProfileVisible, setIsPublicProfileVisible] = useState(profile.enabled); const [isPublicProfileVisible, setIsPublicProfileVisible] = useState(profile.enabled);
@@ -96,6 +96,9 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
}); });
} else { } else {
await updateUserProfile(data); await updateUserProfile(data);
// Need to refresh session because we're editing the user's profile.
await refreshSession();
} }
if (data.enabled === undefined && !isPublicProfileVisible) { if (data.enabled === undefined && !isPublicProfileVisible) {

View File

@@ -7,7 +7,7 @@ import { ChevronLeft } from 'lucide-react';
import { Link, Outlet } from 'react-router'; import { Link, Outlet } from 'react-router';
import LogoIcon from '@documenso/assets/logo_icon.png'; import LogoIcon from '@documenso/assets/logo_icon.png';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@@ -21,7 +21,7 @@ export function meta() {
} }
export default function PublicProfileLayout() { export default function PublicProfileLayout() {
const session = useSession(); const { sessionData } = useOptionalSession();
const [scrollY, setScrollY] = useState(0); const [scrollY, setScrollY] = useState(0);
@@ -37,8 +37,8 @@ export default function PublicProfileLayout() {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
{session ? ( {sessionData ? (
<AuthenticatedHeader user={session.user} teams={session.teams} /> <AuthenticatedHeader user={sessionData.user} teams={sessionData.teams} />
) : ( ) : (
<header <header
className={cn( className={cn(

View File

@@ -44,7 +44,6 @@ export async function loader({ params }: Route.LoaderArgs) {
profileUrl, profileUrl,
}).catch(() => null); }).catch(() => null);
// Todo: Test
if (!publicProfile || !publicProfile.profile.enabled) { if (!publicProfile || !publicProfile.profile.enabled) {
throw new Response('Not Found', { status: 404 }); throw new Response('Not Found', { status: 404 });
} }

View File

@@ -29,7 +29,7 @@ export default function RecipientLayout() {
); );
} }
// Todo: Use generic error boundary. // Todo: (RR7) Use generic error boundary.
export function ErrorBoundary() { export function ErrorBoundary() {
return ( return (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32"> <div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">

View File

@@ -1,4 +1,4 @@
// Todo: Test, used AI to migrate this component from NextJS to Remix. // Todo: (RR7) Test, used AI to migrate this component from NextJS to Remix.
import satori from 'satori'; import satori from 'satori';
import sharp from 'sharp'; import sharp from 'sharp';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';

View File

@@ -4,7 +4,7 @@ import { NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/li
import type { Route } from './+types/share.$slug'; import type { Route } from './+types/share.$slug';
// Todo: Test meta. // Todo: (RR7) Test meta.
export function meta({ params: { slug } }: Route.MetaArgs) { export function meta({ params: { slug } }: Route.MetaArgs) {
return [ return [
{ title: 'Documenso - Share' }, { title: 'Documenso - Share' },

View File

@@ -6,7 +6,7 @@ export type ShareHandlerAPIResponse =
| Awaited<ReturnType<typeof getRecipientOrSenderByShareLinkSlug>> | Awaited<ReturnType<typeof getRecipientOrSenderByShareLinkSlug>>
| { error: string }; | { error: string };
// Todo: Test // Todo: (RR7) Test
export async function loader({ request }: Route.LoaderArgs) { export async function loader({ request }: Route.LoaderArgs) {
try { try {
const url = new URL(request.url); const url = new URL(request.url);

View File

@@ -1,6 +1,6 @@
import { stripeWebhookHandler } from '@documenso/ee/server-only/stripe/webhook/handler'; import { stripeWebhookHandler } from '@documenso/ee/server-only/stripe/webhook/handler';
// Todo // Todo: (RR7)
// export const config = { // export const config = {
// api: { bodyParser: false }, // api: { bodyParser: false },
// }; // };

View File

@@ -2,7 +2,7 @@ import { handlerTriggerWebhooks } from '@documenso/lib/server-only/webhooks/trig
import type { Route } from './+types/webhook.trigger'; import type { Route } from './+types/webhook.trigger';
// Todo // Todo: (RR7)
// export const config = { // export const config = {
// maxDuration: 300, // maxDuration: 300,
// api: { // api: {

View File

@@ -6,7 +6,7 @@ import { EmbedPaywall } from '~/components/embed/embed-paywall';
import type { Route } from './+types/_layout'; import type { Route } from './+types/_layout';
// Todo: Test // Todo: (RR7) Test
export function headers({ loaderHeaders }: Route.HeadersArgs) { export function headers({ loaderHeaders }: Route.HeadersArgs) {
const origin = loaderHeaders.get('Origin') ?? '*'; const origin = loaderHeaders.get('Origin') ?? '*';

View File

@@ -33,7 +33,7 @@ export const filesRoute = new Hono<HonoEnv>()
return c.json({ error: 'No file provided' }, 400); return c.json({ error: 'No file provided' }, 400);
} }
// Todo: This is new. // Todo: (RR7) This is new.
// Add file size validation. // Add file size validation.
// Convert MB to bytes (1 MB = 1024 * 1024 bytes) // Convert MB to bytes (1 MB = 1024 * 1024 bytes)
const MAX_FILE_SIZE = APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024; const MAX_FILE_SIZE = APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024;
@@ -54,7 +54,7 @@ export const filesRoute = new Hono<HonoEnv>()
throw new AppError('INVALID_DOCUMENT_FILE'); throw new AppError('INVALID_DOCUMENT_FILE');
} }
// Todo: Test this. // Todo: (RR7) Test this.
if (!file.name.endsWith('.pdf')) { if (!file.name.endsWith('.pdf')) {
Object.defineProperty(file, 'name', { Object.defineProperty(file, 'name', {
writable: true, writable: true,

View File

@@ -37,13 +37,13 @@ app.route('/api/auth', auth);
// Files route. // Files route.
app.route('/api/files', filesRoute); app.route('/api/files', filesRoute);
// API servers. Todo: Configure max durations, etc? // API servers. Todo: (RR7) Configure max durations, etc?
app.route('/api/v1', tsRestHonoApp); app.route('/api/v1', tsRestHonoApp);
app.use('/api/jobs/*', jobsClient.getApiHandler()); app.use('/api/jobs/*', jobsClient.getApiHandler());
app.use('/api/trpc/*', reactRouterTrpcServer); app.use('/api/trpc/*', reactRouterTrpcServer);
// Unstable API server routes. Order matters for these two. // Unstable API server routes. Order matters for these two.
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument)); app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
app.use(`${API_V2_BETA_URL}/*`, async (c) => openApiTrpcServerHandler(c)); // Todo: Add next()? app.use(`${API_V2_BETA_URL}/*`, async (c) => openApiTrpcServerHandler(c)); // Todo: (RR7) Add next()?
export default app; export default app;

View File

@@ -11,8 +11,8 @@ export const openApiTrpcServerHandler = async (c: Context) => {
return createOpenApiFetchHandler<typeof appRouter>({ return createOpenApiFetchHandler<typeof appRouter>({
endpoint: API_V2_BETA_URL, endpoint: API_V2_BETA_URL,
router: appRouter, router: appRouter,
// Todo: Test this, since it's not using the createContext params. // Todo: (RR7) Test this, since it's not using the createContext params.
// Todo: Reduce calls since we fetch on most request? maybe // Todo: (RR7) Reduce calls since we fetch on most request? maybe
createContext: async () => createTrpcContext({ c, requestSource: 'apiV2' }), createContext: async () => createTrpcContext({ c, requestSource: 'apiV2' }),
req: c.req.raw, req: c.req.raw,
onError: (opts) => handleTrpcRouterError(opts, 'apiV2'), onError: (opts) => handleTrpcRouterError(opts, 'apiV2'),

View File

@@ -18,8 +18,8 @@ tsRestHonoApp
.get('/openapi.json', (c) => c.json(OpenAPIV1)) .get('/openapi.json', (c) => c.json(OpenAPIV1))
.get('/me', async (c) => testCredentialsHandler(c.req.raw)); .get('/me', async (c) => testCredentialsHandler(c.req.raw));
// Zapier. Todo: Check methods. Are these get/post/update requests? // Zapier. Todo: (RR7) Check methods. Are these get/post/update requests?
// Todo: Is there really no validations? // Todo: (RR7) Is there really no validations?
tsRestHonoApp tsRestHonoApp
.all('/zapier/list-documents', async (c) => listDocumentsHandler(c.req.raw)) .all('/zapier/list-documents', async (c) => listDocumentsHandler(c.req.raw))
.all('/zapier/subscribe', async (c) => subscribeHandler(c.req.raw)) .all('/zapier/subscribe', async (c) => subscribeHandler(c.req.raw))

View File

@@ -52,7 +52,7 @@ export const authenticatedMiddleware = <
} }
const metadata: ApiRequestMetadata = { const metadata: ApiRequestMetadata = {
requestMetadata: extractRequestMetadata(request), // Todo: Test requestMetadata: extractRequestMetadata(request), // Todo: (RR7) Test
source: 'apiV1', source: 'apiV1',
auth: 'api', auth: 'api',
auditUser: { auditUser: {

View File

@@ -0,0 +1,144 @@
import { expect, test } from '@playwright/test';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
test('[PUBLIC_PROFILE]: create profile', async ({ page }) => {
const user = await seedUser();
// Create direct template.
const directTemplate = await seedDirectTemplate({
userId: user.id,
});
await apiSignin({
page,
email: user.email,
redirectPath: '/settings/public-profile',
});
const publicProfileUrl = Date.now().toString();
const publicProfileBio = `public-profile-bio`;
await page.getByRole('textbox', { name: 'Public profile URL' }).click();
await page.getByRole('textbox', { name: 'Public profile URL' }).fill(publicProfileUrl);
await page.getByRole('textbox', { name: 'Bio' }).click();
await page.getByRole('textbox', { name: 'Bio' }).fill(publicProfileBio);
await page.getByRole('button', { name: 'Update' }).click();
await expect(page.getByRole('status').first()).toContainText(
'Your public profile has been updated.',
);
// Link direct template to public profile.
await page.getByRole('button', { name: 'Link template' }).click();
await page.getByRole('cell', { name: directTemplate.title }).click();
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('textbox', { name: 'Title *' }).fill('public-direct-template-title');
await page
.getByRole('textbox', { name: 'Description *' })
.fill('public-direct-template-description');
await page.getByRole('button', { name: 'Update' }).click();
// Check that public profile is disabled.
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
await expect(page.locator('body')).toContainText('404 Profile not found');
// Go back to public profile page.
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/settings/public-profile`);
await page.getByRole('switch').click();
// Assert values.
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
await expect(page.getByRole('main')).toContainText(publicProfileBio);
await expect(page.locator('body')).toContainText('public-direct-template-title');
await expect(page.locator('body')).toContainText('public-direct-template-description');
await page.getByRole('link', { name: 'Sign' }).click();
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
await expect(page.getByRole('heading')).toContainText('Document Signed');
});
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
const team = await seedTeam({
createTeamMembers: 1,
});
const user = team.owner;
// Create direct template.
const directTemplate = await seedDirectTemplate({
userId: user.id,
teamId: team.id,
});
// Create non team template to make sure you can only see the team one.
// Will be indirectly asserted because test should fail when 2 elements appear.
await seedDirectTemplate({
userId: user.id,
});
await apiSignin({
page,
email: user.email,
redirectPath: `/t/${team.url}/settings/public-profile`,
});
const publicProfileUrl = team.url;
const publicProfileBio = `public-profile-bio`;
await page.getByRole('textbox', { name: 'Bio' }).click();
await page.getByRole('textbox', { name: 'Bio' }).fill(publicProfileBio);
await page.getByRole('button', { name: 'Update' }).click();
await expect(page.getByRole('status').first()).toContainText(
'Your public profile has been updated.',
);
// Link direct template to public profile.
await page.getByRole('button', { name: 'Link template' }).click();
await page.getByRole('cell', { name: directTemplate.title }).click();
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('textbox', { name: 'Title *' }).fill('public-direct-template-title');
await page
.getByRole('textbox', { name: 'Description *' })
.fill('public-direct-template-description');
await page.getByRole('button', { name: 'Update' }).click();
// Check that public profile is disabled.
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
await expect(page.locator('body')).toContainText('404 Profile not found');
// Go back to public profile page.
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/settings/public-profile`);
await page.getByRole('switch').click();
// Assert values.
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
await expect(page.getByRole('main')).toContainText(publicProfileBio);
await expect(page.locator('body')).toContainText('public-direct-template-title');
await expect(page.locator('body')).toContainText('public-direct-template-description');
await page.getByRole('link', { name: 'Sign' }).click();
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
await expect(page.getByRole('heading')).toContainText('Document Signed');
});

View File

@@ -30,10 +30,10 @@ const getAuthSecret = () => {
export const sessionCookieOptions = { export const sessionCookieOptions = {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: useSecureCookies ? 'none' : 'lax', // Todo: This feels wrong? sameSite: useSecureCookies ? 'none' : 'lax', // Todo: (RR7) This feels wrong?
secure: useSecureCookies, secure: useSecureCookies,
domain: getCookieDomain(), domain: getCookieDomain(),
// Todo: Max age for specific auth cookies. // Todo: (RR7) Max age for specific auth cookies.
} as const; } as const;
export const extractSessionCookieFromHeaders = (headers: Headers): string | null => { export const extractSessionCookieFromHeaders = (headers: Headers): string | null => {

View File

@@ -38,7 +38,7 @@ export const getOptionalSession = async (
}; };
/** /**
* Todo: Rethink, this is pretty sketchy. * Todo: (RR7) Rethink, this is pretty sketchy.
*/ */
const mapRequestToContextForCookie = (c: Context | Request) => { const mapRequestToContextForCookie = (c: Context | Request) => {
if (c instanceof Request) { if (c instanceof Request) {

View File

@@ -144,7 +144,7 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
}, },
data: { data: {
emailVerified: new Date(), emailVerified: new Date(),
password: null, // Todo: Check this password: null, // Todo: (RR7) Check this
}, },
}); });
} }
@@ -182,7 +182,7 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
}); });
await onCreateUserHook(createdUser).catch((err) => { await onCreateUserHook(createdUser).catch((err) => {
// Todo: Add logging. // Todo: (RR7) Add logging.
console.error(err); console.error(err);
}); });

View File

@@ -50,7 +50,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
const csrfCookieToken = await getCsrfCookie(c); const csrfCookieToken = await getCsrfCookie(c);
// Todo: Add logging here. // Todo: (RR7) Add logging here.
if (csrfToken !== csrfCookieToken || !csrfCookieToken) { if (csrfToken !== csrfCookieToken || !csrfCookieToken) {
throw new AppError(AuthenticationErrorCode.InvalidRequest, { throw new AppError(AuthenticationErrorCode.InvalidRequest, {
message: 'Invalid CSRF token', message: 'Invalid CSRF token',

View File

@@ -51,7 +51,7 @@ export const stripeWebhookHandler = async (req: Request) => {
); );
} }
// Todo: I'm not sure about this. // Todo: (RR7) I'm not sure about this.
const clonedReq = req.clone(); const clonedReq = req.clone();
const rawBody = await clonedReq.arrayBuffer(); const rawBody = await clonedReq.arrayBuffer();
const body = Buffer.from(rawBody); const body = Buffer.from(rawBody);

View File

@@ -40,7 +40,7 @@ export const useSession = () => {
return { return {
...context.sessionData, ...context.sessionData,
refresh: context.refresh, refreshSession: context.refresh,
}; };
}; };
@@ -68,7 +68,7 @@ export const SessionProvider = ({ children, initialSession }: SessionProviderPro
} }
const teams = await trpc.team.getTeams.query().catch(() => { const teams = await trpc.team.getTeams.query().catch(() => {
// Todo: Log // Todo: (RR7) Log
return []; return [];
}); });

View File

@@ -92,7 +92,7 @@ export class InngestJobProvider extends BaseJobProvider {
// }; // };
// } // }
// Todo: Do we need to handle the above? // Todo: (RR7) Do we need to handle the above?
public getApiHandler() { public getApiHandler() {
return async (context: HonoContext) => { return async (context: HonoContext) => {
const handler = createHonoPagesRoute({ const handler = createHonoPagesRoute({

View File

@@ -52,18 +52,18 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
data: { data: {
name, name,
email: email.toLowerCase(), email: email.toLowerCase(),
password: hashedPassword, // Todo: Drop password. password: hashedPassword, // Todo: (RR7) Drop password.
signature, signature,
url, url,
}, },
}); });
// Todo: Migrate to use this after RR7. // Todo: (RR7) Migrate to use this after RR7.
// await tx.account.create({ // await tx.account.create({
// data: { // data: {
// userId: user.id, // userId: user.id,
// type: 'emailPassword', // Todo // type: 'emailPassword', // Todo: (RR7)
// provider: 'DOCUMENSO', // Todo: Enums // provider: 'DOCUMENSO', // Todo: (RR7) Enums
// providerAccountId: user.id.toString(), // providerAccountId: user.id.toString(),
// password: hashedPassword, // password: hashedPassword,
// }, // },
@@ -73,7 +73,7 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
}); });
await onCreateUserHook(user).catch((err) => { await onCreateUserHook(user).catch((err) => {
// Todo: Add logging. // Todo: (RR7) Add logging.
console.error(err); console.error(err);
}); });

View File

@@ -23,7 +23,7 @@ datasource db {
directUrl = env("NEXT_PRIVATE_DIRECT_DATABASE_URL") directUrl = env("NEXT_PRIVATE_DIRECT_DATABASE_URL")
} }
// Todo: Remove after RR7 migration. // Todo: (RR7) Remove after RR7 migration.
enum IdentityProvider { enum IdentityProvider {
DOCUMENSO DOCUMENSO
GOOGLE GOOGLE
@@ -41,14 +41,14 @@ model User {
customerId String? @unique customerId String? @unique
email String @unique email String @unique
emailVerified DateTime? emailVerified DateTime?
password String? // Todo: Remove after RR7 migration. password String? // Todo: (RR7) Remove after RR7 migration.
source String? source String?
signature String? signature String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
lastSignedIn DateTime @default(now()) lastSignedIn DateTime @default(now())
roles Role[] @default([USER]) roles Role[] @default([USER])
identityProvider IdentityProvider @default(DOCUMENSO) // Todo: Remove after RR7 migration. identityProvider IdentityProvider @default(DOCUMENSO) // Todo: (RR7) Remove after RR7 migration.
avatarImageId String? avatarImageId String?
disabled Boolean @default(false) disabled Boolean @default(false)

View File

@@ -1,4 +1,4 @@
// Todo: Not sure if this actually makes it client-only. // Todo: (RR7) Not sure if this actually makes it client-only.
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';