diff --git a/src/controllers/getPackagesPackageName.js b/src/controllers/getPackagesPackageName.js index 5cff4d8f..ef29362a 100644 --- a/src/controllers/getPackagesPackageName.js +++ b/src/controllers/getPackagesPackageName.js @@ -3,6 +3,7 @@ */ module.exports = { + version: 2, docs: { summary: "Show package details.", responses: { @@ -42,16 +43,19 @@ module.exports = { * @param {object} context - The Endpoint Context. * @returns {sso} */ - async logic(params, context) { - // Lets first check if this is a bundled package we should return - const isBundled = context.bundled.isNameBundled(params.packageName); + async logic(ctx) { + const { params } = ctx; + // Lets first check if this is a bundled package we should return + ctx.timecop.start("bundle"); + const isBundled = ctx.bundled.isNameBundled(params.packageName); + ctx.timecop.end("bundle"); if (isBundled.ok && isBundled.content) { // This is in fact a bundled package - const bundledData = context.bundled.getBundledPackage(params.packageName); + const bundledData = ctx.bundled.getBundledPackage(params.packageName); if (!bundledData.ok) { - const sso = new context.sso(); + const sso = new ctx.sso(); return sso .notOk() @@ -59,32 +63,37 @@ module.exports = { .addCalls("bundled.isBundled", isBundled); } - const sso = new context.sso(); + const sso = new ctx.sso(); return sso.isOk().addContent(bundledData.content); } - let pack = await context.database.getPackageByName( + ctx.timecop.start("db"); + + let pack = await ctx.database.getPackageByName( params.packageName, true ); + ctx.timecop.end("db"); if (!pack.ok) { - const sso = new context.sso(); + const sso = new ctx.sso(); return sso.notOk().addContent(pack).addCalls("db.getPackageByName", pack); } - pack = await context.models.constructPackageObjectFull(pack.content); + ctx.timecop.start("construct"); + pack = await ctx.models.constructPackageObjectFull(pack.content); if (params.engine !== false) { // query.engine returns false if no valid query param is found. // before using engineFilter we need to check the truthiness of it. - pack = await context.utils.engineFilter(pack, params.engine); + pack = await ctx.utils.engineFilter(pack, params.engine); } + ctx.timecop.end("construct"); - const sso = new context.sso(); + const sso = new ctx.sso(); return sso.isOk().addContent(pack); }, diff --git a/src/models/timecop.js b/src/models/timecop.js new file mode 100644 index 00000000..45dc6b70 --- /dev/null +++ b/src/models/timecop.js @@ -0,0 +1,41 @@ + +module.exports = +class Timecop { + constructor() { + this.timetable = {}; + } + + start(service) { + this.timetable[service] = { + start: performance.now(), + end: undefined, + duration: undefined + }; + } + + end(service) { + if (!this.timetable[service]) { + this.timetable[service] = {}; + this.timetable[service].start = 0; // Wildly incorrect date, more likely + // to be caught rather than letting the time taken be 0ms + } + this.timetable[service].end = performance.now(); + this.timetable[service].duration = + this.timetable[service].end - + this.timetable[service].start; + } + + toHeader() { + let str = ""; + + for (const service in this.timetable) { + if (str.length > 0) { + str = str + ", "; + } + + str = str + `${service};dur=${Number(this.timetable[service].duration).toFixed(2)}`; + } + + return str; + } +} diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 33fd4b58..29c8ae22 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -39,6 +39,22 @@ const authLimit = rateLimit({ }, }); +// TODO: Once all controllers are migrated to v2, or all endpoint tests are HTTP +// based, we can move this to `./context.js`, but until then unit tests rely on +// the structure of Context, and instead of changing it, we can define the builder here +const Timecop = require("./models/timecop.js"); +const buildContext = (req, res, params) => { + return { + req: req, + res: res, + params: params, + timecop: new Timecop(), + ...context, + callStack: new context.callStack(), // Put after spread operator on CTX so + // it overwrites the original callstack uninitialized class + }; +}; + // Set express defaults app.set("trust proxy", true); @@ -56,6 +72,7 @@ const endpointHandler = async function (node, req, res) { await node.preLogic(req, res, context); } + const sharedCtx = buildContext(req, res, params); let obj; try { @@ -64,7 +81,16 @@ const endpointHandler = async function (node, req, res) { // If it's a raw endpoint, they must handle all other steps manually return; } else { - obj = await node.logic(params, context); + switch(node.version) { + case 2: + obj = await node.logic(sharedCtx); + break; + case 1: + default: + // Previous default, implicit version 1 behavior + obj = await node.logic(params, context); + break; + } } } catch (err) { // The main logic request has failed. We will generate our own return obj, @@ -81,6 +107,15 @@ const endpointHandler = async function (node, req, res) { await node.postLogic(req, res, context); } + // Before handling our return check again for our node.version to check for + // extra steps + if (node.version === 2) { + // Server-Timing Header check + if (Object.keys(sharedCtx.timecop.timetable).length > 0) { + res.append("Server-Timing", sharedCtx.timecop.toHeader()); + } + } + obj.addGoodStatus(node.endpoint.successStatus); obj.handleReturnHTTP(req, res, context); diff --git a/tests/full/getPackagesPackageName.test.js b/tests/full/getPackagesPackageName.test.js new file mode 100644 index 00000000..0d764b99 --- /dev/null +++ b/tests/full/getPackagesPackageName.test.js @@ -0,0 +1,44 @@ +const supertest = require("supertest"); +const app = require("../../src/setupEndpoints.js"); +const database = require("../../src/database/_export.js"); +const genPackage = require("../helpers/package.jest.js"); + +describe("Behaves as expected", () => { + test("Returns 404 when a package doesn't exist", async () => { + const res = await supertest(app).get("/api/packages/anything"); + + expect(res).toHaveHTTPCode(404); + expect(res.body.message).toBe("Not Found"); + }); + + test("Returns a package on success", async () => { + await database.insertNewPackage( + genPackage("https://github.com/confused-Techie/get-package-test", { + versions: [ "1.1.0", "1.0.0" ] + }) + ); + + const res = await supertest(app).get("/api/packages/get-package-test"); + + expect(res).toHaveHTTPCode(200); + expect(res.body.name).toBe("get-package-test"); + expect(res.body.owner).toBe("confused-Techie"); + + await database.removePackageByName("get-package-test", true); + }); + + test("Returns a bundled package without it existing in the database", async () => { + const res = await supertest(app).get("/api/packages/settings-view"); + + expect(res).toHaveHTTPCode(200); + expect(res.body.name).toBe("settings-view"); + expect(res.body.owner).toBe("pulsar-edit"); + expect(res.body.repository.url).toBe("https://github.com/pulsar-edit/pulsar"); + }); + + test("Adheres to `Server-Timing` Specification", async () => { + const res = await supertest(app).get("/api/packages/i-dont-exist"); + + expect(res.headers["server-timing"]).toBeTypeof("string"); + }); +}); diff --git a/tests/http/getPackagesPackageName.test.js b/tests/http/getPackagesPackageName.test.js deleted file mode 100644 index f1383737..00000000 --- a/tests/http/getPackagesPackageName.test.js +++ /dev/null @@ -1,60 +0,0 @@ -const endpoint = require("../../src/controllers/getPackagesPackageName.js"); -const database = require("../../src/database/_export.js"); -const context = require("../../src/context.js"); - -const genPackage = require("../helpers/package.jest.js"); - -describe("Behaves as expected", () => { - test("Returns 'not_found' when package doesn't exist", async () => { - const sso = await endpoint.logic( - { - engine: false, - packageName: "anything", - }, - context - ); - - expect(sso.ok).toBe(false); - expect(sso.content.short).toBe("not_found"); - }); - - test("Returns package on success", async () => { - await database.insertNewPackage( - genPackage("https://github.com/confused-Techie/get-package-test", { - versions: ["1.1.0", "1.0.0"], - }) - ); - - const sso = await endpoint.logic( - { - engine: false, - packageName: "get-package-test", - }, - context - ); - - expect(sso.ok).toBe(true); - expect(sso.content.name).toBe("get-package-test"); - expect(sso.content.owner).toBe("confused-Techie"); - expect(sso).toMatchEndpointSuccessObject(endpoint); - await database.removePackageByName("get-package-test", true); - }); - - test("Returns a bundled package without it existing in the database", async () => { - const sso = await endpoint.logic( - { - engine: false, - packageName: "settings-view", - }, - context - ); - - expect(sso.ok).toBe(true); - expect(sso.content.name).toBe("settings-view"); - expect(sso.content.owner).toBe("pulsar-edit"); - expect(sso.content.repository.url).toBe( - "https://github.com/pulsar-edit/pulsar" - ); - expect(sso).toMatchEndpointSuccessObject(endpoint); - }); -});