Skip to content

Commit

Permalink
Confirm session is updated on form POST
Browse files Browse the repository at this point in the history
  • Loading branch information
danielnaab committed Oct 18, 2024
1 parent db21c77 commit c1b1b94
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 96 deletions.
10 changes: 3 additions & 7 deletions packages/server/src/pages/forms/[id].astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ const sessionId = Astro.cookies.get('form_session_id')?.value;
const setFormSessionCookie = (sessionId?: string) => {
if (sessionId) {
Astro.cookies.set('form_session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
Astro.cookies.set('form_session_id', sessionId);
} else {
Astro.cookies.delete('form_session_id');
}
Expand Down Expand Up @@ -58,7 +54,7 @@ if (Astro.request.method === 'POST') {
formRoute
);
if (!submitFormResult.success) {
return new Response(submitFormResult.error.message, {
return new Response(submitFormResult.error, {
status: 500,
});
}
Expand All @@ -84,7 +80,7 @@ if (!sessionResult.success) {
status: 500,
});
}
setFormSessionCookie(sessionResult.data.id);
setFormSessionCookie(sessionResult.data.id || 'fake-cookie-session-id');
const formSession = sessionResult.data.data;
---
Expand Down
226 changes: 137 additions & 89 deletions packages/server/src/pages/forms/[id].test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { getByRole } from '@testing-library/dom';
import { userEvent } from '@testing-library/user-event';
import { experimental_AstroContainer } from 'astro/container';
import { type DOMWindow, JSDOM } from 'jsdom';
import { describe, expect, test } from 'vitest';

import {
type FormService,
type FormSession,
type InputPattern,
type PagePattern,
type PageSetPattern,
Expand All @@ -18,132 +20,155 @@ import {
import { createServerFormService } from '../../config/services.js';
import { createAstroContainer } from '../../config/testing.js';

import FormPage from './[id].astro';
import PageComponent from './[id].astro';

describe('Form page', () => {
test('Returns 404 with non-existent form', async () => {
const serverOptions = await createTestServerOptions();
const response = await renderFormPage(serverOptions, 'does-not-exist');
expect(response.status).toBe(404);
});

test('Renders html form', async () => {
const { formService, serverOptions } = await createTestContext();
const formResult = await createTestForm(formService);

const response = await renderFormPage(serverOptions, formResult.data.id);
type TestContext = {
serverOptions: ServerOptions;
container: experimental_AstroContainer;
formService: FormService;
formId?: string;
};

expect(response.status).toBe(200);
expect(await response.text()).toContain('Form');
describe('Form page', () => {
test('Returns 404 with invalid form ID', async () => {
const ctx = await createTestContext();
await expect(FormPage.getForm(ctx, 'does-not-exist')).rejects.toThrow(
'Failed to load page: Received status 404'
);
});

test('Renders expected form', async () => {
const { formService, serverOptions } = await createTestContext();
const formResult = await createTestForm(formService);
const pom = new FormPagePOM(serverOptions);
const { document, FormData } = await pom.loadFormPage(formResult.data.id);
test('Renders HTML form and generates correct form data when inputs are filled', async () => {
const ctx = await createTestContext();
const formId = await insertTestForm(ctx);
const pom = await FormPage.getForm(ctx, formId);

await userEvent.type(
getByRole(document.body, 'textbox', { name: 'Pattern 1' }),
'pattern one value'
);
await userEvent.type(
getByRole(document.body, 'textbox', { name: 'Pattern 2' }),
'pattern one value'
);
await pom.fillInput('Pattern 1', 'pattern one value');
await pom.fillInput('Pattern 2', 'pattern two value');

const form = getByRole<HTMLFormElement>(document.body, 'form', {
name: 'Test form',
});
const submit = getByRole(document.body, 'button', { name: 'Submit' });
const formData = new FormData(form, submit);
const formData = pom.getFormData();
const values = Object.fromEntries(formData.entries());
expect(values).toEqual({
action: 'submit',
'element-1': 'pattern one value',
'element-2': 'pattern one value',
'element-2': 'pattern two value',
});
const postResponse = await pom.postForm(formResult.data.id, formData);
expect(postResponse.status).toBe(302);
expect(postResponse.headers.get('Location')).toEqual(
`/forms/${formResult.data.id}`
);
});

const { document: document2 } = await pom.loadFormPage(formResult.data.id);
const input1 = getByRole(document2.body, 'textbox', { name: 'Pattern 1' });
const input2 = getByRole(document2.body, 'textbox', { name: 'Pattern 2' });
expect(input1).toHaveValue('pattern one value');
expect(input2).toHaveValue('pattern one value');
test('Submits form and displays stored data correctly', async () => {
const ctx = await createTestContext();
const formId = await insertTestForm(ctx);

// Fill out form and get formData for submission
const pom = await FormPage.getForm(ctx, formId);
await pom.fillInput('Pattern 1', 'pattern one value');
await pom.fillInput('Pattern 2', 'pattern two value');

// Submit form and confirm redirect response
// NOTE: this response does not include the `form_session_id` cookie due to
// a limitation of the Astro experimental container renderer. Revisit this
// in the future, when it hopefully is more feature-complete.
const response = await submitForm(ctx, formId, pom.getFormData());
expect(response.status).toEqual(302);
expect(response.headers.get('Location')).toEqual(`/forms/${formId}`);

// Confirm that new session is stored with correct values
// TODO: Due to the limitation mentioned above, we need to query the
// database for the form session. Revisit this in the future.
const db = await ctx.serverOptions.db.getKysely();
const sessionResult = await db
.selectFrom('form_sessions')
.where('form_id', '=', formId)
.select(['id', 'form_id', 'data'])
.executeTakeFirstOrThrow()
.then(result => {
return {
id: result.id,
formId: result.form_id,
data: JSON.parse(result.data) as FormSession,
};
});
expect(sessionResult.data.data).toEqual({
errors: {},
values: {
'element-1': 'pattern one value',
'element-2': 'pattern two value',
},
});
});
});

const createTestContext = async () => {
const createTestContext = async (): Promise<TestContext> => {
const serverOptions = await createTestServerOptions();
const container = await createAstroContainer();
const formService = createServerFormService(serverOptions, {
isUserLoggedIn: () => true,
});
return {
formService,
serverOptions,
container,
formService,
};
};

const createTestForm = async (formService: FormService) => {
const insertTestForm = async (context: TestContext) => {
const testForm = createTestBlueprint();
const result = await formService.addForm(testForm);
const result = await context.formService.addForm(testForm);
if (!result.success) {
expect.fail('Failed to add test form');
}
return result;
return result.data.id;
};

const renderFormPage = async (serverOptions: ServerOptions, id: string) => {
const container = await createAstroContainer();
return await container.renderToResponse(FormPage, {
locals: {
serverOptions,
session: null,
user: null,
},
params: { id },
request: new Request(`http://localhost/forms/${id}`, {
method: 'GET',
}),
});
};
class FormPage {
private constructor(private window: DOMWindow) {}

static async getForm(
context: TestContext,
formId: string
): Promise<FormPage> {
const response = await context.container.renderToResponse(PageComponent, {
locals: {
serverOptions: context.serverOptions,
session: null,
user: null,
},
params: { id: formId },
request: new Request(`http://localhost/forms/${formId}`, {
method: 'GET',
}),
});

const postFormPage = async (
serverOptions: ServerOptions,
id: string,
body: FormData
) => {
const container = await createAstroContainer();
return await container.renderToResponse(FormPage, {
locals: {
serverOptions,
session: null,
user: null,
},
params: { id },
request: new Request(`http://localhost/forms/${id}`, {
method: 'POST',
body,
}),
});
};
if (!response.ok) {
return expect.fail(
`Failed to load page: Received status ${response.status}`
);
}

class FormPagePOM {
constructor(private serverOptions: ServerOptions) {}
const contentType = response.headers.get('Content-Type');
if (!contentType || !contentType.includes('text/html')) {
return expect.fail('Failed to load page: Expected HTML content');
}

async loadFormPage(id: string): Promise<DOMWindow> {
const response = await renderFormPage(this.serverOptions, id);
const text = await response.text();
const dom = new JSDOM(text);
return dom.window;
return new FormPage(dom.window);
}

async fillInput(label: string, value: string): Promise<void> {
const input = getByRole(this.window.document.body, 'textbox', {
name: label,
});
await userEvent.type(input, value);
}

async postForm(id: string, body: FormData) {
return postFormPage(this.serverOptions, id, body);
getFormData(): FormData {
const form = getByRole<HTMLFormElement>(this.window.document.body, 'form', {
name: 'Test form',
});
const submitButton = getByRole(this.window.document.body, 'button', {
name: 'Submit',
});
return new this.window.FormData(form, submitButton);
}
}

Expand Down Expand Up @@ -195,3 +220,26 @@ export const createTestBlueprint = () => {
}
);
};

const submitForm = async (
context: TestContext,
formId: string,
formData: FormData,
sessionId?: string
): Promise<Response> => {
const response = await context.container.renderToResponse(PageComponent, {
locals: {
serverOptions: context.serverOptions,
session: null,
user: null,
},
params: { id: formId },
request: new Request(`http://localhost/forms/${formId}`, {
method: 'POST',
body: formData,
headers: sessionId ? { Cookie: `form_session_id=${sessionId}` } : {},
}),
});

return response;
};

0 comments on commit c1b1b94

Please sign in to comment.