Skip to content
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

Add test coverage for survey API submission code #1689

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
61 changes: 61 additions & 0 deletions src/features/surveys/utils/prepareSurveyApiSubmission.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import prepareSurveyApiSubmission from './prepareSurveyApiSubmission';

describe('prepareSurveyApiSubmission()', () => {
let formData: NodeJS.Dict<string | string[]>;

beforeEach(() => {
formData = {};
});

it('formats a text response', () => {
formData['123.text'] = 'Lorem ipsum dolor sit amet';
const submission = prepareSurveyApiSubmission(formData);
expect(submission.responses).toMatchObject([
{
question_id: 123,
response: 'Lorem ipsum dolor sit amet',
},
]);
});

it('formats a radio button response', () => {
formData['123.options'] = '456';
const submission = prepareSurveyApiSubmission(formData);
expect(submission.responses).toMatchObject([
{
options: [456],
question_id: 123,
},
]);
});

it('formats a checkbox response', () => {
formData['123.options'] = ['456', '789'];
const submission = prepareSurveyApiSubmission(formData);
expect(submission.responses).toMatchObject([
{
options: [456, 789],
question_id: 123,
},
]);
});

it('signs as the logged-in account when a logged-in user requests to sign as themself', () => {
formData['sig'] = 'user';
const submission = prepareSurveyApiSubmission(formData, true);
expect(submission.signature).toEqual('user');
});

it('signs with custom contact details when a name and email are given', () => {
formData['sig'] = 'email';
formData['sig.email'] = 'testuser@example.org';
formData['sig.first_name'] = 'test';
formData['sig.last_name'] = 'user';
const submission = prepareSurveyApiSubmission(formData);
expect(submission.signature).toMatchObject({
email: 'testuser@example.org',
first_name: 'test',
last_name: 'user',
});
});
});
63 changes: 63 additions & 0 deletions src/features/surveys/utils/prepareSurveyApiSubmission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
ZetkinSurveyApiSubmission,
ZetkinSurveyQuestionResponse,
ZetkinSurveySignaturePayload,
} from 'utils/types/zetkin';

export default function prepareSurveyApiSubmission(
formData: NodeJS.Dict<string | string[]>,
isLoggedIn?: boolean
): ZetkinSurveyApiSubmission {
const responses: ZetkinSurveyQuestionResponse[] = [];
const responseEntries = Object.fromEntries(
Object.entries(formData).filter(([name]) =>
name.match(/^[0-9]+\.(options|text)$/)
)
);

for (const name in responseEntries) {
const value = responseEntries[name];
const fields = name.split('.');
const [id, type] = fields;

if (type == 'text') {
responses.push({
question_id: parseInt(id),
response: value as string,
});
}

if (type === 'options' && typeof value === 'string') {
responses.push({
options: [parseInt(value, 10)],
question_id: parseInt(id, 10),
});
}

if (type === 'options' && Array.isArray(value)) {
responses.push({
options: value.map((o) => parseInt(o, 10)),
question_id: parseInt(id, 10),
});
}
}

let signature: ZetkinSurveySignaturePayload = null;

if (formData.sig === 'user' && isLoggedIn) {
signature = 'user';
}

if (formData.sig == 'email') {
signature = {
email: formData['sig.email'] as string,
first_name: formData['sig.first_name'] as string,
last_name: formData['sig.last_name'] as string,
};
}

return {
responses,
signature,
};
}
61 changes: 4 additions & 57 deletions src/pages/o/[orgId]/surveys/[surveyId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Container } from '@mui/material';
import { FC } from 'react';
import { IncomingMessage } from 'http';
import { parse } from 'querystring';
import prepareSurveyApiSubmission from 'features/surveys/utils/prepareSurveyApiSubmission';
import { scaffold } from 'utils/next';
import SurveyElements from 'features/surveys/components/surveyForm/SurveyElements';
import SurveyHeading from 'features/surveys/components/surveyForm/SurveyHeading';
Expand All @@ -12,8 +13,6 @@ import SurveySubmitButton from 'features/surveys/components/surveyForm/SurveySub
import {
ZetkinSurveyExtended,
ZetkinSurveyFormStatus,
ZetkinSurveyQuestionResponse,
ZetkinSurveySignaturePayload,
} from 'utils/types/zetkin';

const scaffoldOptions = {
Expand Down Expand Up @@ -53,65 +52,13 @@ export const getServerSideProps = scaffold(async (ctx) => {

if (req.method === 'POST') {
formData = await parseRequest(req);
const responses: Record<string, ZetkinSurveyQuestionResponse> = {};

for (const name in formData) {
const isSignature = name.startsWith('sig');
const isPrivacy = name.startsWith('privacy');
const isMetadata = isSignature || isPrivacy;
if (isMetadata) {
continue;
}

const fields = name.split('.');
const questionId = fields[0];
const questionType = fields[1];

const value = formData[name];
if (questionType == 'options') {
if (Array.isArray(value)) {
responses[questionId] = {
options: value.map((o) => parseInt(o)),
question_id: parseInt(fields[0]),
};
} else {
responses[questionId] = {
options: [parseInt((formData[name] as string)!)],
question_id: parseInt(fields[0]),
};
}
} else if (questionType == 'text') {
responses[questionId] = {
question_id: parseInt(fields[0]),
response: formData[name] as string,
};
}
}

let signature: ZetkinSurveySignaturePayload = null;

if (formData.sig === 'user') {
const session = await ctx.z.resource('session').get();
if (session) {
signature = 'user';
}
}

if (formData.sig == 'email') {
signature = {
email: formData['sig.email'] as string,
first_name: formData['sig.first_name'] as string,
last_name: formData['sig.last_name'] as string,
};
}
const session = await ctx.z.resource('session').get();
const submission = prepareSurveyApiSubmission(formData, !!session);

try {
await apiClient.post(
`/api/orgs/${orgId}/surveys/${surveyId}/submissions`,
{
responses: Object.values(responses),
signature,
}
submission
);
status = 'submitted';
} catch (e) {
Expand Down
5 changes: 5 additions & 0 deletions src/utils/types/zetkin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ export type ZetkinSurveyFormStatus =
| 'error'
| 'submitted';

export type ZetkinSurveyApiSubmission = {
responses: ZetkinSurveyQuestionResponse[];
signature: ZetkinSurveySignaturePayload;
};

export enum RESPONSE_TYPE {
OPTIONS = 'options',
TEXT = 'text',
Expand Down
Loading