Replace entityBaseReviver with io-ts decoder
This commit is contained in:
Родитель
e03038b720
Коммит
45baa755d1
|
@ -37,8 +37,8 @@ function main(argv: string[]) {
|
|||
program.version(apiVersion);
|
||||
|
||||
program
|
||||
.command('connect [server]')
|
||||
.description('Connect to a Laboratory service or print connection info.')
|
||||
.command('connect [service]')
|
||||
.description('Connect to a Laboratory [service] or print connection info.')
|
||||
.action(connect);
|
||||
|
||||
program
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
* Server Error: listen EADDRINUSE: address already in use :::3000
|
||||
* Enforce workflow for status changes (e.g. disallow complete to created)
|
||||
* Authentication
|
||||
* x Replace entityBaseReviver with io-ts decoder.
|
||||
* x REVIEW use of TypeError instead of IllegalOperationError or EntityNotFoundError
|
||||
* x REVIEW: net.Server vs http.Server
|
||||
* Are README.md dependencies correct? Does the user have to install anything else?
|
||||
|
@ -59,7 +60,7 @@
|
|||
* deploy command
|
||||
* Bash completion api
|
||||
* = usage configuration in yargs
|
||||
* = Consider using luxon in reviver - probably can't since io-ts uses Date
|
||||
* x Consider using luxon in reviver - probably can't since io-ts uses Date
|
||||
* x Set version
|
||||
* x examples in usage()
|
||||
* x Spike commander as replacement for yargs
|
||||
|
@ -183,10 +184,11 @@
|
|||
* check sqlite error behavior on string value (varchar(256)) too long.
|
||||
* dateColumn decorator does not seem the apply to createdAt and updatedAt
|
||||
* Investigate schema verification in jsonColumn decorator.
|
||||
* Consider removing luxon
|
||||
* x Consider removing luxon - may be using for custom io-ts codec.
|
||||
* Duplicate code
|
||||
* toPOJO()
|
||||
* assertDeepEqual()
|
||||
* Accidentally PUT benchmark to candidates seems error-prone. Also PUT suite/candidate to candidate/suite.
|
||||
* Investigate BSON. https://www.npmjs.com/package/bson
|
||||
*
|
||||
|
||||
|
|
|
@ -28,32 +28,10 @@ import {
|
|||
validate,
|
||||
} from '../logic';
|
||||
|
||||
import { entityBaseReviver } from '../server';
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
function jsonParser(data: any): any {
|
||||
try {
|
||||
return JSON.parse(data, entityBaseReviver);
|
||||
} catch (e) {
|
||||
// Not all payloads are JSON. Some POSTs and PUTS return "OK"
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
// TODO: put credentials here.
|
||||
};
|
||||
|
||||
const configForGet: AxiosRequestConfig = {
|
||||
...config,
|
||||
transformResponse: [jsonParser],
|
||||
};
|
||||
|
||||
const configForPatchPostPut: AxiosRequestConfig = {
|
||||
...config,
|
||||
transformResponse: [jsonParser],
|
||||
};
|
||||
|
||||
export class LaboratoryClient implements ILaboratory {
|
||||
endpoint: string;
|
||||
|
||||
|
@ -61,8 +39,6 @@ export class LaboratoryClient implements ILaboratory {
|
|||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
// TODO: error handling pattern/strategy
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Benchmarks
|
||||
|
@ -70,7 +46,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
/////////////////////////////////////////////////////////////////////////////
|
||||
async allBenchmarks(): Promise<IBenchmark[]> {
|
||||
const url = new URL('benchmarks', this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const benchmarks = validate(BenchmarkArrayType, response.data);
|
||||
return benchmarks;
|
||||
}
|
||||
|
@ -78,7 +54,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
async oneBenchmark(rawName: string): Promise<IBenchmark> {
|
||||
const name = normalizeName(rawName);
|
||||
const url = new URL(`benchmarks/${name}`, this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const benchmark = validate(BenchmarkType, response.data);
|
||||
return benchmark;
|
||||
}
|
||||
|
@ -93,7 +69,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
throw new IllegalOperationError(message);
|
||||
}
|
||||
const url = new URL(`benchmarks/${name}`, this.endpoint);
|
||||
await axios.put(url.toString(), benchmark, configForPatchPostPut);
|
||||
await axios.put(url.toString(), benchmark, config);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -103,7 +79,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
/////////////////////////////////////////////////////////////////////////////
|
||||
async allCandidates(): Promise<ICandidate[]> {
|
||||
const url = new URL('candidates', this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const candidates = validate(CandidateArrayType, response.data);
|
||||
return candidates;
|
||||
}
|
||||
|
@ -111,7 +87,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
async oneCandidate(rawName: string): Promise<ICandidate> {
|
||||
const name = normalizeName(rawName);
|
||||
const url = new URL(`candidates/${name}`, this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const candidate = validate(CandidateType, response.data);
|
||||
return candidate;
|
||||
}
|
||||
|
@ -126,7 +102,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
throw new IllegalOperationError(message);
|
||||
}
|
||||
const url = new URL(`candidates/${name}`, this.endpoint);
|
||||
await axios.put(url.toString(), candidate, configForPatchPostPut);
|
||||
await axios.put(url.toString(), candidate, config);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -136,7 +112,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
/////////////////////////////////////////////////////////////////////////////
|
||||
async allSuites(): Promise<ISuite[]> {
|
||||
const url = new URL('suites', this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const suites = validate(SuiteArrayType, response.data);
|
||||
return suites;
|
||||
}
|
||||
|
@ -144,7 +120,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
async oneSuite(rawName: string): Promise<ISuite> {
|
||||
const name = normalizeName(rawName);
|
||||
const url = new URL(`suites/${name}`, this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const suite = validate(SuiteType, response.data);
|
||||
return suite;
|
||||
}
|
||||
|
@ -156,7 +132,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
throw new IllegalOperationError(message);
|
||||
}
|
||||
const url = new URL(`suites/${name}`, this.endpoint);
|
||||
await axios.put(url.toString(), suite, configForPatchPostPut);
|
||||
await axios.put(url.toString(), suite, config);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -166,7 +142,7 @@ export class LaboratoryClient implements ILaboratory {
|
|||
/////////////////////////////////////////////////////////////////////////////
|
||||
async allRuns(): Promise<IRun[]> {
|
||||
const url = new URL('runs', this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const runs = validate(RunArrayType, response.data);
|
||||
return runs;
|
||||
}
|
||||
|
@ -174,18 +150,14 @@ export class LaboratoryClient implements ILaboratory {
|
|||
async oneRun(rawName: string): Promise<IRun> {
|
||||
const name = normalizeRunName(rawName);
|
||||
const url = new URL(`runs/${name}`, this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const run = validate(RunType, response.data);
|
||||
return run;
|
||||
}
|
||||
|
||||
async createRunRequest(spec: IRunRequest): Promise<IRun> {
|
||||
const url = new URL('runs', this.endpoint);
|
||||
const response = await axios.post(
|
||||
url.toString(),
|
||||
spec,
|
||||
configForPatchPostPut
|
||||
);
|
||||
const response = await axios.post(url.toString(), spec, config);
|
||||
const run = validate(RunType, response.data);
|
||||
return run;
|
||||
}
|
||||
|
@ -194,21 +166,21 @@ export class LaboratoryClient implements ILaboratory {
|
|||
const name = normalizeRunName(rawName);
|
||||
const url = new URL(`runs/${name}`, this.endpoint);
|
||||
const body: IUpdateRunStatus = { status };
|
||||
await axios.patch(url.toString(), body, configForPatchPostPut);
|
||||
await axios.patch(url.toString(), body, config);
|
||||
}
|
||||
|
||||
async reportRunResults(rawName: string, measures: Measures): Promise<void> {
|
||||
const name = normalizeRunName(rawName);
|
||||
const url = new URL(`runs/${name}/results`, this.endpoint);
|
||||
const body: IReportRunResults = { measures };
|
||||
await axios.patch(url.toString(), body, configForPatchPostPut);
|
||||
await axios.patch(url.toString(), body, config);
|
||||
}
|
||||
|
||||
async allRunResults(benchmark: string, suite: string): Promise<IResult[]> {
|
||||
const b = normalizeName(benchmark);
|
||||
const s = normalizeName(suite);
|
||||
const url = new URL(`runs/${b}/${s}`, this.endpoint);
|
||||
const response = await axios.get(url.toString(), configForGet);
|
||||
const response = await axios.get(url.toString(), config);
|
||||
const results = validate(ResultArrayType, response.data);
|
||||
return results;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { either } from 'fp-ts/lib/Either';
|
||||
import * as t from 'io-ts';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export const apiVersion = '0.0.1';
|
||||
|
||||
// tslint:disable-next-line:variable-name
|
||||
const DateType = new t.Type<Date, Date, unknown>(
|
||||
'Date2',
|
||||
(input: unknown): input is Date => input instanceof Date,
|
||||
// `t.success` and `t.failure` are helpers used to build `Either` instances
|
||||
(input, context) =>
|
||||
input instanceof Date ? t.success(input) : t.failure(input, context),
|
||||
// `A` and `O` are the same, so `encode` is just the identity function
|
||||
t.identity
|
||||
const DateType = new t.Type<Date, string, unknown>(
|
||||
'Date',
|
||||
(u): u is Date => u instanceof Date,
|
||||
(u, c) =>
|
||||
either.chain(t.string.validate(u, c), s => {
|
||||
const d = DateTime.fromISO(s);
|
||||
return d.isValid ? t.success(d.toJSDate()) : t.failure(u, c);
|
||||
}),
|
||||
a => a.toISOString()
|
||||
);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -27,8 +27,8 @@ export async function createApp(lab: ILaboratory): Promise<express.Express> {
|
|||
app.use(
|
||||
// tslint:disable-next-line: deprecation
|
||||
bodyParser.json({
|
||||
// TODO: REVIEW: magic number 100kb
|
||||
limit: '100kb',
|
||||
reviver: entityBaseReviver,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -61,12 +61,3 @@ export async function createApp(lab: ILaboratory): Promise<express.Express> {
|
|||
|
||||
return app;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
export function entityBaseReviver(key: string, value: any) {
|
||||
if (key === 'updatedAt' || key === 'createdAt') {
|
||||
return new Date(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,14 @@ import { LaboratoryClient } from '../../../../src/laboratory/client';
|
|||
import { benchmark1, candidate1, run1, suite1, runRequest1 } from '../data';
|
||||
|
||||
import {
|
||||
RunStatus,
|
||||
IUpdateRunStatus,
|
||||
BenchmarkType,
|
||||
CandidateType,
|
||||
IReportRunResults,
|
||||
IUpdateRunStatus,
|
||||
Measures,
|
||||
RunStatus,
|
||||
SuiteType,
|
||||
validate,
|
||||
} from '../../../../src';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
@ -54,7 +58,8 @@ describe('laboratory/client', () => {
|
|||
|
||||
const client = new LaboratoryClient(endpoint);
|
||||
await client.upsertBenchmark(benchmark1);
|
||||
assert.deepEqual(request!, benchmark1);
|
||||
const observed = validate(BenchmarkType, request!);
|
||||
assert.deepEqual(observed, benchmark1);
|
||||
});
|
||||
|
||||
it('upsertBenchmark() - name mismatch', async () => {
|
||||
|
@ -107,7 +112,8 @@ describe('laboratory/client', () => {
|
|||
|
||||
const client = new LaboratoryClient(endpoint);
|
||||
await client.upsertCandidate(candidate1);
|
||||
assert.deepEqual(request!, candidate1);
|
||||
const observed = validate(CandidateType, request!);
|
||||
assert.deepEqual(observed, candidate1);
|
||||
});
|
||||
|
||||
it('upsertCandidate() - name mismatch', async () => {
|
||||
|
@ -160,7 +166,8 @@ describe('laboratory/client', () => {
|
|||
|
||||
const client = new LaboratoryClient(endpoint);
|
||||
await client.upsertSuite(suite1);
|
||||
assert.deepEqual(request!, suite1);
|
||||
const observed = validate(SuiteType, request!);
|
||||
assert.deepEqual(observed, suite1);
|
||||
});
|
||||
|
||||
it('upsertSuite() - name mismatch', async () => {
|
||||
|
|
|
@ -19,6 +19,11 @@ import {
|
|||
export const serviceURL = 'http://localhost:3000'; // TODO: plumb real url.
|
||||
export const blobBase = 'http://blobs';
|
||||
|
||||
export const timestamps = {
|
||||
createdAt: new Date('2020-03-19T21:37:31.452Z'),
|
||||
updatedAt: new Date('2020-03-20T22:37:31.452Z'),
|
||||
};
|
||||
|
||||
export const pipelines: IPipeline[] = [
|
||||
{
|
||||
mode: 'mode1',
|
||||
|
@ -39,6 +44,7 @@ export const benchmark1: IBenchmark = {
|
|||
author: 'author1',
|
||||
version: apiVersion,
|
||||
pipelines,
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const benchmark2: IBenchmark = {
|
||||
|
@ -46,12 +52,14 @@ export const benchmark2: IBenchmark = {
|
|||
author: 'author2',
|
||||
version: apiVersion,
|
||||
pipelines,
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const benchmark3: IBenchmark = {
|
||||
name: 'benchmark3',
|
||||
author: 'author3',
|
||||
version: apiVersion,
|
||||
...timestamps,
|
||||
pipelines,
|
||||
};
|
||||
|
||||
|
@ -62,6 +70,7 @@ export const candidate1: ICandidate = {
|
|||
benchmark: 'benchmark1',
|
||||
mode: 'mode1',
|
||||
image: 'candidate1-image',
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const candidate2: ICandidate = {
|
||||
|
@ -71,6 +80,7 @@ export const candidate2: ICandidate = {
|
|||
benchmark: 'benchmark1',
|
||||
mode: 'mode1',
|
||||
image: 'candidate2-image',
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const candidate3: ICandidate = {
|
||||
|
@ -80,6 +90,7 @@ export const candidate3: ICandidate = {
|
|||
benchmark: 'benchmark1',
|
||||
mode: 'mode1',
|
||||
image: 'candidate3-image',
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const suite1: ISuite = {
|
||||
|
@ -88,6 +99,7 @@ export const suite1: ISuite = {
|
|||
version: apiVersion,
|
||||
benchmark: 'benchmark1',
|
||||
mode: 'mode1',
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const suite2: ISuite = {
|
||||
|
@ -96,6 +108,7 @@ export const suite2: ISuite = {
|
|||
version: apiVersion,
|
||||
benchmark: 'benchmark1',
|
||||
mode: 'mode1',
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const suite3: ISuite = {
|
||||
|
@ -104,6 +117,7 @@ export const suite3: ISuite = {
|
|||
version: apiVersion,
|
||||
benchmark: 'benchmark1',
|
||||
mode: 'mode1',
|
||||
...timestamps,
|
||||
};
|
||||
|
||||
export const runRequest1: IRunRequest = {
|
||||
|
@ -121,4 +135,5 @@ export const run1: IRun = {
|
|||
suite: suite1,
|
||||
blob: new URL(runid, blobBase).toString(),
|
||||
status: RunStatus.CREATED,
|
||||
...timestamps,
|
||||
};
|
||||
|
|
|
@ -74,8 +74,6 @@ describe('laboratory/benchmarks', () => {
|
|||
});
|
||||
|
||||
it('upsertBenchmark()', async () => {
|
||||
console.log('benchmark');
|
||||
|
||||
await lab.upsertBenchmark(benchmark1);
|
||||
const results1 = await lab.allBenchmarks();
|
||||
assertDeepEqual(results1, [benchmark1]);
|
||||
|
|
|
@ -23,6 +23,7 @@ import { createApp } from '../../../../src/laboratory/server';
|
|||
interface XMLHttpRequest {}
|
||||
|
||||
import {
|
||||
BenchmarkType,
|
||||
IBenchmark,
|
||||
ICandidate,
|
||||
IReportRunResults,
|
||||
|
@ -33,6 +34,10 @@ import {
|
|||
IUpdateRunStatus,
|
||||
Measures,
|
||||
RunStatus,
|
||||
validate,
|
||||
CandidateType,
|
||||
SuiteType,
|
||||
RunType,
|
||||
} from '../../../../src';
|
||||
|
||||
import { benchmark1, candidate1, run1, suite1 } from '../data';
|
||||
|
@ -69,20 +74,20 @@ describe('laboratory/server', () => {
|
|||
it('oneBenchmark()', async () => {
|
||||
const lab = new MockLaboratory();
|
||||
|
||||
const expected = 'benchmark1';
|
||||
let observed: string | undefined;
|
||||
let observedName: string | undefined;
|
||||
lab.oneBenchmark = async (name: string): Promise<IBenchmark> => {
|
||||
observed = name;
|
||||
observedName = name;
|
||||
return benchmark1;
|
||||
};
|
||||
|
||||
chai
|
||||
.request(await createApp(lab))
|
||||
.get(`/benchmarks/${expected}`)
|
||||
.get(`/benchmarks/${benchmark1.name}`)
|
||||
.end((err, res) => {
|
||||
assert.equal(res.status, 200);
|
||||
assertDeepEqual(res.body, benchmark1);
|
||||
assert.equal(observed, expected);
|
||||
const observed = validate(BenchmarkType, res.body);
|
||||
assert.deepEqual(observed, benchmark1);
|
||||
assert.equal(observedName, benchmark1.name);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -138,20 +143,20 @@ describe('laboratory/server', () => {
|
|||
it('oneCandidate()', async () => {
|
||||
const lab = new MockLaboratory();
|
||||
|
||||
const expected = 'candidate1';
|
||||
let observed: string | undefined;
|
||||
let observedName: string | undefined;
|
||||
lab.oneCandidate = async (name: string): Promise<ICandidate> => {
|
||||
observed = name;
|
||||
observedName = name;
|
||||
return candidate1;
|
||||
};
|
||||
|
||||
chai
|
||||
.request(await createApp(lab))
|
||||
.get(`/candidates/${expected}`)
|
||||
.get(`/candidates/${candidate1.name}`)
|
||||
.end((err, res) => {
|
||||
assert.equal(res.status, 200);
|
||||
assertDeepEqual(res.body, candidate1);
|
||||
assert.equal(observed, expected);
|
||||
const observed = validate(CandidateType, res.body);
|
||||
assert.deepEqual(observed, candidate1);
|
||||
assert.equal(observedName, candidate1.name);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -207,20 +212,20 @@ describe('laboratory/server', () => {
|
|||
it('oneSuite()', async () => {
|
||||
const lab = new MockLaboratory();
|
||||
|
||||
const expected = 'suite1';
|
||||
let observed: string | undefined;
|
||||
let observedName: string | undefined;
|
||||
lab.oneSuite = async (name: string): Promise<ISuite> => {
|
||||
observed = name;
|
||||
observedName = name;
|
||||
return suite1;
|
||||
};
|
||||
|
||||
chai
|
||||
.request(await createApp(lab))
|
||||
.get(`/suites/${expected}`)
|
||||
.get(`/suites/${suite1.name}`)
|
||||
.end((err, res) => {
|
||||
assert.equal(res.status, 200);
|
||||
assertDeepEqual(res.body, suite1);
|
||||
assert.equal(observed, expected);
|
||||
const observed = validate(SuiteType, res.body);
|
||||
assert.deepEqual(observed, suite1);
|
||||
assert.equal(observedName, suite1.name);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -273,20 +278,20 @@ describe('laboratory/server', () => {
|
|||
it('oneRun()', async () => {
|
||||
const lab = new MockLaboratory();
|
||||
|
||||
const expected = 'run1';
|
||||
let observed: string | undefined;
|
||||
let observedName: string | undefined;
|
||||
lab.oneRun = async (name: string): Promise<IRun> => {
|
||||
observed = name;
|
||||
observedName = name;
|
||||
return run1;
|
||||
};
|
||||
|
||||
chai
|
||||
.request(await createApp(lab))
|
||||
.get(`/runs/${expected}`)
|
||||
.get(`/runs/${run1.name}`)
|
||||
.end((err, res) => {
|
||||
assert.equal(res.status, 200);
|
||||
assertDeepEqual(res.body, run1);
|
||||
assert.equal(observed, expected);
|
||||
const observed = validate(RunType, res.body);
|
||||
assert.deepEqual(observed, run1);
|
||||
assert.equal(observedName, run1.name);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -298,10 +303,10 @@ describe('laboratory/server', () => {
|
|||
suite: run1.suite.name,
|
||||
};
|
||||
|
||||
let observed: IRun;
|
||||
let observedRequest: IRunRequest;
|
||||
lab.createRunRequest = async (spec: IRunRequest): Promise<IRun> => {
|
||||
observed = run1;
|
||||
return observed;
|
||||
observedRequest = spec;
|
||||
return run1;
|
||||
};
|
||||
|
||||
chai
|
||||
|
@ -310,8 +315,9 @@ describe('laboratory/server', () => {
|
|||
.send(runRequest)
|
||||
.end((err, res) => {
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(observedRequest, runRequest);
|
||||
const observed = validate(RunType, res.body);
|
||||
assert.deepEqual(observed, run1);
|
||||
assertDeepEqual(res.body, run1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче