From 31ec3ecceea5e1bf1d05d9bdf67ddcf814a58c15 Mon Sep 17 00:00:00 2001 From: Alexey Yarmosh Date: Wed, 4 Oct 2023 18:37:59 +0200 Subject: [PATCH] test: add tests --- config/init.sql | 2 +- src/adoption-code/route/adoption-code.ts | 4 +- src/adoption-code/schema.ts | 2 +- src/lib/adopted-probes.ts | 4 +- src/lib/get-probe-ip.ts | 5 +- src/lib/http/middleware/only-admin.ts | 2 +- src/probe/route/get-probes.ts | 2 +- .../adoption-code/adoption-code.test.ts | 73 ++++++++++++ test/tests/unit/adopted-probes.test.ts | 108 ++++++++++++++++++ test/tests/unit/middleware/only-admin.test.ts | 26 +++++ wallaby.js | 2 + 11 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 test/tests/integration/adoption-code/adoption-code.test.ts create mode 100644 test/tests/unit/adopted-probes.test.ts create mode 100644 test/tests/unit/middleware/only-admin.test.ts diff --git a/config/init.sql b/config/init.sql index 655382b0..dfdb840c 100644 --- a/config/init.sql +++ b/config/init.sql @@ -13,4 +13,4 @@ CREATE TABLE IF NOT EXISTS adopted_probes ( uuid VARCHAR(255) ); -INSERT IGNORE INTO adopted_probes (id, userId, ip) VALUES ('1', '6191378', '65.19.141.130'); +INSERT IGNORE INTO adopted_probes (id, userId, ip) VALUES ('1', '6191378', '79.205.97.254'); diff --git a/src/adoption-code/route/adoption-code.ts b/src/adoption-code/route/adoption-code.ts index 96aed1c6..ab4cdb6c 100644 --- a/src/adoption-code/route/adoption-code.ts +++ b/src/adoption-code/route/adoption-code.ts @@ -4,7 +4,7 @@ import type { AdoptionCodeRequest } from '../types.js'; import { bodyParser } from '../../lib/http/middleware/body-parser.js'; import { validate } from '../../lib/http/middleware/validate.js'; import { schema } from '../schema.js'; -import { onlyAdminMiddleware } from '../../lib/http/middleware/only-admin.js'; +import { onlyAdmin } from '../../lib/http/middleware/only-admin.js'; import { codeSender } from '../sender.js'; const handle = async (ctx: Context): Promise => { @@ -17,5 +17,5 @@ const handle = async (ctx: Context): Promise => { }; export const registerSendCodeRoute = (router: Router): void => { - router.post('/adoption-code', '/adoption-code', onlyAdminMiddleware(), bodyParser(), validate(schema), handle); + router.post('/adoption-code', '/adoption-code', onlyAdmin(), bodyParser(), validate(schema), handle); }; diff --git a/src/adoption-code/schema.ts b/src/adoption-code/schema.ts index 370e22bd..38cfadaa 100644 --- a/src/adoption-code/schema.ts +++ b/src/adoption-code/schema.ts @@ -2,5 +2,5 @@ import Joi from 'joi'; export const schema = Joi.object({ ip: Joi.string().ip().required(), - code: Joi.string().required().length(6), + code: Joi.string().length(6).required(), }); diff --git a/src/lib/adopted-probes.ts b/src/lib/adopted-probes.ts index 92a85eaa..ed6cea2f 100644 --- a/src/lib/adopted-probes.ts +++ b/src/lib/adopted-probes.ts @@ -26,7 +26,7 @@ export class AdoptedProbes { async syncProbeIds (probeIp: string, probeUuid: string) { const adoptedProbeByIp = this.adoptedProbesByIp.get(probeIp); - if (adoptedProbeByIp && adoptedProbeByIp.uuid === probeUuid) { // Probe is synced + if (adoptedProbeByIp && adoptedProbeByIp.uuid === probeUuid) { // Probe ids are synced return; } else if (adoptedProbeByIp) { // Uuid is wrong await this.updateUuid(probeIp, probeUuid); @@ -35,7 +35,7 @@ export class AdoptedProbes { const adoptedProbeByUuid = this.adoptedProbesByUuid.get(probeUuid); - if (adoptedProbeByUuid) { // Probe not found by ip but found by uuid, ip is wrong + if (adoptedProbeByUuid) { // Probe not found by ip but found by uuid => ip is wrong await this.updateIp(probeIp, probeUuid); } } diff --git a/src/lib/get-probe-ip.ts b/src/lib/get-probe-ip.ts index a9883d3a..257de059 100644 --- a/src/lib/get-probe-ip.ts +++ b/src/lib/get-probe-ip.ts @@ -3,6 +3,10 @@ import requestIp from 'request-ip'; import type { Socket } from 'socket.io'; const getProbeIp = (socket: Socket) => { + if (process.env['NODE_ENV'] === 'test') { + return '1.2.3.4'; + } + // Use random ip assigned by the API if (process.env['FAKE_PROBE_IP'] === 'api') { return _.sample([ @@ -16,7 +20,6 @@ const getProbeIp = (socket: Socket) => { '213.136.174.80', '94.214.253.78', '79.205.97.254', - '65.19.141.130', ]); } diff --git a/src/lib/http/middleware/only-admin.ts b/src/lib/http/middleware/only-admin.ts index 1a38bc9f..875480d6 100644 --- a/src/lib/http/middleware/only-admin.ts +++ b/src/lib/http/middleware/only-admin.ts @@ -1,7 +1,7 @@ import type { Middleware } from 'koa'; import createHttpError from 'http-errors'; -export const onlyAdminMiddleware = (): Middleware => { +export const onlyAdmin = (): Middleware => { return async (ctx, next) => { if (!ctx['isAdmin']) { throw createHttpError(403, 'Forbidden', { type: 'access_forbidden' }); diff --git a/src/probe/route/get-probes.ts b/src/probe/route/get-probes.ts index 1b6bd76e..26dddb6a 100644 --- a/src/probe/route/get-probes.ts +++ b/src/probe/route/get-probes.ts @@ -14,7 +14,7 @@ const handle = async (ctx: ParameterizedContext { + before(async () => { + nockGeoIpProviders(); + + probe = await addFakeProbe({ + 'probe:adoption:code': adoptionCodeStub, + }); + }); + + afterEach(async () => { + sinon.resetHistory(); + }); + + after(async () => { + nock.cleanAll(); + await deleteFakeProbe(probe); + }); + + it('should send code to the requested probe', async () => { + await requestAgent.post('/v1/adoption-code?adminkey=admin') + .send({ + ip: '1.2.3.4', + code: '123456', + }) + .expect(200).expect((response) => { + expect(response.body).to.deep.equal({ + result: 'Code was sent to the probe', + }); + }); + + expect(adoptionCodeStub.callCount).to.equal(1); + expect(adoptionCodeStub.args[0]).to.deep.equal([{ code: '123456' }]); + }); + + it('should return 403 for non-admins', async () => { + await requestAgent.post('/v1/adoption-code?adminkey=wrongkey') + .send({ + ip: '1.2.3.4', + code: '123456', + }) + .expect(403).expect((response) => { + expect(response.body.error.message).to.equal('Forbidden'); + }); + + expect(adoptionCodeStub.callCount).to.equal(0); + }); + + it('should return 422 if probe not found', async () => { + await requestAgent.post('/v1/adoption-code?adminkey=admin') + .send({ + ip: '9.9.9.9', + code: '123456', + }) + .expect(422).expect((response) => { + expect(response.body.error.message).to.equal('No suitable probes found.'); + }); + + expect(adoptionCodeStub.callCount).to.equal(0); + }); +}); diff --git a/test/tests/unit/adopted-probes.test.ts b/test/tests/unit/adopted-probes.test.ts new file mode 100644 index 00000000..57c34fa4 --- /dev/null +++ b/test/tests/unit/adopted-probes.test.ts @@ -0,0 +1,108 @@ +import { expect } from 'chai'; +import { Knex } from 'knex'; +import * as sinon from 'sinon'; +import { AdoptedProbes } from '../../../src/lib/adopted-probes.js'; + +const selectStub = sinon.stub(); +const updateStub = sinon.stub(); +const whereStub = sinon.stub().returns({ + update: updateStub, +}); +const sqlStub = sinon.stub().returns({ + select: selectStub, + where: whereStub, +}); +let clock: sinon.SinonSandbox['clock']; + +describe('AdoptedProbes', () => { + before(() => { + clock = sinon.useFakeTimers(); + }); + + beforeEach(() => { + sinon.resetHistory(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('startSync method should sync the data and start regular syncs', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); + selectStub.resolves([{ + ip: '1.1.1.1', + uuid: '1-1-1-1-1', + }]); + + await adoptedProbes.startSync(); + + expect(sqlStub.callCount).to.equal(1); + expect(sqlStub.args[0]).deep.equal([ 'adopted_probes' ]); + expect(selectStub.callCount).to.equal(1); + expect(selectStub.args[0]).deep.equal([ 'ip', 'uuid' ]); + + await clock.tickAsync(5500); + expect(sqlStub.callCount).to.equal(2); + expect(selectStub.callCount).to.equal(2); + }); + + it('syncProbeIds method should do nothing if probe was not found either by ip or uuid', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); + selectStub.resolves([{ + ip: '1.1.1.1', + uuid: '1-1-1-1-1', + }]); + + await adoptedProbes.startSync(); + await adoptedProbes.syncProbeIds('2.2.2.2', '2-2-2-2-2'); + + expect(whereStub.callCount).to.equal(0); + expect(updateStub.callCount).to.equal(0); + }); + + it('syncProbeIds method should do nothing if probe data is actual', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); + selectStub.resolves([{ + ip: '1.1.1.1', + uuid: '1-1-1-1-1', + }]); + + await adoptedProbes.startSync(); + await adoptedProbes.syncProbeIds('1.1.1.1', '1-1-1-1-1'); + + expect(whereStub.callCount).to.equal(0); + expect(updateStub.callCount).to.equal(0); + }); + + it('syncProbeIds method should update uuid if it is wrong', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); + selectStub.resolves([{ + ip: '1.1.1.1', + uuid: '1-1-1-1-1', + }]); + + await adoptedProbes.startSync(); + await adoptedProbes.syncProbeIds('1.1.1.1', '2-2-2-2-2'); + + expect(whereStub.callCount).to.equal(1); + expect(whereStub.args[0]).to.deep.equal([{ ip: '1.1.1.1' }]); + expect(updateStub.callCount).to.equal(1); + expect(updateStub.args[0]).to.deep.equal([{ uuid: '2-2-2-2-2' }]); + }); + + it('syncProbeIds method should update ip if it is wrong', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); + selectStub.resolves([{ + ip: '1.1.1.1', + uuid: '1-1-1-1-1', + }]); + + await adoptedProbes.startSync(); + await adoptedProbes.syncProbeIds('2.2.2.2', '1-1-1-1-1'); + + expect(whereStub.callCount).to.equal(1); + expect(whereStub.args[0]).to.deep.equal([{ uuid: '1-1-1-1-1' }]); + expect(updateStub.callCount).to.equal(1); + expect(updateStub.args[0]).to.deep.equal([{ ip: '2.2.2.2' }]); + }); +}); diff --git a/test/tests/unit/middleware/only-admin.test.ts b/test/tests/unit/middleware/only-admin.test.ts new file mode 100644 index 00000000..9efa45d0 --- /dev/null +++ b/test/tests/unit/middleware/only-admin.test.ts @@ -0,0 +1,26 @@ +import type { Context } from 'koa'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import createHttpError from 'http-errors'; +import { onlyAdmin } from '../../../../src/lib/http/middleware/only-admin.js'; + +const next = sinon.stub(); + +beforeEach(() => { + sinon.resetHistory(); +}); + +describe('rate limit middleware', () => { + it('should reject requests with "isAdmin" false', async () => { + const ctx = { isAdmin: false } as unknown as Context; + const err = await onlyAdmin()(ctx, next).catch(err => err); + expect(err).to.deep.equal(createHttpError(403, 'Forbidden', { type: 'access_forbidden' })); + expect(next.callCount).to.equal(0); + }); + + it('should accept requests with "isAdmin" true', async () => { + const ctx = { isAdmin: true } as unknown as Context; + await onlyAdmin()(ctx, next); + expect(next.callCount).to.equal(1); + }); +}); diff --git a/wallaby.js b/wallaby.js index c56f2dfe..2f6da412 100644 --- a/wallaby.js +++ b/wallaby.js @@ -9,7 +9,9 @@ export default function wallaby () { 'public/v1/*', 'src/**/*.ts', 'src/**/*.cjs', + 'src/**/*.json', 'config/*', + 'public/**/*.yaml', 'test/utils/**/*.ts', 'test/mocks/**/*', 'test/plugins/**/*',