diff --git a/src/server/routes/assessment/assessment.spec.ts b/src/server/routes/assessment/assessment.spec.ts index 54511831..673f1854 100644 --- a/src/server/routes/assessment/assessment.spec.ts +++ b/src/server/routes/assessment/assessment.spec.ts @@ -14,8 +14,8 @@ import { import { Form } from '../../../form/form.js' import { Submission } from '../../../form/submission.js' import { formSchema } from '../../../schema/form.js' -import { Store } from '../../../storage/store.js' import { portForTest } from '../../../test/portForTest.js' +import { tempJsonFileStore } from '../../../test/tempJsonFileStore.js' import { ulid } from '../../../ulid.js' import { HTTPStatusCode } from '../../response/HttpStatusCode.js' import login from '../login.js' @@ -53,27 +53,6 @@ const simpleForm: Form = { }, ], } -const forms: Record = { - [formId]: simpleForm, -} -const dummyFormStorage: Store
= { - persist: async (id, form) => { - forms[id] = form - }, - get: async (id) => - forms[id] !== undefined ? { id, data: forms[id] } : undefined, - findAll: async () => [], -} - -const submissions: Record> = {} -const dummySubmissionStorage: Store> = { - persist: async (id, form) => { - submissions[id] = form - }, - get: async (id) => - submissions[id] !== undefined ? { id, data: submissions[id] } : undefined, - findAll: async () => [], -} const omnibus = new EventEmitter() @@ -81,11 +60,20 @@ describe('Assessment API', () => { let app: Express let httpServer: Server let r: SuperTest + const cleanups: (() => Promise)[] = [] const adminEmail = `some-admin${ulid()}@example.com` const getExpressCookie = getAuthCookie(1800, [adminEmail]) beforeAll(async () => { + const { cleanup: cleanupFormStorage, store: formStorage } = + await tempJsonFileStore() + cleanups.push(cleanupFormStorage) + await formStorage.persist(formId, simpleForm) + const { cleanup: cleanupSubmissionStorage, store: submissionStorage } = + await tempJsonFileStore>() + cleanups.push(cleanupSubmissionStorage) + app = express() app.use(cookieParser('cookie-secret')) app.use(bodyParser.json({ strict: true })) @@ -97,8 +85,8 @@ describe('Assessment API', () => { assessmentSubmissionHandler({ omnibus, endpoint, - formStorage: dummyFormStorage, - submissionStorage: dummySubmissionStorage, + formStorage, + submissionStorage: submissionStorage, }), ) app.post( @@ -106,8 +94,8 @@ describe('Assessment API', () => { cookieAuth, assessmentsExportHandler({ endpoint, - formStorage: dummyFormStorage, - submissionStorage: dummySubmissionStorage, + formStorage: formStorage, + submissionStorage: submissionStorage, }), ) @@ -124,6 +112,7 @@ describe('Assessment API', () => { }) afterAll(async () => { httpServer.close() + await Promise.all(cleanups) }) describe('POST /assessment', () => { it('should store a valid submission', async () => diff --git a/src/server/routes/assessment/submit.ts b/src/server/routes/assessment/submit.ts index 6a56f58d..dae26b1c 100644 --- a/src/server/routes/assessment/submit.ts +++ b/src/server/routes/assessment/submit.ts @@ -88,12 +88,12 @@ export const assessmentSubmissionHandler = ({ } const id = ulid() - omnibus.emit(events.assessment_created, id, validBody.value, form) await submissionStorage.persist(id, validBody.value) response .status(HTTPStatusCode.Created) .header('Location', new URL(`./assessment/${id}`, endpoint).toString()) .header('ETag', '1') .end() + omnibus.emit(events.assessment_created, id, validBody.value, form) } } diff --git a/src/server/routes/correction/correct.ts b/src/server/routes/correction/correct.ts index f27040e3..09cc1841 100644 --- a/src/server/routes/correction/correct.ts +++ b/src/server/routes/correction/correct.ts @@ -83,8 +83,13 @@ export const assessmentCorrectionHandler = ({ }) } - // TODO: load older corrections to build submission version - const submissionVersion = 1 + // Load older corrections to build submission version + const submissionVersion = + ( + await correctionStorage.findAll({ + submission: validBody.value.submission, + }) + ).length + 1 if (validBody.value.submissionVersion !== submissionVersion) { return respondWithProblem(request, response, { title: `Expected correction for submission version ${submissionVersion}, got ${validBody.value.submissionVersion}.`, @@ -121,6 +126,11 @@ export const assessmentCorrectionHandler = ({ } const id = ulid() + await correctionStorage.persist(id, validBody.value) + response + .status(HTTPStatusCode.Created) + .header('Location', new URL(`./correction/${id}`, endpoint).toString()) + .end() omnibus.emit( events.correction_created, id, @@ -128,10 +138,5 @@ export const assessmentCorrectionHandler = ({ form, submission, ) - await correctionStorage.persist(id, validBody.value) - response - .status(HTTPStatusCode.Created) - .header('Location', new URL(`./correction/${id}`, endpoint).toString()) - .end() } } diff --git a/src/server/routes/correction/correction.spec.ts b/src/server/routes/correction/correction.spec.ts index ae2b3e35..e4fb214c 100644 --- a/src/server/routes/correction/correction.spec.ts +++ b/src/server/routes/correction/correction.spec.ts @@ -15,8 +15,8 @@ import { Correction } from '../../../form/correction.js' import { Form } from '../../../form/form.js' import { Submission } from '../../../form/submission.js' import { formSchema } from '../../../schema/form.js' -import { Store } from '../../../storage/store.js' import { portForTest } from '../../../test/portForTest.js' +import { tempJsonFileStore } from '../../../test/tempJsonFileStore.js' import { ulid } from '../../../ulid.js' import { HTTPStatusCode } from '../../response/HttpStatusCode.js' import login from '../login.js' @@ -53,19 +53,7 @@ const simpleForm: Form = { }, ], } -const forms: Record = { - [formId]: simpleForm, -} -const dummyFormStorage: Store = { - persist: async (id, form) => { - forms[id] = form - }, - get: async (id) => - forms[id] !== undefined ? { id, data: forms[id] } : undefined, - findAll: async () => [], -} -const submissions: Record> = {} const submissionId = ulid() const submission: Static = { form: new URL(`./form/${formId}`, endpoint).toString(), @@ -75,25 +63,6 @@ const submission: Static = { }, }, } -submissions[submissionId] = submission -const dummySubmissionStorage: Store> = { - persist: async (id, form) => { - submissions[id] = form - }, - get: async (id) => - submissions[id] !== undefined ? { id, data: submissions[id] } : undefined, - findAll: async () => [], -} - -const corrections: Record> = {} -const dummyCorrectionStorage: Store> = { - persist: async (id, form) => { - corrections[id] = form - }, - get: async (id) => - corrections[id] !== undefined ? { id, data: corrections[id] } : undefined, - findAll: async () => [], -} const omnibus = new EventEmitter() @@ -101,11 +70,24 @@ describe('Correction API', () => { let app: Express let httpServer: Server let r: SuperTest + const cleanups: (() => Promise)[] = [] const adminEmail = `some-admin${ulid()}@example.com` const getExpressCookie = getAuthCookie(1800, [adminEmail]) beforeAll(async () => { + const { cleanup: cleanupFormStorage, store: formStorage } = + await tempJsonFileStore() + cleanups.push(cleanupFormStorage) + await formStorage.persist(formId, simpleForm) + const { cleanup: cleanupSubmissionStorage, store: submissionStorage } = + await tempJsonFileStore>() + cleanups.push(cleanupSubmissionStorage) + await submissionStorage.persist(submissionId, submission) + const { cleanup: cleanupCorrectionStorage, store: correctionStorage } = + await tempJsonFileStore>() + cleanups.push(cleanupCorrectionStorage) + app = express() app.use(cookieParser('cookie-secret')) app.use(bodyParser.json({ strict: true })) @@ -118,9 +100,9 @@ describe('Correction API', () => { assessmentCorrectionHandler({ omnibus, endpoint, - formStorage: dummyFormStorage, - submissionStorage: dummySubmissionStorage, - correctionStorage: dummyCorrectionStorage, + formStorage, + submissionStorage, + correctionStorage, }), ) app.post( @@ -136,6 +118,7 @@ describe('Correction API', () => { }) afterAll(async () => { httpServer.close() + await Promise.all(cleanups) }) describe('POST /correction', () => { @@ -182,6 +165,49 @@ describe('Correction API', () => { `^http://127.0.0.1:${port}/correction/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$`, ), )) + + it('should store another correction', async () => + r + .post('/correction') + .set('Content-type', 'application/json; charset=utf-8') + .set('Cookie', [`${authCookieName}=${authCookie}`]) + .set('If-Match', '2') + .send({ + form: new URL(`./form/${formId}`, endpoint), + submission: new URL(`./assessment/${submissionId}`, endpoint), + response: { + section1: { + question1: 'Corrected answer, again', + }, + }, + }) + .expect(HTTPStatusCode.Created) + .expect( + 'Location', + new RegExp( + `^http://127.0.0.1:${port}/correction/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$`, + ), + )) + + it.each([['1', '3', 'a']])( + 'should not store a correction on etag mismatch (%s)', + async (etag) => + r + .post('/correction') + .set('Content-type', 'application/json; charset=utf-8') + .set('Cookie', [`${authCookieName}=${authCookie}`]) + .set('If-Match', etag) + .send({ + form: new URL(`./form/${formId}`, endpoint), + submission: new URL(`./assessment/${submissionId}`, endpoint), + response: { + section1: { + question1: 'Corrected answer, again', + }, + }, + }) + .expect(HTTPStatusCode.Conflict), + ) }) }) }) diff --git a/src/server/routes/form/form.spec.ts b/src/server/routes/form/form.spec.ts index c06598d0..f044e1b3 100644 --- a/src/server/routes/form/form.spec.ts +++ b/src/server/routes/form/form.spec.ts @@ -5,25 +5,14 @@ import request, { SuperTest, Test } from 'supertest' import { URL } from 'url' import { Form } from '../../../form/form.js' import { formSchema } from '../../../schema/form.js' -import { Store } from '../../../storage/store.js' import { portForTest } from '../../../test/portForTest.js' +import { tempJsonFileStore } from '../../../test/tempJsonFileStore.js' import { HTTPStatusCode } from '../../response/HttpStatusCode.js' import { formCreationHandler } from './create.js' import { formGetHandler } from './get.js' const port = portForTest(__filename) -const forms: Record = {} - -const dummyStorage: Store = { - persist: async (id, form) => { - forms[id] = form - }, - get: async (id) => - forms[id] !== undefined ? { id, data: forms[id] } : undefined, - findAll: async () => [], -} - const endpoint = new URL(`http://127.0.0.1:${port}`) const schema = formSchema({ @@ -34,16 +23,21 @@ describe('Form API', () => { let app: Express let httpServer: Server let r: SuperTest + const cleanups: (() => Promise)[] = [] beforeAll(async () => { + const { cleanup: cleanupFormStorage, store: formStorage } = + await tempJsonFileStore() + cleanups.push(cleanupFormStorage) + app = express() app.use(bodyParser.json({ strict: true })) - app.get('/form/:id', formGetHandler({ storage: dummyStorage })) + app.get('/form/:id', formGetHandler({ storage: formStorage })) app.post( '/form', formCreationHandler({ endpoint, - storage: dummyStorage, + storage: formStorage, schema, }), ) @@ -55,6 +49,7 @@ describe('Form API', () => { }) afterAll(async () => { httpServer.close() + await Promise.all(cleanups) }) const simpleForm = { diff --git a/src/storage/file.spec.ts b/src/storage/file.spec.ts index c3f32cd0..3583fb12 100644 --- a/src/storage/file.spec.ts +++ b/src/storage/file.spec.ts @@ -1,21 +1,19 @@ -import { promises as fs } from 'fs' -import * as os from 'os' -import * as path from 'path' +import { tempJsonFileStore } from '../test/tempJsonFileStore.js' import { ulid } from '../ulid.js' -import { jsonFileStore } from './file.js' import { Store } from './store.js' describe('File store', () => { let store: Store - let tmpDir: string let id: string + let cleanup: () => Promise beforeAll(async () => { - tmpDir = await fs.mkdtemp(`${os.tmpdir()}${path.sep}`) - store = jsonFileStore({ directory: tmpDir }) + const { store: s, cleanup: c } = await tempJsonFileStore() + cleanup = c + store = s id = ulid() }) afterAll(async () => { - await fs.rm(tmpDir, { recursive: true }) + await cleanup() }) describe('persist()', () => { diff --git a/src/test/tempJsonFileStore.ts b/src/test/tempJsonFileStore.ts new file mode 100644 index 00000000..6953d9c3 --- /dev/null +++ b/src/test/tempJsonFileStore.ts @@ -0,0 +1,21 @@ +import { promises as fs } from 'fs' +import * as os from 'os' +import * as path from 'path' +import { jsonFileStore } from '../storage/file.js' +import { Store } from '../storage/store.js' + +export const tempJsonFileStore = async < + T extends Record, +>(): Promise<{ + cleanup: () => Promise + store: Store +}> => { + const tmpDir = await fs.mkdtemp(`${os.tmpdir()}${path.sep}`) + const store = jsonFileStore({ directory: tmpDir }) + return { + store, + cleanup: async () => { + await fs.rm(tmpDir, { recursive: true }) + }, + } +}