Repository: paularmstrong/build-tracker (ts)

Issue:
Add validation to insert build routing
## Problem

When inserting a build, the content is not validated to be well-formed.

## Solution

The server's API should validate that the build contains `meta` and `artifacts` in an appropriate format with all required pieces (`revision`, `timestamp`, `parentRevision`, etc)

Pull request description:
[server] add api insert validation
<!--
Thank you so much for contributing to open source and the Build Tracker project!
-->

# Problem

There's no validation on `insert` of a build using the API.

# Solution

Add remedial validation to the two most important fields, `revision` and `timestamp`
fixes #25 

# TODO

- [x] 🤓 Add & update tests (always try to _increase_ test coverage)
- [x] 🔬 Ensure CI is passing (`yarn lint:ci`, `yarn test`, `yarn tsc:ci`)
- [x] 📖 Update relevant documentation


Patch:
diff --git a/docs/docs/packages/api-errors.md b/docs/docs/packages/api-errors.md
index b74b192..bf8ec95 100644
--- a/docs/docs/packages/api-errors.md
+++ b/docs/docs/packages/api-errors.md
@@ -6,6 +6,10 @@ sidebar_label: '@build-tracker/api-errors'
 
 This is a shared package for creating and comparing API errors returned from the Build Tracker server's API.
 
+## 400 `ValidationError`
+
+The build that you are trying to insert into the database does not meet requirements. See the specific error message for more information.
+
 ## 401 `AuthError`
 
 If your server's API is protected with an API key and you do not provide it with requests requiring authentication, a 401 unauthorized response will be returned.
diff --git a/src/api-errors/src/index.ts b/src/api-errors/src/index.ts
index dd05c4b..2e122af 100644
--- a/src/api-errors/src/index.ts
+++ b/src/api-errors/src/index.ts
@@ -28,3 +28,12 @@ export class UnimplementedError extends Error {
     Object.setPrototypeOf(this, UnimplementedError.prototype);
   }
 }
+
+export class ValidationError extends Error {
+  public readonly status = 400;
+
+  public constructor(field: string, expected: string, received: string | number | void) {
+    super(`"${field}" expected to receive "${expected}", but value was "${received}"`);
+    Object.setPrototypeOf(this, ValidationError.prototype);
+  }
+}
diff --git a/src/server/src/api/insert.ts b/src/server/src/api/insert.ts
index feeb2bd..b104c85 100644
--- a/src/server/src/api/insert.ts
+++ b/src/server/src/api/insert.ts
@@ -3,9 +3,9 @@
  */
 import Build from '@build-tracker/build';
 import Comparator from '@build-tracker/comparator';
-import { NotFoundError } from '@build-tracker/api-errors';
 import { Queries } from '../types';
 import { ServerConfig } from '../server';
+import { NotFoundError, ValidationError } from '@build-tracker/api-errors';
 import { Request, RequestHandler, Response } from 'express';
 
 export const insertBuild = (
@@ -15,7 +15,25 @@ export const insertBuild = (
 ): RequestHandler => (req: Request, res: Response): void => {
   const { artifacts, meta } = req.body;
   const { artifacts: artifactConfig = {}, budgets } = config;
+
   const build = new Build(meta, artifacts);
+
+  let validationErrors = [];
+  const revision = build.getMetaValue('revision');
+  if (!revision) {
+    validationErrors.push(new ValidationError('revision', 'string', revision));
+  }
+
+  if (isNaN(build.timestamp.valueOf())) {
+    validationErrors.push(new ValidationError('timestamp', 'timestamp', build.meta.timestamp));
+  }
+
+  if (validationErrors.length) {
+    res.status(400);
+    res.send({ errors: validationErrors.map(v => v.message) });
+    return;
+  }
+
   queries
     .insert(build)
     .then(() =>
@@ -54,7 +72,7 @@ export const insertBuild = (
         })
     )
     .catch(error => {
-      res.status(500);
+      res.status(error.status || 500);
       res.send({ error: error.message });
     });
 };


Test patch:
diff --git a/src/server/src/api/__tests__/insert.test.ts b/src/server/src/api/__tests__/insert.test.ts
index c38a6f7..350ebce 100644
--- a/src/server/src/api/__tests__/insert.test.ts
+++ b/src/server/src/api/__tests__/insert.test.ts
@@ -84,6 +84,24 @@ describe('insert build handler', () => {
           expect(res.body.summary).toEqual(comparator.toSummary());
         });
     });
+
+    test('returns an array of validation errors', () => {
+      const handler = insertBuild(queries, config);
+      app.post('/test', handler);
+
+      return request(app)
+        .post('/test')
+        .send({ meta: { timestamp: 'foobar' }, artifacts: [] })
+        .set('Content-Type', 'application/json')
+        .set('Accept', 'application/json')
+        .then(res => {
+          expect(res.status).toEqual(400);
+          expect(res.body.errors).toEqual([
+            '"revision" expected to receive "string", but value was "undefined"',
+            '"timestamp" expected to receive "timestamp", but value was "foobar"'
+          ]);
+        });
+    });
   });
 
   describe('parent revision', () => {