From d7fa36f513656f30171658ddc0c87b6fa24f5b89 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Fri, 10 Apr 2020 16:37:59 +0100 Subject: [PATCH] test coverage for user creation --- packages/common/src/records/validateRecord.js | 86 +++++++++++++++++++ packages/common/test/validateRecord.spec.js | 2 +- .../server/middleware/controllers/user.js | 14 ++- .../routes/neo/tests/couchTestUtils.js | 5 ++ .../middleware/routes/neo/tests/user.spec.js | 49 +++++++++-- 5 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 packages/common/src/records/validateRecord.js diff --git a/packages/common/src/records/validateRecord.js b/packages/common/src/records/validateRecord.js new file mode 100644 index 0000000000..84890c32e0 --- /dev/null +++ b/packages/common/src/records/validateRecord.js @@ -0,0 +1,86 @@ +const { map, reduce, filter, isEmpty, flatten, each, union } = require("lodash/fp"); +const { compileCode } = require("../common/compileCode"); +const { validateFieldParse, validateTypeConstraints } = require("../schema/types"); +import { + validateFieldParse, + validateTypeConstraints, +} from "../schema/types/index.js" +import { $, isNonEmptyString } from "../common/index.mjs" + +const fieldParseError = (fieldName, value) => ({ + fields: [fieldName], + message: `Could not parse field ${fieldName}:${value}`, +}) + +const validateAllFieldParse = (record, model) => + $(model.fields, [ + map(f => ({ name: f.name, parseResult: validateFieldParse(f, record) })), + reduce((errors, f) => { + if (f.parseResult.success) return errors + errors.push(fieldParseError(f.name, f.parseResult.value)) + return errors + }, []), + ]) + +const validateAllTypeConstraints = (record, model) => { + const errors = [] + for (const field of model.fields) { + $(validateTypeConstraints(field, record), [ + filter(isNonEmptyString), + map(m => ({ message: m, fields: [field.name] })), + each(e => errors.push(e)), + ]) + } + return errors +} + +const runRecordValidationRules = (record, model) => { + const runValidationRule = rule => { + const isValid = compileCode(rule.expressionWhenValid) + const expressionContext = { record } + return isValid(expressionContext) + ? { valid: true } + : { + valid: false, + fields: rule.invalidFields, + message: rule.messageWhenInvalid, + } + } + + return $(model.validationRules, [ + map(runValidationRule), + flatten, + filter(r => r.valid === false), + map(r => ({ fields: r.fields, message: r.message })), + ]) +} + +export const validateRecord = (schema, record) => { + const model = schema.findModel(record._modelId) + const fieldParseFails = validateAllFieldParse(record, model) + + // non parsing would cause further issues - exit here + if (!isEmpty(fieldParseFails)) { + return { isValid: false, errors: fieldParseFails } + } + + const recordValidationRuleFails = runRecordValidationRules(record, model) + const typeContraintFails = validateAllTypeConstraints(record, model) + + if ( + isEmpty(fieldParseFails) && + isEmpty(recordValidationRuleFails) && + isEmpty(typeContraintFails) + ) { + return { isValid: true, errors: [] } + } + + return { + isValid: false, + errors: union( + fieldParseFails, + typeContraintFails, + recordValidationRuleFails + ), + } +} diff --git a/packages/common/test/validateRecord.spec.js b/packages/common/test/validateRecord.spec.js index 19fb664c5c..f74f1a464e 100644 --- a/packages/common/test/validateRecord.spec.js +++ b/packages/common/test/validateRecord.spec.js @@ -1,5 +1,5 @@ import { testSchema } from "./testSchema.mjs" -import { validateRecord } from "../src/records/validateRecord.mjs" +import { validateRecord } from "../src/records/validateRecord.js" import { getNewRecord } from "../src/records/getNewRecord.mjs" describe("validateRecord", () => { diff --git a/packages/server/middleware/controllers/user.js b/packages/server/middleware/controllers/user.js index 32680118e2..0aa52d15bd 100644 --- a/packages/server/middleware/controllers/user.js +++ b/packages/server/middleware/controllers/user.js @@ -12,10 +12,20 @@ exports.fetch = async function(ctx) { exports.create = async function(ctx) { const database = couchdb.db.use(ctx.params.databaseId); - ctx.body = await database.insert(ctx.request.body); + const response = await database.insert(ctx.request.body); + ctx.body = { + ...response, + message: `User created successfully.`, + status: 200 + } }; exports.destroy = async function(ctx) { const database = couchdb.db.use(ctx.params.databaseId); - ctx.body = await database.destroy(ctx.params.userId) + const response = await database.destroy(ctx.params.userId) + ctx.body = { + ...response, + message: `User deleted.`, + status: 200 + } }; \ No newline at end of file diff --git a/packages/server/middleware/routes/neo/tests/couchTestUtils.js b/packages/server/middleware/routes/neo/tests/couchTestUtils.js index be4ddcff5f..3cdd9f0316 100644 --- a/packages/server/middleware/routes/neo/tests/couchTestUtils.js +++ b/packages/server/middleware/routes/neo/tests/couchTestUtils.js @@ -30,4 +30,9 @@ exports.createInstanceDatabase = async instanceId => { } } }, '_design/database'); +} + +exports.insertDocument = async (databaseId, document) => { + const { id, ...documentFields } = document; + await couchdb.db.use(databaseId).insert(documentFields, id); } \ No newline at end of file diff --git a/packages/server/middleware/routes/neo/tests/user.spec.js b/packages/server/middleware/routes/neo/tests/user.spec.js index b7c78cf4bc..431bcfbe1a 100644 --- a/packages/server/middleware/routes/neo/tests/user.spec.js +++ b/packages/server/middleware/routes/neo/tests/user.spec.js @@ -1,29 +1,68 @@ const couchdb = require("../../../../db"); const supertest = require("supertest"); const app = require("../../../../app"); -const { createInstanceDatabase } = require("./couchTestUtils"); +const { createInstanceDatabase, insertDocument, destroyDatabase } = require("./couchTestUtils"); const TEST_INSTANCE_ID = "testing-123"; +const TEST_USER = { + name: "Dave" +} describe("/users", () => { let request; + let server; beforeAll(async () => { - const server = await app({ + server = await app({ config: { port: 3000 } }); request = supertest(server); - createInstanceDatabase(TEST_INSTANCE_ID); }); afterAll(async () => { - app.close(); + server.close(); }) + describe("fetch", () => { + beforeEach(async () => { + await createInstanceDatabase(TEST_INSTANCE_ID); + await insertDocument(TEST_INSTANCE_ID, { + id: "cool-user-id", + type: "user", + name: "Dave" + }) + }); + + afterEach(async () => { + await destroyDatabase(TEST_INSTANCE_ID); + }); + + it("returns a list of users from an instance db", done => { + request + .get(`/api/${TEST_INSTANCE_ID}/users`) + .set("Accept", "application/json") + .expect('Content-Type', /json/) + .expect(200) + .end(async (err, res) => { + const createdUser = res.body[0].doc; + expect(createdUser.name).toEqual(TEST_USER.name); + done(); + }); + }) + }); + describe("create", () => { + beforeEach(async () => { + await createInstanceDatabase(TEST_INSTANCE_ID); + }); + + afterEach(async () => { + await destroyDatabase(TEST_INSTANCE_ID); + }); + it("returns a success message when a user is successfully created", done => { request .post(`/api/${TEST_INSTANCE_ID}/users`) @@ -32,7 +71,7 @@ describe("/users", () => { .expect('Content-Type', /json/) .expect(200) .end(async (err, res) => { - expect(res.body.message).toEqual("Instance Database testing-123 successfully provisioned."); + expect(res.body.message).toEqual("User created successfully."); done(); }); })