From 9daa044e00dc345b00d8f1e656678bb54caca73c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 6 Sep 2023 00:38:59 -0700 Subject: [PATCH 01/53] Setup initial `models` and `controllers` folder --- src/controllers/getThemes.js | 76 ++++++++++++++++++++++++++++++++++++ src/models/paginateSso.js | 40 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/controllers/getThemes.js create mode 100644 src/models/paginateSso.js diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js new file mode 100644 index 00000000..191191d8 --- /dev/null +++ b/src/controllers/getThemes.js @@ -0,0 +1,76 @@ + +module.exports = { + endpoint(app, context) { + app.get("/api/themes", context.genericLimit, async (req, res, next) => { + const ret = await this.logic( + { + page: context.query.page(req), + sort: context.query.sort(req), + direction: context.query.dir(req) + }, + context + ); + + if (!ret.ok) { + await context.common_handler.handleError(req, res, ret.content); + return; + } + + // This is a paginated endpoint + res.append("Link", ret.link); + res.append("Query-Total", ret.total); + res.append("Query-Limit", ret.limit); + + res.status(200).json(ret.content); + context.logger.httpLog(req, res); + }); + }, + + /** + * @async + * @memberOf getThemes + * @function logic + * @desc Returns all themes to the user. Based on any filters they've applied + * via query parameters. + * @returns {object} PaginateSSO + */ + async logic(params, context) { + + const packages = await context.db.getSortedPackages(params, true); + + if (!packages.ok) { + context.logger.generic( + 3, + `getThemes-getSortedPackages Not OK: ${packages.content}` + ); + return { + ok: false, + content: packages + }; + } + + const page = packages.pagination.page; + const totPage = packages.pagination.total; + const packObjShort = await context.utils.constructPackageObjectShort( + packages.content + ); + + const packArray = Array.isArray(packObjShort) ? packObjShort : [ packObjShort ]; + + let link = `<${server_url}/api/themes?page=${page}&sort=${params.sort}&order=${params.direction}>; rel="self", <${server_url}/api/themes?page=${totPage}&sort=${params.sort}&order=${params.direction}>; rel="last"`; + + if (page !== totPage) { + link += `, <${server_url}/api/themes?page=${page + 1}&sort=${ + params.sort + }&order=${params.direction}>; rel="next"`; + } + + return { + ok: true, + link: link, + total: packages.pagination.count, + limit: packages.pagination.limit, + content: packArray + }; + } +}; diff --git a/src/models/paginateSso.js b/src/models/paginateSso.js new file mode 100644 index 00000000..faa593b9 --- /dev/null +++ b/src/models/paginateSso.js @@ -0,0 +1,40 @@ +module.exports = +class PaginateSSO { + constructor() { + this.kind = "paginateSSO"; + this.link; + this.total; + this.limit; + this.content; + this.ok; + } + + buildLink(url, currentPage, totalPages, params) { + let paramString = ""; + + for (let param of params) { + paramString += `¶m=${params[param]}`; + } + + let linkString = ""; + + linkString += `<${url}?page=${currentPage}${paramString}>; rel="self", `; + linkString += `<${url}?page=${totalPages}${paramString}>; rel="last"`; + + if (currentPage !== totalPages) { + linkString += `, <${url}?page=${currentPage + 1}${paramString}>; rel="next"`; + } + + this.link = linkString; + } + + httpReturn(req, res, logger) { + + res.append("Link", this.link); + res.append("Query-Total", this.total); + res.append("Query-Limit", this.limit); + + res.status(200).json(ret.content); + logger.httpLog(req, res); + } +} From a1d593aa801d45c004fb0c91e4a6d4b7bd8eec7a Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 6 Sep 2023 00:39:07 -0700 Subject: [PATCH 02/53] Add new documentation of plans --- .../resources/refactor-DONT_KEEP_THIS_FILE.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/resources/refactor-DONT_KEEP_THIS_FILE.md diff --git a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md new file mode 100644 index 00000000..01dedbd7 --- /dev/null +++ b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md @@ -0,0 +1,63 @@ +# DON'T KEEP THIS FILE + +Alright, so lets do a big time refactor, because it's fun, and I get bothered looking at the same code for too long or something. + +Essentially here's the new idea: + +## HTTP Handling + +Stop treating it as if it was special. HTTP handling is essentially only a utility function that's easily **replicable** and should be treated as such. + +The only part of an HTTP handling process that matters is the logic that's preformed. The logic of returning the data depending on states of SSO's or adding pagination or even erroring out is insanely easily replicable. + +So we should abstract away from hardcoding endless functions for HTTP handling as much as possible. So here's my idea: + +Every endpoint is it's own tiny module. This module should export at least two things: + +* `logic()` This function will be called to handle the actual logic of the endpoint, passing all relevant data to it +* `params()` This function will return a parameter object consisting of all query parameters that this endpoint expects to receive +* `endpoint` The endpoint object will then provide the endpoint logic with everything else it needs to define any given endpoint. + +From here the `main.js` module should instead import all modules needed, and iterate through them to create every single endpoint as needed. This may result in a slightly longer startup time, but overall I hope the increase in code readability and less duplication will be worth it. + +So this means that every module is imported, the `endpoint` object is read to setup the endpoint, and from there, it's made available as an endpoint via express, which can then, once hit, use the `params()` function to prepare the query parameters, and then pass those off to the `logic()` function. + +### `endpoint` Structure + +The `path` here is an array since in some instances, we want to accept multiple paths, such as `POST /api/packages` and `POST /api/themes`. + +```javascript +const endpoint = { + method: "GET", + path: [ "/api/themes" ], + rate_limit: "generic", + options: { + // This would be the headers to return for `HTTP OPTIONS` req: + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } +}; +``` + +## Returning HTTP handling + +Again, the logic here is easily replicable. So we shouldn't make it special. And if finally doing a proper rewrite, we can incorporate a proper SSO, and have a different type for every single return. This way the actual handling of any return, can instead be a factor of a `httpReturn()` function of the SSO itself, rather than baked in logic. So that way we can keep the return logic as unique as needed, as the uniqueness depends solely on the uniqueness of the SSO being returned. + +## Tests + +(As always the bane of existence) + +With this refactor, we no longer need true "integration" tests. As integration can be tested on if the proper endpoints being hit call the proper endpoint.logic() function. Beyond that the majority of "integration" testing would be relegated to interactions with external services working as expected. + +Meaning the only tests we would likely need are: + +* `tests` This would be the vast majority of tests, able to be generic, and not needing any fancy setup +* `database` This suite of tests should purely test if DB calls do what we expect +* `integration` A small suite of full integration tests is never a bad idea. To test that API calls have the intended effects on the DB. With a huge focus on having the intended effects. As we are seeing some examples where the expected data is not appearing or being applied to the DB as we want. +* `external` We don't do this currently. But a suite of external tests that are run on a maybe monthly basis is not a bad idea. This could allow us to ensure external APIs are returning data as expected. + +--- + +I think this is plenty to focus on now. At the very least the changes described here would likely mean a rewrite of about or over half the entire codebase. But if all goes to plan, would mean that every single piece of logic is more modular, keeping logic related to itself within the same file, and if tests are effected as hoped, would mean a much more robust testing solution, that who knows, may actually be able to achieve near 100% testing coverage. + +One side effect of all this change, means the possibility of generating documentation of the API based totally on the documentation itself, where we no longer would be reliant on my own `@confused-techie/quick-webserver-docs` module, nor having to ensure comments are updated. From 28c22e43a8c5b7e23902903ba6cebe679f230304 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 7 Sep 2023 00:56:41 -0700 Subject: [PATCH 03/53] Create `setupEndpoints` logic, and utilize pattern in new controller --- .../resources/refactor-DONT_KEEP_THIS_FILE.md | 4 +- src/controllers/getThemes.js | 41 ++--- src/setupEndpoints.js | 147 ++++++++++++++++++ 3 files changed, 166 insertions(+), 26 deletions(-) create mode 100644 src/setupEndpoints.js diff --git a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md index 01dedbd7..2ded9c95 100644 --- a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md +++ b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md @@ -28,8 +28,10 @@ The `path` here is an array since in some instances, we want to accept multiple ```javascript const endpoint = { + // Can be "GET", "POST", "DELETE" method: "GET", - path: [ "/api/themes" ], + paths: [ "/api/themes" ], + // Can be "generic" or "auth" rate_limit: "generic", options: { // This would be the headers to return for `HTTP OPTIONS` req: diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index 191191d8..7c66494b 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -1,29 +1,20 @@ module.exports = { - endpoint(app, context) { - app.get("/api/themes", context.genericLimit, async (req, res, next) => { - const ret = await this.logic( - { - page: context.query.page(req), - sort: context.query.sort(req), - direction: context.query.dir(req) - }, - context - ); - - if (!ret.ok) { - await context.common_handler.handleError(req, res, ret.content); - return; - } - - // This is a paginated endpoint - res.append("Link", ret.link); - res.append("Query-Total", ret.total); - res.append("Query-Limit", ret.limit); - - res.status(200).json(ret.content); - context.logger.httpLog(req, res); - }); + endpoint: { + method: "GET", + paths: [ "/api/themes" ], + rate_limit: "generic", + options: { + Allow: "POST, GET", + "X-Content-Type-Options": "nosniff" + } + }, + params(req, context) { + return { + page: context.query.page(req), + sort: context.query.sort(req), + direction: context.query.dir(req) + }; }, /** @@ -36,7 +27,7 @@ module.exports = { */ async logic(params, context) { - const packages = await context.db.getSortedPackages(params, true); + const packages = await context.database.getSortedPackages(params, true); if (!packages.ok) { context.logger.generic( diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js new file mode 100644 index 00000000..36e8b45e --- /dev/null +++ b/src/setupEndpoints.js @@ -0,0 +1,147 @@ +const express = require("express"); +const rateLimit = require("express-rate-limit"); +const { MemoryStore } = require("express-rate-limit"); + +const app = express(); + +const endpoints = [ + require("./controllers/getThemes.js") +]; + +// The CONST Context - Enables access to all other modules within the system +// By passing this object to everywhere needed allows not only easy access +// but greater control in mocking these later on +const context = { + logger: require("./logger.js"), + database: require("./database.js"), + webhook: require("./webhook.js"), + server_version: require("../package.json").version, + query: require("./query.js"), + vcs: require("./vcs.js"), + config: require("./config.js").getConfig(), + common_handler: require("./handlers/common_handler.js"), + utils: require("./utils.js") +}; + +// Define our Basic Rate Limiters +const genericLimit = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + // Limit each IP per window, 0 disables rate limit + max: process.env.PULSAR_STATUS === "dev" ? 0 : context.config.RATE_LIMIT_GENRIC, + standardHeaders: true, // Return rate limit info in headers + legacyHeaders: true, // Legacy rate limit info in headers + store: new MemoryStore(), // use default memory store + message: "Too many requests, please try again later.", // Message once limit is reached + statusCode: 429, // HTTP Status code once limit is reached + handler: (request, response, next, options) => { + response.status(options.statusCode).json({ message: options.message }); + context.logger.httpLog(request, response); + } +}); + +const authLimit = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + // Limit each IP per window, 0 disables rate limit. + max: process.env.PULSAR_STATUS === "dev" ? 0 : context.config.RATE_LIMIT_AUTH, + standardHeaders: true, // Return rate limit info on headers + legacyHeaders: true, // Legacy rate limit info in headers + store: new MemoryStore(), // use default memory store + message: "Too many requests, please try again later.", // message once limit is reached + statusCode: 429, // HTTP Status code once limit is reached. + handler: (request, response, next, options) => { + response.status(options.statusCode).json({ message: options.message }); + context.logger.httpLog(request, response); + } +}); + +// Set express defaults + +app.set("trust proxy", true); + +app.use("/swagger-ui", express.static("docs/swagger")); + +// Setup all endpoints + +for (const node of endpoints) { + + for (const path of node.endpoint.paths) { + + let limiter = genericLimit; + + if (node.endpoint.rate_limit === "auth") { + limiter = authLimit; + } else if (node.endpoint.rate_limit === "generic") { + limiter = genericLimit; + } + + // Don't break on switch, so default can provide `OPTIONS` endpoint + switch(node.endpoint.method) { + case "GET": + app.get(path, limiter, async (req, res) => { + let params = node.params(req, context); + + let obj = await node.logic(params, context); + + if (!obj.ok) { + obj.handleError(req, res, context); + return; + } + + obj.handleSuccess(req, res, context); + return; + }); + case "POST": + app.post(path, limiter, async (req, res) => { + let params = node.params(req, context); + + let obj = await node.logic(params, context); + + if (!obj.ok) { + obj.handleError(req, res, context); + return; + } + + obj.handleSuccess(req, res, context); + return; + }); + case "DELETE": + app.delete(path, limiter, async (req, res) => { + let params = node.params(req, context); + + let obj = await node.logic(params, context); + + if (!obj.ok) { + obj.handleError(req, res, context); + return; + } + + obj.handleSuccess(req, res, context); + return; + }); + default: + app.options(path, genericLimit, async (req, res) => { + res.header(node.endpoint.options); + res.sendStatus(204); + return; + }); + } + } +} + +app.use(async (err, req, res, next) => { + // Having this as the last route, will handle all other unkown routes. + // Ensure we leave this at the very last position to handle properly. + // We can also check for any unhandled errors passed down the endpoint chain + + if (err) { + console.error( + `An error was encountered handling the request: ${err.toString()}` + ); + await context.common_handler.serverError(req, res, err); + return; + } + + context.common_handler.siteWideNotFound(res, res); +}); + +module.exports = app; From b29cbdd3c7882e3f23ee871513176a08418d1436 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 8 Sep 2023 00:58:23 -0700 Subject: [PATCH 04/53] Refine the `endpoint` object, bake in expected interactions --- src/controllers/getThemes.js | 32 +++------ src/controllers/getThemesFeatured.js | 32 +++++++++ src/models/sso.js | 67 +++++++++++++++++++ src/models/{paginateSso.js => ssoPaginate.js} | 22 +++--- src/setupEndpoints.js | 25 +++---- 5 files changed, 129 insertions(+), 49 deletions(-) create mode 100644 src/controllers/getThemesFeatured.js create mode 100644 src/models/sso.js rename src/models/{paginateSso.js => ssoPaginate.js} (60%) diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index 7c66494b..5f12da94 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -4,6 +4,7 @@ module.exports = { method: "GET", paths: [ "/api/themes" ], rate_limit: "generic", + success_status: 200, options: { Allow: "POST, GET", "X-Content-Type-Options": "nosniff" @@ -30,38 +31,23 @@ module.exports = { const packages = await context.database.getSortedPackages(params, true); if (!packages.ok) { - context.logger.generic( - 3, - `getThemes-getSortedPackages Not OK: ${packages.content}` - ); - return { - ok: false, - content: packages - }; + const sso = new context.sso(); + + return sso.notOk().addContent(packages.content).addCalls("db.getSortedPackages", packages); } - const page = packages.pagination.page; - const totPage = packages.pagination.total; const packObjShort = await context.utils.constructPackageObjectShort( packages.content ); const packArray = Array.isArray(packObjShort) ? packObjShort : [ packObjShort ]; - let link = `<${server_url}/api/themes?page=${page}&sort=${params.sort}&order=${params.direction}>; rel="self", <${server_url}/api/themes?page=${totPage}&sort=${params.sort}&order=${params.direction}>; rel="last"`; + const ssoP = new context.ssoPaginate(); - if (page !== totPage) { - link += `, <${server_url}/api/themes?page=${page + 1}&sort=${ - params.sort - }&order=${params.direction}>; rel="next"`; - } + ssoP.total = packages.pagination.total; + ssoP.limit = packages.pagination.total; + ssoP.buildLink(`${context.config.server_url}/api/themes`, page, params); - return { - ok: true, - link: link, - total: packages.pagination.count, - limit: packages.pagination.limit, - content: packArray - }; + return ssoP.isOk().addContent(packArray); } }; diff --git a/src/controllers/getThemesFeatured.js b/src/controllers/getThemesFeatured.js new file mode 100644 index 00000000..7bb29526 --- /dev/null +++ b/src/controllers/getThemesFeatured.js @@ -0,0 +1,32 @@ +module.exports = { + endpoint: { + method: "GET", + paths: [ "/api/themes/featured" ], + rate_limit: "generic", + success_status: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params(req, context) { + // Currently we don't seem to utilize any query parameters here. + // We likely want to make this match whatever is used in getPackagesFeatured.js + return {}; + }, + async logic(params, context) { + const col = await context.database.getFeaturedThemes(); + + if (!col.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(col.content).addCalls("db.getFeaturedThemes", col); + } + + const newCol = await utils.constructPackageObjectShort(col.content); + + const sso = new context.sso(); + + return sso.isOk().addContent(newCol); + } +}; diff --git a/src/models/sso.js b/src/models/sso.js new file mode 100644 index 00000000..fd500766 --- /dev/null +++ b/src/models/sso.js @@ -0,0 +1,67 @@ +const { performance } = require("node:perf_hooks"); + +module.exports = +class SSO { + constructor() { + this.ok = false; + this.content; + this.safeContent = false; + this.successStatusCode = 200; + this.calls = {}; + } + + isOk() { + this.ok = true; + return this; + } + + notOk() { + this.ok = false; + return this; + } + + addContent(content, safe) { + if (typeof safe === "boolean") { + this.safeContent = safe; + } + + this.content = content; + return this; + } + + addCalls(id, content) { + this.calls[id] = { + content: content, + time: performance.now() + }; + return this; + } + + addGoodStatus(status) { + this.successStatusCode = status; + return this; + } + + handleReturnHTTP(req, res, context) { + if (!this.ok) { + this.handleError(req, res, context); + return; + } + + this.handleSuccess(req, res, context); + return; + } + + handleError(req, res, context) { + // TODO Get rid of the common error handler, and put all the logic here + await context.common_handler.handleError(req, res, this.content); + return; + } + + handleSuccess(req, res, context) { + + res.status(this.successStatusCode).json(this.content); + context.logger.httpLog(req, res); + return; + } +} diff --git a/src/models/paginateSso.js b/src/models/ssoPaginate.js similarity index 60% rename from src/models/paginateSso.js rename to src/models/ssoPaginate.js index faa593b9..82022b9b 100644 --- a/src/models/paginateSso.js +++ b/src/models/ssoPaginate.js @@ -1,15 +1,16 @@ +const SSO = require("./sso.js"); + module.exports = -class PaginateSSO { +class SSOPaginate extends SSO { constructor() { - this.kind = "paginateSSO"; + super(); + this.link; this.total; this.limit; - this.content; - this.ok; } - buildLink(url, currentPage, totalPages, params) { + buildLink(url, currentPage, params) { let paramString = ""; for (let param of params) { @@ -19,22 +20,23 @@ class PaginateSSO { let linkString = ""; linkString += `<${url}?page=${currentPage}${paramString}>; rel="self", `; - linkString += `<${url}?page=${totalPages}${paramString}>; rel="last"`; + linkString += `<${url}?page=${this.total}${paramString}>; rel="last"`; - if (currentPage !== totalPages) { + if (currentPage !== this.total) { linkString += `, <${url}?page=${currentPage + 1}${paramString}>; rel="next"`; } this.link = linkString; } - httpReturn(req, res, logger) { + handleSuccess(req, res, context) { res.append("Link", this.link); res.append("Query-Total", this.total); res.append("Query-Limit", this.limit); - res.status(200).json(ret.content); - logger.httpLog(req, res); + res.status(this.successStatusCode).json(this.content); + context.logger.httpLog(req, res); + return; } } diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 36e8b45e..5188e8d0 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -20,7 +20,9 @@ const context = { vcs: require("./vcs.js"), config: require("./config.js").getConfig(), common_handler: require("./handlers/common_handler.js"), - utils: require("./utils.js") + utils: require("./utils.js"), + sso: require("./models/sso.js"), + ssoPaginate: require("./models/sso.js") }; // Define our Basic Rate Limiters @@ -82,12 +84,9 @@ for (const node of endpoints) { let obj = await node.logic(params, context); - if (!obj.ok) { - obj.handleError(req, res, context); - return; - } + obj.addGoodStatus(node.endpoint.success_status); - obj.handleSuccess(req, res, context); + obj.handleReturnHTTP(req, res, context); return; }); case "POST": @@ -96,12 +95,9 @@ for (const node of endpoints) { let obj = await node.logic(params, context); - if (!obj.ok) { - obj.handleError(req, res, context); - return; - } + obj.addGoodStatus(node.endpoint.success_status); - obj.handleSuccess(req, res, context); + obj.handleReturnHTTP(req, res, context); return; }); case "DELETE": @@ -110,12 +106,9 @@ for (const node of endpoints) { let obj = await node.logic(params, context); - if (!obj.ok) { - obj.handleError(req, res, context); - return; - } + obj.addGoodStatus(node.endpoint.success_status); - obj.handleSuccess(req, res, context); + obj.handleReturnHTTP(req, res, context); return; }); default: From 93aa064d7de5a4757bb751747b4650f4f3f36a3a Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sat, 9 Sep 2023 01:39:37 -0700 Subject: [PATCH 05/53] More endpoints, pre/post logic functions, docs object --- jest.config.js | 1 + src/controllers/getStars.js | 80 ++++++++++++++++++++++++++++++ src/controllers/getThemes.js | 6 +-- src/controllers/getUpdates.js | 40 +++++++++++++++ src/controllers/getUsers.js | 76 ++++++++++++++++++++++++++++ src/controllers/getUsersLogin.js | 64 ++++++++++++++++++++++++ src/models/sso.js | 7 +++ src/setupEndpoints.js | 41 ++++++++++++--- test/controllers/getThemes.test.js | 17 +++++++ 9 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 src/controllers/getStars.js create mode 100644 src/controllers/getUpdates.js create mode 100644 src/controllers/getUsers.js create mode 100644 src/controllers/getUsersLogin.js create mode 100644 test/controllers/getThemes.test.js diff --git a/jest.config.js b/jest.config.js index f5cb0f17..6f29be37 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,6 +29,7 @@ const config = { testMatch: [ "/test/*.unit.test.js", "/test/handlers/**/**.test.js", + "/test/controllers/**.test.js" ], }, { diff --git a/src/controllers/getStars.js b/src/controllers/getStars.js new file mode 100644 index 00000000..d55d890b --- /dev/null +++ b/src/controllers/getStars.js @@ -0,0 +1,80 @@ +module.exports = { + docs: { + summary: "List the authenticated users' starred packages.", + responses: [ + { + 200: { + description: "Return a value similar to `GET /api/packages`, an array of package objects.", + content: {} + } + } + ] + }, + endpoint: { + method: "GET", + paths: [ "/api/stars" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params(req, context) { + return { + auth: query.auth(req) + }; + }, + + /** + * @async + * @memberOf getStars + * @function logic + * @desc Returns an array of all packages the authenticated user has starred. + */ + async logic(params, context) { + let user = await context.auth.verifyAuth(params.auth, context.database); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user.content).addCalls("auth.verifyAuth", user); + } + + let userStars = await context.database.getStarredPointersByUserID(user.content.id); + + if (!userStars.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(userStars.content) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getStarredPointersByUserID", userStars); + } + + if (userStars.content.length === 0) { + // If we have a return with no items, means the user has no stars + // And this will error out later when attempting to collect the data + // for the stars. So we will return early + const sso = new context.sso(); + + return sso.isOk().addContent([]); + } + + let packCol = await context.database.getPackageCollectionByID(userStars.content); + + if (!packCol.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(packCol.content) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getStarredPointersByUserID", userStars) + .addCalls("db.getPackageCollectionByID", packCol); + } + + let newCol = await context.utils.constructPackageObjectShort(packCol.content); + + const sso = new context.sso(); + + return sso.isOk().addContent(newCol); + } +}; diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index 5f12da94..8a9f4cad 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -3,8 +3,8 @@ module.exports = { endpoint: { method: "GET", paths: [ "/api/themes" ], - rate_limit: "generic", - success_status: 200, + rateLimit: "generic", + successStatus: 200, options: { Allow: "POST, GET", "X-Content-Type-Options": "nosniff" @@ -24,7 +24,7 @@ module.exports = { * @function logic * @desc Returns all themes to the user. Based on any filters they've applied * via query parameters. - * @returns {object} PaginateSSO + * @returns {object} ssoPaginate */ async logic(params, context) { diff --git a/src/controllers/getUpdates.js b/src/controllers/getUpdates.js new file mode 100644 index 00000000..35f05083 --- /dev/null +++ b/src/controllers/getUpdates.js @@ -0,0 +1,40 @@ +module.exports = { + docs: { + summary: "List Pulsar Updates", + description: "Currently returns 'Not Implemented' as Squirrel AutoUpdate is not supported.", + responses: [ + { + 200: { + description: "Atom update feed, following the format expected by Squirrel.", + content: {} + } + } + ] + }, + endpoint: { + method: "GET", + paths: [ "/api/updates" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params(req, context) { + return {}; + }, + + /** + * @async + * @memberof getUpdates + * @function logic + * @desc Used to retreive new editor update information. + * @todo This function has never been implemented within Pulsar. + */ + async logic(params, context) { + const sso = new context.sso(); + + return sso.notOk().addShort("not_supported"); + } +}; diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js new file mode 100644 index 00000000..edddf155 --- /dev/null +++ b/src/controllers/getUsers.js @@ -0,0 +1,76 @@ +module.exports = { + docs: { + summary: "Display details of the currently authenticated user. This endpoint is undocumented and is somewhat strange.", + description: "This endpoint only exists on the web version of the upstream API. Having no backend equivolent.", + responses: [ + { + 200: { + description: "Details of the Authenticated User Account.", + content: {} + } + } + ] + }, + endpoint: { + method: "GET", + paths: [ "/api/users" ], + rateLimit: "auth", + successStatus: 200, + options: { + Allow: "GET", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "Content-Type, Authorization, Acces-Control-Allow-Credentials", + "Access-Control-Allow-Origin": "https://web.pulsar-edit.dev", + "Access-Control-Allow-Credentials": true + } + }, + params(req, context) { + return { + auth: context.query.auth(req) + }; + }, + preLogic(req, res, context) { + res.header("Access-Control-Allow-Methods", "GET"); + res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Access-Control-Allow-Credentials"); + res.header("Access-Control-Allow-Origin", "https://web.pulsar-edit.dev"); + res.header("Access-Control-Allow-Credentials", true); + }, + postLogic(req, res, context) { + res.set({ "Access-Control-Allow-Credentials": true }); + } + + /** + * @async + * @memberOf getUsers + * @desc Returns the currently authenticated Users User Details. + */ + async logic(params, context) { + const user = await context.auth.verifyAuth(params.auth, context.database); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user.content) + .addCalls("auth.verifyAuth", user); + } + + // TODO We need to find a way to add the users published pacakges here + // When we do we will want to match the schema in ./docs/returns.md#userobjectfull + // Until now we will return the public details of their account. + const returnUser = { + username: user.content.username, + avatar: user.content.avatar, + created_at: user.content.created_at, + data: user.content.data, + node_id: user.content.node_id, + token: user.content.token, // Since this is for the auth user we can provide token + packages: [], // Included as it should be used in the future + }; + + // Now with the user, since this is the authenticated user we can return all account details. + + const sso = new context.sso(); + + return sso.isOk().addContent(returnUser); + } +}; diff --git a/src/controllers/getUsersLogin.js b/src/controllers/getUsersLogin.js new file mode 100644 index 00000000..74944404 --- /dev/null +++ b/src/controllers/getUsersLogin.js @@ -0,0 +1,64 @@ +module.exports = { + docs: { + summary: "Display the details of any user, as well as the packages they have published.", + responses: [ + { + 200: { + description: "The returned details of a specific user.", + content: {} + } + } + ] + }, + endpoint: { + method: "GET", + paths: [ "/api/users/:login" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params(req, context) { + return { + login: context.query.login(req) + }; + }, + + /** + * @async + * @memberOf getUserLogin + * @desc Returns the user account details of another user. Including all + * packages published. + */ + async logic(params, context) { + let user = await context.database.getUserByName(params.login); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user.content) + .addCalls("db.getUserByName", user); + } + + // TODO We need to find a way to add the users published pacakges here + // When we do we will want to match the schema in ./docs/returns.md#userobjectfull + // Until now we will return the public details of their account. + + // Although now we have a user to return, but we need to ensure to strip any + // sensitive details since this return will go to any user. + const returnUser = { + username: user.content.username, + avatar: user.content.avatar, + created_at: user.content.created_at, + data: user.content.data, + packages: [], // included as it should be used in the future + }; + + + const sso = new context.sso(); + + return sso.isOk().addContent(returnUser); + } +}; diff --git a/src/models/sso.js b/src/models/sso.js index fd500766..2f7260b7 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -5,6 +5,7 @@ class SSO { constructor() { this.ok = false; this.content; + this.short; this.safeContent = false; this.successStatusCode = 200; this.calls = {}; @@ -37,6 +38,12 @@ class SSO { return this; } + addShort(enum) { + // TODO Validate enum being assigned + this.short = enum; + return this; + } + addGoodStatus(status) { this.successStatusCode = status; return this; diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 5188e8d0..a188759b 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -5,7 +5,12 @@ const { MemoryStore } = require("express-rate-limit"); const app = express(); const endpoints = [ - require("./controllers/getThemes.js") + require("./controllers/getStars.js"), + require("./controllers/getThemes.js"), + require("./controllers/getThemesFeatured.js"), + require("./controllers/getUpdates.js"), + require("./controllers/getUsers.js"), + require("./controllers/getusersLogin.js") ]; // The CONST Context - Enables access to all other modules within the system @@ -70,9 +75,9 @@ for (const node of endpoints) { let limiter = genericLimit; - if (node.endpoint.rate_limit === "auth") { + if (node.endpoint.rateLimit === "auth") { limiter = authLimit; - } else if (node.endpoint.rate_limit === "generic") { + } else if (node.endpoint.rateLimit === "generic") { limiter = genericLimit; } @@ -82,9 +87,17 @@ for (const node of endpoints) { app.get(path, limiter, async (req, res) => { let params = node.params(req, context); + if (typeof node.preLogic === "function") { + node.preLogic(req, res, context); + } + let obj = await node.logic(params, context); - obj.addGoodStatus(node.endpoint.success_status); + if (typeof node.postLogic === "function") { + node.postLogic(req, res, context); + } + + obj.addGoodStatus(node.endpoint.successStatus); obj.handleReturnHTTP(req, res, context); return; @@ -93,9 +106,17 @@ for (const node of endpoints) { app.post(path, limiter, async (req, res) => { let params = node.params(req, context); + if (typeof node.preLogic === "function") { + node.preLogic(req, res, context); + } + let obj = await node.logic(params, context); - obj.addGoodStatus(node.endpoint.success_status); + if (typeof node.postLogic === "function") { + node.postLogic(req, res, context); + } + + obj.addGoodStatus(node.endpoint.successStatus); obj.handleReturnHTTP(req, res, context); return; @@ -104,9 +125,17 @@ for (const node of endpoints) { app.delete(path, limiter, async (req, res) => { let params = node.params(req, context); + if (typeof node.preLogic === "function") { + node.preLogic(req, res, context); + } + let obj = await node.logic(params, context); - obj.addGoodStatus(node.endpoint.success_status); + if (typeof node.postLogic === "function") { + node.postLogic(req, res, context); + } + + obj.addGoodStatus(node.endpoint.successStatus); obj.handleReturnHTTP(req, res, context); return; diff --git a/test/controllers/getThemes.test.js b/test/controllers/getThemes.test.js new file mode 100644 index 00000000..356edb44 --- /dev/null +++ b/test/controllers/getThemes.test.js @@ -0,0 +1,17 @@ +const getThemes = require("../../src/controllers/getThemes.js"); + +describe("Returns the expected query parameters", () => { + test("with empty request object", () => { + const req = { query: {} }; + const context = { + query: require("../../src/query.js") + }; + + const ret = getThemes.params(req, context); + + expect(ret.page).toBeDefined(); + expect(ret.sort).toBeDefined(); + expect(ret.direction).toBeDefined(); + + }); +}); From ae3f29d8de7ae94dfa8c0c8a8071b37a0abc077d Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sat, 9 Sep 2023 03:32:14 -0700 Subject: [PATCH 06/53] Small leanup --- src/controllers/getUsers.js | 2 +- src/models/sso.js | 8 ++++---- src/models/ssoPaginate.js | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js index edddf155..a477c3ec 100644 --- a/src/controllers/getUsers.js +++ b/src/controllers/getUsers.js @@ -37,7 +37,7 @@ module.exports = { }, postLogic(req, res, context) { res.set({ "Access-Control-Allow-Credentials": true }); - } + }, /** * @async diff --git a/src/models/sso.js b/src/models/sso.js index 2f7260b7..5833bbb7 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -4,8 +4,8 @@ module.exports = class SSO { constructor() { this.ok = false; - this.content; - this.short; + this.content = {}; + this.short = ""; this.safeContent = false; this.successStatusCode = 200; this.calls = {}; @@ -38,9 +38,9 @@ class SSO { return this; } - addShort(enum) { + addShort(enumValue) { // TODO Validate enum being assigned - this.short = enum; + this.short = enumValue; return this; } diff --git a/src/models/ssoPaginate.js b/src/models/ssoPaginate.js index 82022b9b..ebe2c0c4 100644 --- a/src/models/ssoPaginate.js +++ b/src/models/ssoPaginate.js @@ -5,9 +5,9 @@ class SSOPaginate extends SSO { constructor() { super(); - this.link; - this.total; - this.limit; + this.link = ""; + this.total = 0; + this.limit = 0; } buildLink(url, currentPage, params) { From 0864d96be4dd24e346bd85b6d743b3d37243be06 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 10 Sep 2023 20:07:24 -0700 Subject: [PATCH 07/53] Use `param` objects --- src/controllers/getStars.js | 6 ++---- src/controllers/getThemes.js | 10 ++++------ src/controllers/getThemesFeatured.js | 3 +-- src/controllers/getUpdates.js | 4 +--- src/controllers/getUsers.js | 6 ++---- src/controllers/getUsersLogin.js | 6 ++---- src/setupEndpoints.js | 18 +++++++++++++++--- 7 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/controllers/getStars.js b/src/controllers/getStars.js index d55d890b..11f0712b 100644 --- a/src/controllers/getStars.js +++ b/src/controllers/getStars.js @@ -20,10 +20,8 @@ module.exports = { "X-Content-Type-Options": "nosniff" } }, - params(req, context) { - return { - auth: query.auth(req) - }; + params: { + auth: (context, req) => { return context.query.auth(req); } }, /** diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index 8a9f4cad..10a801aa 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -10,12 +10,10 @@ module.exports = { "X-Content-Type-Options": "nosniff" } }, - params(req, context) { - return { - page: context.query.page(req), - sort: context.query.sort(req), - direction: context.query.dir(req) - }; + params: { + page: (context, req) => { return context.query.page(req); }, + sort: (context, req) => { return context.query.sort(req); }, + direction: (context, req) => { return context.query.dir(req); } }, /** diff --git a/src/controllers/getThemesFeatured.js b/src/controllers/getThemesFeatured.js index 7bb29526..37aad164 100644 --- a/src/controllers/getThemesFeatured.js +++ b/src/controllers/getThemesFeatured.js @@ -9,10 +9,9 @@ module.exports = { "X-Content-Type-Options": "nosniff" } }, - params(req, context) { + params: { // Currently we don't seem to utilize any query parameters here. // We likely want to make this match whatever is used in getPackagesFeatured.js - return {}; }, async logic(params, context) { const col = await context.database.getFeaturedThemes(); diff --git a/src/controllers/getUpdates.js b/src/controllers/getUpdates.js index 35f05083..314fe06f 100644 --- a/src/controllers/getUpdates.js +++ b/src/controllers/getUpdates.js @@ -21,9 +21,7 @@ module.exports = { "X-Content-Type-Options": "nosniff" } }, - params(req, context) { - return {}; - }, + params: {}, /** * @async diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js index a477c3ec..389285d6 100644 --- a/src/controllers/getUsers.js +++ b/src/controllers/getUsers.js @@ -24,10 +24,8 @@ module.exports = { "Access-Control-Allow-Credentials": true } }, - params(req, context) { - return { - auth: context.query.auth(req) - }; + params: { + auth: (context, req) => { return context.query.auth(req); } }, preLogic(req, res, context) { res.header("Access-Control-Allow-Methods", "GET"); diff --git a/src/controllers/getUsersLogin.js b/src/controllers/getUsersLogin.js index 74944404..d5fdfc54 100644 --- a/src/controllers/getUsersLogin.js +++ b/src/controllers/getUsersLogin.js @@ -20,10 +20,8 @@ module.exports = { "X-Content-Type-Options": "nosniff" } }, - params(req, context) { - return { - login: context.query.login(req) - }; + params: { + login: (context, req) => { return context.query.login(req); } }, /** diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index a188759b..59844f7a 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -85,7 +85,11 @@ for (const node of endpoints) { switch(node.endpoint.method) { case "GET": app.get(path, limiter, async (req, res) => { - let params = node.params(req, context); + let params = {}; + + for (const param in node.params) { + params[param] = node.params[param](context, req); + } if (typeof node.preLogic === "function") { node.preLogic(req, res, context); @@ -104,7 +108,11 @@ for (const node of endpoints) { }); case "POST": app.post(path, limiter, async (req, res) => { - let params = node.params(req, context); + let params = {}; + + for (const param in node.params) { + params[param] = node.params[param](context, req); + } if (typeof node.preLogic === "function") { node.preLogic(req, res, context); @@ -123,7 +131,11 @@ for (const node of endpoints) { }); case "DELETE": app.delete(path, limiter, async (req, res) => { - let params = node.params(req, context); + let params = {}; + + for (const param in node.params) { + params[param] = node.params[param](context, req); + } if (typeof node.preLogic === "function") { node.preLogic(req, res, context); From 2564295b7d3f78546426c99435d2c90e3aaf055d Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 10 Sep 2023 20:38:10 -0700 Subject: [PATCH 08/53] Implement additional endpoints --- src/controllers/getUsersLoginStars.js | 80 +++++++++++++++++++ ...eNameVersionsVersionNameEventsUninstall.js | 38 +++++++++ src/setupEndpoints.js | 6 +- 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/controllers/getUsersLoginStars.js create mode 100644 src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js diff --git a/src/controllers/getUsersLoginStars.js b/src/controllers/getUsersLoginStars.js new file mode 100644 index 00000000..e2b0196c --- /dev/null +++ b/src/controllers/getUsersLoginStars.js @@ -0,0 +1,80 @@ +module.exports = { + docs: { + summary: "List a user's starred packages.", + responses: [ + { + 200: { + description: "Return value is similar to `GET /api/packages`.", + content: {} + } + } + ] + }, + endpoint: { + method: "GET", + paths: [ "/api/users/:login/stars" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + login: (context, req) => { return context.query.login(req); } + }, + async logic(params, context) { + const user = await context.database.getUserByName(params.login); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user.content) + .addCalls("db.getUserByName", user); + } + + let pointerCollection = await context.database.getStarredPointersByUserID(user.content.id); + + if (!pointerCollection.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(pointerCollection.content) + .addCalls("db.getUserByName", user) + .addCalls("db.getStarredPointersByUserID", pointerCollection); + } + + // Since even if the pointerCollection is okay, it could be empty. Meaning the user + // has no stars. This is okay, but getPackageCollectionByID will fail, and result + // in a not found when discovering no packages by the ids passed, which is none. + // So we will catch the exception of pointerCollection being an empty array. + + if ( + Array.isArray(pointerCollection.content) && + pointerCollection.content.length === 0 + ) { + // Check for array to protect from an unexpected return + const sso = new context.sso(); + + return sso.isOk().addContent([]); + } + + let packageCollection = await context.database.getPackageCollectionByID( + pointerCollection.content + ); + + if (!packageCollection.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(packageCollection.content) + .addCalls("db.getUserByName", user) + .addCalls("db.getStarredPointersByUserID", pointerCollection) + .addCalls("db.getPackageCollectionByID", packageCollection); + } + + packageCollection = await utils.constructPackageObjectShort(packageCollection.content); + + const sso = new context.sso(); + + return sso.isOk().addContent(packageCollection); + } +}; diff --git a/src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js b/src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js new file mode 100644 index 00000000..5fac901c --- /dev/null +++ b/src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js @@ -0,0 +1,38 @@ +module.exports = { + docs: { + summary: "Previously undocumented endpoint. Since v1.0.2 has no effect.", + }, + endpoint: { + method: "POST", + paths: [ + "/api/packages/:packageName/versions/:versionName/events/uninstall", + "/api/themes/:packageName/versions/:versionName/events/uninstall" + ], + rateLimit: "auth", + successStatus: 201, + options: { + Allow: "POST", + "X-Content-Type-Options": "nosniff" + } + }, + params: {}, + async logic(params, context) { + /** + Used when a package is uninstalled, decreases the download count by 1. + Originally an undocumented endpoint. + The decision to return a '201' is based on how other POST endpoints return, + during a successful event. + This endpoint has now been deprecated, as it serves no useful features, + and on further examination may have been intended as a way to collect + data on users, which is not something we implement. + * Deprecated since v1.0.2 + * see: https://github.com/atom/apm/blob/master/src/uninstall.coffee + * While decoupling HTTP handling from logic, the function has been removed + entirely: https://github.com/pulsar-edit/package-backend/pull/171 + */ + + const sso = new context.sso(); + + return sso.isOk().addContent({ ok: true }); + } +} diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 59844f7a..0eed61ca 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -10,7 +10,9 @@ const endpoints = [ require("./controllers/getThemesFeatured.js"), require("./controllers/getUpdates.js"), require("./controllers/getUsers.js"), - require("./controllers/getusersLogin.js") + require("./controllers/getusersLogin.js"), + require("./controllers/getUsersLoginStars.js"), + require("./controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js") ]; // The CONST Context - Enables access to all other modules within the system @@ -34,7 +36,7 @@ const context = { const genericLimit = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes // Limit each IP per window, 0 disables rate limit - max: process.env.PULSAR_STATUS === "dev" ? 0 : context.config.RATE_LIMIT_GENRIC, + max: process.env.PULSAR_STATUS === "dev" ? 0 : context.config.RATE_LIMIT_GENERIC, standardHeaders: true, // Return rate limit info in headers legacyHeaders: true, // Legacy rate limit info in headers store: new MemoryStore(), // use default memory store From 4a45d42a33d01f0fddcaa90c3e78a20d01495789 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 11 Sep 2023 00:37:05 -0700 Subject: [PATCH 09/53] Installation endpoint --- ...esPackageNameVersionsVersionNameTarball.js | 97 +++++++++++++++++++ src/models/sso.js | 4 +- src/models/ssoRedirect.js | 13 +++ src/setupEndpoints.js | 3 +- 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js create mode 100644 src/models/ssoRedirect.js diff --git a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js new file mode 100644 index 00000000..e91eddb3 --- /dev/null +++ b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js @@ -0,0 +1,97 @@ +const { URL } = require("node:url"); + +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ + "/api/packages/:packageName/versions/:versionName/tarball", + "/api/themes/:packageName/versions/:versionName/tarball" + ], + rateLimit: "generic", + successStatus: 302, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + packageName: (context, req) => { return context.query.packageName(req); }, + versionName: (context, req) => { return context.query.engine(req); } + }, + async logic(params, context) { + + // First ensure our version is valid + if (params.versionName === false) { + // since query.engine gives false if invalid, we can check the truthiness + // but returning early uses less compute, as a false version will never be found + const sso = new context.sso(); + + return sso.notOk().addShort("Not Found"); + } + + const pack = await context.database.getPackageVersionByNameAndVersion( + params.packageName, + params.versionName + ); + + if (!pack.ok) { + const sso = new context.sso(); + + return sso.noOk().addContent(pack.content) + .addCalls("db.getPackageVersionByNameAndVersion", pack); + } + + const save = await context.database.updatePackageIncrementDownloadByName(params.packageName); + + if (!save.ok) { + context.logger.generic(3, "Failed to Update Downloads Count", { + type: "object", + obj: save.content + }); + // We don't want to exit on failed update to download count, only log + } + + // For simplicity, we will redirect the request to gh tarball url + // Allowing downloads to take place via GitHub Servers + // But before this is done, we will preform some checks to ensure the URL is correct/legit + const tarballURL = + pack.content.meta?.tarball_url ?? pack.content.meta?.dist?.tarball ?? ""; + let hostname = ""; + + // Try to extract the hostname + try { + const tbUrl = new URL(tarballURL); + hostname = tbUrl.hostname; + } catch (err) { + context.logger.generic( + 3, + `Malformed tarball URL for version ${params.versionName} of ${params.packageName}` + ); + const sso = new context.sso(); + + return sso.notOk().addContent(err).addShort("Server Error"); + } + + const allowedHostnames = [ + "codeload.github.com", + "api.github.com", + "github.com", + "raw.githubusercontent.com" + ]; + + if ( + !allowedHostnames.includes(hostname) && + process.env.PULSAR_STATUS !== "dev" + ) { + const sso = new context.sso(); + + return sso.notOk().addContent(`Invalid Domain for Download Redirect: ${hostname}`).addShort("Server Error"); + } + + const sso = new context.sso(); + return sso.isOk().addContent(tarballURL); + } +}; diff --git a/src/models/sso.js b/src/models/sso.js index 5833bbb7..eab80f13 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -40,7 +40,9 @@ class SSO { addShort(enumValue) { // TODO Validate enum being assigned - this.short = enumValue; + if (typeof this.short !== "string") { + this.short = enumValue; + } return this; } diff --git a/src/models/ssoRedirect.js b/src/models/ssoRedirect.js new file mode 100644 index 00000000..7e7714f5 --- /dev/null +++ b/src/models/ssoRedirect.js @@ -0,0 +1,13 @@ +const SSO = require("./sso.js"); + +module.exports = +class SSORedirect extends SSO { + constructor() { + super(); + } + + handleSuccess(req, res, context) { + res.redirect(this.content); + context.logger.httpLog(req, res); + } +} diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 0eed61ca..ca238583 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -29,7 +29,8 @@ const context = { common_handler: require("./handlers/common_handler.js"), utils: require("./utils.js"), sso: require("./models/sso.js"), - ssoPaginate: require("./models/sso.js") + ssoPaginate: require("./models/ssoPaginate.js"), + ssoRedirect: require("./models/ssoRedirect.js") }; // Define our Basic Rate Limiters From be5bc7c98871fce6e3f6fa3f2b1d87cf01d344d8 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 11 Sep 2023 00:37:17 -0700 Subject: [PATCH 10/53] More docs --- .../getPackagesPackageNameVersionsVersionNameTarball.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js index e91eddb3..47b03ace 100644 --- a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js +++ b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js @@ -2,7 +2,14 @@ const { URL } = require("node:url"); module.exports = { docs: { - + summary: "Previously undocumented endpoint. Allows for installation of a package.", + responses: [ + { + 302: { + description: "Redirect to the GitHub tarball URL." + } + } + ] }, endpoint: { method: "GET", From 123fcdd0392eb637645acde18580c284f02f3941 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 11 Sep 2023 17:56:19 -0700 Subject: [PATCH 11/53] New endpoint, better error handling, support for messages --- ...ePackagesPackageNameVersionsVersionName.js | 86 +++++++++++++++++++ ...esPackageNameVersionsVersionNameTarball.js | 15 ++-- src/controllers/getStars.js | 6 +- src/controllers/getThemes.js | 2 +- src/controllers/getThemesFeatured.js | 2 +- src/controllers/getUsers.js | 2 +- src/controllers/getUsersLogin.js | 2 +- src/controllers/getUsersLoginStars.js | 6 +- src/models/sso.js | 72 ++++++++++++++-- 9 files changed, 173 insertions(+), 20 deletions(-) create mode 100644 src/controllers/deletePackagesPackageNameVersionsVersionName.js diff --git a/src/controllers/deletePackagesPackageNameVersionsVersionName.js b/src/controllers/deletePackagesPackageNameVersionsVersionName.js new file mode 100644 index 00000000..2c330f14 --- /dev/null +++ b/src/controllers/deletePackagesPackageNameVersionsVersionName.js @@ -0,0 +1,86 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "DELETE", + paths: [ + "/api/packages/:packageName/versions/:versionName", + "/api/themes/:packageName/versions/:versionName" + ], + rateLimit: "auth", + successStatus: 204, + options: { + Allow: "GET, DELETE", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + auth: (context, req) => { return context.query.auth(req); }, + packageName: (context, req) => { return context.query.packageName(req); }, + versionName: (context, req) => { return context.query.engine(req.params.versionName); } + }, + + async logic(params, context) { + // Moving this forward to do the least computationally expensive task first. + // Check version validity + if (params.versionName === false) { + const sso = new context.sso(); + + return sso.notOk().addShort("not_found") + .addMessage("The version provided is invalid."); + } + + // Verify the user has local and remote permissions + const user = await context.auth.verifyAuth(params.auth, context.database); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user) + .addCalls("auth.verifyAuth", user); + } + + // Lets also first check to make sure the package exists + const packageExists = await context.database.getPackageByName(params.packageName, true); + + if (!packageExists.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(packageExists) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packageExists); + } + + const gitowner = await context.vcs.ownership(user.content, packageExists.content); + + if (!gitowner.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(gitowner) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packageExists) + .addCalls("vcs.ownership", gitowner); + } + + // Mark the specified version for deletion, if version is valid + const removeVersion = await context.database.removePackageVersion( + params.packageName, + params.versionName + ); + + if (!removeVersion.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(removeVersion) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packageExists) + .addCalls("vcs.ownership", gitowner) + .addCalls("db.removePackageVersion", removeVersion); + } + + const sso = new context.sso(); + + return sso.isOk().addContent(false); + } +}; diff --git a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js index 47b03ace..2ed8e04c 100644 --- a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js +++ b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js @@ -36,7 +36,8 @@ module.exports = { // but returning early uses less compute, as a false version will never be found const sso = new context.sso(); - return sso.notOk().addShort("Not Found"); + return sso.notOk().addShort("not_found") + .addMessage("The version provided is invalid."); } const pack = await context.database.getPackageVersionByNameAndVersion( @@ -47,7 +48,7 @@ module.exports = { if (!pack.ok) { const sso = new context.sso(); - return sso.noOk().addContent(pack.content) + return sso.notOk().addContent(pack) .addCalls("db.getPackageVersionByNameAndVersion", pack); } @@ -58,6 +59,7 @@ module.exports = { type: "object", obj: save.content }); + // TODO We will probably want to revisit this after rewriting logging // We don't want to exit on failed update to download count, only log } @@ -79,7 +81,9 @@ module.exports = { ); const sso = new context.sso(); - return sso.notOk().addContent(err).addShort("Server Error"); + return sso.notOk().addContent(err) + .addShort("server_error") + .addMessage(`The URL to download this package seems invalid: ${tarballURL}.`); } const allowedHostnames = [ @@ -95,10 +99,11 @@ module.exports = { ) { const sso = new context.sso(); - return sso.notOk().addContent(`Invalid Domain for Download Redirect: ${hostname}`).addShort("Server Error"); + return sso.notOk().addShort("server_error") + .addMessage(`Invalid Domain for Download Redirect: ${hostname}`); } - const sso = new context.sso(); + const sso = new context.ssoRedirect(); return sso.isOk().addContent(tarballURL); } }; diff --git a/src/controllers/getStars.js b/src/controllers/getStars.js index 11f0712b..cf751284 100644 --- a/src/controllers/getStars.js +++ b/src/controllers/getStars.js @@ -36,7 +36,7 @@ module.exports = { if (!user.ok) { const sso = new context.sso(); - return sso.notOk().addContent(user.content).addCalls("auth.verifyAuth", user); + return sso.notOk().addContent(user).addCalls("auth.verifyAuth", user); } let userStars = await context.database.getStarredPointersByUserID(user.content.id); @@ -44,7 +44,7 @@ module.exports = { if (!userStars.ok) { const sso = new context.sso(); - return sso.notOk().addContent(userStars.content) + return sso.notOk().addContent(userStars) .addCalls("auth.verifyAuth", user) .addCalls("db.getStarredPointersByUserID", userStars); } @@ -63,7 +63,7 @@ module.exports = { if (!packCol.ok) { const sso = new context.sso(); - return sso.notOk().addContent(packCol.content) + return sso.notOk().addContent(packCol) .addCalls("auth.verifyAuth", user) .addCalls("db.getStarredPointersByUserID", userStars) .addCalls("db.getPackageCollectionByID", packCol); diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index 10a801aa..47400a98 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -31,7 +31,7 @@ module.exports = { if (!packages.ok) { const sso = new context.sso(); - return sso.notOk().addContent(packages.content).addCalls("db.getSortedPackages", packages); + return sso.notOk().addContent(packages).addCalls("db.getSortedPackages", packages); } const packObjShort = await context.utils.constructPackageObjectShort( diff --git a/src/controllers/getThemesFeatured.js b/src/controllers/getThemesFeatured.js index 37aad164..360d1047 100644 --- a/src/controllers/getThemesFeatured.js +++ b/src/controllers/getThemesFeatured.js @@ -19,7 +19,7 @@ module.exports = { if (!col.ok) { const sso = new context.sso(); - return sso.notOk().addContent(col.content).addCalls("db.getFeaturedThemes", col); + return sso.notOk().addContent(col).addCalls("db.getFeaturedThemes", col); } const newCol = await utils.constructPackageObjectShort(col.content); diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js index 389285d6..b49ddc1a 100644 --- a/src/controllers/getUsers.js +++ b/src/controllers/getUsers.js @@ -48,7 +48,7 @@ module.exports = { if (!user.ok) { const sso = new context.sso(); - return sso.notOk().addContent(user.content) + return sso.notOk().addContent(user) .addCalls("auth.verifyAuth", user); } diff --git a/src/controllers/getUsersLogin.js b/src/controllers/getUsersLogin.js index d5fdfc54..e49df4a5 100644 --- a/src/controllers/getUsersLogin.js +++ b/src/controllers/getUsersLogin.js @@ -36,7 +36,7 @@ module.exports = { if (!user.ok) { const sso = new context.sso(); - return sso.notOk().addContent(user.content) + return sso.notOk().addContent(user) .addCalls("db.getUserByName", user); } diff --git a/src/controllers/getUsersLoginStars.js b/src/controllers/getUsersLoginStars.js index e2b0196c..7c137a2f 100644 --- a/src/controllers/getUsersLoginStars.js +++ b/src/controllers/getUsersLoginStars.js @@ -29,7 +29,7 @@ module.exports = { if (!user.ok) { const sso = new context.sso(); - return sso.notOk().addContent(user.content) + return sso.notOk().addContent(user) .addCalls("db.getUserByName", user); } @@ -38,7 +38,7 @@ module.exports = { if (!pointerCollection.ok) { const sso = new context.sso(); - return sso.notOk().addContent(pointerCollection.content) + return sso.notOk().addContent(pointerCollection) .addCalls("db.getUserByName", user) .addCalls("db.getStarredPointersByUserID", pointerCollection); } @@ -65,7 +65,7 @@ module.exports = { if (!packageCollection.ok) { const sso = new context.sso(); - return sso.notOk().addContent(packageCollection.content) + return sso.notOk().addContent(packageCollection) .addCalls("db.getUserByName", user) .addCalls("db.getStarredPointersByUserID", pointerCollection) .addCalls("db.getPackageCollectionByID", packageCollection); diff --git a/src/models/sso.js b/src/models/sso.js index eab80f13..6b6a3eba 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -1,11 +1,33 @@ const { performance } = require("node:perf_hooks"); +const validEnums = [ + "not_found", + "server_error", + "not_supported" +]; + +const enumDetails = { + "not_found": { + code: 404, + message: "Not Found" + }, + "server_error": { + code: 500, + message: "Application Error" + }, + "not_supported": { + code: 501, + message: "While under development this feature is not supported." + } +}; + module.exports = class SSO { constructor() { this.ok = false; this.content = {}; this.short = ""; + this.message = ""; this.safeContent = false; this.successStatusCode = 200; this.calls = {}; @@ -39,13 +61,18 @@ class SSO { } addShort(enumValue) { - // TODO Validate enum being assigned - if (typeof this.short !== "string") { + if (typeof this.short !== "string" && typeof enumValue === "string" && validEnums.includes(enumValue)) { + // Only assign short once this.short = enumValue; } return this; } + addMessage(msg) { + this.message = msg; + return this; + } + addGoodStatus(status) { this.successStatusCode = status; return this; @@ -62,14 +89,49 @@ class SSO { } handleError(req, res, context) { - // TODO Get rid of the common error handler, and put all the logic here - await context.common_handler.handleError(req, res, this.content); + + let shortToUse, msgToUse; + + if (typeof this.short === "string") { + // Use the short given to us during the build stage + shortToUse = this.short; + + } else if (typeof this.content?.short === "string") { + // Use the short that's bubbled up from other calls + shortToUse = this.content.short; + + } else { + // Use the default short + shortToUse = "server_error"; + } + + // Now that we have our short, we must determine the text of our message. + msgToUse = enumDetails[shortToUse].msg; + + if (typeof this.message === "string") { + msgToUse += `: ${this.message}`; + } + // TODO We should make use of our `calls` here. + // Not only for logging more details. + // But we also could use this to get more information to return. Such as + // providing helpful error logs and such. + + res.status(enumDetails[shortToUse].code).json({ + message: msgToUse + }); + + // TODO Log our error too! + context.logger.httpLog(req, res); return; } handleSuccess(req, res, context) { - res.status(this.successStatusCode).json(this.content); + if (typeof this.content === "boolean" && this.content === false) { + res.status(this.successStatusCode).send(); + } else { + res.status(this.successStatusCode).json(this.content); + } context.logger.httpLog(req, res); return; } From c89ce740e34ae87ebd83e18ba630b8d7d1671970 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 11 Sep 2023 19:12:49 -0700 Subject: [PATCH 12/53] More endpoints, new http cycle functions --- ...tPackagesPackageNameVersionsVersionName.js | 52 ++++ ...esPackageNameVersionsVersionNameTarball.js | 2 +- src/controllers/getUsers.js | 4 +- .../postPackagesPackageNameVersions.js | 225 ++++++++++++++++++ src/models/sso.js | 12 +- src/setupEndpoints.js | 33 ++- 6 files changed, 317 insertions(+), 11 deletions(-) create mode 100644 src/controllers/getPackagesPackageNameVersionsVersionName.js create mode 100644 src/controllers/postPackagesPackageNameVersions.js diff --git a/src/controllers/getPackagesPackageNameVersionsVersionName.js b/src/controllers/getPackagesPackageNameVersionsVersionName.js new file mode 100644 index 00000000..26389f42 --- /dev/null +++ b/src/controllers/getPackagesPackageNameVersionsVersionName.js @@ -0,0 +1,52 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ + "/api/packages/:packageName/versions/:versionName", + "/api/themes/:packageName/versions/:versionName" + ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET, DELETE", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + packageName: (context, req) => { return context.query.packageName(req); }, + versionName: (context, req) => { return context.query.engine(req.params.versionName); } + }, + + async logic(params, context) { + // Check the truthiness of the returned query engine + if (params.versionName === false) { + const sso = new context.sso(); + + return sso.notOk().addShort("not_found") + .addMessage("The version provided is invalid."); + } + + // Now we know the version is a valid semver. + + const pack = await context.database.getPackageVersionByNameAndVersion( + params.packageName, + params.versionName + ); + + if (!pack.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(pack) + .addCalls("db.getPackageVersionByNameAndVersion", pack); + } + + const packRes = await context.utils.constructPackageObjectJSON(pack.content); + + const sso = new context.sso(); + + return sso.isOk().addContent(packRes); + } +} diff --git a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js index 2ed8e04c..de203dcc 100644 --- a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js +++ b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js @@ -26,7 +26,7 @@ module.exports = { }, params: { packageName: (context, req) => { return context.query.packageName(req); }, - versionName: (context, req) => { return context.query.engine(req); } + versionName: (context, req) => { return context.query.engine(req.params.versionName); } }, async logic(params, context) { diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js index b49ddc1a..f16a2265 100644 --- a/src/controllers/getUsers.js +++ b/src/controllers/getUsers.js @@ -27,13 +27,13 @@ module.exports = { params: { auth: (context, req) => { return context.query.auth(req); } }, - preLogic(req, res, context) { + async preLogic(req, res, context) { res.header("Access-Control-Allow-Methods", "GET"); res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Access-Control-Allow-Credentials"); res.header("Access-Control-Allow-Origin", "https://web.pulsar-edit.dev"); res.header("Access-Control-Allow-Credentials", true); }, - postLogic(req, res, context) { + async postLogic(req, res, context) { res.set({ "Access-Control-Allow-Credentials": true }); }, diff --git a/src/controllers/postPackagesPackageNameVersions.js b/src/controllers/postPackagesPackageNameVersions.js new file mode 100644 index 00000000..bd37414a --- /dev/null +++ b/src/controllers/postPackagesPackageNameVersions.js @@ -0,0 +1,225 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "POST", + paths: [ + "/api/packages/:packageName/versions", + "/api/themes/:packageName/versions" + ], + rateLimit: "auth", + successStatus: 201, + options: { + Allow: "POST", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + rename: (context, req) => { return context.query.rename(req); }, + auth: (context, req) => { return context.query.auth(req); }, + packageName: (context, req) => { return context.query.packageName(req); } + }, + async postReturnHTTP(req, res, context, obj) { + // We use postReturnHTTP to ensure the user doesn't wait on these other actions + await context.webhook.alertPublishVersion(obj.webhook.pack, obj.webhook.user); + + // Now to call for feature detection + let features = await context.vcs.featureDetection( + obj.featureDetection.user, + obj.featureDetection.ownerRepo, + obj.featureDetection.service + ); + + if (!features.ok) { + context.logger.generic(3, features); + return; + } + + // THen we know we don't need to apply any special features for a standard + // package, so we will check that early + if (features.content.standard) { + return; + } + + let featureApply = await context.database.applyFeatures( + features.content, + obj.webhook.pack.name, + obj.webhook.pack.version + ); + + if (!featureApply.ok) { + logger.generic(3, featureApply); + return; + } + + // Otherwise we have completed successfully, while we could log, lets return + return; + }, + + async logic(params, context) { + // On renaming: + // When a package is being renamed, we will expect that packageName will + // match a previously published package. + // But then the `name` of their `package.json` will be different. + // And if they are, we expect that `rename` is true. Because otherwise it will fail. + // That's the methodology, the logic here just needs to catch up. + + const user = await context.auth.verifyAuth(params.auth, context.database); + + if (!user.ok) { + // TODO LOG + const sso = new context.sso(); + + return sso.notOk().addShort("unauthorized") + .addContent(user) + .addCalls("auth.verifyAuth", user) + .addMessage("User Authentication Failed when attempting to publish package version!"); + } + + context.logger.generic( + 6, + `${user.content.username} Attempting to publish a new package version - ${param.packageName}` + ); + + // To support a rename, we need to check if they have permissions over this + // packages new name. Which means we have to check if they have ownership AFTER + // we collect it's data. + + const packExists = await context.database.getPackageByName(params.packageName, true); + + if (!packExists.ok) { + // TODO LOG + const sso = new context.sso(); + + return sso.notOk().addShort("not_found") + .addContent(packExists) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packExists) + .addMessage("The server was unable to locate your package when publishing a new version."); + } + + // Get `owner/repo` string format from package. + let ownerRepo = context.utils.getOwnerRepoFromPackage(packExists.content.data); + + // Using our new VCS Service + // TODO: The "git" service shouldn't always be hardcoded. + let packMetadata = await vcs.newVersionData(user.content, ownerRepo, "git"); + + if (!packMetadata.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(packMetadata) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packExists) + .addCalls("vcs.newVersionData", packMetadata); + } + + const newName = packMetadata.content.name; + + const currentName = packExists.content.name; + if (newName !== currentName && !params.rename) { + const sso = new context.sso(); + + return sso.notOk().addShort("bad_repo") + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packExists) + .addCalls("vcs.newVersionData", packMetadata) + .addMessage("Package name doesn't match local name, with rename false."); + } + + // Else we will continue, and trust the name provided from the package as being accurate. + // And now we can ensure the user actually owns this repo, with our updated name. + + // By passing `packMetadata` explicitely, it ensures that data we use to check + // ownership is fresh, allowing for things like a package rename. + + const gitowner = await context.vcs.ownership(user.content, packMetadata.content); + + if (!gitowner.ok) { + const sso = new context.sso(); + + return sso.notOk().addShort("unauthorized") + .addContent(gitowner) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packExists) + .addCalls("vcs.newVersionData", packMetadata) + .addCalls("vcs.ownership", gitowner) + .addMessage("User failed git ownership check!"); + } + + // Now the only thing left to do, is add this new version with the name from the package. + // And check again if the name is incorrect, since it'll need a new entry onto the names. + + const rename = newName !== currentName && params.rename; + + if (rename) { + // Before allowing the rename of a package, ensure the new name isn't banned + const isBanned = await context.utils.isPackageNameBanned(newName); + + if (isBanned.ok) { + const sso = new context.sso(); + + return sso.notOk().addShort("server_error") + .addContent(isBanned) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packExists) + .addCalls("vcs.newVersionData", packMetadata) + .addCalls("vcs.ownership", gitowner) + .addMessage("This package Name is Banned on the Pulsar Registry"); + } + + const isAvailable = await context.database.packageNameAvailability(newName); + + if (isAvailable.ok) { + const sso = new context.sso(); + + return sso.notOk().addShort("server_error") + .addContent(isAvailable) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packExists) + .addCalls("vcs.newVersionData", packMetadata) + .addCalls("vcs.ownership", gitowner) + .addCalls("db.packageNameAvailability", isAvailable) + .addMessage(`The Package Name: ${newName} is not available.`); + } + } + + // Now add the new version key + const addVer = await context.database.insertNewPackageVersion( + packMetadata.content, + rename ? currentName : null + ); + + if (!addVer.ok) { + // TODO Use hardcoded message until we can verify messages from db are safe + const sso = new context.sso(); + + return sso.notOk().addShort("server_error") + .addContent(addVer) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packExists) + .addCalls("vcs.newVersionData", packMetadata) + .addCalls("vcs.ownership", gitowner) + .addCalls("db.packageNameAvailability", isAvailable) + .addCalls("db.insertNewPackageVersion", addVer) + .addMessage("Failed to add the new package version to the database."); + } + + const sso = new context.sso(); + + // TODO the following reduces the good things an object builder gets us + sso.webhook = { + pack: packMetadata.content, + user: user.content + }; + + sso.featureDetection = { + user: user.content, + service: "git", // TODO stop hardcoding git + ownerRepo: ownerRepo + }; + + return sso.isOk().addContent(addVer.content); + } +}; diff --git a/src/models/sso.js b/src/models/sso.js index 6b6a3eba..d0489cbb 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -3,7 +3,9 @@ const { performance } = require("node:perf_hooks"); const validEnums = [ "not_found", "server_error", - "not_supported" + "not_supported", + "unauthorized", + "bad_repo" ]; const enumDetails = { @@ -18,6 +20,14 @@ const enumDetails = { "not_supported": { code: 501, message: "While under development this feature is not supported." + }, + "unauthorized": { + code: 401, + message: "Unauthorized" + }, + "bad_repo": { + code: 400, + message: "That repo does not exist, or is inaccessible" } }; diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index ca238583..1f53c268 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -12,7 +12,11 @@ const endpoints = [ require("./controllers/getUsers.js"), require("./controllers/getusersLogin.js"), require("./controllers/getUsersLoginStars.js"), - require("./controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js") + require("./controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js"), + require("./controllers/deletePackagesPackageNameVersionsVersionName.js"), + require("./controllers/getPackagesPackageNameVersionsVersionName.js"), + require("./controllers/getPackagesPackageNameVersionsVersionNameTarball.js"), + require("./controllers/postPackagesPackageNameVersions.js") ]; // The CONST Context - Enables access to all other modules within the system @@ -95,18 +99,23 @@ for (const node of endpoints) { } if (typeof node.preLogic === "function") { - node.preLogic(req, res, context); + await node.preLogic(req, res, context); } let obj = await node.logic(params, context); if (typeof node.postLogic === "function") { - node.postLogic(req, res, context); + await node.postLogic(req, res, context); } obj.addGoodStatus(node.endpoint.successStatus); obj.handleReturnHTTP(req, res, context); + + if (typeof node.postReturnHTTP === "function") { + await node.postReturnHTTP(req, res, context, obj); + } + return; }); case "POST": @@ -118,18 +127,23 @@ for (const node of endpoints) { } if (typeof node.preLogic === "function") { - node.preLogic(req, res, context); + await node.preLogic(req, res, context); } let obj = await node.logic(params, context); if (typeof node.postLogic === "function") { - node.postLogic(req, res, context); + await node.postLogic(req, res, context); } obj.addGoodStatus(node.endpoint.successStatus); obj.handleReturnHTTP(req, res, context); + + if (typeof node.postReturnHTTP === "function") { + await node.postReturnHTTP(req, res, context, obj); + } + return; }); case "DELETE": @@ -141,18 +155,23 @@ for (const node of endpoints) { } if (typeof node.preLogic === "function") { - node.preLogic(req, res, context); + await node.preLogic(req, res, context); } let obj = await node.logic(params, context); if (typeof node.postLogic === "function") { - node.postLogic(req, res, context); + await node.postLogic(req, res, context); } obj.addGoodStatus(node.endpoint.successStatus); obj.handleReturnHTTP(req, res, context); + + if (typeof node.postReturnHTTP === "function") { + await node.postReturnHTTP(req, res, context, obj); + } + return; }); default: From 445adf18a437bac80431f2ca9755dc350408b86a Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 11 Sep 2023 23:06:49 -0700 Subject: [PATCH 13/53] Begin work on tests, transferring over everything possible --- jest.OUTDATED.config.js | 54 +++++++++++++++++ jest.config.js | 55 +++++++---------- package.json | 2 +- src/auth.js | 8 +-- src/context.js | 19 ++++++ .../deletePackagesPackageNameStar.js | 46 ++++++++++++++ src/controllers/endpoints.js | 19 ++++++ .../getPackagesPackageNameStargazers.js | 58 ++++++++++++++++++ src/controllers/getRoot.js | 27 +++++++++ src/controllers/getStars.js | 4 +- src/controllers/getThemesFeatured.js | 4 +- .../postPackagesPackageNameStar.js | 60 +++++++++++++++++++ src/database.js | 6 +- src/handlers/nonMigratedHandlers.js | 48 +++++++++++++++ src/models/sso.js | 16 +++-- src/models/ssoHTML.js | 13 ++++ src/server.js | 2 +- src/setupEndpoints.js | 42 +++---------- .../database/applyFeatures.test.js | 0 .../database/extensionFilter.test.js | 0 {test => tests/helpers}/global.setup.jest.js | 28 +++++++++ .../helpers}/handlers.setup.jest.js | 2 +- .../helpers}/httpMock.helper.jest.js | 6 +- .../http/root.test.js | 2 +- .../http}/stars.handler.integration.test.js | 12 ++-- .../unit/config.test.js | 2 +- tests/unit/endpoints.test.js | 40 +++++++++++++ .../postPackagesPackageNameVersions.test.js | 13 ++++ ...VersionsVersionNameEventsUninstall.test.js | 24 ++++++++ .../unit/query.test.js | 2 +- .../unit/utils.test.js | 6 +- .../unit/webhook.test.js | 6 +- 32 files changed, 522 insertions(+), 104 deletions(-) create mode 100644 jest.OUTDATED.config.js create mode 100644 src/context.js create mode 100644 src/controllers/deletePackagesPackageNameStar.js create mode 100644 src/controllers/endpoints.js create mode 100644 src/controllers/getPackagesPackageNameStargazers.js create mode 100644 src/controllers/getRoot.js create mode 100644 src/controllers/postPackagesPackageNameStar.js create mode 100644 src/handlers/nonMigratedHandlers.js create mode 100644 src/models/ssoHTML.js rename {test => tests}/database/applyFeatures.test.js (100%) rename {test => tests}/database/extensionFilter.test.js (100%) rename {test => tests/helpers}/global.setup.jest.js (59%) rename {test => tests/helpers}/handlers.setup.jest.js (98%) rename {test => tests/helpers}/httpMock.helper.jest.js (93%) rename test/root.handler.integration.test.js => tests/http/root.test.js (92%) rename {test => tests/http}/stars.handler.integration.test.js (78%) rename test/config.unit.test.js => tests/unit/config.test.js (97%) create mode 100644 tests/unit/endpoints.test.js create mode 100644 tests/unit/postPackagesPackageNameVersions.test.js create mode 100644 tests/unit/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js rename test/query.unit.test.js => tests/unit/query.test.js (99%) rename test/utils.unit.test.js => tests/unit/utils.test.js (98%) rename test/webhook.unit.test.js => tests/unit/webhook.test.js (94%) diff --git a/jest.OUTDATED.config.js b/jest.OUTDATED.config.js new file mode 100644 index 00000000..6f29be37 --- /dev/null +++ b/jest.OUTDATED.config.js @@ -0,0 +1,54 @@ +const config = { + setupFilesAfterEnv: ["/test/global.setup.jest.js"], + verbose: true, + collectCoverage: true, + coverageReporters: ["text", "clover"], + coveragePathIgnorePatterns: [ + "/src/tests_integration/fixtures/**", + "/test/fixtures/**", + "/node_modules/**", + ], + projects: [ + { + displayName: "Integration-Tests", + globalSetup: "/node_modules/@databases/pg-test/jest/globalSetup", + globalTeardown: + "/node_modules/@databases/pg-test/jest/globalTeardown", + setupFilesAfterEnv: [ + "/test/handlers.setup.jest.js", + "/test/global.setup.jest.js", + ], + testMatch: [ + "/test/*.integration.test.js", + "/test/database/**/**.js", + ], + }, + { + displayName: "Unit-Tests", + setupFilesAfterEnv: ["/test/global.setup.jest.js"], + testMatch: [ + "/test/*.unit.test.js", + "/test/handlers/**/**.test.js", + "/test/controllers/**.test.js" + ], + }, + { + displayName: "VCS-Tests", + setupFilesAfterEnv: ["/test/global.setup.jest.js"], + testMatch: ["/test/*.vcs.test.js"], + }, + { + displayName: "Handler-Tests", + globalSetup: "/node_modules/@databases/pg-test/jest/globalSetup", + globalTeardown: + "/node_modules/@databases/pg-test/jest/globalTeardown", + setupFilesAfterEnv: [ + "/test/handlers.setup.jest.js", + "/test/global.setup.jest.js", + ], + testMatch: ["/test/*.handler.integration.test.js"], + }, + ], +}; + +module.exports = config; diff --git a/jest.config.js b/jest.config.js index 6f29be37..9a9c9832 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,54 +1,41 @@ const config = { - setupFilesAfterEnv: ["/test/global.setup.jest.js"], + setupFilesAfterEnv: [ + "/tests/helpers/global.setup.jest.js" + ], verbose: true, collectCoverage: true, - coverageReporters: ["text", "clover"], + coverageReporters: [ + "text", + "clover" + ], coveragePathIgnorePatterns: [ - "/src/tests_integration/fixtures/**", - "/test/fixtures/**", - "/node_modules/**", + "/tests/fixtures/**", + "/node_modules/**" ], projects: [ { displayName: "Integration-Tests", globalSetup: "/node_modules/@databases/pg-test/jest/globalSetup", - globalTeardown: - "/node_modules/@databases/pg-test/jest/globalTeardown", + globalTeardown: "/node_modules/@databases/pg-test/jest/globalTeardown", setupFilesAfterEnv: [ - "/test/handlers.setup.jest.js", - "/test/global.setup.jest.js", + "/tests/helpers/handlers.setup.jest.js", + "/tests/helpers/global.setup.jest.js" ], testMatch: [ - "/test/*.integration.test.js", - "/test/database/**/**.js", - ], + "/tests/database/**.test.js", + "/tests/http/**.test.js", + ] }, { displayName: "Unit-Tests", - setupFilesAfterEnv: ["/test/global.setup.jest.js"], - testMatch: [ - "/test/*.unit.test.js", - "/test/handlers/**/**.test.js", - "/test/controllers/**.test.js" - ], - }, - { - displayName: "VCS-Tests", - setupFilesAfterEnv: ["/test/global.setup.jest.js"], - testMatch: ["/test/*.vcs.test.js"], - }, - { - displayName: "Handler-Tests", - globalSetup: "/node_modules/@databases/pg-test/jest/globalSetup", - globalTeardown: - "/node_modules/@databases/pg-test/jest/globalTeardown", setupFilesAfterEnv: [ - "/test/handlers.setup.jest.js", - "/test/global.setup.jest.js", + "/tests/helpers/global.setup.jest.js" ], - testMatch: ["/test/*.handler.integration.test.js"], - }, - ], + testMatch: [ + "/tests/unit/**.test.js" + ] + } + ] }; module.exports = config; diff --git a/package.json b/package.json index 2098d42e..b0c946c1 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test:unit": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Unit-Tests", "test:integration": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests", "start:dev": "cross-env PULSAR_STATUS=dev node ./src/dev_server.js", - "test": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests Unit-Tests VCS-Tests", + "test": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests Unit-Tests", "test:vcs": "cross-env NODE_ENV=test PULSAR_STATUS=dev MOCK_GH=false jest --selectProjects VCS-Tests", "test:handlers": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Handler-Tests", "api-docs": "quick-webserver-docs -i ./src/main.js -o ./docs/reference/API_Definition.md", diff --git a/src/auth.js b/src/auth.js index 0b5c73a8..afd7c691 100644 --- a/src/auth.js +++ b/src/auth.js @@ -23,7 +23,7 @@ async function verifyAuth(token, db) { return { ok: false, - short: "Bad Auth", + short: "unauthorized", content: "User Token not a valid format.", }; } @@ -47,7 +47,7 @@ async function verifyAuth(token, db) { case 401: // When the user provides bad authentication, lets tell them it's bad auth. logger.generic(6, "auth.verifyAuth() API Call Returning Bad Auth"); - return { ok: false, short: "Bad Auth", content: userData }; + return { ok: false, short: "unauthorized", content: userData }; break; default: logger.generic( @@ -56,7 +56,7 @@ async function verifyAuth(token, db) { { type: "object", obj: userData } ); - return { ok: false, short: "Server Error", content: userData }; + return { ok: false, short: "server_error", content: userData }; } } @@ -99,7 +99,7 @@ async function verifyAuth(token, db) { return { ok: false, - short: "Server Error", + short: "server_error", content: "An unexpected Error occured while verifying your user.", }; } diff --git a/src/context.js b/src/context.js new file mode 100644 index 00000000..48fbb8e3 --- /dev/null +++ b/src/context.js @@ -0,0 +1,19 @@ +// The CONST Context - Enables access to all other modules within the system +// By passing this object to everywhere needed allows not only easy access +// but greater control in mocking these later on +module.exports = { + logger: require("./logger.js"), + database: require("./database.js"), + webhook: require("./webhook.js"), + server_version: require("../package.json").version, + query: require("./query.js"), + vcs: require("./vcs.js"), + config: require("./config.js").getConfig(), + common_handler: require("./handlers/common_handler.js"), + utils: require("./utils.js"), + auth: require("./auth.js"), + sso: require("./models/sso.js"), + ssoPaginate: require("./models/ssoPaginate.js"), + ssoRedirect: require("./models/ssoRedirect.js"), + ssoHTML: require("./models/ssoHTML.js") +}; diff --git a/src/controllers/deletePackagesPackageNameStar.js b/src/controllers/deletePackagesPackageNameStar.js new file mode 100644 index 00000000..3c2d9e06 --- /dev/null +++ b/src/controllers/deletePackagesPackageNameStar.js @@ -0,0 +1,46 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "DELETE", + paths: [ + "/api/packages/:packageName/star", + "/api/themes/:packageName/star" + ], + rateLimit: "auth", + successStatus: 201, + options: { + Allow: "DELETE, POST", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + auth: (context, req) => { return context.query.auth(req); }, + packageName: (context, req) => { return context.query.packageName(req); } + }, + async logic(params, context) { + const user = await context.auth.verifyAuth(params.auth, context.database); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user) + .addCalls("auth.verifyAuth", user); + } + + const unstar = await context.database.updateDecrementStar(user.content, params.packageName); + + if (!unstar.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(unstar) + .addCalls("auth.verifyAuth", user) + .addCalls("db.updateDecrementStar", unstar); + } + + const sso = new context.sso(); + + return sso.isOk().addContent(false); + } +}; diff --git a/src/controllers/endpoints.js b/src/controllers/endpoints.js new file mode 100644 index 00000000..13e83223 --- /dev/null +++ b/src/controllers/endpoints.js @@ -0,0 +1,19 @@ +// Exports all the endpoints that need to be required +module.exports = [ + require("./deletePackagesPackageNameStar.js"), + require("./deletePackagesPackageNameVersionsVersionName.js"), + require("./getPackagesPackageNameStargazers.js"), + require("./getPackagesPackageNameVersionsVersionName.js"), + require("./getPackagesPackageNameVersionsVersionNameTarball.js"), + require("./getRoot.js"), + require("./getStars.js"), + require("./getThemes.js"), + require("./getThemesFeatured.js"), + require("./getUpdates.js"), + require("./getUsers.js"), + require("./getUsersLogin.js"), + require("./getUsersLoginStars.js"), + require("./postPackagesPackageNameStar.js"), + require("./postPackagesPackageNameVersions.js"), + require("./postPackagesPackageNameVersionsVersionNameEventsUninstall.js") +]; diff --git a/src/controllers/getPackagesPackageNameStargazers.js b/src/controllers/getPackagesPackageNameStargazers.js new file mode 100644 index 00000000..afc17565 --- /dev/null +++ b/src/controllers/getPackagesPackageNameStargazers.js @@ -0,0 +1,58 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ + "/api/packages/:packageName/stargazers", + "/api/themes/:packageName/stargazers" + ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + packageName: (context, req) => { return context.query.packageName(req); } + }, + + async logic(params, context) { + // The following can't be executed in user mode because we need the pointer + const pack = await context.database.getPackageByName(params.packageName); + + if (!pack.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(pack) + .addCalls("db.getPackageByName", pack); + } + + const stars = await context.database.getStarringUsersByPointer(pack.content); + + if (!stars.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(stars) + .addCalls("db.getPackageByName", pack) + .addCalls("db.getStarringUsersByPointer", stars); + } + + const gazers = await context.database.getUserCollectionById(stars.content); + + if (!gazers.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(gazers) + .addCalls("db.getPackageByName", pack) + .addCalls("db.getStarringUsersByPointer", stars) + .addCalls("db.getUserCollectionById", gazers); + } + + const sso = new context.sso(); + + return sso.isOk().addContent(gazers.content); + } +}; diff --git a/src/controllers/getRoot.js b/src/controllers/getRoot.js new file mode 100644 index 00000000..3caec6e2 --- /dev/null +++ b/src/controllers/getRoot.js @@ -0,0 +1,27 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: {}, + async logic(params, context) { + const str = ` +

Server is up and running Version ${context.server_version}

+ Swagger UI
+ Documentation + `; + + const sso = new context.ssoHTML(); + + return sso.isOk().addContent(str); + } +}; diff --git a/src/controllers/getStars.js b/src/controllers/getStars.js index cf751284..c73556d4 100644 --- a/src/controllers/getStars.js +++ b/src/controllers/getStars.js @@ -36,7 +36,9 @@ module.exports = { if (!user.ok) { const sso = new context.sso(); - return sso.notOk().addContent(user).addCalls("auth.verifyAuth", user); + return sso.notOk().addContent(user) + .addMessage("Please update your token if you haven't done so recently.") + .addCalls("auth.verifyAuth", user); } let userStars = await context.database.getStarredPointersByUserID(user.content.id); diff --git a/src/controllers/getThemesFeatured.js b/src/controllers/getThemesFeatured.js index 360d1047..932f47f8 100644 --- a/src/controllers/getThemesFeatured.js +++ b/src/controllers/getThemesFeatured.js @@ -2,8 +2,8 @@ module.exports = { endpoint: { method: "GET", paths: [ "/api/themes/featured" ], - rate_limit: "generic", - success_status: 200, + rateLimit: "generic", + successStatus: 200, options: { Allow: "GET", "X-Content-Type-Options": "nosniff" diff --git a/src/controllers/postPackagesPackageNameStar.js b/src/controllers/postPackagesPackageNameStar.js new file mode 100644 index 00000000..5f171c05 --- /dev/null +++ b/src/controllers/postPackagesPackageNameStar.js @@ -0,0 +1,60 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "POST", + paths: [ + "/api/packages/:packageName/star", + "/api/themes/:packageName/star" + ], + rateLimit: "auth", + successStatus: 200, + options: { + Allow: "DELETE, POST", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + auth: (context, req) => { return context.query.auth(req); }, + packageName: (context, req) => { return context.query.packageName(req); } + }, + async logic(params, context) { + const user = await context.auth.verifyAuth(params.auth, context.database); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user) + .addCalls("auth.verifyAuth", user); + } + + const star = await context.database.updateIncrementStar(user.content, params.packageName); + + if (!star.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(star) + .addCalls("auth.verifyAuth", user) + .addCalls("db.updateIncrementStar", star); + } + + // Now with a success we want to return the package back in this query + let pack = await context.database.getPackageByName(params.packageName, true); + + if (!pack.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(pack) + .addCalls("auth.verifyAuth", user) + .addCalls("db.updateIncrementStar", star) + .addCalls("db.getPackageByName", pack); + } + + pack = await context.utils.constructPackageObjectFull(pack.content); + + const sso = new context.sso(); + + return sso.isOk().addContent(pack); + } +}; diff --git a/src/database.js b/src/database.js index 28ec4a81..b94667e8 100644 --- a/src/database.js +++ b/src/database.js @@ -1128,7 +1128,7 @@ async function getUserByNodeID(id) { return { ok: false, content: `Unable to get User By NODE_ID: ${id}`, - short: "Not Found", + short: "not_found", }; } @@ -1137,13 +1137,13 @@ async function getUserByNodeID(id) { : { ok: false, content: `Unable to get User By NODE_ID: ${id}`, - short: "Server Error", + short: "server_error", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } diff --git a/src/handlers/nonMigratedHandlers.js b/src/handlers/nonMigratedHandlers.js new file mode 100644 index 00000000..fb25d803 --- /dev/null +++ b/src/handlers/nonMigratedHandlers.js @@ -0,0 +1,48 @@ +const oauth_handler = require("./oauth_handler.js"); + +function setup(app, genericLimit, authLimit) { + + app.get("/api/login", authLimit, async (req, res) => { + await oauth_handler.getLogin(req, res); + }); + + app.options("/api/login", genericLimit, async (req, res) => { + res.header({ + Allow: "GET", + "X-Content-Type-Options": "nosniff", + }); + res.sendStatus(204); + }); + + //============================================================= + + app.get("/api/oauth", authLimit, async (req, res) => { + await oauth_handler.getOauth(req, res); + }); + + app.options("/api/oauth", genericLimit, async (req, res) => { + res.header({ + Allow: "GET", + "X-Content-Type-Options": "nosniff", + }); + res.sendStatus(204); + }); + + //============================================================= + + app.get("/api/pat", authLimit, async (req, res) => { + await oauth_handler.getPat(req, res); + }); + + app.options("/api/pat", genericLimit, async (req, res) => { + res.header({ + Allow: "GET", + "X-Content-Type-Options": "nosniff", + }); + res.sendStatus(204); + }); + + return app; +} + +module.exports = setup; diff --git a/src/models/sso.js b/src/models/sso.js index d0489cbb..14dcd1ac 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -99,14 +99,16 @@ class SSO { } handleError(req, res, context) { + console.log(this.content); + console.log(this.calls); - let shortToUse, msgToUse; + let shortToUse, msgToUse, codeToUse; - if (typeof this.short === "string") { + if (typeof this.short === "string" && this.short.length > 0) { // Use the short given to us during the build stage shortToUse = this.short; - } else if (typeof this.content?.short === "string") { + } else if (typeof this.content?.short === "string" && this.content.short.length > 0) { // Use the short that's bubbled up from other calls shortToUse = this.content.short; @@ -116,9 +118,11 @@ class SSO { } // Now that we have our short, we must determine the text of our message. - msgToUse = enumDetails[shortToUse].msg; + msgToUse = enumDetails[shortToUse]?.message ?? `Server Error: From ${shortToUse}`; - if (typeof this.message === "string") { + codeToUse = enumDetails[shortToUse]?.code ?? 500; + + if (typeof this.message === "string" && this.message.length > 0) { msgToUse += `: ${this.message}`; } // TODO We should make use of our `calls` here. @@ -126,7 +130,7 @@ class SSO { // But we also could use this to get more information to return. Such as // providing helpful error logs and such. - res.status(enumDetails[shortToUse].code).json({ + res.status(codeToUse).json({ message: msgToUse }); diff --git a/src/models/ssoHTML.js b/src/models/ssoHTML.js new file mode 100644 index 00000000..e8c4343b --- /dev/null +++ b/src/models/ssoHTML.js @@ -0,0 +1,13 @@ +const SSO = require("./sso.js"); + +module.exports = +class SSOHTML extends SSO { + constructor() { + super(); + } + + handleSuccess(req, res, context) { + res.send(this.content); + context.logger.httpLog(req, res); + } +} diff --git a/src/server.js b/src/server.js index 263771c9..8cbd667e 100644 --- a/src/server.js +++ b/src/server.js @@ -4,7 +4,7 @@ * to listen on. As well as handling a graceful shutdown of the server. */ -const app = require("./main.js"); +const app = require("./setupEndpoints.js"); const { port } = require("./config.js").getConfig(); const logger = require("./logger.js"); const database = require("./database.js"); diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 1f53c268..95fb396a 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -2,40 +2,10 @@ const express = require("express"); const rateLimit = require("express-rate-limit"); const { MemoryStore } = require("express-rate-limit"); -const app = express(); +const endpoints = require("./controllers/endpoints.js"); +const context = require("./context.js"); -const endpoints = [ - require("./controllers/getStars.js"), - require("./controllers/getThemes.js"), - require("./controllers/getThemesFeatured.js"), - require("./controllers/getUpdates.js"), - require("./controllers/getUsers.js"), - require("./controllers/getusersLogin.js"), - require("./controllers/getUsersLoginStars.js"), - require("./controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js"), - require("./controllers/deletePackagesPackageNameVersionsVersionName.js"), - require("./controllers/getPackagesPackageNameVersionsVersionName.js"), - require("./controllers/getPackagesPackageNameVersionsVersionNameTarball.js"), - require("./controllers/postPackagesPackageNameVersions.js") -]; - -// The CONST Context - Enables access to all other modules within the system -// By passing this object to everywhere needed allows not only easy access -// but greater control in mocking these later on -const context = { - logger: require("./logger.js"), - database: require("./database.js"), - webhook: require("./webhook.js"), - server_version: require("../package.json").version, - query: require("./query.js"), - vcs: require("./vcs.js"), - config: require("./config.js").getConfig(), - common_handler: require("./handlers/common_handler.js"), - utils: require("./utils.js"), - sso: require("./models/sso.js"), - ssoPaginate: require("./models/ssoPaginate.js"), - ssoRedirect: require("./models/ssoRedirect.js") -}; +const app = express(); // Define our Basic Rate Limiters const genericLimit = rateLimit({ @@ -74,6 +44,12 @@ app.set("trust proxy", true); app.use("/swagger-ui", express.static("docs/swagger")); +// Some endpoints likely won't see any kind of migration anytime soon. +// Because of their complexity, and how vital they are to keep working. +const nonMigratedHandlers = require("./handlers/nonMigratedHandlers.js"); + +nonMigratedHandlers(app, genericLimit, authLimit); + // Setup all endpoints for (const node of endpoints) { diff --git a/test/database/applyFeatures.test.js b/tests/database/applyFeatures.test.js similarity index 100% rename from test/database/applyFeatures.test.js rename to tests/database/applyFeatures.test.js diff --git a/test/database/extensionFilter.test.js b/tests/database/extensionFilter.test.js similarity index 100% rename from test/database/extensionFilter.test.js rename to tests/database/extensionFilter.test.js diff --git a/test/global.setup.jest.js b/tests/helpers/global.setup.jest.js similarity index 59% rename from test/global.setup.jest.js rename to tests/helpers/global.setup.jest.js index 10ef4f63..a95b5bfb 100644 --- a/test/global.setup.jest.js +++ b/tests/helpers/global.setup.jest.js @@ -21,6 +21,34 @@ expect.extend({ }; } }, + // `expect().toBeTypeof(typeof)` + toBeTypeof(actual, want) { + if (typeof actual === want) { + return { + pass: true, + message: () => "" + }; + } else { + return { + pass: false, + message: () => `Expected "${want}" but got "${typeof actual}"` + }; + } + }, + // `expect().toBeIncludedBy(ARRAY)` + toBeIncludedBy(actual, want) { + if (Array.isArray(want) && want.includes(actual)) { + return { + pass: true, + message: () => "" + }; + } else { + return { + pass: false, + message: () => `Expected ${want} to include ${actual}` + }; + } + }, // `expect().toHaveHTTPCode()` toHaveHTTPCode(req, want) { // Type coercion here because the statusCode in the request object could be set as a string. diff --git a/test/handlers.setup.jest.js b/tests/helpers/handlers.setup.jest.js similarity index 98% rename from test/handlers.setup.jest.js rename to tests/helpers/handlers.setup.jest.js index d2a164e4..f22eb9c5 100644 --- a/test/handlers.setup.jest.js +++ b/tests/helpers/handlers.setup.jest.js @@ -2,7 +2,7 @@ // This mainly means to properly set timeouts, and to ensure that required // env vars are set properly. -jest.setTimeout(3000000); +//jest.setTimeout(3000000); const dbUrl = process.env.DATABASE_URL; // this gives us something like postgres://test-user@localhost:5432/test-db diff --git a/test/httpMock.helper.jest.js b/tests/helpers/httpMock.helper.jest.js similarity index 93% rename from test/httpMock.helper.jest.js rename to tests/helpers/httpMock.helper.jest.js index 854c36b2..c5c1d022 100644 --- a/test/httpMock.helper.jest.js +++ b/tests/helpers/httpMock.helper.jest.js @@ -6,10 +6,10 @@ * And encoding data into `base64` as expected on the fly. */ -const Git = require("../src/vcs_providers/git.js"); +const Git = require("../../src/vcs_providers/git.js"); -const auth = require("../src/auth.js"); -const vcs = require("../src/vcs.js"); +const auth = require("../../src/auth.js"); +const vcs = require("../../src/vcs.js"); class HTTP { constructor(path) { diff --git a/test/root.handler.integration.test.js b/tests/http/root.test.js similarity index 92% rename from test/root.handler.integration.test.js rename to tests/http/root.test.js index b971fe79..54e24459 100644 --- a/test/root.handler.integration.test.js +++ b/tests/http/root.test.js @@ -1,5 +1,5 @@ const request = require("supertest"); -const app = require("../src/main.js"); +const app = require("../../src/setupEndpoints.js"); describe("Get /", () => { test("Should respond with an HTML document noting the server version", async () => { diff --git a/test/stars.handler.integration.test.js b/tests/http/stars.handler.integration.test.js similarity index 78% rename from test/stars.handler.integration.test.js rename to tests/http/stars.handler.integration.test.js index 344b72e0..cf9345f2 100644 --- a/test/stars.handler.integration.test.js +++ b/tests/http/stars.handler.integration.test.js @@ -1,7 +1,7 @@ const request = require("supertest"); -const app = require("../src/main.js"); +const app = require("../../src/setupEndpoints.js"); -const { authMock } = require("./httpMock.helper.jest.js"); +const { authMock } = require("../helpers/httpMock.helper.jest.js"); let tmpMock; @@ -9,7 +9,7 @@ describe("GET /api/stars", () => { test("Returns Unauthenticated Status Code for Invalid User", async () => { tmpMock = authMock({ ok: false, - short: "Bad Auth", + short: "unauthorized", content: "Bad Auth Mock Return for Dev User", }); @@ -17,12 +17,12 @@ describe("GET /api/stars", () => { .get("/api/stars") .set("Authorization", "invalid"); expect(res).toHaveHTTPCode(401); - expect(res.body.message).toEqual(msg.badAuth); + expect(res.body.message).toEqual("Unauthorized: Please update your token if you haven't done so recently."); tmpMock.mockClear(); }); - test("Valid User with No Stars Returns array", async () => { + test.skip("Valid User with No Stars Returns array", async () => { tmpMock = authMock({ ok: true, content: { @@ -44,7 +44,7 @@ describe("GET /api/stars", () => { tmpMock.mockClear(); }); - test("Valid User with Stars Returns 200 Status Code", async () => { + test.skip("Valid User with Stars Returns 200 Status Code", async () => { tmpMock = authMock({ ok: true, content: { diff --git a/test/config.unit.test.js b/tests/unit/config.test.js similarity index 97% rename from test/config.unit.test.js rename to tests/unit/config.test.js index daa6ad39..a979b607 100644 --- a/test/config.unit.test.js +++ b/tests/unit/config.test.js @@ -1,4 +1,4 @@ -const config = require("../src/config.js"); +const config = require("../../src/config.js"); const Joi = require("joi"); describe("Config Returns all Expected Values", () => { diff --git a/tests/unit/endpoints.test.js b/tests/unit/endpoints.test.js new file mode 100644 index 00000000..0364b9d2 --- /dev/null +++ b/tests/unit/endpoints.test.js @@ -0,0 +1,40 @@ +const endpoints = require("../../src/controllers/endpoints.js"); + +describe("All endpoints are valid", () => { + test("Have expected objects", () => { + + for (const node of endpoints) { + + for (const item in node) { + + const validItems = [ + "docs", + "endpoint", + "params", + "logic", + "preLogic", + "postLogic", + "postReturnHTTP" + ]; + + expect(validItems.includes(item)); + } + } + + }); + + test("Have a valid 'endpoint' object", () => { + for (const node of endpoints) { + const endpoint = node.endpoint; + + expect(endpoint.method).toBeTypeof("string"); + expect(endpoint.method).toBeIncludedBy([ "GET", "POST", "DELETE" ]); + expect(endpoint.paths).toBeArray(); + expect(endpoint.rateLimit).toBeTypeof("string"); + expect(endpoint.rateLimit).toBeIncludedBy([ "generic", "auth" ]); + expect(endpoint.successStatus).toBeTypeof("number"); + expect(endpoint.options).toBeDefined(); + } + + }); +}); diff --git a/tests/unit/postPackagesPackageNameVersions.test.js b/tests/unit/postPackagesPackageNameVersions.test.js new file mode 100644 index 00000000..438c9c99 --- /dev/null +++ b/tests/unit/postPackagesPackageNameVersions.test.js @@ -0,0 +1,13 @@ +const endpoint = require("../../src/controllers/postPackagesPackageNameVersions.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "POST", + rateLimit: "auth", + successStatus: 201 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); +}); diff --git a/tests/unit/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js b/tests/unit/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js new file mode 100644 index 00000000..f987cb32 --- /dev/null +++ b/tests/unit/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js @@ -0,0 +1,24 @@ +const endpoint = require("../../src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js"); +const context = require("../../src/context.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "POST", + rateLimit: "auth", + successStatus: 201 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); +}); + +describe("Returns as expected", () => { + test("Returns simple OK object", async () => { + const res = await endpoint.logic({}, context); + + expect(res.ok).toBe(true); + expect(res.content).toBeDefined(); + expect(res.content.ok).toBe(true); + }); +}); diff --git a/test/query.unit.test.js b/tests/unit/query.test.js similarity index 99% rename from test/query.unit.test.js rename to tests/unit/query.test.js index aae88196..d4d8c111 100644 --- a/test/query.unit.test.js +++ b/tests/unit/query.test.js @@ -1,4 +1,4 @@ -const query = require("../src/query.js"); +const query = require("../../src/query.js"); // Page Testing diff --git a/test/utils.unit.test.js b/tests/unit/utils.test.js similarity index 98% rename from test/utils.unit.test.js rename to tests/unit/utils.test.js index 7d6db16e..62779759 100644 --- a/test/utils.unit.test.js +++ b/tests/unit/utils.test.js @@ -1,7 +1,7 @@ -jest.mock("../src/storage.js"); -const getBanList = require("../src/storage.js").getBanList; +jest.mock("../../src/storage.js"); +const getBanList = require("../../src/storage.js").getBanList; -const utils = require("../src/utils.js"); +const utils = require("../../src/utils.js"); describe("isPackageNameBanned Tests", () => { test("Returns true correctly for banned item", async () => { diff --git a/test/webhook.unit.test.js b/tests/unit/webhook.test.js similarity index 94% rename from test/webhook.unit.test.js rename to tests/unit/webhook.test.js index f2727883..7e338e3a 100644 --- a/test/webhook.unit.test.js +++ b/tests/unit/webhook.test.js @@ -1,8 +1,8 @@ -const webhook = require("../src/webhook.js"); +const webhook = require("../../src/webhook.js"); const superagent = require("superagent"); -const logger = require("../src/logger.js"); +const logger = require("../../src/logger.js"); -jest.mock("../src/logger.js", () => { +jest.mock("../../src/logger.js", () => { return { generic: jest.fn(), }; From b65c5815ec616a837e54efb91f07e45123a67173 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Tue, 12 Sep 2023 18:18:59 -0700 Subject: [PATCH 14/53] Migrate everything but oauth handlers --- src/controllers/deletePackagesPackageName.js | 76 +++++++ src/controllers/endpoints.js | 6 + src/controllers/getPackages.js | 47 +++++ src/controllers/getPackagesFeatured.js | 36 ++++ src/controllers/getPackagesPackageName.js | 46 +++++ src/controllers/getPackagesSearch.js | 79 ++++++++ src/controllers/getThemes.js | 4 +- src/controllers/getThemesSearch.js | 64 ++++++ src/controllers/postPackages.js | 203 +++++++++++++++++++ src/models/sso.js | 7 +- src/models/ssoPaginate.js | 10 +- 11 files changed, 574 insertions(+), 4 deletions(-) create mode 100644 src/controllers/deletePackagesPackageName.js create mode 100644 src/controllers/getPackages.js create mode 100644 src/controllers/getPackagesFeatured.js create mode 100644 src/controllers/getPackagesPackageName.js create mode 100644 src/controllers/getPackagesSearch.js create mode 100644 src/controllers/getThemesSearch.js create mode 100644 src/controllers/postPackages.js diff --git a/src/controllers/deletePackagesPackageName.js b/src/controllers/deletePackagesPackageName.js new file mode 100644 index 00000000..9e2f7dd6 --- /dev/null +++ b/src/controllers/deletePackagesPackageName.js @@ -0,0 +1,76 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "DELETE", + paths: [ + "/api/packages/:packageName", + "/api/themes/:packageName" + ], + rateLimit: "auth", + successStatus: 204, + options: { + Allow: "DELETE, GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + auth: (context, req) => { return context.query.auth(req); }, + packageName: (context, req) => { return context.query.packageName(req); } + }, + + async logic(params, context) { + const user = await context.auth.verifyAuth(params.auth, context.database); + + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user) + .addMessage("Please update your token if you haven't done so recently.") + .addCalls("auth.verifyAuth", user); + } + + // Lets also first check to make sure the package exists + const packageExists = await context.database.getPackageByName(params.packageName, true); + + if (!packageExists.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(packageExists) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packageExists); + } + + // Get `owner/repo` string format from pacakge + const ownerRepo = context.utils.getOwnerRepoFromPackage(packageExists.content.data); + + const gitowner = await context.vcs.ownership(user.content, ownerRepo); + + if (!gitowner.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(gitowner) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packageExists) + .addCalls("vcs.ownership", gitowner); + } + + // Now they are logged in locally, and have permissions over the GitHub repo + const rm = await context.database.removePackageByName(params.packageName); + + if (!rm.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(rm) + .addCalls("auth.verifyAuth", user) + .addCalls("db.getPackageByName", packageExists) + .addCalls("vcs.ownership", gitowner) + .addCalls("db.removePackageByName", rm); + } + + const sso = new context.sso(); + + return sso.isOk().addContent(false); + } +}; diff --git a/src/controllers/endpoints.js b/src/controllers/endpoints.js index 13e83223..72ba6118 100644 --- a/src/controllers/endpoints.js +++ b/src/controllers/endpoints.js @@ -1,18 +1,24 @@ // Exports all the endpoints that need to be required module.exports = [ + require("./deletePackagesPackageName.js"), require("./deletePackagesPackageNameStar.js"), require("./deletePackagesPackageNameVersionsVersionName.js"), + require("./getPackagesFeatured.js"), + require("./getPackagesPackageName.js"), require("./getPackagesPackageNameStargazers.js"), require("./getPackagesPackageNameVersionsVersionName.js"), require("./getPackagesPackageNameVersionsVersionNameTarball.js"), + require("./getPackagesSearch.js"), require("./getRoot.js"), require("./getStars.js"), require("./getThemes.js"), require("./getThemesFeatured.js"), + require("./getThemesSearch.js"), require("./getUpdates.js"), require("./getUsers.js"), require("./getUsersLogin.js"), require("./getUsersLoginStars.js"), + require("./postPackages.js"), require("./postPackagesPackageNameStar.js"), require("./postPackagesPackageNameVersions.js"), require("./postPackagesPackageNameVersionsVersionNameEventsUninstall.js") diff --git a/src/controllers/getPackages.js b/src/controllers/getPackages.js new file mode 100644 index 00000000..dd052c6a --- /dev/null +++ b/src/controllers/getPackages.js @@ -0,0 +1,47 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/api/packages" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "POST, GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + page: (context, req) => { return context.query.page(req); }, + sort: (context, req) => { return context.query.sort(req); }, + direction: (context, req) => { return context.query.dir(req); }, + serviceType: (context, req) => { return context.query.serviceType(req); }, + service: (context, req) => { return context.query.service(req); }, + serviceVersion: (context, req) => { return context.query.serviceVersion(req); }, + fileExtension: (context, req) => { return context.query.fileExtension(req); } + }, + + async logic(params, context) { + const packages = await context.database.getSortedPackages(params); + + if (!packages.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(packages) + .addCalls("db.getSortedPackages", packages); + } + + const packObjShort = await context.utils.constructPackageObjectShort(packages.content); + + const packArray = Array.isArray(packObjShort) ? packObjShort : [ packObjShort ]; + + const ssoP = new context.ssoPaginate(); + + ssoP.total = packages.pagination.total; + ssoP.limit = packages.pagination.limit; + ssoP.buildLink(`${context.config.server_url}/api/packages`, packages.pagination.page, params); + + return ssoP.isOk().addContent(packArray); + } +}; diff --git a/src/controllers/getPackagesFeatured.js b/src/controllers/getPackagesFeatured.js new file mode 100644 index 00000000..0241ff57 --- /dev/null +++ b/src/controllers/getPackagesFeatured.js @@ -0,0 +1,36 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/api/packages/featured" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: {}, + async logic(params, context) { + // TODO: Does not support engine query parameter as of now + const packs = await context.database.getFeaturedPackages(); + + if (!packs.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(packs) + .addCalls("db.getFeaturedPackages", packs); + } + + const packObjShort = await context.utils.constructPackageObjectShort(packs.content); + + // The endpoint using this ufnction needs an array + const packArray = Array.isArray(packObjShort) ? packObjShort : [ packObjShort ]; + + const sso = new context.sso(); + + return sso.isOk().addContent(packArray); + } +}; diff --git a/src/controllers/getPackagesPackageName.js b/src/controllers/getPackagesPackageName.js new file mode 100644 index 00000000..0418b803 --- /dev/null +++ b/src/controllers/getPackagesPackageName.js @@ -0,0 +1,46 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ + "/api/packages/:packageName", + "/api/themes/:packageName" + ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "DELETE, GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + engine: (context, req) => { return context.query.engine(req.query.engine); }, + name: (context, req) => { return context.query.packageName(req); } + }, + + async logic(params, context) { + let pack = await context.database.getPackageByName(params.name, true); + + if (!pack.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(pack) + .addCalls("db.getPackageByName", pack); + } + + pack = await context.utils.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); + } + + const sso = new context.sso(); + + return sso.isOk().addcontent(pack); + } +}; diff --git a/src/controllers/getPackagesSearch.js b/src/controllers/getPackagesSearch.js new file mode 100644 index 00000000..5c1d91c3 --- /dev/null +++ b/src/controllers/getPackagesSearch.js @@ -0,0 +1,79 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/api/packages/search" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + sort: (context, req) => { return context.query.sort(req); }, + page: (context, req) => { return context.query.page(req); }, + direction: (context, req) => { return context.qeury.dir(req); }, + query: (context, req) => { return context.query.query(req); } + }, + + async logic(params, context) { + // Because the task of implementing the custom search engine is taking longer + // than expected, this will instead use super basic text searching on the DB side. + // This is only an effort to get this working quickly and should be changed later. + // This also means for now, the default sorting method will be downloads, not relevance. + + const packs = await context.database.simpleSearch( + params.query, + params.page, + params.direction, + params.sort + ); + + + if (!packs.ok) { + if (packs.short === "not_found") { + // Because getting not found from the search, means the users + // search just had no matches, we will specially handle this to return + // an empty array instead. + // TODO: Re-evaluate if this is needed. The empty result + // returning 'Not Found' has been resolved via the DB. + // But this check still might come in handy, so it'll be left in. + + const sso = new context.ssoPaginate(); + + return sso.isOk().addContent([]); + } + + const sso = new context.sso(); + + return sso.notOk().addContent(packs) + .addCalls("db.simpleSearch", packs); + } + + const newPacks = await context.utils.constructPackageObjectShort(packs.content); + + let packArray = null; + + if (Array.isArray(newPacks)) { + packArray = newPacks; + } else if (Object.keys(newPacks).length < 1) { + packArray = []; + // This also helps protect against misreturned searches. As in getting a 404 rather + // than empty search results. + // See: https://github.com/confused-Techie/atom-backend/issues/59 + } else { + packArray = [newPacks]; + } + + const ssoP = new context.ssoPaginate(); + + ssoP.total = packs.pagination.total; + ssoP.limit = packs.pagination.limit; + ssoP.buildLink(`${context.config.server_url}/api/packages/search`, packs.pagination.page, params); + + return ssoP.isOk().addContent(packArray); + } +}; diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index 47400a98..664b4e4a 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -43,8 +43,8 @@ module.exports = { const ssoP = new context.ssoPaginate(); ssoP.total = packages.pagination.total; - ssoP.limit = packages.pagination.total; - ssoP.buildLink(`${context.config.server_url}/api/themes`, page, params); + ssoP.limit = packages.pagination.limit; + ssoP.buildLink(`${context.config.server_url}/api/themes`, packages.pagination.page, params); return ssoP.isOk().addContent(packArray); } diff --git a/src/controllers/getThemesSearch.js b/src/controllers/getThemesSearch.js new file mode 100644 index 00000000..1b2d13e4 --- /dev/null +++ b/src/controllers/getThemesSearch.js @@ -0,0 +1,64 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/api/themes/search" ], + rateLimit: "generic", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + } + }, + params: { + sort: (context, req) => { return context.query.sort(req); }, + page: (context, req) => { return context.query.page(req); }, + direction: (context, req) => { return context.query.dir(req); }, + query: (context, req) => { return context.query.query(req); } + }, + + async logic(params, context) { + const packs = await context.database.simpleSearch( + params.query, + params.page, + params.direction, + params.sort, + true + ); + + if (!packs.ok) { + if (packs.short === "not_found") { + const sso = new context.ssoPaginate(); + + return sso.isOk().addContent([]); + } + + const sso = new context.sso(); + + return sso.notOk().addContent(packs) + .addCalls("db.simpleSearch", packs); + } + + const newPacks = await context.utils.constructPackageObjectShort(packs.content); + + let packArray = null; + + if (Array.isArray(newPacks)) { + packArray = newPacks; + } else if (Object.keys(newPacks).length < 1) { + packArray = []; + } else { + packArray = [ newPacks ]; + } + + const ssoP = new context.ssoPaginate(); + + ssoP.total = packs.pagination.total; + ssoP.limit = packs.pagination.limit; + ssoP.buildLink(`${context.config.server_url}/api/themes/search`, packs.pagination.page, params); + + return ssoP.isOk().addContent(packArray); + } +}; diff --git a/src/controllers/postPackages.js b/src/controllers/postPackages.js new file mode 100644 index 00000000..2e8704c7 --- /dev/null +++ b/src/controllers/postPackages.js @@ -0,0 +1,203 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "POST", + paths: [ + "/api/packages", + "/api/themes" + ], + rateLimit: "auth", + successStauts: 201, + options: { + Allow: "POST, GET", + "X-Content-Type-Options": "nosniff" + } + }, + async postReturnHTTP(req, res, context, obj) { + // Return to user before wbehook call, so user doesn't wait on it + await context.webhook.alertPublishPackage(obj.webhook.pack, obj.webhook.user); + // Now to call for feature detection + let features = await context.vcs.featureDetection( + obj.featureDetection.user, + obj.featureDetection.ownerRepo, + obj.featureDetection.service + ); + + if (!features.ok) { + // TODO: LOG + return; + } + + // Then we know we don't need to apply any special features for a standard + // package, so we will check that early + if (features.content.standard) { + return; + } + + let featureApply = await context.database.applyFeatures( + features.content, + obj.webhook.pack.name, + obj.webhook.pack.version + ); + + if (!featureApply.ok) { + // TODO LOG + return; + } + + // Now everything has completed successfully + return; + }, + + async logic(params, context) { + const user = await auth.verifyAuth(params.auth, context.database); + // Check authentication + if (!user.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(user) + .addCalls("auth.verifyAuth", user); + } + + // Check repository format validity. + if (params.repository === "" || typeof params.repository !== "string") { + // repository format is invalid + const sso = new context.sso(); + + return sso.notOk().addShort("bad_repo") + .addMessage("Repository is missing."); + } + + // Currently though the repository is in `owner/repo` format, + // meanwhile needed functions expects just `repo` + + const repo = params.repository.split("/")[1]?.toLowerCase(); + + if (repo === undefined) { + const sso = new context.sso(); + + return sso.notOk().addShort("bad_repo") + .addMessage("Repository format is invalid."); + } + + // Now check if the name is banned. + const isBanned = await context.utils.isPackageNameBanned(repo); + + if (isBanned.ok) { + // The package name is banned + const sso = new context.sso(); + + return sso.notOk().addShort("server_error") + .addMessage("This package name is banned."); + } + + // Check that the package does NOT exists + // We will utilize our database.packageNameAvailability to see if the name is available + const nameAvailable = await context.database.packageNameAvailability(repo); + + if (!nameAvailable.ok) { + // We need to ensure the error is not found or otherwise + if (nameAvailable.short !== "not_found") { + // the server failed for some other bubbled reason + const sso = new context.sso(); + + return sso.notOk().addContent(nameAvailable) + .addCalls("auth.verifyAuth", user) + .addCalls("db.packageNameAvailability", nameAvailable); + } + + // But if the short is in fact "not_found" we can report the package as not being + // available at this name + const sso = new context.sso(); + + return sso.notOk().addShort("package_exists") + .addCalls("auth.verifyAuth", user) + .addCalls("db.packageNameAvailability", nameAvailable); + } + + // Now we know the package doesn't exist. And we want to check that the user + // has permissions to this package + const gitowner = await context.vcs.ownership(user.content, params.repository); + + if (!gitowner.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(gitowner) + .addCalls("auth.verifyAuth", user) + .addCalls("db.packageNameAvailability", nameAvailable) + .addCalls("vcs.ownership", gitowner); + } + + // Now knowing they own the git repo, and it doesn't exist here, lets publish. + // TODO: Stop hardcoding `git` as service + const newPack = await context.vcs.newPackageData( + user.content, + params.repository, + "git" + ); + + if (!newPack.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(newPack) + .addCalls("auth.verifyAuth", user) + .addCalls("db.packageNameAvailability", nameAvailable) + .addCalls("vcs.ownership", gitowner) + .addCalls("vcs.newPackageData", newPack); + } + + // Now with valid package data, we can insert them into the DB + const insertedNewPack = await context.database.insertNewPackage(newPack.content); + + if (!insertedNewPack.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(insertedNewPack) + .addCalls("auth.verifyAuth", user) + .addCalls("db.packageNameAvailability", nameAvailable) + .addCalls("vcs.ownership", gitowner) + .addCalls("vcs.newPackageData", newPack) + .addCalls("db.insertNewPackage", insertedNewPack); + } + + // Finally we can return what was actually put into the databse. + // Retreive the data from database.getPackageByName() and + // convert it inot a package object full format + const newDbPack = await context.database.getPackageByName(repo, true); + + if (!newDbPack.ok) { + const sso = new context.sso(); + + return sso.notOk().addContent(newDbPack) + .addCalls("auth.verifyAuth", user) + .addCalls("db.packageNameAvailability", nameAvailable) + .addCalls("vcs.ownership", gitowner) + .addCalls("vcs.newPackageData", newPack) + .addCalls("db.insertNewPackage", insertedNewPack) + .addCalls("db.getPackageByName", newDbPack); + } + + const packageObjectFull = await context.utils.constructPackageObjectFull( + newDbPack.content + ); + + // Since this is a webhook call, we will return with some extra data + // Although this kinda defeats the point of the object builder + const sso = new context.sso(); + + sso.webhook = { + pack: packageObjectFull, + user: user.content + }; + + sso.featureDetection = { + user: user.content, + service: "git", // TODO stop hardcoding git + ownerRepo: params.repository + }; + + return sso.isOk().addContent(packageObjectFull); + } +}; diff --git a/src/models/sso.js b/src/models/sso.js index 14dcd1ac..146c3a91 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -5,7 +5,8 @@ const validEnums = [ "server_error", "not_supported", "unauthorized", - "bad_repo" + "bad_repo", + "package_exists" ]; const enumDetails = { @@ -28,6 +29,10 @@ const enumDetails = { "bad_repo": { code: 400, message: "That repo does not exist, or is inaccessible" + }, + "package_exists": { + code: 409, + message: "A Package by that name already exists." } }; diff --git a/src/models/ssoPaginate.js b/src/models/ssoPaginate.js index ebe2c0c4..fa04398c 100644 --- a/src/models/ssoPaginate.js +++ b/src/models/ssoPaginate.js @@ -14,7 +14,15 @@ class SSOPaginate extends SSO { let paramString = ""; for (let param of params) { - paramString += `¶m=${params[param]}`; + if (param === "query") { + // Since we know we want to keep search queries safe strings + const safeQuery = encodeURIComponent( + params[param].replace(/[<>"':;\\/]+/g, "") + ); + paramString += `&${param}=${safeQuery}`; + } else { + paramString += `&${param}=${params[param]}`; + } } let linkString = ""; From 97bdd568779f81106ea082a4c362945d57413da7 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Tue, 12 Sep 2023 18:20:41 -0700 Subject: [PATCH 15/53] Fix `successStatus` --- src/controllers/postPackages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/postPackages.js b/src/controllers/postPackages.js index 2e8704c7..97881eb8 100644 --- a/src/controllers/postPackages.js +++ b/src/controllers/postPackages.js @@ -9,7 +9,7 @@ module.exports = { "/api/themes" ], rateLimit: "auth", - successStauts: 201, + successStatus: 201, options: { Allow: "POST, GET", "X-Content-Type-Options": "nosniff" From 39d961a58a95f7948885fc91b3fee5062c7b9cd1 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 13 Sep 2023 23:43:26 -0700 Subject: [PATCH 16/53] Migrate last handlers, support RAW endpoint types for increased control of HTTP handling --- src/controllers/endpoints.js | 4 + src/controllers/getLogin.js | 44 +++++++++ src/controllers/getOauth.js | 124 +++++++++++++++++++++++++ src/controllers/getPat.js | 98 ++++++++++++++++++++ src/handlers/nonMigratedHandlers.js | 48 ---------- src/setupEndpoints.js | 134 ++++++++++------------------ tests/unit/endpoints.test.js | 5 ++ 7 files changed, 324 insertions(+), 133 deletions(-) create mode 100644 src/controllers/getLogin.js create mode 100644 src/controllers/getOauth.js create mode 100644 src/controllers/getPat.js delete mode 100644 src/handlers/nonMigratedHandlers.js diff --git a/src/controllers/endpoints.js b/src/controllers/endpoints.js index 72ba6118..7028a4c5 100644 --- a/src/controllers/endpoints.js +++ b/src/controllers/endpoints.js @@ -3,12 +3,16 @@ module.exports = [ require("./deletePackagesPackageName.js"), require("./deletePackagesPackageNameStar.js"), require("./deletePackagesPackageNameVersionsVersionName.js"), + require("./getLogin.js"), + require("./getOauth.js"), + require("./getPackages.js"), require("./getPackagesFeatured.js"), require("./getPackagesPackageName.js"), require("./getPackagesPackageNameStargazers.js"), require("./getPackagesPackageNameVersionsVersionName.js"), require("./getPackagesPackageNameVersionsVersionNameTarball.js"), require("./getPackagesSearch.js"), + require("./getPat.js"), require("./getRoot.js"), require("./getStars.js"), require("./getThemes.js"), diff --git a/src/controllers/getLogin.js b/src/controllers/getLogin.js new file mode 100644 index 00000000..2ec2f716 --- /dev/null +++ b/src/controllers/getLogin.js @@ -0,0 +1,44 @@ +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/api/login" ], + rateLimit: "auth", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + }, + endpointKind: "raw" + }, + + async logic(req, res, context) { + // The first point of contact to log into the app. + // Since this will be the endpoint for a user to login, we need + // to redirect to GH. + // @see https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps + + // Generate a random key + const stateKey = context.utils.generateRandomString(64); + + // Before redirect, save the key into the db + const saveStateKey = await context.database.authStoreStateKey(stateKey); + + if (!saveStateKey.ok) { + res.status(500).json({ + message: "Application Error: Failed to generate secure state key." + }); + context.logger.httpLog(req, res); + return; + } + + res.status(302).redirect( + `https://github.com/login/oauth/authorize?client_id=${context.config.GH_CLIENTID}&redirect_uri=${context.config.GH_REDIRECTURI}&state=${stateKey}&scope=public_repo%20read:org` + ); + + context.logger.httpLog(req, res); + return; + } +}; diff --git a/src/controllers/getOauth.js b/src/controllers/getOauth.js new file mode 100644 index 00000000..4bcd1af8 --- /dev/null +++ b/src/controllers/getOauth.js @@ -0,0 +1,124 @@ +const superagent = require("superagent"); + +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/api/oauth" ], + rateLimit: "auth", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + }, + endpointKind: "raw" + }, + + async logic(req, res, context) { + let params = { + state: req.query.state ?? "", + code: req.query.code ?? "" + }; + + // First we want to ensure that the received state key is valid + const validStateKey = await context.database.authCheckAndDeleteStateKey(params.state); + + if (!validStateKey.ok) { + res.status(500).json({ + message: "Application Error: Invalid State Key provided." + }); + context.logger.httpLog(req, res); + return; + } + + // Retrieve access token + const initialAuth = await superagent + .post("https://github.com/login/oauth/access_token") + .query({ + code: params.code, + redirect_uri: context.config.GH_REDIRECTURI, + client_id: context.config.GH_CLIENTID, + client_secret: context.config.GH_CLIENTSECRET + }); + + const accessToken = iniitalAuth.body?.access_token; + + if (accessToken === null || initialAuth.body?.token_type === null) { + res.status(500).json({ + message: "Application Error: Authentication to GitHub failed." + }); + context.logger.httpLog(req, res); + return; + } + + try { + // Request the user data using the access token + const userData = await superagent + .get("https://api.github.com/user") + .set({ Authorization: `Bearer ${accessToken}` }) + .set({ "User-Agent": context.config.GH_USERAGENT }); + + if (userData.status !== 200) { + res.status(500).json({ + message: `Application Error: Received HTTP Status ${userData.status}` + }); + context.logger.httpLog(req, res); + return; + } + + // Now retrieve the user data that we need to store into the DB + const username = userData.body.login; + const userId = userData.body.node_id; + const userAvatar = userData.body.avatar_url; + + const userExists = await context.database.getUserByNodeID(userId); + + if (userExists.ok) { + // This means that the user does in fact already exist. + // And from there they are likely reauthenticating, + // But since we don't save any type of auth tokens, the user just needs + // a new one and we should return their new one to them. + + // Now we redirect to the frontend site + res.redirect(`https://web.pulsar-edit.dev/users?token=${accessToken}`); + context.logger.httpLog(req, res); + return; + } + + // The user does not exist, so we save its data into the DB + let createdUser = await context.database.insertNewUser( + username, + userId, + userAvatar + ); + + if (!createdUser.ok) { + res.status(500).json({ + message: "Application Error: Creating the user account failed!" + }); + context.logger.httpLog(req, res); + return; + } + + // Before returning, lets append their access token + createdUser.content.token = accessToken; + + // Now re redirect to the frontend site + res.redirect( + `https://web.pulsar-edit.dev/users?token=${createdUser.content.token}` + ); + context.logger.httpLog(req, res); + return; + + } catch(err) { + context.logger.generic(2, "/api/oauth Caught an Error!", { type: "error", err: err }); + res.status(500).json({ + message: "Application Error: The server encountered an error processing the request." + }); + context.logger.httpLog(req, res); + return; + } + } +}; diff --git a/src/controllers/getPat.js b/src/controllers/getPat.js new file mode 100644 index 00000000..ed148172 --- /dev/null +++ b/src/controllers/getPat.js @@ -0,0 +1,98 @@ +const superagent = require("superagent"); + +module.exports = { + docs: { + + }, + endpoint: { + method: "GET", + paths: [ "/api/pat" ], + rateLimit: "auth", + successStatus: 200, + options: { + Allow: "GET", + "X-Content-Type-Options": "nosniff" + }, + endpointKind: "raw" + }, + + async logic(req, res, context) { + let params = { + token: req.query.token ?? "" + }; + + if (params.token === "") { + res.status(404).json({ + message: "Not Found: Parameter 'token' is empty." + }); + context.logger.httpLog(req, res); + return; + } + + try { + const userData = await superagent + .get("https://api.github.com/user") + .set({ Authorization: `Bearer ${params.token}` }) + .set({ "User-Agent": context.config.GH_USERAGENT }); + + if (userData.status !== 200) { + context.logger.generic(2, "User Data request to GitHub failed!", { + type: "object", + obj: userData + }); + res.status(500).json({ + message: `Application Error: Received HTTP Status ${userData.status} when contacting GitHub!` + }); + context.logger.httpLog(req, res); + return; + } + + // Now to build a valid user object + const username = userData.body.login; + const userId = userData.body.node_id; + const userAvatar = userData.body.avatar_url; + + const userExists = await context.database.getUserByNodeID(userId); + + if (userExists.ok) { + // If we plan to allow updating the user name or image, we would do so here + + // Now to redirect to the frontend site. + res.redirect(`https://web.pulsar-edit.dev/users?token=${params.token}`); + context.logger.httpLog(req, res); + return; + } + + let createdUser = await context.database.insertNewUser( + username, + userId, + userAvatar + ); + + if (!createdUser.ok) { + context.logger.generic(2, `Creating user failed! ${username}`); + res.status(500).json({ + message: "Application Error: Creating the user account failed!" + }); + context.logger.httpLog(req, res); + return; + } + + // Before returning, lets append their PAT token + createdUser.content.token = params.token; + + res.redirect( + `https://web.pulsar-edit.dev/users?token=${createdUser.cotnent.token}` + ); + context.logger.httpLog(req, res); + return; + } catch(err) { + context.logger.generic(2, "/api/pat Caught an Error!", { type: "error", err: err }); + res.status(500).json({ + message: "Application Error: The server encountered an error processing the request." + }); + context.logger.httpLog(req, res); + return; + } + } +}; diff --git a/src/handlers/nonMigratedHandlers.js b/src/handlers/nonMigratedHandlers.js deleted file mode 100644 index fb25d803..00000000 --- a/src/handlers/nonMigratedHandlers.js +++ /dev/null @@ -1,48 +0,0 @@ -const oauth_handler = require("./oauth_handler.js"); - -function setup(app, genericLimit, authLimit) { - - app.get("/api/login", authLimit, async (req, res) => { - await oauth_handler.getLogin(req, res); - }); - - app.options("/api/login", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - }); - - //============================================================= - - app.get("/api/oauth", authLimit, async (req, res) => { - await oauth_handler.getOauth(req, res); - }); - - app.options("/api/oauth", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - }); - - //============================================================= - - app.get("/api/pat", authLimit, async (req, res) => { - await oauth_handler.getPat(req, res); - }); - - app.options("/api/pat", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - }); - - return app; -} - -module.exports = setup; diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 95fb396a..351c1537 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -44,16 +44,48 @@ app.set("trust proxy", true); app.use("/swagger-ui", express.static("docs/swagger")); -// Some endpoints likely won't see any kind of migration anytime soon. -// Because of their complexity, and how vital they are to keep working. -const nonMigratedHandlers = require("./handlers/nonMigratedHandlers.js"); +const endpointHandler = async function(node, req, res) { + let params = {}; -nonMigratedHandlers(app, genericLimit, authLimit); + for (const param in node.params) { + params[param] = node.params[param](context, req); + } + + if (typeof node.preLogic === "function") { + await node.preLogic(req, res, context); + } + + let obj; + + if (node.endpoint.endpointKind === "raw") { + obj = await node.logic(req, res, context); + // If it's a raw endpoint, they must handle all other steps manually + return; + + } else { + obj = await node.logic(params, context); + } + + if (typeof node.postLogic === "function") { + await node.postLogic(req, res, context); + } + + obj.addGoodStatus(node.endpoint.successStatus); + + obj.handleReturnHTTP(req, res, context); + + if (typeof node.postReturnHTTP === "function") { + await node.postReturnHTTP(req, res, context, obj); + } + + return; +}; // Setup all endpoints -for (const node of endpoints) { +const pathOptions = []; +for (const node of endpoints) { for (const path of node.endpoint.paths) { let limiter = genericLimit; @@ -68,94 +100,26 @@ for (const node of endpoints) { switch(node.endpoint.method) { case "GET": app.get(path, limiter, async (req, res) => { - let params = {}; - - for (const param in node.params) { - params[param] = node.params[param](context, req); - } - - if (typeof node.preLogic === "function") { - await node.preLogic(req, res, context); - } - - let obj = await node.logic(params, context); - - if (typeof node.postLogic === "function") { - await node.postLogic(req, res, context); - } - - obj.addGoodStatus(node.endpoint.successStatus); - - obj.handleReturnHTTP(req, res, context); - - if (typeof node.postReturnHTTP === "function") { - await node.postReturnHTTP(req, res, context, obj); - } - - return; + await endpointHandler(node, req, res); }); case "POST": app.post(path, limiter, async (req, res) => { - let params = {}; - - for (const param in node.params) { - params[param] = node.params[param](context, req); - } - - if (typeof node.preLogic === "function") { - await node.preLogic(req, res, context); - } - - let obj = await node.logic(params, context); - - if (typeof node.postLogic === "function") { - await node.postLogic(req, res, context); - } - - obj.addGoodStatus(node.endpoint.successStatus); - - obj.handleReturnHTTP(req, res, context); - - if (typeof node.postReturnHTTP === "function") { - await node.postReturnHTTP(req, res, context, obj); - } - - return; + await endpointHandler(node, req, res); }); case "DELETE": app.delete(path, limiter, async (req, res) => { - let params = {}; - - for (const param in node.params) { - params[param] = node.params[param](context, req); - } - - if (typeof node.preLogic === "function") { - await node.preLogic(req, res, context); - } - - let obj = await node.logic(params, context); - - if (typeof node.postLogic === "function") { - await node.postLogic(req, res, context); - } - - obj.addGoodStatus(node.endpoint.successStatus); - - obj.handleReturnHTTP(req, res, context); - - if (typeof node.postReturnHTTP === "function") { - await node.postReturnHTTP(req, res, context, obj); - } - - return; + await endpointHandler(node, req, res); }); default: - app.options(path, genericLimit, async (req, res) => { - res.header(node.endpoint.options); - res.sendStatus(204); - return; - }); + if (!pathOptions.includes(path)) { + // Only add one "OPTIONS" entry per path + app.options(path, genericLimit, async (req, res) => { + res.header(node.endpoint.options); + res.sendStatus(204); + return; + }); + pathOptions.push(path); + } } } } diff --git a/tests/unit/endpoints.test.js b/tests/unit/endpoints.test.js index 0364b9d2..b4c00381 100644 --- a/tests/unit/endpoints.test.js +++ b/tests/unit/endpoints.test.js @@ -34,6 +34,11 @@ describe("All endpoints are valid", () => { expect(endpoint.rateLimit).toBeIncludedBy([ "generic", "auth" ]); expect(endpoint.successStatus).toBeTypeof("number"); expect(endpoint.options).toBeDefined(); + + if (endpoint.endpointKind) { + expect(endpoint.endpointKind).toBeTypeof("string"); + expect(endpoint.endpointKind).toBeIncludedBy([ "raw", "default" ]); + } } }); From f1bad98688ed18b084eb6f763789289227b798f9 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 14 Sep 2023 00:34:14 -0700 Subject: [PATCH 17/53] Enable more tests, work on in module docs --- src/controllers/getPackagesSearch.js | 49 +++++++++++++++++++- tests/http/stars.handler.integration.test.js | 4 +- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/controllers/getPackagesSearch.js b/src/controllers/getPackagesSearch.js index 5c1d91c3..cbf6542f 100644 --- a/src/controllers/getPackagesSearch.js +++ b/src/controllers/getPackagesSearch.js @@ -1,6 +1,53 @@ module.exports = { docs: { - + parameters: [ + { + name: "q", + in: "query", + schema: { type: "string" }, + example: "generic-lsp", + required: true, + description: "Search query." + }, + { + name: "page", + in: "query", + schema: { type: "number", minimum: 1, default: 1 }, + example: 1, + allowEmptyValue: true, + description: "The page of search results to return." + }, + { + name: "sort", + in: "query", + schema: { + type: "string", + enum: [ + "downloads", + "created_at", + "updated_at", + "stars", + "relevance" + ], + default: "relevance" + }, + example: "downloads", + allowEmptyValue: true, + description: "Method to sort the results." + }, + { + name: "direction", + in: "query", + schema: { + type: "string", + enum: [ "desc", "asc" ], + default: "desc" + }, + example: "desc", + allowEmptyValue: true, + description: "Direction to list search results." + } + ] }, endpoint: { method: "GET", diff --git a/tests/http/stars.handler.integration.test.js b/tests/http/stars.handler.integration.test.js index cf9345f2..2714edee 100644 --- a/tests/http/stars.handler.integration.test.js +++ b/tests/http/stars.handler.integration.test.js @@ -22,7 +22,7 @@ describe("GET /api/stars", () => { tmpMock.mockClear(); }); - test.skip("Valid User with No Stars Returns array", async () => { + test("Valid User with No Stars Returns array", async () => { tmpMock = authMock({ ok: true, content: { @@ -44,7 +44,7 @@ describe("GET /api/stars", () => { tmpMock.mockClear(); }); - test.skip("Valid User with Stars Returns 200 Status Code", async () => { + test("Valid User with Stars Returns 200 Status Code", async () => { tmpMock = authMock({ ok: true, content: { From 6eed248836d4975f53629e0572958a9dc27f9ae7 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 14 Sep 2023 00:47:37 -0700 Subject: [PATCH 18/53] Even better docs. Use the `endpoint.params` object to get details of parameters from the `parameters` folder --- .../resources/refactor-DONT_KEEP_THIS_FILE.md | 10 ++++ src/controllers/getPackagesSearch.js | 49 +------------------ src/parameters/direction.js | 15 ++++++ src/parameters/page.js | 12 +++++ src/parameters/query.js | 10 ++++ src/parameters/sort.js | 18 +++++++ 6 files changed, 66 insertions(+), 48 deletions(-) create mode 100644 src/parameters/direction.js create mode 100644 src/parameters/page.js create mode 100644 src/parameters/query.js create mode 100644 src/parameters/sort.js diff --git a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md index 2ded9c95..9c7b0e44 100644 --- a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md +++ b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md @@ -63,3 +63,13 @@ Meaning the only tests we would likely need are: I think this is plenty to focus on now. At the very least the changes described here would likely mean a rewrite of about or over half the entire codebase. But if all goes to plan, would mean that every single piece of logic is more modular, keeping logic related to itself within the same file, and if tests are effected as hoped, would mean a much more robust testing solution, that who knows, may actually be able to achieve near 100% testing coverage. One side effect of all this change, means the possibility of generating documentation of the API based totally on the documentation itself, where we no longer would be reliant on my own `@confused-techie/quick-webserver-docs` module, nor having to ensure comments are updated. + +## Documentation + +Alright, so some cool ideas about documentation. + +Within `./src/parameters` we have modules named after the common name of any given parameter. + +Then to ensure our OpenAPI docs are ALWAYS up to date with the actual code, we take the `params` object of every single endpoint module, and we actually iterate through the `params` there and match those against these modules within `parameters`. + +So that the parameters within our docs will always match exactly with what's actually used. This does mean the name we use for the parameter within code has to match up with the names of the files. diff --git a/src/controllers/getPackagesSearch.js b/src/controllers/getPackagesSearch.js index cbf6542f..5c1d91c3 100644 --- a/src/controllers/getPackagesSearch.js +++ b/src/controllers/getPackagesSearch.js @@ -1,53 +1,6 @@ module.exports = { docs: { - parameters: [ - { - name: "q", - in: "query", - schema: { type: "string" }, - example: "generic-lsp", - required: true, - description: "Search query." - }, - { - name: "page", - in: "query", - schema: { type: "number", minimum: 1, default: 1 }, - example: 1, - allowEmptyValue: true, - description: "The page of search results to return." - }, - { - name: "sort", - in: "query", - schema: { - type: "string", - enum: [ - "downloads", - "created_at", - "updated_at", - "stars", - "relevance" - ], - default: "relevance" - }, - example: "downloads", - allowEmptyValue: true, - description: "Method to sort the results." - }, - { - name: "direction", - in: "query", - schema: { - type: "string", - enum: [ "desc", "asc" ], - default: "desc" - }, - example: "desc", - allowEmptyValue: true, - description: "Direction to list search results." - } - ] + }, endpoint: { method: "GET", diff --git a/src/parameters/direction.js b/src/parameters/direction.js new file mode 100644 index 00000000..a2ae641e --- /dev/null +++ b/src/parameters/direction.js @@ -0,0 +1,15 @@ +module.exports = { + name: "direction", + in: "query", + schema: { + type: "string", + enum: [ + "desc", + "asc" + ], + default: "desc" + }, + example: "desc", + allowEmptyValue: true, + description: "Direction to list search results." +}; diff --git a/src/parameters/page.js b/src/parameters/page.js new file mode 100644 index 00000000..86ae65c3 --- /dev/null +++ b/src/parameters/page.js @@ -0,0 +1,12 @@ +module.exports = { + name: "page", + in: "query", + schema: { + type: "number", + minimum: 1, + default: 1 + }, + example: 1, + allowEmptyValue: true, + description: "The page of available results to return." +}; diff --git a/src/parameters/query.js b/src/parameters/query.js new file mode 100644 index 00000000..ab80a63d --- /dev/null +++ b/src/parameters/query.js @@ -0,0 +1,10 @@ +module.exports = { + name: "q", + in: "query", + schema: { + type: "string" + }, + example: "generic-lsp", + required: true, + description: "Search query." +}; diff --git a/src/parameters/sort.js b/src/parameters/sort.js new file mode 100644 index 00000000..8f5509e6 --- /dev/null +++ b/src/parameters/sort.js @@ -0,0 +1,18 @@ +module.exports = { + name: "sort", + in: "query", + schema: { + type: "string", + enum: [ + "downloads", + "created_at", + "updated_at", + "stars", + "relevance" + ], + default: "relevance" + }, + example: "downloads", + allowEmptyValue: true, + description: "Method to sort the results." +}; From 8964ae18efdab23665b5be8d64da38a7d0b7dce2 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 14 Sep 2023 17:51:18 -0700 Subject: [PATCH 19/53] Vastly improved documentation of new modules --- src/controllers/getPackages.js | 15 ++++++++++++++- src/controllers/getPackagesFeatured.js | 13 ++++++++++++- src/controllers/getPackagesPackageName.js | 19 ++++++++++++++++--- .../getPackagesPackageNameStargazers.js | 15 ++++++++++++++- ...tPackagesPackageNameVersionsVersionName.js | 15 ++++++++++++++- ...esPackageNameVersionsVersionNameTarball.js | 14 ++++++++++++++ src/controllers/getPackagesSearch.js | 16 +++++++++++++++- src/parameters/engine.js | 10 ++++++++++ src/parameters/fileExtension.js | 10 ++++++++++ src/parameters/packageName.js | 11 +++++++++++ src/parameters/service.js | 10 ++++++++++ src/parameters/serviceType.js | 16 ++++++++++++++++ src/parameters/serviceVersion.js | 10 ++++++++++ src/parameters/versionName.js | 11 +++++++++++ 14 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 src/parameters/engine.js create mode 100644 src/parameters/fileExtension.js create mode 100644 src/parameters/packageName.js create mode 100644 src/parameters/service.js create mode 100644 src/parameters/serviceType.js create mode 100644 src/parameters/serviceVersion.js create mode 100644 src/parameters/versionName.js diff --git a/src/controllers/getPackages.js b/src/controllers/getPackages.js index dd052c6a..c1a8e713 100644 --- a/src/controllers/getPackages.js +++ b/src/controllers/getPackages.js @@ -1,6 +1,10 @@ +/** + * @module getPackages + */ + module.exports = { docs: { - + summary: "List all packages" }, endpoint: { method: "GET", @@ -22,6 +26,15 @@ module.exports = { fileExtension: (context, req) => { return context.query.fileExtension(req); } }, + /** + * @async + * @memberof getPackages + * @function logic + * @desc Returns all packages to user, filtered by query params. + * @param {object} params - The available query parameters. + * @param {object} context - The Endpoint Context. + * @returns {ssoPaginate} + */ async logic(params, context) { const packages = await context.database.getSortedPackages(params); diff --git a/src/controllers/getPackagesFeatured.js b/src/controllers/getPackagesFeatured.js index 0241ff57..59338924 100644 --- a/src/controllers/getPackagesFeatured.js +++ b/src/controllers/getPackagesFeatured.js @@ -1,6 +1,10 @@ +/** + * @module getPackagesFeatured + */ + module.exports = { docs: { - + summary: "Returns all featured packages. Previously undocumented endpoint." }, endpoint: { method: "GET", @@ -13,6 +17,13 @@ module.exports = { } }, params: {}, + + /** + * @async + * @memberof getPackagesFeatured + * @function logic + * @desc Retreived a list of the featured packages, as Package Object Shorts. + */ async logic(params, context) { // TODO: Does not support engine query parameter as of now const packs = await context.database.getFeaturedPackages(); diff --git a/src/controllers/getPackagesPackageName.js b/src/controllers/getPackagesPackageName.js index 0418b803..7e220310 100644 --- a/src/controllers/getPackagesPackageName.js +++ b/src/controllers/getPackagesPackageName.js @@ -1,6 +1,10 @@ +/** + * @module getPackagesPackageName + */ + module.exports = { docs: { - + summary: "Show package details." }, endpoint: { method: "GET", @@ -17,11 +21,20 @@ module.exports = { }, params: { engine: (context, req) => { return context.query.engine(req.query.engine); }, - name: (context, req) => { return context.query.packageName(req); } + packageName: (context, req) => { return context.query.packageName(req); } }, + /** + * @async + * @memberof getPackagesPackageName + * @function logic + * @desc Returns the data of a single requested package, as a Package Object Full. + * @param {object} params - The available query parameters. + * @param {object} context - The Endpoint Context. + * @returns {sso} + */ async logic(params, context) { - let pack = await context.database.getPackageByName(params.name, true); + let pack = await context.database.getPackageByName(params.packageName, true); if (!pack.ok) { const sso = new context.sso(); diff --git a/src/controllers/getPackagesPackageNameStargazers.js b/src/controllers/getPackagesPackageNameStargazers.js index afc17565..cf4cd77f 100644 --- a/src/controllers/getPackagesPackageNameStargazers.js +++ b/src/controllers/getPackagesPackageNameStargazers.js @@ -1,6 +1,10 @@ +/** + * @module getPackagesPackageNameStargazers + */ + module.exports = { docs: { - + summary: "List the users that have starred a package." }, endpoint: { method: "GET", @@ -19,6 +23,15 @@ module.exports = { packageName: (context, req) => { return context.query.packageName(req); } }, + /** + * @async + * @memberof getPackagesPackageNameStargazers + * @function logic + * @desc Returns an array of `star_gazers` from a specified package. + * @param {object} params - The available query parameters. + * @param {object} context - The Endpoint Context. + * @returns {sso} + */ async logic(params, context) { // The following can't be executed in user mode because we need the pointer const pack = await context.database.getPackageByName(params.packageName); diff --git a/src/controllers/getPackagesPackageNameVersionsVersionName.js b/src/controllers/getPackagesPackageNameVersionsVersionName.js index 26389f42..97574d54 100644 --- a/src/controllers/getPackagesPackageNameVersionsVersionName.js +++ b/src/controllers/getPackagesPackageNameVersionsVersionName.js @@ -1,6 +1,10 @@ +/** + * @module getPackagesPackageNameVersionsVersionName + */ + module.exports = { docs: { - + summary: "Get the details of a specific package version." }, endpoint: { method: "GET", @@ -20,6 +24,15 @@ module.exports = { versionName: (context, req) => { return context.query.engine(req.params.versionName); } }, + /** + * @async + * @memberof getPackagesPackageNameVersionsVersionName + * @function logic + * @desc Used to retreive the details of a specific version of a package. + * @param {object} params - The available query parameters. + * @param {object} context - The Endpoint Context. + * @returns {sso} + */ async logic(params, context) { // Check the truthiness of the returned query engine if (params.versionName === false) { diff --git a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js index de203dcc..f32a74b7 100644 --- a/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js +++ b/src/controllers/getPackagesPackageNameVersionsVersionNameTarball.js @@ -1,3 +1,7 @@ +/** + * @module getPackagesPackageNameVersionsVersionNameTarball + */ + const { URL } = require("node:url"); module.exports = { @@ -28,6 +32,16 @@ module.exports = { packageName: (context, req) => { return context.query.packageName(req); }, versionName: (context, req) => { return context.query.engine(req.params.versionName); } }, + + /** + * @async + * @memberof getPackagesPackageNameVersionsVersionNameTarball + * @function logic + * @desc Get the tarball of a specific package version. + * @param {object} params - The available query parameters. + * @param {object} context - The Endpoint Context. + * @returns {sso} + */ async logic(params, context) { // First ensure our version is valid diff --git a/src/controllers/getPackagesSearch.js b/src/controllers/getPackagesSearch.js index 5c1d91c3..8eb18868 100644 --- a/src/controllers/getPackagesSearch.js +++ b/src/controllers/getPackagesSearch.js @@ -1,6 +1,10 @@ +/** + * @module getPackagesSearch + */ + module.exports = { docs: { - + summary: "Searches all packages." }, endpoint: { method: "GET", @@ -19,6 +23,16 @@ module.exports = { query: (context, req) => { return context.query.query(req); } }, + /** + * @async + * @memberof getPackagesSearch + * @function logic + * @desc Allows user to search through all packages. Using specified query params. + * @param {object} params - The available query parameters. + * @param {object} context - The Endpoint Context. + * @todo Use custom LCS search. + * @returns {ssoPaginate} + */ async logic(params, context) { // Because the task of implementing the custom search engine is taking longer // than expected, this will instead use super basic text searching on the DB side. diff --git a/src/parameters/engine.js b/src/parameters/engine.js new file mode 100644 index 00000000..addc33f1 --- /dev/null +++ b/src/parameters/engine.js @@ -0,0 +1,10 @@ +module.exports = { + name: "engine", + in: "query", + schema: { + type: "string" + }, + example: "1.0.0", + allowEmptyValue: true, + description: "Only show packages compatible with this Pulsar version. Must be a valid Semver." +}; diff --git a/src/parameters/fileExtension.js b/src/parameters/fileExtension.js new file mode 100644 index 00000000..08fdecc5 --- /dev/null +++ b/src/parameters/fileExtension.js @@ -0,0 +1,10 @@ +module.exports = { + name: "fileExtension", + in: "query", + schema: { + type: "string" + }, + example: "coffee", + allowEmptyValue: true, + description: "File extension of which to only show compatible grammar package's of." +}; diff --git a/src/parameters/packageName.js b/src/parameters/packageName.js new file mode 100644 index 00000000..c364d9e6 --- /dev/null +++ b/src/parameters/packageName.js @@ -0,0 +1,11 @@ +module.exports = { + name: "packageName", + in: "path", + schema: { + type: "string" + }, + required: true, + allowEmptyValue: false, + example: "autocomplete-powershell", + description: "The name of the package to return details for. Must be URL escaped." +}; diff --git a/src/parameters/service.js b/src/parameters/service.js new file mode 100644 index 00000000..db5e8fdf --- /dev/null +++ b/src/parameters/service.js @@ -0,0 +1,10 @@ +module.exports = { + name: "service", + in: "query", + schema: { + type: "string" + }, + example: "autocomplete.watchEditor", + allowEmptyValue: true, + description: "The service of which to filter packages by." +}; diff --git a/src/parameters/serviceType.js b/src/parameters/serviceType.js new file mode 100644 index 00000000..2cf69aca --- /dev/null +++ b/src/parameters/serviceType.js @@ -0,0 +1,16 @@ +module.exports = { + name: "serviceType", + in: "query", + schema: { + type: "string", + enum: [ + "consumed", + "provided" + ] + }, + example: "consumed", + allowEmptyValue: true, + description: "Chooses whether to display 'consumer' or 'provider's of the specified 'service'." +}; +// TODO determine if there's a way to indicate this is a required field when +// using the 'service' query param. diff --git a/src/parameters/serviceVersion.js b/src/parameters/serviceVersion.js new file mode 100644 index 00000000..6b0f4fce --- /dev/null +++ b/src/parameters/serviceVersion.js @@ -0,0 +1,10 @@ +module.exports = { + name: "serviceVersion", + in: "query", + schema: { + type: "string" + }, + example: "0.0.1", + allowEmptyValue: true, + description: "Filter by a specific version of the 'service'." +}; diff --git a/src/parameters/versionName.js b/src/parameters/versionName.js new file mode 100644 index 00000000..132ef28b --- /dev/null +++ b/src/parameters/versionName.js @@ -0,0 +1,11 @@ +module.exports = { + name: "versionName", + in: "path", + schema: { + type: "string" + }, + required: true, + allowEmptyValue: false, + example: "1.0.0", + description: "The version of the package to access." +}; From 7708f482bd7161c54d8fdbf37a2088c258c14ac3 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 14 Sep 2023 18:11:04 -0700 Subject: [PATCH 20/53] New params, more docs --- src/controllers/deletePackagesPackageName.js | 6 +++++- src/controllers/deletePackagesPackageNameStar.js | 6 +++++- .../deletePackagesPackageNameVersionsVersionName.js | 6 +++++- src/controllers/getLogin.js | 6 +++++- src/controllers/getOauth.js | 6 +++++- src/controllers/getPat.js | 6 +++++- src/controllers/getRoot.js | 6 +++++- src/controllers/getStars.js | 4 ++++ src/controllers/getThemes.js | 6 ++++++ src/controllers/getThemesFeatured.js | 7 +++++++ src/controllers/getThemesSearch.js | 6 +++++- src/controllers/getUpdates.js | 4 ++++ src/controllers/getUsers.js | 4 ++++ src/controllers/getUsersLogin.js | 4 ++++ src/controllers/getUsersLoginStars.js | 4 ++++ src/controllers/postPackages.js | 6 +++++- src/controllers/postPackagesPackageNameStar.js | 6 +++++- src/controllers/postPackagesPackageNameVersions.js | 6 +++++- ...esPackageNameVersionsVersionNameEventsUninstall.js | 4 ++++ src/parameters/auth.js | 10 ++++++++++ src/parameters/login.js | 11 +++++++++++ 21 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 src/parameters/auth.js create mode 100644 src/parameters/login.js diff --git a/src/controllers/deletePackagesPackageName.js b/src/controllers/deletePackagesPackageName.js index 9e2f7dd6..1fc9a1aa 100644 --- a/src/controllers/deletePackagesPackageName.js +++ b/src/controllers/deletePackagesPackageName.js @@ -1,6 +1,10 @@ +/** + * @module deletePackagesPackageName + */ + module.exports = { docs: { - + summary: "Delete a package." }, endpoint: { method: "DELETE", diff --git a/src/controllers/deletePackagesPackageNameStar.js b/src/controllers/deletePackagesPackageNameStar.js index 3c2d9e06..adfe96a4 100644 --- a/src/controllers/deletePackagesPackageNameStar.js +++ b/src/controllers/deletePackagesPackageNameStar.js @@ -1,6 +1,10 @@ +/** + * @module DeletePackagesPackageNameStar + */ + module.exports = { docs: { - + summary: "Unstar a package." }, endpoint: { method: "DELETE", diff --git a/src/controllers/deletePackagesPackageNameVersionsVersionName.js b/src/controllers/deletePackagesPackageNameVersionsVersionName.js index 2c330f14..f6205855 100644 --- a/src/controllers/deletePackagesPackageNameVersionsVersionName.js +++ b/src/controllers/deletePackagesPackageNameVersionsVersionName.js @@ -1,6 +1,10 @@ +/** + * @module deletePackagesPackageNameVersionsVersionName + */ + module.exports = { docs: { - + summary: "Deletes a package version. Once a version is deleted, it cannot be used again." }, endpoint: { method: "DELETE", diff --git a/src/controllers/getLogin.js b/src/controllers/getLogin.js index 2ec2f716..34aa9e88 100644 --- a/src/controllers/getLogin.js +++ b/src/controllers/getLogin.js @@ -1,6 +1,10 @@ +/** + * @module getLogin + */ + module.exports = { docs: { - + summary: "OAuth callback URL." }, endpoint: { method: "GET", diff --git a/src/controllers/getOauth.js b/src/controllers/getOauth.js index 4bcd1af8..249a8673 100644 --- a/src/controllers/getOauth.js +++ b/src/controllers/getOauth.js @@ -1,8 +1,12 @@ +/** + * @module getOauth + */ + const superagent = require("superagent"); module.exports = { docs: { - + summary: "OAuth Callback URL." }, endpoint: { method: "GET", diff --git a/src/controllers/getPat.js b/src/controllers/getPat.js index ed148172..fdec38f0 100644 --- a/src/controllers/getPat.js +++ b/src/controllers/getPat.js @@ -1,8 +1,12 @@ +/** + * @module getPat + */ + const superagent = require("superagent"); module.exports = { docs: { - + summary: "PAT Token Signup URL." }, endpoint: { method: "GET", diff --git a/src/controllers/getRoot.js b/src/controllers/getRoot.js index 3caec6e2..d77f357c 100644 --- a/src/controllers/getRoot.js +++ b/src/controllers/getRoot.js @@ -1,6 +1,10 @@ +/** + * @module getRoot + */ + module.exports = { docs: { - + summary: "Non-Essential endpoint to return status message, and link to Swagger Instance." }, endpoint: { method: "GET", diff --git a/src/controllers/getStars.js b/src/controllers/getStars.js index c73556d4..7115043d 100644 --- a/src/controllers/getStars.js +++ b/src/controllers/getStars.js @@ -1,3 +1,7 @@ +/** + * @module getStars + */ + module.exports = { docs: { summary: "List the authenticated users' starred packages.", diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index 664b4e4a..a1ea7d13 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -1,5 +1,11 @@ +/** + * @module getThemes + */ module.exports = { + docs: { + summary: "List all packages that are themes." + }, endpoint: { method: "GET", paths: [ "/api/themes" ], diff --git a/src/controllers/getThemesFeatured.js b/src/controllers/getThemesFeatured.js index 932f47f8..391f5ead 100644 --- a/src/controllers/getThemesFeatured.js +++ b/src/controllers/getThemesFeatured.js @@ -1,4 +1,11 @@ +/** + * @module getThemesFeatured + */ + module.exports = { + docs: { + summary: "Display featured packages that are themes." + }, endpoint: { method: "GET", paths: [ "/api/themes/featured" ], diff --git a/src/controllers/getThemesSearch.js b/src/controllers/getThemesSearch.js index 1b2d13e4..2880d3da 100644 --- a/src/controllers/getThemesSearch.js +++ b/src/controllers/getThemesSearch.js @@ -1,6 +1,10 @@ +/** + * @module getThemesSearch + */ + module.exports = { docs: { - + summary: "Get featured packages that are themes. Previously undocumented." }, endpoint: { method: "GET", diff --git a/src/controllers/getUpdates.js b/src/controllers/getUpdates.js index 314fe06f..e8104195 100644 --- a/src/controllers/getUpdates.js +++ b/src/controllers/getUpdates.js @@ -1,3 +1,7 @@ +/** + * @module getUpdates + */ + module.exports = { docs: { summary: "List Pulsar Updates", diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js index f16a2265..57f0ee38 100644 --- a/src/controllers/getUsers.js +++ b/src/controllers/getUsers.js @@ -1,3 +1,7 @@ +/** + * @module getUsers + */ + module.exports = { docs: { summary: "Display details of the currently authenticated user. This endpoint is undocumented and is somewhat strange.", diff --git a/src/controllers/getUsersLogin.js b/src/controllers/getUsersLogin.js index e49df4a5..ef3f593f 100644 --- a/src/controllers/getUsersLogin.js +++ b/src/controllers/getUsersLogin.js @@ -1,3 +1,7 @@ +/** + * @module getUsersLogin + */ + module.exports = { docs: { summary: "Display the details of any user, as well as the packages they have published.", diff --git a/src/controllers/getUsersLoginStars.js b/src/controllers/getUsersLoginStars.js index 7c137a2f..48536e75 100644 --- a/src/controllers/getUsersLoginStars.js +++ b/src/controllers/getUsersLoginStars.js @@ -1,3 +1,7 @@ +/** + * @module getUsersLoginStars + */ + module.exports = { docs: { summary: "List a user's starred packages.", diff --git a/src/controllers/postPackages.js b/src/controllers/postPackages.js index 97881eb8..17d6ff1e 100644 --- a/src/controllers/postPackages.js +++ b/src/controllers/postPackages.js @@ -1,6 +1,10 @@ +/** + * @module postPackages + */ + module.exports = { docs: { - + summary: "Publishes a new Package." }, endpoint: { method: "POST", diff --git a/src/controllers/postPackagesPackageNameStar.js b/src/controllers/postPackagesPackageNameStar.js index 5f171c05..d7cd0b31 100644 --- a/src/controllers/postPackagesPackageNameStar.js +++ b/src/controllers/postPackagesPackageNameStar.js @@ -1,6 +1,10 @@ +/** + * @module postPackagesPackageNameStar + */ + module.exports = { docs: { - + summary: "Star a package." }, endpoint: { method: "POST", diff --git a/src/controllers/postPackagesPackageNameVersions.js b/src/controllers/postPackagesPackageNameVersions.js index bd37414a..5a5b4d56 100644 --- a/src/controllers/postPackagesPackageNameVersions.js +++ b/src/controllers/postPackagesPackageNameVersions.js @@ -1,6 +1,10 @@ +/** + * @module postPackagesPackageNameVersions + */ + module.exports = { docs: { - + summary: "Creates a new package version." }, endpoint: { method: "POST", diff --git a/src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js b/src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js index 5fac901c..401a34a9 100644 --- a/src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js +++ b/src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js @@ -1,3 +1,7 @@ +/** + * @module postPackagesPackageNameVersionsVersionNameEventsUninstall + */ + module.exports = { docs: { summary: "Previously undocumented endpoint. Since v1.0.2 has no effect.", diff --git a/src/parameters/auth.js b/src/parameters/auth.js new file mode 100644 index 00000000..21441c11 --- /dev/null +++ b/src/parameters/auth.js @@ -0,0 +1,10 @@ +module.exports = { + name: "auth", + in: "header", + schema: { + type: "string" + }, + required: true, + allowEmptyValue: false, + description: "Authorization Headers." +}; diff --git a/src/parameters/login.js b/src/parameters/login.js new file mode 100644 index 00000000..c2dde762 --- /dev/null +++ b/src/parameters/login.js @@ -0,0 +1,11 @@ +module.exports = { + name: "login", + in: "path", + schema: { + type: "string" + }, + required: true, + allowEmptyValue: false, + example: "confused-Techie", + description: "The User from the URL path." +}; From 18c5e9bf4ae858f2a29e2ee8e09a816c98ace550 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 14 Sep 2023 22:35:18 -0700 Subject: [PATCH 21/53] Add more tests back, fix short assignment within SSO --- jest.config.js | 2 +- src/models/sso.js | 9 ++++-- .../http/login.test.js | 2 +- ...dler.integration.test.js => stars.test.js} | 0 .../http/updates.test.js | 3 +- tests/unit/controllers/getUpdates.test.js | 31 +++++++++++++++++++ .../postPackagesPackageNameVersions.test.js | 2 +- ...VersionsVersionNameEventsUninstall.test.js | 4 +-- 8 files changed, 43 insertions(+), 10 deletions(-) rename test/login.handler.integration.test.js => tests/http/login.test.js (86%) rename tests/http/{stars.handler.integration.test.js => stars.test.js} (100%) rename test/updates.handler.integration.test.js => tests/http/updates.test.js (80%) create mode 100644 tests/unit/controllers/getUpdates.test.js rename tests/unit/{ => controllers}/postPackagesPackageNameVersions.test.js (74%) rename tests/unit/{ => controllers}/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js (75%) diff --git a/jest.config.js b/jest.config.js index 9a9c9832..84968de8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -32,7 +32,7 @@ const config = { "/tests/helpers/global.setup.jest.js" ], testMatch: [ - "/tests/unit/**.test.js" + "/tests/unit/**/**.test.js" ] } ] diff --git a/src/models/sso.js b/src/models/sso.js index 146c3a91..69d7dcd2 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -76,7 +76,11 @@ class SSO { } addShort(enumValue) { - if (typeof this.short !== "string" && typeof enumValue === "string" && validEnums.includes(enumValue)) { + if ( + !this.short?.length > 0 && + typeof enumValue === "string" && + validEnums.includes(enumValue) + ) { // Only assign short once this.short = enumValue; } @@ -104,8 +108,7 @@ class SSO { } handleError(req, res, context) { - console.log(this.content); - console.log(this.calls); + console.log(this); let shortToUse, msgToUse, codeToUse; diff --git a/test/login.handler.integration.test.js b/tests/http/login.test.js similarity index 86% rename from test/login.handler.integration.test.js rename to tests/http/login.test.js index 39094e94..7f0bc86e 100644 --- a/test/login.handler.integration.test.js +++ b/tests/http/login.test.js @@ -1,5 +1,5 @@ const request = require("supertest"); -const app = require("../src/main.js"); +const app = require("../../src/setupEndpoints.js"); describe("Get /api/login", () => { test("Returns proper Status Code", async () => { diff --git a/tests/http/stars.handler.integration.test.js b/tests/http/stars.test.js similarity index 100% rename from tests/http/stars.handler.integration.test.js rename to tests/http/stars.test.js diff --git a/test/updates.handler.integration.test.js b/tests/http/updates.test.js similarity index 80% rename from test/updates.handler.integration.test.js rename to tests/http/updates.test.js index 93196e86..a314af4e 100644 --- a/test/updates.handler.integration.test.js +++ b/tests/http/updates.test.js @@ -1,8 +1,7 @@ const request = require("supertest"); -const app = require("../src/main.js"); +const app = require("../../src/setupEndpoints.js"); describe("GET /api/updates", () => { - test.todo("/api/updates currentlty returns Not Supported."); test("Returns NotSupported Status Code.", async () => { const res = await request(app).get("/api/updates"); expect(res).toHaveHTTPCode(501); diff --git a/tests/unit/controllers/getUpdates.test.js b/tests/unit/controllers/getUpdates.test.js new file mode 100644 index 00000000..38797558 --- /dev/null +++ b/tests/unit/controllers/getUpdates.test.js @@ -0,0 +1,31 @@ +const endpoint = require("../../../src/controllers/getUpdates.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "GET", + rateLimit: "generic", + successStatus: 200, + paths: [ "/api/updates" ] + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("Has correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + }); +}); + +describe("Functions as expected", () => { + test("Returns correct SSO Object", async () => { + const sso = await endpoint.logic( + {}, + require("../../../src/context.js") + ); + + expect(sso.ok).toBe(false); + expect(sso.short).toBe("not_supported"); + + }); +}); diff --git a/tests/unit/postPackagesPackageNameVersions.test.js b/tests/unit/controllers/postPackagesPackageNameVersions.test.js similarity index 74% rename from tests/unit/postPackagesPackageNameVersions.test.js rename to tests/unit/controllers/postPackagesPackageNameVersions.test.js index 438c9c99..5a1f860a 100644 --- a/tests/unit/postPackagesPackageNameVersions.test.js +++ b/tests/unit/controllers/postPackagesPackageNameVersions.test.js @@ -1,4 +1,4 @@ -const endpoint = require("../../src/controllers/postPackagesPackageNameVersions.js"); +const endpoint = require("../../../src/controllers/postPackagesPackageNameVersions.js"); describe("Has features expected", () => { test("Has correct endpoint features", () => { diff --git a/tests/unit/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js b/tests/unit/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js similarity index 75% rename from tests/unit/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js rename to tests/unit/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js index f987cb32..9853c087 100644 --- a/tests/unit/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js +++ b/tests/unit/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.test.js @@ -1,5 +1,5 @@ -const endpoint = require("../../src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js"); -const context = require("../../src/context.js"); +const endpoint = require("../../../src/controllers/postPackagesPackageNameVersionsVersionNameEventsUninstall.js"); +const context = require("../../../src/context.js"); describe("Has features expected", () => { test("Has correct endpoint features", () => { From 3fdcf057cb85f44652c607482c337cf9ec10a21c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Thu, 14 Sep 2023 23:39:40 -0700 Subject: [PATCH 22/53] `getStars` tests --- tests/unit/controllers/getStars.test.js | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unit/controllers/getStars.test.js diff --git a/tests/unit/controllers/getStars.test.js b/tests/unit/controllers/getStars.test.js new file mode 100644 index 00000000..8283815c --- /dev/null +++ b/tests/unit/controllers/getStars.test.js @@ -0,0 +1,30 @@ +const endpoint = require("../../../src/controllers/getStars.js"); +const context = require("../../../src/context.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "GET", + paths: [ "/api/stars" ], + rateLimit: "generic", + successStatus: 200 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); +}); + +describe("Returns as expected", () => { + test("When 'auth.verifyAuth' fails", async () => { + const localContext = context; + localContext.auth = { + verifyAuth: () => { return { ok: false, content: "Test Failure" }; } + }; + + const res = await endpoint.logic({}, localContext); + + expect(res.ok).toBe(false); + expect(res.content).toBeDefined(); + expect(res.content.content).toBe("Test Failure"); + }); +}); From 701d0e3c4a714db9b0cb73cc1a685fff5abab08c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Fri, 15 Sep 2023 00:43:41 -0700 Subject: [PATCH 23/53] 100% coverage in `getStars.js` --- tests/unit/controllers/getStars.test.js | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/unit/controllers/getStars.test.js b/tests/unit/controllers/getStars.test.js index 8283815c..022cfd7e 100644 --- a/tests/unit/controllers/getStars.test.js +++ b/tests/unit/controllers/getStars.test.js @@ -27,4 +27,71 @@ describe("Returns as expected", () => { expect(res.content).toBeDefined(); expect(res.content.content).toBe("Test Failure"); }); + + test("When 'db.getStarredPointersByUserID' fails", async () => { + const localContext = context; + localContext.auth = { + verifyAuth: () => { return { ok: true, content: { id: 1 } }; } + }; + localContext.database = { + getStarredPointersByUserID: () => { return { ok: false, content: "db Test Failure" }; } + }; + + const res = await endpoint.logic({}, localContext); + + expect(res.ok).toBe(false); + expect(res.content).toBeDefined(); + expect(res.content.content).toBe("db Test Failure"); + }); + + test("When the user has no stars", async () => { + const localContext = context; + localContext.auth = { + verifyAuth: () => { return { ok: true, content: { id: 1 } }; } + }; + localContext.database = { + getStarredPointersByUserID: () => { return { ok: true, content: [] }; } + }; + + const res = await endpoint.logic({}, localContext); + + expect(res.ok).toBe(true); + expect(res.content).toBeArray(); + expect(res.content.length).toBe(0); + }); + + test("When 'db.getPackageCollectionByID' fails", async () => { + const localContext = context; + localContext.auth = { + verifyAuth: () => { return { ok: true, content: { id: 1 } }; } + }; + localContext.database = { + getStarredPointersByUserID: () => { return { ok: true, content: [ "an_id" ] }; }, + getPackageCollectionByID: () => { return { ok: false, content: "Another DB Error" }; } + }; + + const res = await endpoint.logic({}, localContext); + + expect(res.ok).toBe(false); + expect(res.content.content).toBe("Another DB Error"); + }); + + test("When request succeeds", async () => { + const localContext = context; + localContext.auth = { + verifyAuth: () => { return { ok: true, content: { id: 1 } }; } + }; + localContext.database = { + getStarredPointersByUserID: () => { return { ok: true, content: [ "an_id" ] }; }, + getPackageCollectionByID: () => { return { ok: true, content: {} }; } + }; + localContext.utils = { + constructPackageObjectShort: () => { return { item: "is_a_package" }; } + }; + + const res = await endpoint.logic({}, localContext); + + expect(res.ok).toBe(true); + expect(res.content.item).toBe("is_a_package"); + }); }); From 0d2cca76c1ece262f4b19085f59f4f53241809ec Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sat, 16 Sep 2023 01:21:49 -0700 Subject: [PATCH 24/53] Tests, bug fixes, strict objects * Added more tests * Fixed a previously uncaught bug of the type of data returned within the user object * Created a system for strictly checking the objects returned from endpoints, to ensure they match what's intended --- .../resources/refactor-DONT_KEEP_THIS_FILE.md | 5 ++ jest.config.js | 5 +- src/controllers/getUsersLogin.js | 31 +++++++---- src/database.js | 7 ++- tests/helpers/utils.helper.jest.js | 23 ++++++++ tests/http/getUsersLogin.test.js | 53 +++++++++++++++++++ tests/models/message.js | 20 +++++++ tests/models/userObjectPublic.js | 40 ++++++++++++++ tests/unit/controllers/getUsersLogin.test.js | 35 ++++++++++++ 9 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 tests/helpers/utils.helper.jest.js create mode 100644 tests/http/getUsersLogin.test.js create mode 100644 tests/models/message.js create mode 100644 tests/models/userObjectPublic.js create mode 100644 tests/unit/controllers/getUsersLogin.test.js diff --git a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md index 9c7b0e44..4f07769d 100644 --- a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md +++ b/docs/resources/refactor-DONT_KEEP_THIS_FILE.md @@ -73,3 +73,8 @@ Within `./src/parameters` we have modules named after the common name of any giv Then to ensure our OpenAPI docs are ALWAYS up to date with the actual code, we take the `params` object of every single endpoint module, and we actually iterate through the `params` there and match those against these modules within `parameters`. So that the parameters within our docs will always match exactly with what's actually used. This does mean the name we use for the parameter within code has to match up with the names of the files. + +Additionally, for the content, we can reference a fileName from ./tests/models so that we can accomplish the following: + +* Use that object's `schema` and `example` to generate an object +* We can also possibly use this to ensure the return matches what we expect. diff --git a/jest.config.js b/jest.config.js index 84968de8..4ec305d6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,8 +9,9 @@ const config = { "clover" ], coveragePathIgnorePatterns: [ - "/tests/fixtures/**", - "/node_modules/**" + "/tests/", + "/test/", + "/node_modules/" ], projects: [ { diff --git a/src/controllers/getUsersLogin.js b/src/controllers/getUsersLogin.js index ef3f593f..3068b76c 100644 --- a/src/controllers/getUsersLogin.js +++ b/src/controllers/getUsersLogin.js @@ -1,18 +1,31 @@ /** * @module getUsersLogin */ - + module.exports = { docs: { summary: "Display the details of any user, as well as the packages they have published.", - responses: [ - { - 200: { - description: "The returned details of a specific user.", - content: {} + responses: { + 200: { + description: "The returned details of a specific user.", + content: { + // This references the file name of a `./tests/models` model + "application/json": "$userObjectPublic" + } + }, + 404: { + description: "The User requested cannot be found.", + content: { + "application/json": "$message" + } + }, + 500: { + description: "An error has occured.", + content: { + "application/json": "$message" } } - ] + } }, endpoint: { method: "GET", @@ -53,8 +66,8 @@ module.exports = { const returnUser = { username: user.content.username, avatar: user.content.avatar, - created_at: user.content.created_at, - data: user.content.data, + created_at: `${user.content.created_at}`, + data: user.content.data ?? {}, packages: [], // included as it should be used in the future }; diff --git a/src/database.js b/src/database.js index b94667e8..09b273f8 100644 --- a/src/database.js +++ b/src/database.js @@ -1096,14 +1096,13 @@ async function getUserByName(username) { : { ok: false, content: `Unable to query for user: ${username}`, - short: "Not Found", + short: "not_found", }; } catch (err) { return { ok: false, - content: "Generic Error", - short: "Server Error", - error: err, + content: err, + short: "server_error" }; } } diff --git a/tests/helpers/utils.helper.jest.js b/tests/helpers/utils.helper.jest.js new file mode 100644 index 00000000..14ae65ac --- /dev/null +++ b/tests/helpers/utils.helper.jest.js @@ -0,0 +1,23 @@ + +// Used to ensure the current returned data of an SSO matches +// What the `successStatus` code of the endpoints docs say it should be + +function matchesSuccessObject(sso, endpoint) { + + for (const response in endpoint.docs.responses) { + if (response == endpoint.endpoint.successStatus) { + let obj = endpoint.docs.responses[response].content["application/json"]; + + if (obj.startsWith("$")) { + obj = require(`../models/${obj.replace("$","")}.js`); + } + + expect(sso.content).toMatchObject(obj.test); + return true; + } + } +} + +module.exports = { + matchesSuccessObject, +}; diff --git a/tests/http/getUsersLogin.test.js b/tests/http/getUsersLogin.test.js new file mode 100644 index 00000000..b9a4b133 --- /dev/null +++ b/tests/http/getUsersLogin.test.js @@ -0,0 +1,53 @@ +const endpoint = require("../../src/controllers/getUsersLogin.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); +const userObject = require("../models/userObjectPublic.js"); + +describe("Behaves as expected", () => { + + test("Calls the correct db function", async () => { + const localContext = context; + const spy = jest.spyOn(localContext.database, "getUserByName"); + + const res = await endpoint.logic({}, localContext); + + expect(spy).toBeCalledTimes(1); + }); + + test("Returns bad SSO on failure", async () => { + const userName = "our-test-user"; + + const res = await endpoint.logic({ login: userName }, context); + + expect(res.ok).toBe(false); + expect(res.content.short).toBe("not_found"); + }); + + test("Returns Correct SSO on Success", async () => { + const userObj = userObject.example; + userObj.username = "our-test-user"; + + // First we add a new fake user + await database.insertNewUser(userObj.username, "id", userObj.avatar); + + const res = await endpoint.logic( + { + login: userObj.username + }, + context + ); + + expect(res.ok).toBe(true); + // First make sure the user object matches expectations broadly, then specifically + expect(res.content).toMatchObject(userObject.test); + expect(res.content.username).toBe(userObj.username); + expect(res.content.avatar).toBe(userObj.avatar); + + // We also want to ensure that the object matches what the docs say it should + const { matchesSuccessObject } = require("../helpers/utils.helper.jest.js"); + + const match = matchesSuccessObject(res, endpoint); + expect(match).toBeTruthy(); + // TODO delete once there's a method to do so + }); +}); diff --git a/tests/models/message.js b/tests/models/message.js new file mode 100644 index 00000000..c658ca51 --- /dev/null +++ b/tests/models/message.js @@ -0,0 +1,20 @@ +module.exports = { + schema: { + description: "A generic object that could contain status information or error messages.", + type: "object", + required: [ + "message" + ], + properties: { + message: { + type: "string" + } + } + }, + example: { + message: "This is some message content." + }, + test: { + message: expect.toBeTypeof("string") + } +}; diff --git a/tests/models/userObjectPublic.js b/tests/models/userObjectPublic.js new file mode 100644 index 00000000..04346a47 --- /dev/null +++ b/tests/models/userObjectPublic.js @@ -0,0 +1,40 @@ +module.exports = { + schema: { + description: "Publicaly returned information of users on Pulsar.", + type: "object", + required: [ + "username", "avatar", "data", "created_at", "packages" + ], + properties: { + username: { + type: "string" + }, + avatar: { + type: "string" + }, + data: { + type: "object" + }, + created_at: { + type: "string" + }, + packages: { + type: "array" + } + } + }, + example: { + username: "confused-Techie", + avatar: "https://avatar.url", + data: {}, + created_at: "2023-09-16T00:58:36.755Z", + packages: [] + }, + test: { + username: expect.toBeTypeof("string"), + avatar: expect.toBeTypeof("string"), + data: {}, + created_at: expect.toBeTypeof("string"), + packages: expect.toBeArray() + } +}; diff --git a/tests/unit/controllers/getUsersLogin.test.js b/tests/unit/controllers/getUsersLogin.test.js new file mode 100644 index 00000000..dd2f95b1 --- /dev/null +++ b/tests/unit/controllers/getUsersLogin.test.js @@ -0,0 +1,35 @@ +const endpoint = require("../../../src/controllers/getUsersLogin.js"); +const context = require("../../../src/context.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "GET", + paths: [ "/api/users/:login" ], + rateLimit: "generic", + successStatus: 200 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("Returns params as provided", () => { + const req = { + params: { + login: "test-user" + } + }; + + const res = endpoint.params.login(context, req); + + expect(res).toBe("test-user"); + }); + + test("Returns params when missing", () => { + const req = { params: {} }; + + const res = endpoint.params.login(context, req); + + expect(res).toBe(""); + }); +}); From e302c2da708fbaa82ab13f76d6e7d91563b1c672 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sat, 16 Sep 2023 18:10:19 -0700 Subject: [PATCH 25/53] Total test coverage of `getUsers.js` --- src/controllers/getUsers.js | 14 +-- src/controllers/getUsersLogin.js | 8 +- tests/http/getUsers.test.js | 94 ++++++++++++++++++++ tests/http/getUsersLogin.test.js | 5 +- tests/models/userObjectPrivate.js | 50 +++++++++++ tests/unit/controllers/getUsers.test.js | 49 ++++++++++ tests/unit/controllers/getUsersLogin.test.js | 3 + 7 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 tests/http/getUsers.test.js create mode 100644 tests/models/userObjectPrivate.js create mode 100644 tests/unit/controllers/getUsers.test.js diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js index 57f0ee38..e167a74e 100644 --- a/src/controllers/getUsers.js +++ b/src/controllers/getUsers.js @@ -1,19 +1,19 @@ /** * @module getUsers */ - + module.exports = { docs: { summary: "Display details of the currently authenticated user. This endpoint is undocumented and is somewhat strange.", description: "This endpoint only exists on the web version of the upstream API. Having no backend equivolent.", - responses: [ - { - 200: { - description: "Details of the Authenticated User Account.", - content: {} + responses: { + 200: { + description: "Details of the Authenticated User Account.", + content: { + "application/json": "$userObjectPrivate" } } - ] + } }, endpoint: { method: "GET", diff --git a/src/controllers/getUsersLogin.js b/src/controllers/getUsersLogin.js index 3068b76c..b1af684c 100644 --- a/src/controllers/getUsersLogin.js +++ b/src/controllers/getUsersLogin.js @@ -7,7 +7,7 @@ module.exports = { summary: "Display the details of any user, as well as the packages they have published.", responses: { 200: { - description: "The returned details of a specific user.", + description: "The public details of a specific user.", content: { // This references the file name of a `./tests/models` model "application/json": "$userObjectPublic" @@ -18,12 +18,6 @@ module.exports = { content: { "application/json": "$message" } - }, - 500: { - description: "An error has occured.", - content: { - "application/json": "$message" - } } } }, diff --git a/tests/http/getUsers.test.js b/tests/http/getUsers.test.js new file mode 100644 index 00000000..99b41ea4 --- /dev/null +++ b/tests/http/getUsers.test.js @@ -0,0 +1,94 @@ +const endpoint = require("../../src/controllers/getUsers.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); +const userObject = require("../models/userObjectPrivate.js"); +const { matchesSuccessObject } = require("../helpers/utils.helper.jest.js"); + +describe("Behaves as expected", () => { + + test("Calls the correct function", async () => { + const localContext = context; + localContext.auth = { + verifyAuth: () => { return { ok: false }; } + }; + + const spy = jest.spyOn(localContext.auth, "verifyAuth"); + + await endpoint.logic({}, localContext); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); + + test("Returns bad SSO on failure", async () => { + const localContext = context; + localContext.auth = { + verifyAuth: () => { return { ok: false, content: "A test fail" }; } + }; + + const sso = await endpoint.logic({}, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.content).toBe("A test fail"); + }); + + test("Returns good SSO on success", async () => { + const testUser = userObject.example; + testUser.username = "test-user"; + + const localContext = context; + localContext.auth = { + verifyAuth: () => { + return { + ok: true, + content: testUser + }; + } + }; + + const sso = await endpoint.logic({}, localContext); + + expect(sso.ok).toBe(true); + expect(sso.content).toMatchObject(testUser); + + const match = matchesSuccessObject(sso, endpoint); + expect(match).toBeTruthy(); + }); +}); + +describe("Extra functions behave", () => { + test("preLogic adds headers as needed", async () => { + const headerObj = {}; + const res = { + header: (name, val) => { headerObj[name] = val; } + }; + + await endpoint.preLogic({}, res, {}); + + const expected = { + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "Content-Type, Authorization, Access-Control-Allow-Credentials", + "Access-Control-Allow-Origin": "https://web.pulsar-edit.dev", + "Access-Control-Allow-Credentials": true + }; + + expect(headerObj).toMatchObject(expected); + }); + + test("postLogic adds headers as needed", async () => { + let headerObj = {}; + + const res = { + set: (obj) => { headerObj = obj; } + }; + + await endpoint.postLogic({}, res, {}); + + const expected = { + "Access-Control-Allow-Credentials": true + }; + + expect(headerObj).toMatchObject(expected); + }); +}); diff --git a/tests/http/getUsersLogin.test.js b/tests/http/getUsersLogin.test.js index b9a4b133..77eab0dc 100644 --- a/tests/http/getUsersLogin.test.js +++ b/tests/http/getUsersLogin.test.js @@ -2,6 +2,7 @@ const endpoint = require("../../src/controllers/getUsersLogin.js"); const database = require("../../src/database.js"); const context = require("../../src/context.js"); const userObject = require("../models/userObjectPublic.js"); +const { matchesSuccessObject } = require("../helpers/utils.helper.jest.js"); describe("Behaves as expected", () => { @@ -12,6 +13,8 @@ describe("Behaves as expected", () => { const res = await endpoint.logic({}, localContext); expect(spy).toBeCalledTimes(1); + + spy.mockClear(); }); test("Returns bad SSO on failure", async () => { @@ -44,8 +47,6 @@ describe("Behaves as expected", () => { expect(res.content.avatar).toBe(userObj.avatar); // We also want to ensure that the object matches what the docs say it should - const { matchesSuccessObject } = require("../helpers/utils.helper.jest.js"); - const match = matchesSuccessObject(res, endpoint); expect(match).toBeTruthy(); // TODO delete once there's a method to do so diff --git a/tests/models/userObjectPrivate.js b/tests/models/userObjectPrivate.js new file mode 100644 index 00000000..ab182ee9 --- /dev/null +++ b/tests/models/userObjectPrivate.js @@ -0,0 +1,50 @@ +module.exports = { + schema: { + description: "Privately returned information of users on Pulsar.", + type: "object", + required: [ + "username", "avatar", "data", "created_at", "packages" + ], + properties: { + username: { + type: "string" + }, + avatar: { + type: "string" + }, + data: { + type: "object" + }, + node_id: { + type: "string" + }, + token: { + type: "string" + }, + created_at: { + type: "string" + }, + packages: { + type: "array" + } + } + }, + example: { + username: "confused-Techie", + avatar: "https://avatar.url", + data: {}, + node_id: "users-node-id", + token: "user-api-token", + created_at: "2023-09-16T00:58:36.755Z", + packages: [] + }, + test: { + username: expect.toBeTypeof("string"), + avatar: expect.toBeTypeof("string"), + data: {}, + node_id: expect.toBeTypeof("string"), + token: expect.toBeTypeof("string"), + created_at: expect.toBeTypeof("string"), + packages: expect.toBeArray() + } +}; diff --git a/tests/unit/controllers/getUsers.test.js b/tests/unit/controllers/getUsers.test.js new file mode 100644 index 00000000..65ac2dca --- /dev/null +++ b/tests/unit/controllers/getUsers.test.js @@ -0,0 +1,49 @@ +const endpoint = require("../../../src/controllers/getUsers.js"); +const context = require("../../../src/context.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "GET", + paths: [ "/api/users" ], + rateLimit: "auth", + successStatus: 200 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("Has correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + expect(endpoint.preLogic).toBeTypeof("function"); + expect(endpoint.postLogic).toBeTypeof("function"); + }); +}); + +describe("Parameters function as expected", () => { + test("Returns params as provided", () => { + const req = { + get: (wants) => { + if (wants === "Authorization") { + return "Auth-Token"; + } else { + return ""; + } + } + }; + + const res = endpoint.params.auth(context, req); + + expect(res).toBe("Auth-Token"); + }); + + test("Returns params when missing", () => { + const req = { + get: () => { return ""; } + }; + + const res = endpoint.params.auth(context, req); + + expect(res).toBe(""); + }); +}); diff --git a/tests/unit/controllers/getUsersLogin.test.js b/tests/unit/controllers/getUsersLogin.test.js index dd2f95b1..f475b58f 100644 --- a/tests/unit/controllers/getUsersLogin.test.js +++ b/tests/unit/controllers/getUsersLogin.test.js @@ -13,6 +13,9 @@ describe("Has features expected", () => { expect(endpoint.endpoint).toMatchObject(expected); }); +}); + +describe("Parameters function as expected", () => { test("Returns params as provided", () => { const req = { params: { From 10768d2e77464f772a433badfedb22a8b28853a9 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sat, 16 Sep 2023 19:27:05 -0700 Subject: [PATCH 26/53] Move `matchesSuccessObject()` to expect extension --- tests/helpers/global.setup.jest.js | 29 +++++++++++++++++++++++++++++ tests/helpers/utils.helper.jest.js | 23 ----------------------- tests/http/getUsers.test.js | 5 +---- tests/http/getUsersLogin.test.js | 6 +----- 4 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 tests/helpers/utils.helper.jest.js diff --git a/tests/helpers/global.setup.jest.js b/tests/helpers/global.setup.jest.js index a95b5bfb..604ac7ad 100644 --- a/tests/helpers/global.setup.jest.js +++ b/tests/helpers/global.setup.jest.js @@ -49,6 +49,35 @@ expect.extend({ }; } }, + // `expect().toMatchEndpointSuccessObject(endpoint)` + toMatchEndpointSuccessObject(sso, endpoint) { + let done = false; + for (const response in endpoint.docs.responses) { + // We use `==` to facilitate type coercion + if (response == endpoint.endpoint.successStatus) { + let obj = endpoint.docs.responses[response].content["application/json"]; + + if (obj.startsWith("$")) { + obj = require(`../models/${obj.replace("$","")}.js`); + } + + expect(sso.content).toMatchObject(obj.test); + done = true; + break; + } + } + if (done) { + return { + pass: true, message: () => "" + }; + } else { + return { + pass: false, + message: () => + `Unable to find ${endpoint.endpoint.successStatus}.` + }; + } + }, // `expect().toHaveHTTPCode()` toHaveHTTPCode(req, want) { // Type coercion here because the statusCode in the request object could be set as a string. diff --git a/tests/helpers/utils.helper.jest.js b/tests/helpers/utils.helper.jest.js deleted file mode 100644 index 14ae65ac..00000000 --- a/tests/helpers/utils.helper.jest.js +++ /dev/null @@ -1,23 +0,0 @@ - -// Used to ensure the current returned data of an SSO matches -// What the `successStatus` code of the endpoints docs say it should be - -function matchesSuccessObject(sso, endpoint) { - - for (const response in endpoint.docs.responses) { - if (response == endpoint.endpoint.successStatus) { - let obj = endpoint.docs.responses[response].content["application/json"]; - - if (obj.startsWith("$")) { - obj = require(`../models/${obj.replace("$","")}.js`); - } - - expect(sso.content).toMatchObject(obj.test); - return true; - } - } -} - -module.exports = { - matchesSuccessObject, -}; diff --git a/tests/http/getUsers.test.js b/tests/http/getUsers.test.js index 99b41ea4..b03fa7f3 100644 --- a/tests/http/getUsers.test.js +++ b/tests/http/getUsers.test.js @@ -2,7 +2,6 @@ const endpoint = require("../../src/controllers/getUsers.js"); const database = require("../../src/database.js"); const context = require("../../src/context.js"); const userObject = require("../models/userObjectPrivate.js"); -const { matchesSuccessObject } = require("../helpers/utils.helper.jest.js"); describe("Behaves as expected", () => { @@ -51,9 +50,7 @@ describe("Behaves as expected", () => { expect(sso.ok).toBe(true); expect(sso.content).toMatchObject(testUser); - - const match = matchesSuccessObject(sso, endpoint); - expect(match).toBeTruthy(); + expect(sso).toMatchEndpointSuccessObject(endpoint); }); }); diff --git a/tests/http/getUsersLogin.test.js b/tests/http/getUsersLogin.test.js index 77eab0dc..8c414d24 100644 --- a/tests/http/getUsersLogin.test.js +++ b/tests/http/getUsersLogin.test.js @@ -2,7 +2,6 @@ const endpoint = require("../../src/controllers/getUsersLogin.js"); const database = require("../../src/database.js"); const context = require("../../src/context.js"); const userObject = require("../models/userObjectPublic.js"); -const { matchesSuccessObject } = require("../helpers/utils.helper.jest.js"); describe("Behaves as expected", () => { @@ -45,10 +44,7 @@ describe("Behaves as expected", () => { expect(res.content).toMatchObject(userObject.test); expect(res.content.username).toBe(userObj.username); expect(res.content.avatar).toBe(userObj.avatar); - - // We also want to ensure that the object matches what the docs say it should - const match = matchesSuccessObject(res, endpoint); - expect(match).toBeTruthy(); + expect(res).toMatchEndpointSuccessObject(endpoint); // TODO delete once there's a method to do so }); }); From 1f47b119f53cbfebf5f22143b57b9c3508f7f676 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 17 Sep 2023 17:03:41 -0700 Subject: [PATCH 27/53] Theme testing, stop outdated migrations --- .../0002-post-star-test.sql | 0 .../0003-post-package-version-test.sql | 0 .../0004-delete-package-test.sql | 0 .../0005-get-user-test.sql | 0 .../0006-get-stars-test.sql | 0 .../0007-get-package.sql | 0 .../0008-migrated-initial.sql | 120 +++++++++++++++++ scripts/migrations/0001-initial-migration.sql | 123 ------------------ src/controllers/getThemes.js | 10 +- src/database.js | 4 +- src/models/ssoPaginate.js | 8 +- tests/http/getThemes.test.js | 113 ++++++++++++++++ tests/http/stars.test.js | 2 +- tests/unit/controllers/getThemes.js | 52 ++++++++ 14 files changed, 303 insertions(+), 129 deletions(-) rename scripts/{migrations => deprecated-migrations}/0002-post-star-test.sql (100%) rename scripts/{migrations => deprecated-migrations}/0003-post-package-version-test.sql (100%) rename scripts/{migrations => deprecated-migrations}/0004-delete-package-test.sql (100%) rename scripts/{migrations => deprecated-migrations}/0005-get-user-test.sql (100%) rename scripts/{migrations => deprecated-migrations}/0006-get-stars-test.sql (100%) rename scripts/{migrations => deprecated-migrations}/0007-get-package.sql (100%) create mode 100644 scripts/deprecated-migrations/0008-migrated-initial.sql create mode 100644 tests/http/getThemes.test.js create mode 100644 tests/unit/controllers/getThemes.js diff --git a/scripts/migrations/0002-post-star-test.sql b/scripts/deprecated-migrations/0002-post-star-test.sql similarity index 100% rename from scripts/migrations/0002-post-star-test.sql rename to scripts/deprecated-migrations/0002-post-star-test.sql diff --git a/scripts/migrations/0003-post-package-version-test.sql b/scripts/deprecated-migrations/0003-post-package-version-test.sql similarity index 100% rename from scripts/migrations/0003-post-package-version-test.sql rename to scripts/deprecated-migrations/0003-post-package-version-test.sql diff --git a/scripts/migrations/0004-delete-package-test.sql b/scripts/deprecated-migrations/0004-delete-package-test.sql similarity index 100% rename from scripts/migrations/0004-delete-package-test.sql rename to scripts/deprecated-migrations/0004-delete-package-test.sql diff --git a/scripts/migrations/0005-get-user-test.sql b/scripts/deprecated-migrations/0005-get-user-test.sql similarity index 100% rename from scripts/migrations/0005-get-user-test.sql rename to scripts/deprecated-migrations/0005-get-user-test.sql diff --git a/scripts/migrations/0006-get-stars-test.sql b/scripts/deprecated-migrations/0006-get-stars-test.sql similarity index 100% rename from scripts/migrations/0006-get-stars-test.sql rename to scripts/deprecated-migrations/0006-get-stars-test.sql diff --git a/scripts/migrations/0007-get-package.sql b/scripts/deprecated-migrations/0007-get-package.sql similarity index 100% rename from scripts/migrations/0007-get-package.sql rename to scripts/deprecated-migrations/0007-get-package.sql diff --git a/scripts/deprecated-migrations/0008-migrated-initial.sql b/scripts/deprecated-migrations/0008-migrated-initial.sql new file mode 100644 index 00000000..9a320c6d --- /dev/null +++ b/scripts/deprecated-migrations/0008-migrated-initial.sql @@ -0,0 +1,120 @@ +-- Enter our Test data into the Database. + +INSERT INTO packages (pointer, package_type, name, creation_method, downloads, stargazers_count, data, original_stargazers) +VALUES ( + 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'package', 'language-css', 'user made', 400004, 1, + '{"name": "language-css", "readme": "Cool readme", "metadata": {"bugs": {"url": "https://github.com/atom/language-css/issues"}, + "name": "language-css", "engines": {"atom": "*","node":"*"},"license":"MIT","version":"0.45.7","homepage":"http://atom.github.io/language-css", + "keywords":["tree-sitter"],"repository":{"url":"https://github.com/atom/language-css.git","type":"git"},"description":"CSS Support in Atom", + "dependencies":{"tree-sitter-css":"^0.19.0"},"devDependencies":{"coffeelint":"^1.10.1"}},"repository":{"url":"https://github.com/atom/langauge-css", + "type":"git"}}', 76 +), ( + 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 'package', 'language-cpp', 'user made', 849156, 1, + '{"name": "language-cpp", "description": "C++ Support in Atom", "keywords": ["tree-sitter"]}', 91 +), ( + 'ee87223f-65ab-4a1d-8f45-09fcf8e64423', 'package', 'hydrogen', 'Migrated from Atom.io', 2562844, 1, + '{"name": "hydrogen", "readme": "Hydrogen Readme", "metadata": { "main": "./dist/main", "name": "Hydrogen", + "author": "nteract contributors", "engines": {"atom": ">=1.28.0 <2.0.0"}, "license": "MIT", "version": "2.16.3"}}', 821 +), ( + 'aea26882-8459-4725-82ad-41bf7aa608c3', 'package', 'atom-clock', 'Migrated from Atom.io', 1090899, 1, + '{"name": "atom-clock", "readme": "Atom-clok!", "metadata": { "main": "./lib/atom-clock", "name": "atom-clock", + "author": { "url": "https://github.com/b3by", "name": "Antonio Bevilacqua", "email": "b3by.in.the3.sky@gmail.com"}}}', 528 +), ( + '1e19da12-322a-4b37-99ff-64f866cc0cfa', 'package', 'hey-pane', 'Migrated from Atom.io', 206804, 1, + '{"name": "hey-pane", "readme": "hey-pane!", "metadata": { "main": "./lib/hey-pane", "license": "MIT"}}', 176 +), ( + 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc', 'theme', 'atom-material-ui', 'Migrated from Atom.io', 2509605, 1, + '{"name": "atom-material-ui", "readme": "ATOM!"}', 1772 +), ( + '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac', 'theme', 'atom-material-syntax', 'Migrated from Atom.io', 1743927, 1, + '{"name": "atom-material-syntax"}', 1309 +), ( + '504cd079-a6a4-4435-aa06-daab631b1243', 'theme', 'atom-dark-material-ui', 'Migrated from Atom.io', 300, 1, + '{"name": "atom-dark-material-ui"}', 2 +); + +INSERT INTO names (name, pointer) +VALUES ( + 'language-css', 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b' +), ( + 'language-cpp', 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1' +), ( + 'hydrogen', 'ee87223f-65ab-4a1d-8f45-09fcf8e64423' +), ( + 'atom-clock', 'aea26882-8459-4725-82ad-41bf7aa608c3' +), ( + 'hey-pane', '1e19da12-322a-4b37-99ff-64f866cc0cfa' +), ( + 'atom-material-ui', 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc' +), ( + 'atom-material-syntax', '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac' +), ( + 'atom-dark-material-ui', '504cd079-a6a4-4435-aa06-daab631b1243' +); + +INSERT INTO versions (package, status, semver, license, engine, meta) +VALUES ( + 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'published', '0.45.7', 'MIT', '{"atom": "*", "node": "*"}', + '{"name": "language-css", "description": "CSS Support in Atom", "keywords": ["tree-sitter"], + "tarball_url": "https://github.com/pulsar-edit/language-css"}' +), ( + 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'latest', '0.46.0', 'MIT', '{"atom": "*", "node": "*"}', + '{"name": "language-css", "description": "CSS Support in Atom", "keywords": ["tree-sitter"]}' +), ( + 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'published', '0.45.0', 'MIT', '{"atom": "*", "node": "*"}', + '{"name":"language-css", "description": "CSS Support in Atom", "keywords": ["tree-sitter"]}' +), ( + 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 'published', '0.11.8', 'MIT', '{"atom": "*", "node": "*"}', + '{"name": "language-cpp", "description": "C++ Support in Atom", "keywords": ["tree-sitter"]}' +), ( + 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 'latest', '0.11.9', 'MIT', '{"atom": "*", "node": "*"}', + '{"name": "language-cpp", "description": "C++ Support in Atom", "keywords": ["tree-sitter"]}' +), ( + 'ee87223f-65ab-4a1d-8f45-09fcf8e64423', 'latest', '2.16.3', 'MIT', '{"atom": "*"}', + '{"name": "Hydrogen", "dist": {"tarball": "https://www.atom.io/api/packages/hydrogen/version/2.16.3/tarball"}}' +), ( + 'aea26882-8459-4725-82ad-41bf7aa608c3', 'latest', '0.1.18', 'MIT', '{"atom": "*"}', + '{"name": "atom-clock", "dist": {"tarball": "https://www.atom.io/api/packages/atom-clock/version/1.18.0/tarball"}}' +), ( + '1e19da12-322a-4b37-99ff-64f866cc0cfa', 'latest', '1.2.0', 'MIT', '{"atom": "*"}', + '{"name":"hey-pane", "dist": {"tarball": "https://www.atom.io/api/packages/hydrogen/version/1.2.0/tarball"}}' +), ( + 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc', 'latest', '2.1.3', 'MIT', '{"atom": "*"}', + '{"name": "atom-material-ui", "dist": {"tarball": "https://www.atom.io/api/packages/atom-material-ui/version/2.1.3/tarball"}}' +), ( + '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac', 'latest', '1.0.8', 'MIT', '{"atom":"*"}', + '{"name": "atom-material-syntax", "dist": {"tarball":"https://www.atom/io/api/packages/atom-material-syntax/version/1.0.8/tarball"}}' +), ( + '504cd079-a6a4-4435-aa06-daab631b1243', 'latest', '1.0.0', 'MIT', '{"atom": "*"}', + '{"name":"atom-dark-material-ui", "dist": {"tarball": "https://www.atom.io/api/packages/atom-dark-material-ui/versions/1.0.0/tarball"}}' +); + +INSERT INTO users (username, node_id, avatar) +VALUES ( + 'dever', 'dever-nodeid', 'https://roadtonowhere.com' +), ( + 'no_perm_user', 'no-perm-user-nodeid', 'https://roadtonowhere.com' +), ( + 'admin_user', 'admin-user-nodeid', 'https://roadtonowhere.com' +), ( + 'has-no-stars', 'has-no-stars-nodeid', 'https://roadtonowhere.com' +), ( + 'has-all-stars', 'has-all-stars-nodeid', 'https://roadtonowhere.com' +); + +INSERT INTO stars (package, userid) +VALUES ( + 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 5 +), ( + 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 5 +), ( + 'ee87223f-65ab-4a1d-8f45-09fcf8e64423', 5 +), ( + 'aea26882-8459-4725-82ad-41bf7aa608c3', 5 +), ( + '1e19da12-322a-4b37-99ff-64f866cc0cfa', 5 +), ( + 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc', 5 +), ( + '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac', 5 +); diff --git a/scripts/migrations/0001-initial-migration.sql b/scripts/migrations/0001-initial-migration.sql index 403d9e2f..5a81dc18 100644 --- a/scripts/migrations/0001-initial-migration.sql +++ b/scripts/migrations/0001-initial-migration.sql @@ -103,126 +103,3 @@ CREATE TABLE authstate ( keycode VARCHAR(256) NOT NULL UNIQUE, created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); - ------------------------------------------------------------------------------- - --- Enter our Test data into the Database. - -INSERT INTO packages (pointer, package_type, name, creation_method, downloads, stargazers_count, data, original_stargazers) -VALUES ( - 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'package', 'language-css', 'user made', 400004, 1, - '{"name": "language-css", "readme": "Cool readme", "metadata": {"bugs": {"url": "https://github.com/atom/language-css/issues"}, - "name": "language-css", "engines": {"atom": "*","node":"*"},"license":"MIT","version":"0.45.7","homepage":"http://atom.github.io/language-css", - "keywords":["tree-sitter"],"repository":{"url":"https://github.com/atom/language-css.git","type":"git"},"description":"CSS Support in Atom", - "dependencies":{"tree-sitter-css":"^0.19.0"},"devDependencies":{"coffeelint":"^1.10.1"}},"repository":{"url":"https://github.com/atom/langauge-css", - "type":"git"}}', 76 -), ( - 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 'package', 'language-cpp', 'user made', 849156, 1, - '{"name": "language-cpp", "description": "C++ Support in Atom", "keywords": ["tree-sitter"]}', 91 -), ( - 'ee87223f-65ab-4a1d-8f45-09fcf8e64423', 'package', 'hydrogen', 'Migrated from Atom.io', 2562844, 1, - '{"name": "hydrogen", "readme": "Hydrogen Readme", "metadata": { "main": "./dist/main", "name": "Hydrogen", - "author": "nteract contributors", "engines": {"atom": ">=1.28.0 <2.0.0"}, "license": "MIT", "version": "2.16.3"}}', 821 -), ( - 'aea26882-8459-4725-82ad-41bf7aa608c3', 'package', 'atom-clock', 'Migrated from Atom.io', 1090899, 1, - '{"name": "atom-clock", "readme": "Atom-clok!", "metadata": { "main": "./lib/atom-clock", "name": "atom-clock", - "author": { "url": "https://github.com/b3by", "name": "Antonio Bevilacqua", "email": "b3by.in.the3.sky@gmail.com"}}}', 528 -), ( - '1e19da12-322a-4b37-99ff-64f866cc0cfa', 'package', 'hey-pane', 'Migrated from Atom.io', 206804, 1, - '{"name": "hey-pane", "readme": "hey-pane!", "metadata": { "main": "./lib/hey-pane", "license": "MIT"}}', 176 -), ( - 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc', 'theme', 'atom-material-ui', 'Migrated from Atom.io', 2509605, 1, - '{"name": "atom-material-ui", "readme": "ATOM!"}', 1772 -), ( - '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac', 'theme', 'atom-material-syntax', 'Migrated from Atom.io', 1743927, 1, - '{"name": "atom-material-syntax"}', 1309 -), ( - '504cd079-a6a4-4435-aa06-daab631b1243', 'theme', 'atom-dark-material-ui', 'Migrated from Atom.io', 300, 1, - '{"name": "atom-dark-material-ui"}', 2 -); - -INSERT INTO names (name, pointer) -VALUES ( - 'language-css', 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b' -), ( - 'language-cpp', 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1' -), ( - 'hydrogen', 'ee87223f-65ab-4a1d-8f45-09fcf8e64423' -), ( - 'atom-clock', 'aea26882-8459-4725-82ad-41bf7aa608c3' -), ( - 'hey-pane', '1e19da12-322a-4b37-99ff-64f866cc0cfa' -), ( - 'atom-material-ui', 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc' -), ( - 'atom-material-syntax', '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac' -), ( - 'atom-dark-material-ui', '504cd079-a6a4-4435-aa06-daab631b1243' -); - -INSERT INTO versions (package, status, semver, license, engine, meta) -VALUES ( - 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'published', '0.45.7', 'MIT', '{"atom": "*", "node": "*"}', - '{"name": "language-css", "description": "CSS Support in Atom", "keywords": ["tree-sitter"], - "tarball_url": "https://github.com/pulsar-edit/language-css"}' -), ( - 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'latest', '0.46.0', 'MIT', '{"atom": "*", "node": "*"}', - '{"name": "language-css", "description": "CSS Support in Atom", "keywords": ["tree-sitter"]}' -), ( - 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 'published', '0.45.0', 'MIT', '{"atom": "*", "node": "*"}', - '{"name":"language-css", "description": "CSS Support in Atom", "keywords": ["tree-sitter"]}' -), ( - 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 'published', '0.11.8', 'MIT', '{"atom": "*", "node": "*"}', - '{"name": "language-cpp", "description": "C++ Support in Atom", "keywords": ["tree-sitter"]}' -), ( - 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 'latest', '0.11.9', 'MIT', '{"atom": "*", "node": "*"}', - '{"name": "language-cpp", "description": "C++ Support in Atom", "keywords": ["tree-sitter"]}' -), ( - 'ee87223f-65ab-4a1d-8f45-09fcf8e64423', 'latest', '2.16.3', 'MIT', '{"atom": "*"}', - '{"name": "Hydrogen", "dist": {"tarball": "https://www.atom.io/api/packages/hydrogen/version/2.16.3/tarball"}}' -), ( - 'aea26882-8459-4725-82ad-41bf7aa608c3', 'latest', '0.1.18', 'MIT', '{"atom": "*"}', - '{"name": "atom-clock", "dist": {"tarball": "https://www.atom.io/api/packages/atom-clock/version/1.18.0/tarball"}}' -), ( - '1e19da12-322a-4b37-99ff-64f866cc0cfa', 'latest', '1.2.0', 'MIT', '{"atom": "*"}', - '{"name":"hey-pane", "dist": {"tarball": "https://www.atom.io/api/packages/hydrogen/version/1.2.0/tarball"}}' -), ( - 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc', 'latest', '2.1.3', 'MIT', '{"atom": "*"}', - '{"name": "atom-material-ui", "dist": {"tarball": "https://www.atom.io/api/packages/atom-material-ui/version/2.1.3/tarball"}}' -), ( - '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac', 'latest', '1.0.8', 'MIT', '{"atom":"*"}', - '{"name": "atom-material-syntax", "dist": {"tarball":"https://www.atom/io/api/packages/atom-material-syntax/version/1.0.8/tarball"}}' -), ( - '504cd079-a6a4-4435-aa06-daab631b1243', 'latest', '1.0.0', 'MIT', '{"atom": "*"}', - '{"name":"atom-dark-material-ui", "dist": {"tarball": "https://www.atom.io/api/packages/atom-dark-material-ui/versions/1.0.0/tarball"}}' -); - -INSERT INTO users (username, node_id, avatar) -VALUES ( - 'dever', 'dever-nodeid', 'https://roadtonowhere.com' -), ( - 'no_perm_user', 'no-perm-user-nodeid', 'https://roadtonowhere.com' -), ( - 'admin_user', 'admin-user-nodeid', 'https://roadtonowhere.com' -), ( - 'has-no-stars', 'has-no-stars-nodeid', 'https://roadtonowhere.com' -), ( - 'has-all-stars', 'has-all-stars-nodeid', 'https://roadtonowhere.com' -); - -INSERT INTO stars (package, userid) -VALUES ( - 'd28c7ce5-c9c4-4fb6-a499-a7c6dcec355b', 5 -), ( - 'd27dbd37-e58e-4e02-b804-9e3e6ae02fb1', 5 -), ( - 'ee87223f-65ab-4a1d-8f45-09fcf8e64423', 5 -), ( - 'aea26882-8459-4725-82ad-41bf7aa608c3', 5 -), ( - '1e19da12-322a-4b37-99ff-64f866cc0cfa', 5 -), ( - 'a0ef01cb-720e-4c0d-80c5-f0ed441f31fc', 5 -), ( - '28952de5-ddbf-41a8-8d87-5d7e9d7ad7ac', 5 -); diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index a1ea7d13..f989b196 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -4,7 +4,15 @@ module.exports = { docs: { - summary: "List all packages that are themes." + summary: "List all packages that are themes.", + responses: { + 200: { + description: "A paginated response of themes.", + content: { + "application/json": "$packageObjectShortArray" + } + } + } }, endpoint: { method: "GET", diff --git a/src/database.js b/src/database.js index 09b273f8..261c692f 100644 --- a/src/database.js +++ b/src/database.js @@ -121,8 +121,8 @@ async function insertNewPackage(pack) { return await sqlStorage .begin(async (sqlTrans) => { const packageType = - typeof pack.metadata.themes === "string" && - pack.metadata.themes.match(/^(?:themes|ui)$/i) !== null + typeof pack.metadata.theme === "string" && + pack.metadata.theme.match(/^(?:syntax|ui)$/i) !== null ? "theme" : "package"; diff --git a/src/models/ssoPaginate.js b/src/models/ssoPaginate.js index fa04398c..ef3caa47 100644 --- a/src/models/ssoPaginate.js +++ b/src/models/ssoPaginate.js @@ -13,7 +13,11 @@ class SSOPaginate extends SSO { buildLink(url, currentPage, params) { let paramString = ""; - for (let param of params) { + for (let param in params) { + // We manually assign the page query so we will skip + if (param === "page") { + continue; + } if (param === "query") { // Since we know we want to keep search queries safe strings const safeQuery = encodeURIComponent( @@ -31,7 +35,7 @@ class SSOPaginate extends SSO { linkString += `<${url}?page=${this.total}${paramString}>; rel="last"`; if (currentPage !== this.total) { - linkString += `, <${url}?page=${currentPage + 1}${paramString}>; rel="next"`; + linkString += `, <${url}?page=${parseInt(currentPage) + 1}${paramString}>; rel="next"`; } this.link = linkString; diff --git a/tests/http/getThemes.test.js b/tests/http/getThemes.test.js new file mode 100644 index 00000000..e602486b --- /dev/null +++ b/tests/http/getThemes.test.js @@ -0,0 +1,113 @@ +const endpoint = require("../../src/controllers/getThemes.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("Behaves as expected", () => { + + test("Calls the correct function", async () => { + const context = require("../../src/context.js"); + const localContext = context; + const spy = jest.spyOn(localContext.database, "getSortedPackages"); + + await endpoint.logic({}, localContext); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); + + test("Returns empty array with no packages present", async () => { + // Testing for if no packages exist + const sso = await endpoint.logic( + { + page: "1", + sort: "downloads", + direction: "desc" + }, + context + ); + + expect(sso.ok).toBe(true); + expect(sso.content).toBeArray(); + expect(sso.content.length).toBe(0); + expect(sso.link).toBe( + ";" + + ' rel="self", ' + + ";" + + ' rel="last"' + ); + }); + test("Returns proper data on success", async () => { + const addName = await database.insertNewPackage({ + name: "test-package", + repository: "https://github.com/confused-Techie/package-backend", + creation_method: "Test Package", + releases: { + latest: "1.1.0" + }, + readme: "This is a readme!", + metadata: { + name: "test-package", + theme: "syntax" + }, + versions: { + "1.1.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "test-package" + }, + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "test-package" + } + } + }); + + const sso = await endpoint.logic( + { + page: "1", + sort: "downloads", + direction: "desc" + }, + context + ); + console.log(sso); + expect(false).toBeTruthy(); + expect(sso.ok).toBe(true); + expect(sso.content).toBeArray(); + expect(sso.content.length).toBe(1); + expect(sso.content[0].name).toBe("test-package"); + expect(sso.link).toBe( + ";" + + ' rel="self", ' + + ";" + + ' rel="last"' + ); + + await database.removePackageByName("test-package", true); + }); + + test("Returns bad SSO on failure", async () => { + const localContext = context; + localContext.database = { + getSortedPackages: () => { return { ok: false, content: "Test Failure" }; } + }; + + const sso = await endpoint.logic( + { + page: "1", + sort: "downloads", + direction: "desc" + }, + localContext + ); + + expect(sso.ok).toBe(false); + expect(sso.content.content).toBe("Test Failure"); + }); +}); diff --git a/tests/http/stars.test.js b/tests/http/stars.test.js index 2714edee..19b45e42 100644 --- a/tests/http/stars.test.js +++ b/tests/http/stars.test.js @@ -44,7 +44,7 @@ describe("GET /api/stars", () => { tmpMock.mockClear(); }); - test("Valid User with Stars Returns 200 Status Code", async () => { + test.skip("Valid User with Stars Returns 200 Status Code", async () => { tmpMock = authMock({ ok: true, content: { diff --git a/tests/unit/controllers/getThemes.js b/tests/unit/controllers/getThemes.js new file mode 100644 index 00000000..31af2f78 --- /dev/null +++ b/tests/unit/controllers/getThemes.js @@ -0,0 +1,52 @@ +const endpoint = require("../../../src/controllers/getThemes.js"); +const context = require("../../../src/context.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "GET", + paths: [ "/api/themes" ], + rateLimit: "generic", + successStatus: 200 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("Has correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + }); +}); + +describe("Parameters behave as expected", () => { + test("Returns valid 'page'", () => { + const req = { + params: { + page: "1" + } + }; + + const res = endpoint.params.page(context, req); + expect(res).toBe("1"); + }); + test("Returns valid 'sort'", () => { + const req = { + params: { + sort: "downloads" + } + }; + + const res = endpoint.params.sort(context, req); + expect(res).toBe("downloads"); + }); + test("Returns valid 'direction'", () => { + const req = { + params: { + direction: "desc" + } + }; + + const res = endpoint.params.direction(context, req); + expect(res).toBe("desc"); + }); +}); From fafca78f4d8aed5d1cd221478e5201408a81d2fd Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 17 Sep 2023 17:04:35 -0700 Subject: [PATCH 28/53] Remove logging --- tests/http/getThemes.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/http/getThemes.test.js b/tests/http/getThemes.test.js index e602486b..b4ab6325 100644 --- a/tests/http/getThemes.test.js +++ b/tests/http/getThemes.test.js @@ -76,8 +76,7 @@ describe("Behaves as expected", () => { }, context ); - console.log(sso); - expect(false).toBeTruthy(); + expect(sso.ok).toBe(true); expect(sso.content).toBeArray(); expect(sso.content.length).toBe(1); From db669bb327f8deab37b1e5c9e4d397d5bba594d1 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 17 Sep 2023 18:41:26 -0700 Subject: [PATCH 29/53] Add back a few tests, fix endpoint registration issues --- src/controllers/endpoints.js | 30 +++++++++++-------- src/controllers/getUsers.js | 2 +- src/setupEndpoints.js | 22 +++++++------- .../http/options.test.js | 2 +- .../unit/cache.test.js | 2 +- .../unit/debug_utils.test.js | 2 +- .../unit/logger.test.js | 2 +- .../unit/storage.test.js | 2 +- 8 files changed, 36 insertions(+), 28 deletions(-) rename test/other.handler.integration.test.js => tests/http/options.test.js (99%) rename test/cache.unit.test.js => tests/unit/cache.test.js (97%) rename test/debug_utils.unit.test.js => tests/unit/debug_utils.test.js (89%) rename test/logger.unit.test.js => tests/unit/logger.test.js (98%) rename test/storage.unit.test.js => tests/unit/storage.test.js (91%) diff --git a/src/controllers/endpoints.js b/src/controllers/endpoints.js index 7028a4c5..78630d6a 100644 --- a/src/controllers/endpoints.js +++ b/src/controllers/endpoints.js @@ -1,16 +1,14 @@ // Exports all the endpoints that need to be required + +// We register endpoints in a specific order because the loosy paths are always +// matched first. So we ensured to insert paths from strictest to loosest +// based on each parent slug or http method utilized. +// In simple terms, when a path has a parameter, add them longest path to shortest module.exports = [ - require("./deletePackagesPackageName.js"), - require("./deletePackagesPackageNameStar.js"), - require("./deletePackagesPackageNameVersionsVersionName.js"), require("./getLogin.js"), require("./getOauth.js"), - require("./getPackages.js"), + require("./getPackages"), require("./getPackagesFeatured.js"), - require("./getPackagesPackageName.js"), - require("./getPackagesPackageNameStargazers.js"), - require("./getPackagesPackageNameVersionsVersionName.js"), - require("./getPackagesPackageNameVersionsVersionNameTarball.js"), require("./getPackagesSearch.js"), require("./getPat.js"), require("./getRoot.js"), @@ -20,10 +18,18 @@ module.exports = [ require("./getThemesSearch.js"), require("./getUpdates.js"), require("./getUsers.js"), - require("./getUsersLogin.js"), - require("./getUsersLoginStars.js"), require("./postPackages.js"), - require("./postPackagesPackageNameStar.js"), + // Items with path parameters + require("./deletePackagesPackageNameVersionsVersionName.js"), + require("./deletePackagesPackageNameStar.js"), + require("./deletePackagesPackageName.js"), + require("./getPackagesPackageNameVersionsVersionNameTarball.js"), + require("./getPackagesPackageNameVersionsVersionName.js"), + require("./getPackagesPackageNameStargazers.js"), + require("./getPackagesPackageName.js"), + require("./postPackagesPackageNameVersionsVersionNameEventsUninstall.js"), require("./postPackagesPackageNameVersions.js"), - require("./postPackagesPackageNameVersionsVersionNameEventsUninstall.js") + require("./postPackagesPackageNameStar.js"), + require("./getUsersLoginStars.js"), + require("./getUsersLogin.js"), ]; diff --git a/src/controllers/getUsers.js b/src/controllers/getUsers.js index e167a74e..ee0c8ba1 100644 --- a/src/controllers/getUsers.js +++ b/src/controllers/getUsers.js @@ -23,7 +23,7 @@ module.exports = { options: { Allow: "GET", "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type, Authorization, Acces-Control-Allow-Credentials", + "Access-Control-Allow-Headers": "Content-Type, Authorization, Access-Control-Allow-Credentials", "Access-Control-Allow-Origin": "https://web.pulsar-edit.dev", "Access-Control-Allow-Credentials": true } diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 351c1537..dd27b9c5 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -96,7 +96,16 @@ for (const node of endpoints) { limiter = genericLimit; } - // Don't break on switch, so default can provide `OPTIONS` endpoint + if (!pathOptions.includes(path)) { + app.options(path, genericLimit, async (req, res) => { + res.header(node.endpoint.options); + res.sendStatus(204); + return; + }); + + pathOptions.push(path); + } + switch(node.endpoint.method) { case "GET": app.get(path, limiter, async (req, res) => { @@ -111,16 +120,9 @@ for (const node of endpoints) { await endpointHandler(node, req, res); }); default: - if (!pathOptions.includes(path)) { - // Only add one "OPTIONS" entry per path - app.options(path, genericLimit, async (req, res) => { - res.header(node.endpoint.options); - res.sendStatus(204); - return; - }); - pathOptions.push(path); - } + console.log(`Unsupported method: ${node.endpoint.method} for ${path}`); } + } } diff --git a/test/other.handler.integration.test.js b/tests/http/options.test.js similarity index 99% rename from test/other.handler.integration.test.js rename to tests/http/options.test.js index 538415aa..25fdcb95 100644 --- a/test/other.handler.integration.test.js +++ b/tests/http/options.test.js @@ -1,5 +1,5 @@ const request = require("supertest"); -const app = require("../src/main.js"); +const app = require("../../src/setupEndpoints.js"); describe("Ensure Options Method Returns as Expected", () => { const rateLimitHeaderCheck = (res) => { diff --git a/test/cache.unit.test.js b/tests/unit/cache.test.js similarity index 97% rename from test/cache.unit.test.js rename to tests/unit/cache.test.js index ce65b543..ae425739 100644 --- a/test/cache.unit.test.js +++ b/tests/unit/cache.test.js @@ -1,4 +1,4 @@ -const cache = require("../src/cache.js"); +const cache = require("../../src/cache.js"); const Joi = require("joi"); test("Cache Creates Object As Expected", async () => { diff --git a/test/debug_utils.unit.test.js b/tests/unit/debug_utils.test.js similarity index 89% rename from test/debug_utils.unit.test.js rename to tests/unit/debug_utils.test.js index aad676fa..0823b2a1 100644 --- a/test/debug_utils.unit.test.js +++ b/tests/unit/debug_utils.test.js @@ -1,4 +1,4 @@ -const debug_utils = require("../src/debug_utils.js"); +const debug_utils = require("../../src/debug_utils.js"); describe("Test lengths Returned by different Variables", () => { const objectCases = [ diff --git a/test/logger.unit.test.js b/tests/unit/logger.test.js similarity index 98% rename from test/logger.unit.test.js rename to tests/unit/logger.test.js index 365bfb66..a445de87 100644 --- a/test/logger.unit.test.js +++ b/tests/unit/logger.test.js @@ -1,4 +1,4 @@ -const logger = require("../src/logger.js"); +const logger = require("../../src/logger.js"); global.console.log = jest.fn(); diff --git a/test/storage.unit.test.js b/tests/unit/storage.test.js similarity index 91% rename from test/storage.unit.test.js rename to tests/unit/storage.test.js index 8fc17a1f..eadf0a4a 100644 --- a/test/storage.unit.test.js +++ b/tests/unit/storage.test.js @@ -1,4 +1,4 @@ -const storage = require("../src/storage.js"); +const storage = require("../../src/storage.js"); describe("Functions Return Proper Values", () => { test("getBanList Returns Array", async () => { From 8d283774709abb5f3482a9edc01b0c36b678c39f Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 17 Sep 2023 18:47:05 -0700 Subject: [PATCH 30/53] Use envrons for finding serverurl in testing --- tests/http/getThemes.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/http/getThemes.test.js b/tests/http/getThemes.test.js index b4ab6325..70894010 100644 --- a/tests/http/getThemes.test.js +++ b/tests/http/getThemes.test.js @@ -31,9 +31,9 @@ describe("Behaves as expected", () => { expect(sso.content).toBeArray(); expect(sso.content.length).toBe(0); expect(sso.link).toBe( - ";" + `<${process.env.SERVERURL}/api/themes?page=0&sort=downloads&direction=desc>;` + ' rel="self", ' - + ";" + + `<${process.env.SERVERURL}/api/themes?page=0&sort=downloads&direction=desc>;` + ' rel="last"' ); }); @@ -82,9 +82,9 @@ describe("Behaves as expected", () => { expect(sso.content.length).toBe(1); expect(sso.content[0].name).toBe("test-package"); expect(sso.link).toBe( - ";" + `<${process.env.SERVERURL}/api/themes?page=1&sort=downloads&direction=desc>;` + ' rel="self", ' - + ";" + + `<${process.env.SERVERURL}/api/themes?page=1&sort=downloads&direction=desc>;` + ' rel="last"' ); From eb0e5bddd9dbab97f7c2ed1e2011cc02286ed72a Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 17 Sep 2023 19:35:23 -0700 Subject: [PATCH 31/53] Rewrite root testing following new procedure --- tests/http/getRoot.test.js | 28 ++++++++++++++++++++++++++++ tests/http/getThemes.test.js | 10 +++++----- tests/http/root.test.js | 22 ---------------------- tests/unit/controllers/getRoot.js | 18 ++++++++++++++++++ 4 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 tests/http/getRoot.test.js delete mode 100644 tests/http/root.test.js create mode 100644 tests/unit/controllers/getRoot.js diff --git a/tests/http/getRoot.test.js b/tests/http/getRoot.test.js new file mode 100644 index 00000000..c4d4cec7 --- /dev/null +++ b/tests/http/getRoot.test.js @@ -0,0 +1,28 @@ +const endpoint = require("../../src/controllers/getRoot.js"); +const context = require("../../src/context.js"); + +describe("Behaves as expected", () => { + test("Should respond with an HTML document", async () => { + const sso = await endpoint.logic({}, context); + + expect(sso.ok).toBe(true); + expect(sso.content).toEqual( + expect.stringContaining("Server is up and running Version") + ); + }); +}); + +describe("HTTP Handling works", () => { + test("Calls the right function", async () => { + const request = require("supertest"); + const app = require("../../src/setupEndpoints.js"); + + const spy = jest.spyOn(endpoint, "logic"); + + const res = await request(app).get("/"); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); +}); diff --git a/tests/http/getThemes.test.js b/tests/http/getThemes.test.js index 70894010..8a29b8c2 100644 --- a/tests/http/getThemes.test.js +++ b/tests/http/getThemes.test.js @@ -31,9 +31,9 @@ describe("Behaves as expected", () => { expect(sso.content).toBeArray(); expect(sso.content.length).toBe(0); expect(sso.link).toBe( - `<${process.env.SERVERURL}/api/themes?page=0&sort=downloads&direction=desc>;` + `<${context.config.server_url}/api/themes?page=0&sort=downloads&direction=desc>;` + ' rel="self", ' - + `<${process.env.SERVERURL}/api/themes?page=0&sort=downloads&direction=desc>;` + + `<${context.config.server_url}/api/themes?page=0&sort=downloads&direction=desc>;` + ' rel="last"' ); }); @@ -82,12 +82,12 @@ describe("Behaves as expected", () => { expect(sso.content.length).toBe(1); expect(sso.content[0].name).toBe("test-package"); expect(sso.link).toBe( - `<${process.env.SERVERURL}/api/themes?page=1&sort=downloads&direction=desc>;` + `<${context.config.server_url}/api/themes?page=1&sort=downloads&direction=desc>;` + ' rel="self", ' - + `<${process.env.SERVERURL}/api/themes?page=1&sort=downloads&direction=desc>;` + + `<${context.config.server_url}/api/themes?page=1&sort=downloads&direction=desc>;` + ' rel="last"' ); - + // TODO test object structure to known good await database.removePackageByName("test-package", true); }); diff --git a/tests/http/root.test.js b/tests/http/root.test.js deleted file mode 100644 index 54e24459..00000000 --- a/tests/http/root.test.js +++ /dev/null @@ -1,22 +0,0 @@ -const request = require("supertest"); -const app = require("../../src/setupEndpoints.js"); - -describe("Get /", () => { - test("Should respond with an HTML document noting the server version", async () => { - const res = await request(app) - .get("/") - .expect("Content-Type", "text/html; charset=utf-8"); - - expect(res.text).toEqual( - expect.stringContaining("Server is up and running Version") - ); - }); - test("Should Return valid status code", async () => { - const res = await request(app).get("/"); - expect(res).toHaveHTTPCode(200); - }); - test("Should 404 on invalid method", async () => { - const res = await request(app).patch("/"); - expect(res).toHaveHTTPCode(404); - }); -}); diff --git a/tests/unit/controllers/getRoot.js b/tests/unit/controllers/getRoot.js new file mode 100644 index 00000000..b81e64e9 --- /dev/null +++ b/tests/unit/controllers/getRoot.js @@ -0,0 +1,18 @@ +const endpoint = require("../../../src/controllers/getRoot.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "GET", + paths: [ "/" ], + rateLimit: "generic", + successStatus: 200 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("Has correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + }); +}); From 104cf0b840fedf238b25144967f6f4266b51a16e Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 17 Sep 2023 22:13:14 -0700 Subject: [PATCH 32/53] Testing for HTTP handling, refactored root testing, update testing --- tests/http/getThemes.test.js | 15 +++++++++++ tests/http/getUpdates.test.js | 26 +++++++++++++++++++ tests/http/getUsers.test.js | 15 +++++++++++ tests/http/getUsersLogin.test.js | 15 +++++++++++ tests/http/updates.test.js | 13 ---------- .../{getRoot.js => getRoot.test.js} | 0 6 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 tests/http/getUpdates.test.js delete mode 100644 tests/http/updates.test.js rename tests/unit/controllers/{getRoot.js => getRoot.test.js} (100%) diff --git a/tests/http/getThemes.test.js b/tests/http/getThemes.test.js index 8a29b8c2..671c9619 100644 --- a/tests/http/getThemes.test.js +++ b/tests/http/getThemes.test.js @@ -110,3 +110,18 @@ describe("Behaves as expected", () => { expect(sso.content.content).toBe("Test Failure"); }); }); + +describe("HTTP Handling works", () => { + test("Calls the right function", async () => { + const request = require("supertest"); + const app = require("../../src/setupEndpoints.js"); + + const spy = jest.spyOn(endpoint, "logic"); + + const res = await request(app).get("/api/themes"); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); +}); diff --git a/tests/http/getUpdates.test.js b/tests/http/getUpdates.test.js new file mode 100644 index 00000000..2cdeaa53 --- /dev/null +++ b/tests/http/getUpdates.test.js @@ -0,0 +1,26 @@ +const endpoint = require("../../src/controllers/getUpdates.js"); +const context = require("../../src/context.js"); + +describe("Behaves as expected", () => { + test("Returns properly", async () => { + const sso = await endpoint.logic({}, context); + + expect(sso.ok).toBe(false); + expect(sso.short).toBe("not_supported"); + }); +}); + +describe("HTTP Handling works", () => { + test("Calls the right function", async () => { + const request = require("supertest"); + const app = require("../../src/setupEndpoints.js"); + + const spy = jest.spyOn(endpoint, "logic"); + + const res = await request(app).get("/api/updates"); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); +}); diff --git a/tests/http/getUsers.test.js b/tests/http/getUsers.test.js index b03fa7f3..ebf0bb1b 100644 --- a/tests/http/getUsers.test.js +++ b/tests/http/getUsers.test.js @@ -89,3 +89,18 @@ describe("Extra functions behave", () => { expect(headerObj).toMatchObject(expected); }); }); + +describe("HTTP Handling works", () => { + test("Calls the right function", async () => { + const request = require("supertest"); + const app = require("../../src/setupEndpoints.js"); + + const spy = jest.spyOn(endpoint, "logic"); + + const res = await request(app).get("/api/users"); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); +}); diff --git a/tests/http/getUsersLogin.test.js b/tests/http/getUsersLogin.test.js index 8c414d24..6e83a5e3 100644 --- a/tests/http/getUsersLogin.test.js +++ b/tests/http/getUsersLogin.test.js @@ -48,3 +48,18 @@ describe("Behaves as expected", () => { // TODO delete once there's a method to do so }); }); + +describe("HTTP Handling works", () => { + test("Calls the right function", async () => { + const request = require("supertest"); + const app = require("../../src/setupEndpoints.js"); + + const spy = jest.spyOn(endpoint, "logic"); + + const res = await request(app).get("/api/users/confused-Techie"); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); +}); diff --git a/tests/http/updates.test.js b/tests/http/updates.test.js deleted file mode 100644 index a314af4e..00000000 --- a/tests/http/updates.test.js +++ /dev/null @@ -1,13 +0,0 @@ -const request = require("supertest"); -const app = require("../../src/setupEndpoints.js"); - -describe("GET /api/updates", () => { - test("Returns NotSupported Status Code.", async () => { - const res = await request(app).get("/api/updates"); - expect(res).toHaveHTTPCode(501); - }); - test("Returns NotSupported Message", async () => { - const res = await request(app).get("/api/updates"); - expect(res.body.message).toEqual(msg.notSupported); - }); -}); diff --git a/tests/unit/controllers/getRoot.js b/tests/unit/controllers/getRoot.test.js similarity index 100% rename from tests/unit/controllers/getRoot.js rename to tests/unit/controllers/getRoot.test.js From 70a4a8f32e7abd425b26aeae40a8866f84308939 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 18 Sep 2023 18:25:30 -0700 Subject: [PATCH 33/53] Finish TODO of reading from filesystem instead of hardcoded dev returns in storage --- src/storage.js | 100 +++++++++++++------------------------ tests/unit/storage.test.js | 6 +-- 2 files changed, 37 insertions(+), 69 deletions(-) diff --git a/src/storage.js b/src/storage.js index 16a0bff2..f9dd2850 100644 --- a/src/storage.js +++ b/src/storage.js @@ -26,6 +26,34 @@ function setupGCS() { }); } +async function getGcpContent(file) { + if ( + GOOGLE_APPLICATION_CREDENTIALS === "nofile" || + process.env.PULSAR_STATUS === "dev" + ) { + // This catches the instance when tests are being run, without access + // or good reason to reach to 3rd party servers. + // We will instead return local data + // Setting GOOGLE_APPLICATION_CREDENTIALS to "nofile" will be the recommended + // method for running locally. + const fs = require("fs"); + const path = require("path"); + + const contents = fs.readFileSync(path.resolve(`./docs/resources/${file}`), { encoding: "utf8" }); + return contents; + } else { + // This is a production request + gcsStorage ??= setupGCS(); + + const contents = await gcsStorage + .bucket(GCLOUD_STORAGE_BUCKET) + .file(file) + .download(); + + return contents; + } +} + /** * @async * @function getBanList @@ -36,31 +64,12 @@ function setupGCS() { * @returns {Array} Parsed JSON Array of all Banned Packages. */ async function getBanList() { - gcsStorage ??= setupGCS(); const getNew = async function () { - if ( - GOOGLE_APPLICATION_CREDENTIALS === "nofile" || - process.env.PULSAR_STATUS === "dev" - ) { - // This catches the instance when tests are being run, without access - // or good reason to reach to 3rd party servers. - // We will log a warning, and return preset test data. - // Setting GOOGLE_APPLICATION_CREDENTIALS to "nofile" will be the recommended - // method for running locally. - // TODO: Have this read the data from the ban list locally - console.log("storage.js.getBanList() Returning Development Set of Data."); - let list = ["slothoki", "slot-pulsa", "slot-dana", "hoki-slot"]; - cachedBanlist = new CacheObject(list); - cachedBanlist.last_validate = Date.now(); - return new ServerStatus().isOk().setContent(cachedBanlist.data).build(); - } try { - let contents = await gcsStorage - .bucket(GCLOUD_STORAGE_BUCKET) - .file("name_ban_list.json") - .download(); + const contents = await getGcpContent("name_ban_list.json"); + cachedBanlist = new CacheObject(JSON.parse(contents)); cachedBanlist.last_validate = Date.now(); return new ServerStatus().isOk().setContent(cachedBanlist.data).build(); @@ -95,34 +104,12 @@ async function getBanList() { * @returns {Array} Parsed JSON Array of all Featured Packages. */ async function getFeaturedPackages() { - gcsStorage ??= setupGCS(); const getNew = async function () { - if ( - GOOGLE_APPLICATION_CREDENTIALS === "nofile" || - process.env.PULSAR_STATUS === "dev" - ) { - // This catches the instance when tests are being run, without access - // or good reason to reach to 3rd party servers. - // We will log a warning, and return preset test data. - // TODO: Have this read the featured packages locally - console.log( - "storage.js.getFeaturedPackages() Returning Development Set of Data." - ); - let list = ["hydrogen", "atom-clock", "hey-pane"]; - cachedFeaturedlist = new CacheObject(list); - cachedFeaturedlist.last_validate = Date.now(); - return new ServerStatus() - .isOk() - .setContent(cachedFeaturedlist.data) - .build(); - } try { - let contents = await gcsStorage - .bucket(GCLOUD_STORAGE_BUCKET) - .file("featured_packages.json") - .download(); + const contents = await getGcpContent("featured_packages.json"); + cachedFeaturedlist = new CacheObject(JSON.parse(contents)); cachedFeaturedlist.last_validate = Date.now(); return new ServerStatus() @@ -162,31 +149,12 @@ async function getFeaturedPackages() { * @returns {Array} JSON Parsed Array of Featured Theme Names. */ async function getFeaturedThemes() { - gcsStorage ??= setupGCS(); const getNew = async function () { - if ( - GOOGLE_APPLICATION_CREDENTIALS === "nofile" || - process.env.PULSAR_STATUS === "dev" - ) { - // This catches the instance when tests are being run, without access - // or good reason to reach to 3rd party servers. - // We will log a warning, and return preset test data. - // TODO: Have this read the featured themes locally - console.log( - "storage.js.getFeaturedThemes() Returning Development Set of Data." - ); - let list = ["atom-material-ui", "atom-material-syntax"]; - cachedThemelist = new CacheObject(list); - cachedThemelist.last_validate = Date.now(); - return new ServerStatus().isOk().setContent(cachedThemelist.data).build(); - } try { - let contents = await gcsStorage - .bucket(GCLOUD_STORAGE_BUCKET) - .file("featured_themes.json") - .download(); + const contents = await getGcpContent("featured_themes.json"); + cachedThemelist = new CacheObject(JSON.parse(contents)); cachedThemelist.last_validate = Date.now(); return new ServerStatus().isOk().setContent(cachedThemelist.data).build(); diff --git a/tests/unit/storage.test.js b/tests/unit/storage.test.js index eadf0a4a..7264467f 100644 --- a/tests/unit/storage.test.js +++ b/tests/unit/storage.test.js @@ -3,16 +3,16 @@ const storage = require("../../src/storage.js"); describe("Functions Return Proper Values", () => { test("getBanList Returns Array", async () => { let value = await storage.getBanList(); - expect(Array.isArray(value.content)).toBeTruthy(); + expect(value.content).toBeArray(); }); test("getFeaturedPackages Returns Array", async () => { let value = await storage.getFeaturedPackages(); - expect(Array.isArray(value.content)).toBeTruthy(); + expect(value.content).toBeArray(); }); test("getFeaturedThemes Returns Array", async () => { let value = await storage.getFeaturedThemes(); - expect(Array.isArray(value.content)).toBeTruthy(); + expect(value.content).toBeArray(); }); }); From 03c4eebdf45f0bb249831f40d6d026c7a43f9167 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 18 Sep 2023 18:25:50 -0700 Subject: [PATCH 34/53] Add `breaks` in setupEndpoint to ensure we stop handling in the switch statement --- src/setupEndpoints.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index dd27b9c5..81908df6 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -111,14 +111,17 @@ for (const node of endpoints) { app.get(path, limiter, async (req, res) => { await endpointHandler(node, req, res); }); + break; case "POST": app.post(path, limiter, async (req, res) => { await endpointHandler(node, req, res); }); + break; case "DELETE": app.delete(path, limiter, async (req, res) => { await endpointHandler(node, req, res); }); + break; default: console.log(`Unsupported method: ${node.endpoint.method} for ${path}`); } From 81e91e3a8793ed4ca0b806d851410f609e3cee94 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 18 Sep 2023 18:48:18 -0700 Subject: [PATCH 35/53] Tests for `getThemesFeatured` --- package.json | 2 +- src/controllers/getThemesFeatured.js | 12 ++++- src/database.js | 4 +- test/controllers/getThemes.test.js | 17 ------- tests/http/getThemesFeatured.test.js | 67 ++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 22 deletions(-) delete mode 100644 test/controllers/getThemes.test.js create mode 100644 tests/http/getThemesFeatured.test.js diff --git a/package.json b/package.json index b0c946c1..df50d5a5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test:unit": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Unit-Tests", "test:integration": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests", "start:dev": "cross-env PULSAR_STATUS=dev node ./src/dev_server.js", - "test": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests Unit-Tests", + "test": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests Unit-Tests --runInBand", "test:vcs": "cross-env NODE_ENV=test PULSAR_STATUS=dev MOCK_GH=false jest --selectProjects VCS-Tests", "test:handlers": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Handler-Tests", "api-docs": "quick-webserver-docs -i ./src/main.js -o ./docs/reference/API_Definition.md", diff --git a/src/controllers/getThemesFeatured.js b/src/controllers/getThemesFeatured.js index 391f5ead..b83d6821 100644 --- a/src/controllers/getThemesFeatured.js +++ b/src/controllers/getThemesFeatured.js @@ -4,7 +4,15 @@ module.exports = { docs: { - summary: "Display featured packages that are themes." + summary: "Display featured packages that are themes.", + respones: { + 200: { + description: "An array of featured themes.", + content: { + "application/json": "$packageObjectShortArray" + } + } + } }, endpoint: { method: "GET", @@ -29,7 +37,7 @@ module.exports = { return sso.notOk().addContent(col).addCalls("db.getFeaturedThemes", col); } - const newCol = await utils.constructPackageObjectShort(col.content); + const newCol = await context.utils.constructPackageObjectShort(col.content); const sso = new context.sso(); diff --git a/src/database.js b/src/database.js index 261c692f..18be0d88 100644 --- a/src/database.js +++ b/src/database.js @@ -715,12 +715,12 @@ async function getPackageCollectionByName(packArray) { return command.count !== 0 ? { ok: true, content: command } - : { ok: false, content: "No packages found.", short: "Not Found" }; + : { ok: false, content: "No packages found.", short: "not_found" }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } diff --git a/test/controllers/getThemes.test.js b/test/controllers/getThemes.test.js deleted file mode 100644 index 356edb44..00000000 --- a/test/controllers/getThemes.test.js +++ /dev/null @@ -1,17 +0,0 @@ -const getThemes = require("../../src/controllers/getThemes.js"); - -describe("Returns the expected query parameters", () => { - test("with empty request object", () => { - const req = { query: {} }; - const context = { - query: require("../../src/query.js") - }; - - const ret = getThemes.params(req, context); - - expect(ret.page).toBeDefined(); - expect(ret.sort).toBeDefined(); - expect(ret.direction).toBeDefined(); - - }); -}); diff --git a/tests/http/getThemesFeatured.test.js b/tests/http/getThemesFeatured.test.js new file mode 100644 index 00000000..2354f754 --- /dev/null +++ b/tests/http/getThemesFeatured.test.js @@ -0,0 +1,67 @@ +const endpoint = require("../../src/controllers/getThemesFeatured.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("Behaves as expected", () => { + test("Calls the correct function", async () => { + const localContext = context; + const spy = jest.spyOn(localContext.database, "getFeaturedThemes"); + + await endpoint.logic({}, localContext); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); + + test("Returns not found with no packages present", async () => { + const sso = await endpoint.logic({}, context); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("not_found"); + }); + + test("Returns proper data on success", async () => { + const addPack = await database.insertNewPackage({ + // We know a currently featured package is 'atom-material-ui' + name: "atom-material-ui", + repository: "https://github.com/confused-Techie/package-backend", + creation_method: "Test Package", + releases: { + latest: "1.1.0" + }, + readme: "This is a readme!", + metadata: { + name: "atom-material-ui", + theme: "ui" + }, + versions: { + "1.1.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "atom-material-ui" + }, + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "atom-material-ui" + } + } + }); + + const sso = await endpoint.logic({}, context); + + expect(sso.ok).toBe(true); + expect(sso.content).toBeArray(); + expect(sso.content.length).toBe(1); + expect(sso.content[0].name).toBe("atom-material-ui"); + // TODO test object structure to known good + let res = await database.removePackageByName("atom-material-ui", true); + console.log("Res remove"); + console.log(res); + }); +}); From 1f3d6b765a2829b2d4cfe4b18a253025cf283668 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 18 Sep 2023 19:44:37 -0700 Subject: [PATCH 36/53] Remove dev logging --- tests/http/getThemesFeatured.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/http/getThemesFeatured.test.js b/tests/http/getThemesFeatured.test.js index 2354f754..a3e54d27 100644 --- a/tests/http/getThemesFeatured.test.js +++ b/tests/http/getThemesFeatured.test.js @@ -60,8 +60,6 @@ describe("Behaves as expected", () => { expect(sso.content.length).toBe(1); expect(sso.content[0].name).toBe("atom-material-ui"); // TODO test object structure to known good - let res = await database.removePackageByName("atom-material-ui", true); - console.log("Res remove"); - console.log(res); + await database.removePackageByName("atom-material-ui", true); }); }); From d33183afe66adda1b3524972989c13d0e4b0dd95 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 18 Sep 2023 19:48:28 -0700 Subject: [PATCH 37/53] Cleanup `package.json` --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index df50d5a5..89093698 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,11 @@ "start:dev": "cross-env PULSAR_STATUS=dev node ./src/dev_server.js", "test": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests Unit-Tests --runInBand", "test:vcs": "cross-env NODE_ENV=test PULSAR_STATUS=dev MOCK_GH=false jest --selectProjects VCS-Tests", - "test:handlers": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Handler-Tests", "api-docs": "quick-webserver-docs -i ./src/main.js -o ./docs/reference/API_Definition.md", "lint": "prettier --check -u -w .", "complex": "cr --newmi --config .complexrc .", "js-docs": "jsdoc2md -c ./jsdoc.conf.js ./src/*.js ./src/handlers/*.js ./docs/resources/jsdoc_typedef.js > ./docs/reference/Source_Documentation.md", "contributors:add": "all-contributors add", - "test_search": "node ./scripts/tools/search.js", "migrations": "pg-migrations apply --directory ./scripts/migrations", "ignore": "compactignore", "tool:delete": "node ./scripts/tools/manual-delete-package.js", From 90d40ce4515e9fd9ae516d18db429d2e28c6d4b4 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 18 Sep 2023 19:56:39 -0700 Subject: [PATCH 38/53] Move `storage.js` from `ServerStatusObject` --- src/storage.js | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/src/storage.js b/src/storage.js index f9dd2850..01016eff 100644 --- a/src/storage.js +++ b/src/storage.js @@ -8,7 +8,7 @@ const { Storage } = require("@google-cloud/storage"); const logger = require("./logger.js"); const { CacheObject } = require("./cache.js"); -const ServerStatus = require("./ServerStatusObject.js"); +const sso = require("./models/sso.js"); const { GCLOUD_STORAGE_BUCKET, GOOGLE_APPLICATION_CREDENTIALS } = require("./config.js").getConfig(); @@ -72,13 +72,11 @@ async function getBanList() { cachedBanlist = new CacheObject(JSON.parse(contents)); cachedBanlist.last_validate = Date.now(); - return new ServerStatus().isOk().setContent(cachedBanlist.data).build(); + return new sso().isOk().addContent(cachedBanlist.data); } catch (err) { - return new ServerStatus() - .notOk() - .setShort("Server Error") - .setContent(err) - .build(); + return new sso().notOk() + .addShort("server_error") + .addCalls("getGcpContent", err); } }; @@ -89,7 +87,7 @@ async function getBanList() { if (!cachedBanlist.Expired) { logger.generic(5, "Ban List Cache NOT Expired."); - return new ServerStatus().isOk().setContent(cachedBanlist.data).build(); + return new sso().isOk().addContent(cachedBanlist.data); } logger.generic(5, "Ban List Cache IS Expired."); @@ -112,16 +110,12 @@ async function getFeaturedPackages() { cachedFeaturedlist = new CacheObject(JSON.parse(contents)); cachedFeaturedlist.last_validate = Date.now(); - return new ServerStatus() - .isOk() - .setContent(cachedFeaturedlist.data) - .build(); + return new sso().isOk() + .addContent(cachedFeaturedlist.data); } catch (err) { - return new ServerStatus() - .notOk() - .setShort("Server Error") - .setContent(err) - .build(); + return new sso().notOk() + .addShort("server_error") + .addCalls("getGcpContent", err); } }; @@ -132,10 +126,8 @@ async function getFeaturedPackages() { if (!cachedFeaturedlist.Expired) { logger.generic(5, "Ban List Cache NOT Expired."); - return new ServerStatus() - .isOk() - .setContent(cachedFeaturedlist.data) - .build(); + return new sso().isOk() + .addContent(cachedFeaturedlist.data); } logger.generic(5, "Ban List Cache IS Expired."); @@ -157,13 +149,11 @@ async function getFeaturedThemes() { cachedThemelist = new CacheObject(JSON.parse(contents)); cachedThemelist.last_validate = Date.now(); - return new ServerStatus().isOk().setContent(cachedThemelist.data).build(); + return new sso().isOk().addContent(cachedThemelist.data); } catch (err) { - return new ServerStatus() - .notOk() - .setShort("Server Error") - .setContent(err) - .build(); + return new sso().notOk() + .addShort("server_error") + .addCalls("getGcpContent", err); } }; @@ -174,7 +164,7 @@ async function getFeaturedThemes() { if (!cachedThemelist.Expired) { logger.generic(5, "Theme List Cache NOT Expired."); - return new ServerStatus().isOk().setContent(cachedThemelist.data).build(); + return new sso().isOk().addContent(cachedThemelist.data); } logger.generic(5, "Theme List Cache IS Expired."); From 37def970c5319cef9f3daca893a25b95fc335b7f Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 18 Sep 2023 21:18:51 -0700 Subject: [PATCH 39/53] `getThemesSearch` tests --- src/controllers/getThemesSearch.js | 6 -- src/database.js | 14 +-- tests/http/getThemesFeatured.test.js | 2 +- tests/http/getThemesSearch.test.js | 93 +++++++++++++++++++ .../{getThemes.js => getThemes.test.js} | 8 +- .../unit/controllers/getThemesSearch.test.js | 54 +++++++++++ 6 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 tests/http/getThemesSearch.test.js rename tests/unit/controllers/{getThemes.js => getThemes.test.js} (93%) create mode 100644 tests/unit/controllers/getThemesSearch.test.js diff --git a/src/controllers/getThemesSearch.js b/src/controllers/getThemesSearch.js index 2880d3da..72f361b7 100644 --- a/src/controllers/getThemesSearch.js +++ b/src/controllers/getThemesSearch.js @@ -33,12 +33,6 @@ module.exports = { ); if (!packs.ok) { - if (packs.short === "not_found") { - const sso = new context.ssoPaginate(); - - return sso.isOk().addContent([]); - } - const sso = new context.sso(); return sso.notOk().addContent(packs) diff --git a/src/database.js b/src/database.js index 18be0d88..f5e28e7d 100644 --- a/src/database.js +++ b/src/database.js @@ -138,7 +138,7 @@ async function insertNewPackage(pack) { RETURNING pointer; `; } catch (e) { - throw `A constraint has been violated while inserting ${pack.name} in packages table.`; + throw `A constraint has been violated while inserting ${pack.name} in packages table: ${e.toString()}`; } if (!insertNewPack?.count) { @@ -204,11 +204,11 @@ async function insertNewPackage(pack) { }) .catch((err) => { return typeof err === "string" - ? { ok: false, content: err, short: "Server Error" } + ? { ok: false, content: err, short: "server_error" } : { ok: false, content: `A generic error occurred while inserting ${pack.name} package`, - short: "Server Error", + short: "server_error", error: err, }; }); @@ -337,11 +337,11 @@ async function insertNewPackageVersion(packJSON, oldName = null) { }) .catch((err) => { return typeof err === "string" - ? { ok: false, content: err, short: "Server Error" } + ? { ok: false, content: err, short: "server_error" } : { ok: false, content: `A generic error occured while inserting the new package version ${packJSON.name}`, - short: "Server Error", + short: "server_error", error: err, }; }); @@ -1437,7 +1437,7 @@ async function simpleSearch(term, page, dir, sort, themes = false) { return { ok: false, content: `Unrecognized Sorting Method Provided: ${sort}`, - short: "Server Error", + short: "server_error", }; } @@ -1498,7 +1498,7 @@ async function simpleSearch(term, page, dir, sort, themes = false) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } diff --git a/tests/http/getThemesFeatured.test.js b/tests/http/getThemesFeatured.test.js index a3e54d27..d984b575 100644 --- a/tests/http/getThemesFeatured.test.js +++ b/tests/http/getThemesFeatured.test.js @@ -54,7 +54,7 @@ describe("Behaves as expected", () => { }); const sso = await endpoint.logic({}, context); - + expect(sso.ok).toBe(true); expect(sso.content).toBeArray(); expect(sso.content.length).toBe(1); diff --git a/tests/http/getThemesSearch.test.js b/tests/http/getThemesSearch.test.js new file mode 100644 index 00000000..5a47d5d5 --- /dev/null +++ b/tests/http/getThemesSearch.test.js @@ -0,0 +1,93 @@ +const endpoint = require("../../src/controllers/getThemesSearch.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("Behaves as expected", () => { + test("Calls the correct function", async () => { + const localContext = context; + const spy = jest.spyOn(localContext.database, "simpleSearch"); + + await endpoint.logic({}, localContext); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); + + test("Returns zero length array when not found", async () => { + const sso = await endpoint.logic({ + sort: "downloads", + page: "1", + direction: "desc", + query: "hello-world" + }, context); + + expect(sso.ok).toBe(true); + expect(sso.content).toBeArray(); + expect(sso.content.length).toBe(0); + }); + + test("Returns array on success", async () => { + const newPack = await database.insertNewPackage({ + name: "atom-material-syntax", + repository: "https://github.com/confused-Techie/package-backend", + creation_method: "Test Package", + releases: { + latest: "1.1.0" + }, + reamde: "This is a readme!", + metadata: { + name: "atom-material-syntax", + theme: "ui" + }, + versions: { + "1.1.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "atom-material-syntax" + }, + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "atom-material-syntax" + } + } + }); + + const sso = await endpoint.logic({ + sort: "downloads", + page: "1", + direction: "desc", + query: "atom-material" + }, context); + + expect(sso.ok).toBe(true); + expect(sso.content).toBeArray(); + expect(sso.content.length).toBe(1); + expect(sso.content[0].name).toBe("atom-material-syntax"); + // TODO ensure it matches known good object + await database.removePackageByName("atom-material-syntax", true); + }); + + test("Returns error on db call failure", async () => { + // Moved to last position, since it modifies our shallow copied context + const localContext = context; + localContext.database = { + simpleSearch: () => { return { ok: false, content: "Test Error" } } + }; + + const sso = await endpoint.logic({ + sort: "downloads", + page: "1", + direction: "desc", + query: "hello-world" + }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.content).toBe("Test Error"); + }); +}); diff --git a/tests/unit/controllers/getThemes.js b/tests/unit/controllers/getThemes.test.js similarity index 93% rename from tests/unit/controllers/getThemes.js rename to tests/unit/controllers/getThemes.test.js index 31af2f78..ff6e25a6 100644 --- a/tests/unit/controllers/getThemes.js +++ b/tests/unit/controllers/getThemes.test.js @@ -21,17 +21,17 @@ describe("Has features expected", () => { describe("Parameters behave as expected", () => { test("Returns valid 'page'", () => { const req = { - params: { + query: { page: "1" } }; const res = endpoint.params.page(context, req); - expect(res).toBe("1"); + expect(res).toBe(1); }); test("Returns valid 'sort'", () => { const req = { - params: { + query: { sort: "downloads" } }; @@ -41,7 +41,7 @@ describe("Parameters behave as expected", () => { }); test("Returns valid 'direction'", () => { const req = { - params: { + query: { direction: "desc" } }; diff --git a/tests/unit/controllers/getThemesSearch.test.js b/tests/unit/controllers/getThemesSearch.test.js new file mode 100644 index 00000000..655d5fb7 --- /dev/null +++ b/tests/unit/controllers/getThemesSearch.test.js @@ -0,0 +1,54 @@ +const endpoint = require("../../../src/controllers/getThemesSearch.js"); +const context = require("../../../src/context.js"); + +describe("Has features expected", () => { + test("Has correct endpoint features", () => { + const expected = { + method: "GET", + paths: [ "/api/themes/search" ], + rateLimit: "generic", + successStatus: 200 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("Has correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + }); +}); + +describe("Parameters behave as expected", () => { + test("Returns valid 'sort'", () => { + const req = { + query: { sort: "downloads" } + }; + + const res = endpoint.params.sort(context, req); + expect(res).toBe("downloads"); + }); + test("Returns valid 'page'", () => { + const req = { + query: { page: "1" } + }; + + const res = endpoint.params.page(context, req); + expect(res).toBe(1); + }); + test("Returns valid 'direction'", () => { + const req = { + query: { direction: "desc" } + }; + + const res = endpoint.params.direction(context, req); + expect(res).toBe("desc"); + }); + test("Returns valid 'query'", () => { + const req = { + query: { q: "hello" } + }; + + const res = endpoint.params.query(context, req); + expect(res).toBe("hello"); + }); +}); From c74f305b56a98907650752957546ab006d634d2b Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Tue, 19 Sep 2023 00:38:51 -0700 Subject: [PATCH 40/53] Testing for `getPackagesPackageName` integrate Joi for testing object structures --- src/controllers/getPackagesPackageName.js | 12 ++- src/database.js | 7 +- src/utils.js | 4 +- tests/helpers/global.setup.jest.js | 7 +- tests/http/getPackagesPackageName.test.js | 72 ++++++++++++++ tests/http/getThemesSearch.test.js | 2 +- tests/http/getUsersLogin.test.js | 1 - tests/models/message.js | 7 +- tests/models/packageObjectFull.js | 111 ++++++++++++++++++++++ tests/models/userObjectPrivate.js | 19 ++-- tests/models/userObjectPublic.js | 15 +-- 11 files changed, 226 insertions(+), 31 deletions(-) create mode 100644 tests/http/getPackagesPackageName.test.js create mode 100644 tests/models/packageObjectFull.js diff --git a/src/controllers/getPackagesPackageName.js b/src/controllers/getPackagesPackageName.js index 7e220310..60d88ba3 100644 --- a/src/controllers/getPackagesPackageName.js +++ b/src/controllers/getPackagesPackageName.js @@ -4,7 +4,15 @@ module.exports = { docs: { - summary: "Show package details." + summary: "Show package details.", + responses: { + 200: { + description: "A 'Package Object Full' of the requested package.", + content: { + "application/json": "$packageObjectFull" + } + } + } }, endpoint: { method: "GET", @@ -54,6 +62,6 @@ module.exports = { const sso = new context.sso(); - return sso.isOk().addcontent(pack); + return sso.isOk().addContent(pack); } }; diff --git a/src/database.js b/src/database.js index f5e28e7d..ea216b1d 100644 --- a/src/database.js +++ b/src/database.js @@ -605,14 +605,13 @@ async function getPackageByName(name, user = false) { : { ok: false, content: `package ${name} not found.`, - short: "Not Found", + short: "not_found", }; } catch (err) { return { ok: false, - content: "Generic Error", - short: "Server Error", - error: err, + content: err, + short: "server_error" }; } } diff --git a/src/utils.js b/src/utils.js index 5c2eb011..960f0034 100644 --- a/src/utils.js +++ b/src/utils.js @@ -52,7 +52,7 @@ async function constructPackageObjectFull(pack) { for (const v of vers) { retVer[v.semver] = v.meta; retVer[v.semver].license = v.license; - retVer[v.semver].engine = v.engine; + retVer[v.semver].engines = v.engines; retVer[v.semver].dist = { tarball: `${server_url}/api/packages/${pack.name}/versions/${v.semver}/tarball`, }; @@ -182,7 +182,7 @@ async function constructPackageObjectJSON(pack) { } newPack.dist ??= {}; newPack.dist.tarball = `${server_url}/api/packages/${v.meta.name}/versions/${v.semver}/tarball`; - newPack.engines = v.engine; + newPack.engines = v.engines; logger.generic(6, "Single Package Object JSON finished without Error"); return newPack; }; diff --git a/tests/helpers/global.setup.jest.js b/tests/helpers/global.setup.jest.js index 604ac7ad..de08a48a 100644 --- a/tests/helpers/global.setup.jest.js +++ b/tests/helpers/global.setup.jest.js @@ -1,5 +1,8 @@ // Add `expect().toMatchSchema()` to Jest, for matching against Joi Schemas - +const Joi = require("joi"); +global.Joi = Joi; +// We add Joi to the global context so that the `models/* .test` object doesn't need +// to worry about `require`ing the module. const jestJoi = require("jest-joi"); expect.extend(jestJoi.matchers); @@ -61,7 +64,7 @@ expect.extend({ obj = require(`../models/${obj.replace("$","")}.js`); } - expect(sso.content).toMatchObject(obj.test); + expect(sso.content).toMatchSchema(obj.test); done = true; break; } diff --git a/tests/http/getPackagesPackageName.test.js b/tests/http/getPackagesPackageName.test.js new file mode 100644 index 00000000..148c4ab4 --- /dev/null +++ b/tests/http/getPackagesPackageName.test.js @@ -0,0 +1,72 @@ +const endpoint = require("../../src/controllers/getPackagesPackageName.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("Behaves as expected", () => { + test("Calls the correct function", async () => { + const localContext = context; + const spy = jest.spyOn(localContext.database, "getPackageByName"); + + await endpoint.logic({}, localContext); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); + + 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({ + name: "get-package-test", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, + creation_method: "Test Package", + releases: { + latest: "1.1.0" + }, + readme: "This is a readme!", + metadata: { + name: "get-package-test" + }, + versions: { + "1.1.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "get-package-test" + }, + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "get-package-test" + } + } + }); + + 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).toMatchEndpointSuccessObject(endpoint); + await database.removePackageByName("get-package-test", true); + }); + + +}); diff --git a/tests/http/getThemesSearch.test.js b/tests/http/getThemesSearch.test.js index 5a47d5d5..4d854332 100644 --- a/tests/http/getThemesSearch.test.js +++ b/tests/http/getThemesSearch.test.js @@ -35,7 +35,7 @@ describe("Behaves as expected", () => { releases: { latest: "1.1.0" }, - reamde: "This is a readme!", + readme: "This is a readme!", metadata: { name: "atom-material-syntax", theme: "ui" diff --git a/tests/http/getUsersLogin.test.js b/tests/http/getUsersLogin.test.js index 6e83a5e3..19012752 100644 --- a/tests/http/getUsersLogin.test.js +++ b/tests/http/getUsersLogin.test.js @@ -41,7 +41,6 @@ describe("Behaves as expected", () => { expect(res.ok).toBe(true); // First make sure the user object matches expectations broadly, then specifically - expect(res.content).toMatchObject(userObject.test); expect(res.content.username).toBe(userObj.username); expect(res.content.avatar).toBe(userObj.avatar); expect(res).toMatchEndpointSuccessObject(endpoint); diff --git a/tests/models/message.js b/tests/models/message.js index c658ca51..eba46891 100644 --- a/tests/models/message.js +++ b/tests/models/message.js @@ -14,7 +14,8 @@ module.exports = { example: { message: "This is some message content." }, - test: { - message: expect.toBeTypeof("string") - } + test: + Joi.object({ + message: Joi.string().required() + }) }; diff --git a/tests/models/packageObjectFull.js b/tests/models/packageObjectFull.js new file mode 100644 index 00000000..51e02947 --- /dev/null +++ b/tests/models/packageObjectFull.js @@ -0,0 +1,111 @@ +module.exports = { + schema: { + description: "A 'Package Object Full' of a package on the PPR.", + type: "object", + required: [ + "name", "readme", "metadata", "releases", "versions", + "repository", "creation_method", "downloads", "stargazers_count", "badges" + ], + properties: { + name: { type: "string" }, + readme: { type: "string" }, + metadata: { type: "object" }, + releases: { type: "object" }, + versions: { type: "object" }, + repository: { type: "object" }, + creation_method: { type: "string" }, + downloads: { type: "string" }, + stargazers_count: { type: "string" }, + badges: { type: "array" } + } + }, + example: { + // This is nearly the full return of `language-powershell-revised` + name: "language-powershell-revised", + readme: "This is the full content of a readme file!", + metadata: { + // The metadata field is the `package.json` of the most recent version + // With the `dist` object added + dist: { + sha: "604a047247ded9df50e7325345405c93871868e5", + tarball: "https://api.github.com/repos/confused-Techie/language-powershell-revised/tarball/refs/tags/v1.0.0" + }, + name: "language-powershell-revised", + engines: { + atom: ">=1.0.0 <2.0.0" + }, + license: "MIT", + version: "1.0.0", + keywords: [], + // This may be a repository object + repository: "https://github.com/confused-Techie/language-powershell-revised", + description: "Updated, revised PowerShell Syntax Highlighting Support in Pulsar." + }, + releases: { + latest: "1.0.0" + }, + versions: { + "1.0.0": { + // This is the `package.json` of every version + // With a `dist` key added + dist: { + tarball: "https://api.pulsar-edit.dev/api/packages/language-powershell-revised/versions/1.0.0/tarball" + }, + name: "language-powershell-revised", + engines: { + atom: ">=1.0.0 <2.0.0" + }, + license: "MIT", + version: "1.0.0", + keywords: [], + repository: "https://github.com/confsued-Techie/language-powershell-revised", + description: "Updated, revised PowerShell Syntax Highlighting Support in Pulsar" + } + }, + repository: { + // This is the repo object for the VCS Service + url: "https://github.com/confsued-Techie/langauge-powershell-revised", + type: "git" + }, + // This can be either `User Made Package` or `Migrated Package` + creation_method: "User Made Package", + // Note how some fields here are strings not numbers + downloads: "54", + stargazers_count: "0", + badges: [ + // Some badges are baked in, some are applied at render time. + { + title: "Made for Pulsar!", + type: "success" + } + ] + }, + test: + Joi.object({ + name: Joi.string().required(), + readme: Joi.string().required(), + metadata: Joi.object().required(), + releases: Joi.object().required(), + versions: Joi.object().required(), + repository: Joi.object().required(), + creation_method: Joi.string().required(), + downloads: Joi.string().required(), + stargazers_count: Joi.string().required(), + badges: Joi.array().items( + Joi.object({ + title: Joi.string().valid( + "Outdated", + "Made for Pulsar!", + "Broken", + "Archived", + "Deprecated" + ).required(), + type: Joi.string().valid( + "warn", "info", "success" + ).required(), + text: Joi.string(), + link: Joi.string() + }) + ).required() + }) +}; diff --git a/tests/models/userObjectPrivate.js b/tests/models/userObjectPrivate.js index ab182ee9..744b77b2 100644 --- a/tests/models/userObjectPrivate.js +++ b/tests/models/userObjectPrivate.js @@ -38,13 +38,14 @@ module.exports = { created_at: "2023-09-16T00:58:36.755Z", packages: [] }, - test: { - username: expect.toBeTypeof("string"), - avatar: expect.toBeTypeof("string"), - data: {}, - node_id: expect.toBeTypeof("string"), - token: expect.toBeTypeof("string"), - created_at: expect.toBeTypeof("string"), - packages: expect.toBeArray() - } + test: + Joi.object({ + username: Joi.string().required(), + avatar: Joi.string().required(), + data: Joi.object().required(), + node_id: Joi.string().required(), + token: Joi.string().required(), + created_at: Joi.string().required(), + packages: Joi.array().required() + }) }; diff --git a/tests/models/userObjectPublic.js b/tests/models/userObjectPublic.js index 04346a47..1b0f97a3 100644 --- a/tests/models/userObjectPublic.js +++ b/tests/models/userObjectPublic.js @@ -30,11 +30,12 @@ module.exports = { created_at: "2023-09-16T00:58:36.755Z", packages: [] }, - test: { - username: expect.toBeTypeof("string"), - avatar: expect.toBeTypeof("string"), - data: {}, - created_at: expect.toBeTypeof("string"), - packages: expect.toBeArray() - } + test: + Joi.object({ + username: Joi.string().required(), + avatar: Joi.string().required(), + data: Joi.object().required(), + created_at: Joi.string().required(), + packages: Joi.array().required() + }) }; From 2ea0e16d41ae22a842ff68edc857c3515bf71fa0 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Tue, 19 Sep 2023 20:40:07 -0700 Subject: [PATCH 41/53] Improved testing of package objects, fixed bugs found by this, implemented it's usage elsewhere --- src/controllers/getThemesFeatured.js | 2 +- src/controllers/getThemesSearch.js | 10 +- src/utils.js | 3 + tests/http/getThemes.test.js | 7 +- tests/http/getThemesFeatured.test.js | 9 +- tests/http/getThemesSearch.test.js | 7 +- tests/models/packageObjectFull.js | 29 ++++-- tests/models/packageObjectFullArray.js | 12 +++ tests/models/packageObjectShort.js | 131 ++++++++++++++++++++++++ tests/models/packageObjectShortArray.js | 12 +++ 10 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 tests/models/packageObjectFullArray.js create mode 100644 tests/models/packageObjectShort.js create mode 100644 tests/models/packageObjectShortArray.js diff --git a/src/controllers/getThemesFeatured.js b/src/controllers/getThemesFeatured.js index b83d6821..6bc83646 100644 --- a/src/controllers/getThemesFeatured.js +++ b/src/controllers/getThemesFeatured.js @@ -5,7 +5,7 @@ module.exports = { docs: { summary: "Display featured packages that are themes.", - respones: { + responses: { 200: { description: "An array of featured themes.", content: { diff --git a/src/controllers/getThemesSearch.js b/src/controllers/getThemesSearch.js index 72f361b7..97709720 100644 --- a/src/controllers/getThemesSearch.js +++ b/src/controllers/getThemesSearch.js @@ -4,7 +4,15 @@ module.exports = { docs: { - summary: "Get featured packages that are themes. Previously undocumented." + summary: "Get featured packages that are themes. Previously undocumented.", + responses: { + 200: { + description: "A paginated response of themes.", + content: { + "application/json": "$packageObjectShortArray" + } + } + } }, endpoint: { method: "GET", diff --git a/src/utils.js b/src/utils.js index 960f0034..cc57fdf9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -121,6 +121,9 @@ async function constructPackageObjectShort(pack) { newPack.badges.push({ title: "Made for Pulsar!", type: "success" }); } + // Remove keys that aren't intended to exist in a Package Object Short + delete newPack.versions; + return newPack; }; diff --git a/tests/http/getThemes.test.js b/tests/http/getThemes.test.js index 671c9619..56b9dfb1 100644 --- a/tests/http/getThemes.test.js +++ b/tests/http/getThemes.test.js @@ -40,7 +40,10 @@ describe("Behaves as expected", () => { test("Returns proper data on success", async () => { const addName = await database.insertNewPackage({ name: "test-package", - repository: "https://github.com/confused-Techie/package-backend", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, creation_method: "Test Package", releases: { latest: "1.1.0" @@ -87,7 +90,7 @@ describe("Behaves as expected", () => { + `<${context.config.server_url}/api/themes?page=1&sort=downloads&direction=desc>;` + ' rel="last"' ); - // TODO test object structure to known good + expect(sso).toMatchEndpointSuccessObject(endpoint); await database.removePackageByName("test-package", true); }); diff --git a/tests/http/getThemesFeatured.test.js b/tests/http/getThemesFeatured.test.js index d984b575..bcce1672 100644 --- a/tests/http/getThemesFeatured.test.js +++ b/tests/http/getThemesFeatured.test.js @@ -25,7 +25,10 @@ describe("Behaves as expected", () => { const addPack = await database.insertNewPackage({ // We know a currently featured package is 'atom-material-ui' name: "atom-material-ui", - repository: "https://github.com/confused-Techie/package-backend", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, creation_method: "Test Package", releases: { latest: "1.1.0" @@ -54,12 +57,12 @@ describe("Behaves as expected", () => { }); const sso = await endpoint.logic({}, context); - + expect(sso.ok).toBe(true); expect(sso.content).toBeArray(); expect(sso.content.length).toBe(1); expect(sso.content[0].name).toBe("atom-material-ui"); - // TODO test object structure to known good + expect(sso).toMatchEndpointSuccessObject(endpoint); await database.removePackageByName("atom-material-ui", true); }); }); diff --git a/tests/http/getThemesSearch.test.js b/tests/http/getThemesSearch.test.js index 4d854332..2e2ca9fa 100644 --- a/tests/http/getThemesSearch.test.js +++ b/tests/http/getThemesSearch.test.js @@ -30,7 +30,10 @@ describe("Behaves as expected", () => { test("Returns array on success", async () => { const newPack = await database.insertNewPackage({ name: "atom-material-syntax", - repository: "https://github.com/confused-Techie/package-backend", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, creation_method: "Test Package", releases: { latest: "1.1.0" @@ -69,7 +72,7 @@ describe("Behaves as expected", () => { expect(sso.content).toBeArray(); expect(sso.content.length).toBe(1); expect(sso.content[0].name).toBe("atom-material-syntax"); - // TODO ensure it matches known good object + expect(sso).toMatchEndpointSuccessObject(endpoint); await database.removePackageByName("atom-material-syntax", true); }); diff --git a/tests/models/packageObjectFull.js b/tests/models/packageObjectFull.js index 51e02947..24a6b4f0 100644 --- a/tests/models/packageObjectFull.js +++ b/tests/models/packageObjectFull.js @@ -85,12 +85,29 @@ module.exports = { name: Joi.string().required(), readme: Joi.string().required(), metadata: Joi.object().required(), - releases: Joi.object().required(), + releases: Joi.object({ + latest: Joi.string().required() + }).required(), versions: Joi.object().required(), - repository: Joi.object().required(), - creation_method: Joi.string().required(), - downloads: Joi.string().required(), - stargazers_count: Joi.string().required(), + repository: Joi.object({ + url: Joi.string().required(), + type: Joi.string().valid( + "git", + "bit", + "sfr", + "lab", + "berg", + "unknown", + "na" + ).required() + }).required(), + creation_method: Joi.string().valid( + "User Made Package", + "Migrated from Atom.io", + "Test Package" // Should only be used during tests + ).required(), + downloads: Joi.string().pattern(/^[0-9]+$/).required(), + stargazers_count: Joi.string().pattern(/^[0-9]+$/).required(), badges: Joi.array().items( Joi.object({ title: Joi.string().valid( @@ -107,5 +124,5 @@ module.exports = { link: Joi.string() }) ).required() - }) + }).required() }; diff --git a/tests/models/packageObjectFullArray.js b/tests/models/packageObjectFullArray.js new file mode 100644 index 00000000..6945e887 --- /dev/null +++ b/tests/models/packageObjectFullArray.js @@ -0,0 +1,12 @@ +module.exports = { + schema: { + + }, + example: [ + require("./packageObjectFull.js").example + ] + test: + Joi.array().items( + require("./packageObjectFull.js").test + ).required() +}; diff --git a/tests/models/packageObjectShort.js b/tests/models/packageObjectShort.js new file mode 100644 index 00000000..b39f3cd8 --- /dev/null +++ b/tests/models/packageObjectShort.js @@ -0,0 +1,131 @@ +module.exports = { + schema: { + description: "A 'Package Object Short' of a package on the PPR.", + type: "object", + required: [ + "name", "readme", "metadata", "repository", "downloads", "stargazers_count", + "releases", "badges" + ], + properties: { + name: { type: "string" }, + readme: { type: "string" }, + metadata: { type: "object" }, + repository: { type: "object" }, + creation_method: { type: "string" }, + downloads: { type: "string" }, + stargazers_count: { type: "string" }, + releases: { type: "object" }, + badges: { type: "array" } + } + }, + example: { + // Example taken from `platformio-ide-terminal` + name: "platformio-ide-terminal", + readme: "This is the full content of a readme file!", + metadata: { + main: "./lib/plaformio-ide-terminal", + name: "platformio-ide-terminal", + // This could be an author object + author: "Jeremy Ebneyamin", + engines: { + atom: ">=1.12.2 <2.0.0" + }, + license: "MIT", + version: "2.10.1", + homepage: "https://atom.io/packages/platformio=ide-terminal", + keywords: [ + "PlatformIO", + "terminal-plus", + "terminal" + ], + repository: "https://github.com/platformio/platformio-iatom-ide-terminal", + description: "A terminal package for Atom, complete with themes, API and more for PlatformIO IDE. Fork of terminal-plus.", + contributors: [ + { + url: "http://platformio.org", + name: "Ivan Kravets", + email: "me@kravets.com" + } + ], + dependencies: { + "term.js": "https://github.com/jeremyramin/term.js/tarball/master", + underscore: "^1.8.3", + "atom-psace-pen-views": "^2.2.0", + "node-pty-prebuilt-multiarch": "^0.9.0" + }, + activationHooks: [ + "core:loaded-shell-encironmnet" + ], + consumedServices: { + "status-bar": { + versions: { + "^1.0.0": "consumeStatusBar" + } + } + }, + providedServices: { + runInTerminal: { + versions: { + "0.14.5": "provideRunInTerminal" + }, + description: "Deprecated API for PlatformIO IDE 1.0" + } + } + }, + repository: { + url: "https://github.com/platformio/platformio-atom-ide-terminal", + type: "git" + }, + creation_method: "User Made Package", + downloads: "16997915", + stargazers_count: "1114", + releases: { + latest: "2.10.1" + }, + badges: [] + }, + test: + Joi.object({ + name: Joi.string().required(), + readme: Joi.string().required(), + metadata: Joi.object().required(), + releases: Joi.object({ + latest: Joi.string().required() + }).required(), + repository: Joi.object({ + url: Joi.string().required(), + type: Joi.string().valid( + "git", + "bit", + "sfr", + "lab", + "berg", + "unknown", + "na" + ).required() + }).required(), + creation_method: Joi.string().valid( + "User Made Package", + "Migrated from Atom.io", + "Test Package" // Should only be used during tests + ).required(), + downloads: Joi.string().pattern(/^[0-9]+$/).required(), + stargazers_count: Joi.string().pattern(/^[0-9]+$/).required(), + badges:Joi.array().items( + Joi.object({ + title: Joi.string().valid( + "Outdated", + "Made for Pulsar!", + "Broken", + "Archived", + "Deprecated" + ).required(), + type: Joi.string().valid( + "warn", "info", "success" + ).required(), + text: Joi.string(), + link: Joi.string() + }) + ).required() + }).required() +}; diff --git a/tests/models/packageObjectShortArray.js b/tests/models/packageObjectShortArray.js new file mode 100644 index 00000000..bb27a389 --- /dev/null +++ b/tests/models/packageObjectShortArray.js @@ -0,0 +1,12 @@ +module.exports = { + schema: { + + }, + example: [ + require("./packageObjectShort.js").example + ], + test: + Joi.array().items( + require("./packageObjectShort.js").test + ).required() +}; From 7b138ea059a685e438a1013af4d583e407b01a2c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 25 Sep 2023 18:13:18 -0700 Subject: [PATCH 42/53] Ensure we properly return a `204` here, as PPM expects it --- src/controllers/deletePackagesPackageNameStar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/deletePackagesPackageNameStar.js b/src/controllers/deletePackagesPackageNameStar.js index adfe96a4..97c39f40 100644 --- a/src/controllers/deletePackagesPackageNameStar.js +++ b/src/controllers/deletePackagesPackageNameStar.js @@ -13,7 +13,7 @@ module.exports = { "/api/themes/:packageName/star" ], rateLimit: "auth", - successStatus: 201, + successStatus: 204, options: { Allow: "DELETE, POST", "X-Content-Type-Options": "nosniff" From 59f439977c999419888419360269d50b2a3f338c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 25 Sep 2023 18:50:59 -0700 Subject: [PATCH 43/53] Implement testing for featured packages, minor cleanup --- .../deletePackagesPackageNameStar.js | 7 +- src/controllers/getPackagesFeatured.js | 10 ++- tests/http/getPackagesFeatured.test.js | 68 +++++++++++++++++++ tests/models/packageObjectFullArray.js | 2 +- 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tests/http/getPackagesFeatured.test.js diff --git a/src/controllers/deletePackagesPackageNameStar.js b/src/controllers/deletePackagesPackageNameStar.js index 97c39f40..55b55e94 100644 --- a/src/controllers/deletePackagesPackageNameStar.js +++ b/src/controllers/deletePackagesPackageNameStar.js @@ -4,7 +4,12 @@ module.exports = { docs: { - summary: "Unstar a package." + summary: "Unstar a package.", + responses: { + 204: { + description: "An empty response, indicating success." + } + } }, endpoint: { method: "DELETE", diff --git a/src/controllers/getPackagesFeatured.js b/src/controllers/getPackagesFeatured.js index 59338924..7f7ec78b 100644 --- a/src/controllers/getPackagesFeatured.js +++ b/src/controllers/getPackagesFeatured.js @@ -4,7 +4,15 @@ module.exports = { docs: { - summary: "Returns all featured packages. Previously undocumented endpoint." + summary: "Returns all featured packages. Previously undocumented endpoint.", + responses: { + 200: { + description: "An array of features packages.", + content: { + "application/json": "$packageObjectShortArray" + } + } + } }, endpoint: { method: "GET", diff --git a/tests/http/getPackagesFeatured.test.js b/tests/http/getPackagesFeatured.test.js new file mode 100644 index 00000000..1131a872 --- /dev/null +++ b/tests/http/getPackagesFeatured.test.js @@ -0,0 +1,68 @@ +const endpoint = require("../../src/controllers/getPackagesFeatured.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("Behaves as expected", () => { + test("Calls the correct function", async () => { + const localContext = context; + const spy = jest.spyOn(localContext.database, "getFeaturedPackages"); + + await endpoint.logic({}, localContext); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + }); + + test("Returns not found with no packages present", async () => { + const sso = await endpoint.logic({}, context); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("not_found"); + }); + + test("Returns proper data on success", async () => { + const addPack = await database.insertNewPackage({ + // We know a currently featured package is 'x-terminal-reloaded' + name: "x-terminal-reloaded", + repository: { + url: "https://github.com/Spiker985/x-terminal-reloaded", + type: "git" + }, + creation_method: "Test Package", + releases: { + latest: "1.1.0" + }, + readme: "This is a readme!", + metadata: { + name: "atom-material-ui" + }, + versions: { + "1.1.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "x-terminal-reloaded" + }, + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "x-terminal-reloaded" + } + } + }); + + const sso = await endpoint.logic({}, context); + + expect(sso.ok).toBe(true); + expect(sso.content).toBeArray(); + expect(sso.content.length).toBe(1); + expect(sso.content[0].name).toBe("x-terminal-reloaded"); + expect(sso).toMatchEndpointSuccessObject(endpoint); + + await database.removePackageByName("x-terminal-reloaded", true); + }); +}); diff --git a/tests/models/packageObjectFullArray.js b/tests/models/packageObjectFullArray.js index 6945e887..6e397930 100644 --- a/tests/models/packageObjectFullArray.js +++ b/tests/models/packageObjectFullArray.js @@ -4,7 +4,7 @@ module.exports = { }, example: [ require("./packageObjectFull.js").example - ] + ], test: Joi.array().items( require("./packageObjectFull.js").test From 0269e14d71961e160616f20794cfd313128f61fe Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 25 Sep 2023 23:08:07 -0700 Subject: [PATCH 44/53] Add return data to `deletePackagesPackageName` --- src/controllers/deletePackagesPackageName.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/controllers/deletePackagesPackageName.js b/src/controllers/deletePackagesPackageName.js index 1fc9a1aa..c5e96c7e 100644 --- a/src/controllers/deletePackagesPackageName.js +++ b/src/controllers/deletePackagesPackageName.js @@ -4,7 +4,12 @@ module.exports = { docs: { - summary: "Delete a package." + summary: "Delete a package.", + responses: { + 204: { + description: "An empty response, indicating success." + } + } }, endpoint: { method: "DELETE", From 1dc2f472d0d934d5a3dc731988566508a732e13f Mon Sep 17 00:00:00 2001 From: confused_techie Date: Fri, 20 Oct 2023 12:53:32 -0700 Subject: [PATCH 45/53] Additional endpoint testing --- src/config.js | 61 ++++++++----------- .../deletePackagesPackageName.test.js | 18 ++++++ .../deletePackagesPackageNameStar.js | 18 ++++++ ...agesPackageNameVersionsVersionName.test.js | 18 ++++++ 4 files changed, 81 insertions(+), 34 deletions(-) create mode 100644 tests/unit/controllers/deletePackagesPackageName.test.js create mode 100644 tests/unit/controllers/deletePackagesPackageNameStar.js create mode 100644 tests/unit/controllers/deletePackagesPackageNameVersionsVersionName.test.js diff --git a/src/config.js b/src/config.js index 45a94078..238c5cf2 100644 --- a/src/config.js +++ b/src/config.js @@ -59,42 +59,35 @@ function getConfig() { // But we will create a custom object here to return, with all values, and choosing between the env vars and config // Since if this is moved to Google App Engine, these variables will all be environment variables. So we will look for both. + const findValue = (key, def) => { + return process.env[key] ?? data.env_variables[key] ?? def ?? undefined; + }; + return { - port: process.env.PORT ?? data.env_variables.PORT, - server_url: process.env.SERVERURL ?? data.env_variables.SERVERURL, - paginated_amount: process.env.PAGINATE ?? data.env_variables.PAGINATE, + port: findValue("PORT", 8080), + server_url: findValue("SERVERURL"), + paginated_amount: findValue("PAGINATE", 30), prod: process.env.NODE_ENV === "production" ? true : false, - cache_time: process.env.CACHETIME ?? data.env_variables.CACHETIME, - GCLOUD_STORAGE_BUCKET: - process.env.GCLOUD_STORAGE_BUCKET ?? - data.env_variables.GCLOUD_STORAGE_BUCKET, - GOOGLE_APPLICATION_CREDENTIALS: - process.env.GOOGLE_APPLICATION_CREDENTIALS ?? - data.env_variables.GOOGLE_APPLICATION_CREDENTIALS, - GH_CLIENTID: process.env.GH_CLIENTID ?? data.env_variables.GH_CLIENTID, - GH_USERAGENT: process.env.GH_USERAGENT ?? data.env_variables.GH_USERAGENT, - GH_REDIRECTURI: - process.env.GH_REDIRECTURI ?? data.env_variables.GH_REDIRECTURI, - GH_CLIENTSECRET: - process.env.GH_CLIENTSECRET ?? data.env_variables.GH_CLIENTSECRET, - DB_HOST: process.env.DB_HOST ?? data.env_variables.DB_HOST, - DB_USER: process.env.DB_USER ?? data.env_variables.DB_USER, - DB_PASS: process.env.DB_PASS ?? data.env_variables.DB_PASS, - DB_DB: process.env.DB_DB ?? data.env_variables.DB_DB, - DB_PORT: process.env.DB_PORT ?? data.env_variables.DB_PORT, - DB_SSL_CERT: process.env.DB_SSL_CERT ?? data.env_variables.DB_SSL_CERT, - LOG_LEVEL: process.env.LOG_LEVEL ?? data.env_variables.LOG_LEVEL, - LOG_FORMAT: process.env.LOG_FORMAT ?? data.env_variables.LOG_FORMAT, - RATE_LIMIT_GENERIC: - process.env.RATE_LIMIT_GENERIC ?? data.env_variables.RATE_LIMIT_GENERIC, - RATE_LIMIT_AUTH: - process.env.RATE_LIMIT_AUTH ?? data.env_variables.RATE_LIMIT_AUTH, - WEBHOOK_PUBLISH: - process.env.WEBHOOK_PUBLISH ?? data.env_variables.WEBHOOK_PUBLISH, - WEBHOOK_VERSION: - process.env.WEBHOOK_VERSION ?? data.env_variables.WEBHOOK_VERSION, - WEBHOOK_USERNAME: - process.env.WEBHOOK_USERNAME ?? data.env_variables.WEBHOOK_USERNAME, + cache_time: findValue("CACHETIME"), + GCLOUD_STORAGE_BUCKET: findValue("GCLOUD_STORAGE_BUCKET"), + GOOGLE_APPLICATION_CREDENTIALS: findValue("GOOGLE_APPLICATION_CREDENTIALS"), + GH_CLIENTID: findValue("GH_CLIENTID"), + GH_CLIENTSECRET: findValue("GH_CLIENTSECRET"), + GH_USERAGENT: findValue("GH_USERAGENT"), // todo maybe default? + GH_REDIRECTURI: findValue("GH_REDIRECTURI"), + DB_HOST: findValue("DB_HOST"), + DB_USER: findValue("DB_USER"), + DB_PASS: findValue("DB_PASS"), + DB_DB: findValue("DB_DB"), + DB_PORT: findValue("DB_PORT"), + DB_SSL_CERT: findValue("DB_SSL_CERT"), + LOG_LEVEL: findValue("LOG_LEVEL", 6), + LOG_FORMAT: findValue("LOG_FORMAT", "stdout"), + RATE_LIMIT_GENERIC: findValue("RATE_LIMIT_GENERIC"), + RATE_LIMIT_AUTH: findValue("RATE_LIMIT_AUTH"), + WEBHOOK_PUBLISH: findValue("WEBHOOK_PUBLISH"), + WEBHOOK_VERSION: findValue("WEBHOOK_VERSION"), + WEBHOOK_USERNAME: findValue("WEBHOOK_USERNAME"), }; } diff --git a/tests/unit/controllers/deletePackagesPackageName.test.js b/tests/unit/controllers/deletePackagesPackageName.test.js new file mode 100644 index 00000000..9acac063 --- /dev/null +++ b/tests/unit/controllers/deletePackagesPackageName.test.js @@ -0,0 +1,18 @@ +const endpoint = require("../../../src/controllers/deletePackagesPackageName.js"); + +describe("Has features expected", () => { + test("endpoint features", () => { + const expected = { + method: "DELETE", + paths: [ "/api/packages/:packageName", "/api/themes/:packageName" ], + rateLimit: "auth", + successStatus: 204 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + }); +}); diff --git a/tests/unit/controllers/deletePackagesPackageNameStar.js b/tests/unit/controllers/deletePackagesPackageNameStar.js new file mode 100644 index 00000000..7a84a507 --- /dev/null +++ b/tests/unit/controllers/deletePackagesPackageNameStar.js @@ -0,0 +1,18 @@ +const endpoint = require("../../../src/controllers/deletePackagesPackageStar.js"); + +describe("Has features expected", () => { + test("endpoint features", () => { + const expected = { + method: "DELETE", + paths: [ "/api/packages/:packageName/star", "/api/themes/:packageName/star" ], + rateLimit: "auth", + successStatus: 204 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + }); +}); diff --git a/tests/unit/controllers/deletePackagesPackageNameVersionsVersionName.test.js b/tests/unit/controllers/deletePackagesPackageNameVersionsVersionName.test.js new file mode 100644 index 00000000..cf5c9673 --- /dev/null +++ b/tests/unit/controllers/deletePackagesPackageNameVersionsVersionName.test.js @@ -0,0 +1,18 @@ +const endpoint = require("../../../src/controllers/deletePackagesPackageNameVersionsVersionName.js"); + +describe("Has features expected", () => { + test("endpoint features", () => { + const expected = { + method: "DELETE", + paths: [ "/api/packages/:packageName/versions/:versionName", "/api/themes/:packageName/versions/:versionName" ], + rateLimit: "auth", + successStatus: 204 + }; + + expect(endpoint.endpoint).toMatchObject(expected); + }); + + test("correct functions", () => { + expect(endpoint.logic).toBeTypeof("function"); + }); +}); From 836c2b56d683adcbc5f49236294c3daa5709e82f Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 22 Oct 2023 18:21:51 -0700 Subject: [PATCH 46/53] Migrate and rewrite majority of remaining tests --- jest.config.js | 1 + src/controllers/postPackages.js | 6 +- .../postPackagesPackageNameStar.js | 10 +- src/database.js | 89 ++--- .../post.packages.handler.integration.test.js | 303 ------------------ tests/database/applyFeatures.test.js | 2 +- .../database/database.test.js | 39 ++- .../valid_multi_version.js | 0 .../valid_one_version.js | 0 .../database}/fixtures/lifetime/package-a.js | 0 .../database}/fixtures/lifetime/user-a.js | 0 tests/http/postPackages.test.js | 122 +++++++ .../http/postPackagesPackageNameStar.test.js | 100 ++++++ .../postPackagesPackageNameVersions.test.js | 24 ++ .../unit/PackageObject.test.js | 2 +- .../unit/ServerStatusObject.test.js | 2 +- {test => tests/vcs}/github.vcs.test.js | 4 +- {test => tests/vcs}/vcs.unit.test.js | 2 +- {test => tests/vcs}/vcs.vcs.test.js | 4 +- 19 files changed, 347 insertions(+), 363 deletions(-) delete mode 100644 test/post.packages.handler.integration.test.js rename test/database.integration.test.js => tests/database/database.test.js (95%) rename {test => tests/database}/fixtures/git.createPackage_returns/valid_multi_version.js (100%) rename {test => tests/database}/fixtures/git.createPackage_returns/valid_one_version.js (100%) rename {test => tests/database}/fixtures/lifetime/package-a.js (100%) rename {test => tests/database}/fixtures/lifetime/user-a.js (100%) create mode 100644 tests/http/postPackages.test.js create mode 100644 tests/http/postPackagesPackageNameStar.test.js create mode 100644 tests/http/postPackagesPackageNameVersions.test.js rename test/PackageObject.unit.test.js => tests/unit/PackageObject.test.js (95%) rename test/ServerStatusObject.unit.test.js => tests/unit/ServerStatusObject.test.js (94%) rename {test => tests/vcs}/github.vcs.test.js (96%) rename {test => tests/vcs}/vcs.unit.test.js (98%) rename {test => tests/vcs}/vcs.vcs.test.js (99%) diff --git a/jest.config.js b/jest.config.js index 4ec305d6..63514535 100644 --- a/jest.config.js +++ b/jest.config.js @@ -25,6 +25,7 @@ const config = { testMatch: [ "/tests/database/**.test.js", "/tests/http/**.test.js", + "/tests/vcs/**.test.js" ] }, { diff --git a/src/controllers/postPackages.js b/src/controllers/postPackages.js index 17d6ff1e..6c604816 100644 --- a/src/controllers/postPackages.js +++ b/src/controllers/postPackages.js @@ -19,6 +19,10 @@ module.exports = { "X-Content-Type-Options": "nosniff" } }, + params: { + repository: (context, req) => { return context.query.repo(req); }, + auth: (context, req) => { return context.query.auth(req); } + }, async postReturnHTTP(req, res, context, obj) { // Return to user before wbehook call, so user doesn't wait on it await context.webhook.alertPublishPackage(obj.webhook.pack, obj.webhook.user); @@ -56,7 +60,7 @@ module.exports = { }, async logic(params, context) { - const user = await auth.verifyAuth(params.auth, context.database); + const user = await context.auth.verifyAuth(params.auth, context.database); // Check authentication if (!user.ok) { const sso = new context.sso(); diff --git a/src/controllers/postPackagesPackageNameStar.js b/src/controllers/postPackagesPackageNameStar.js index d7cd0b31..ebe0cd99 100644 --- a/src/controllers/postPackagesPackageNameStar.js +++ b/src/controllers/postPackagesPackageNameStar.js @@ -4,7 +4,15 @@ module.exports = { docs: { - summary: "Star a package." + summary: "Star a package.", + responses: { + 200: { + description: "A 'Package Object Full' of the modified package", + content: { + "application/json": "$packageObjectFull" + } + } + } }, endpoint: { method: "POST", diff --git a/src/database.js b/src/database.js index ea216b1d..9e02a358 100644 --- a/src/database.js +++ b/src/database.js @@ -93,13 +93,13 @@ async function packageNameAvailability(name) { : { ok: false, content: `${name} is not available to be used for a new package.`, - short: "Not Found", + short: "not_found", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -371,7 +371,7 @@ async function applyFeatures(featureObj, packName, packVersion) { return { ok: false, content: `Unable to find the pointer of ${packName}`, - short: "Not Found", + short: "not_found", }; } @@ -388,7 +388,7 @@ async function applyFeatures(featureObj, packName, packVersion) { return { ok: false, content: `Unable to set 'has_snippets' flag to true for ${packName}`, - short: "Server Error", + short: "server_error", }; } } @@ -404,7 +404,7 @@ async function applyFeatures(featureObj, packName, packVersion) { return { ok: false, content: `Unable to set 'has_grammar' flag to true for ${packName}`, - short: "Server Error", + short: "server_error", }; } } @@ -424,7 +424,7 @@ async function applyFeatures(featureObj, packName, packVersion) { return { ok: false, content: `Unable to add supportedLanguages to ${packName}`, - short: "Server Error", + short: "server_error", }; } } @@ -436,7 +436,7 @@ async function applyFeatures(featureObj, packName, packVersion) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err.toString(), // TODO this must be implemented within the logger // seems the custom PostgreSQL error object doesn't have native to string methods // or otherwise logging the error within an object doesn't trigger the toString method @@ -469,7 +469,7 @@ async function insertNewPackageName(newName, oldName) { return { ok: false, content: `Unable to find the original pointer of ${oldName}`, - short: "Not Found", + short: "not_found", }; } @@ -516,7 +516,7 @@ async function insertNewPackageName(newName, oldName) { : { ok: false, content: `A generic error occurred while inserting the new package name ${newName}`, - short: "Server Error", + short: "server_error", error: err, }; }); @@ -552,7 +552,7 @@ async function insertNewUser(username, id, avatar) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -638,13 +638,13 @@ async function getPackageByNameSimple(name) { : { ok: false, content: `Package ${name} not found.`, - short: "Not Found", + short: "not_found", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -674,13 +674,13 @@ async function getPackageVersionByNameAndVersion(name, version) { : { ok: false, content: `Package ${name} and Version ${version} not found.`, - short: "Not Found", + short: "not_found", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -752,7 +752,7 @@ async function getPackageCollectionByID(packArray) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -800,13 +800,13 @@ async function updatePackageStargazers(name, pointer = null) { : { ok: false, content: "Unable to Update Package Stargazers", - short: "Server Error", + short: "server_error", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -842,7 +842,7 @@ async function updatePackageIncrementDownloadByName(name) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -872,13 +872,13 @@ async function updatePackageDecrementDownloadByName(name) { : { ok: false, content: "Unable to decrement Package Download Count", - short: "Server Error", + short: "server_error", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -954,11 +954,11 @@ async function removePackageByName(name, exterminate = false) { }) .catch((err) => { return typeof err === "string" - ? { ok: false, content: err, short: "Server Error" } + ? { ok: false, content: err, short: "server_error" } : { ok: false, content: `A generic error occurred while inserting ${name} package`, - short: "Server Error", + short: "server_error", error: err, }; }); @@ -987,7 +987,7 @@ async function removePackageVersion(packName, semVer) { return { ok: false, content: `Unable to find the pointer of ${packName}`, - short: "Not Found", + short: "not_found", }; } @@ -1018,7 +1018,7 @@ async function removePackageVersion(packName, semVer) { return { ok: false, content: `Unable to remove ${semVer} version of ${packName} package.`, - short: "Not Found", + short: "not_found", }; } @@ -1029,11 +1029,11 @@ async function removePackageVersion(packName, semVer) { }) .catch((err) => { return typeof err === "string" - ? { ok: false, content: err, short: "Server Error" } + ? { ok: false, content: err, short: "server_error" } : { ok: false, content: `A generic error occurred while inserting ${packName} package`, - short: "Server Error", + short: "server_error", error: err, }; }); @@ -1167,7 +1167,7 @@ async function getUserByID(id) { return { ok: false, content: `Unable to get user by ID: ${id}`, - short: "Server Error", + short: "server_error", }; } @@ -1176,13 +1176,13 @@ async function getUserByID(id) { : { ok: false, content: `Unable to get user by ID: ${id}`, - short: "Server Error", + short: "server_error", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -1206,7 +1206,7 @@ async function updateIncrementStar(user, pack) { return { ok: false, content: `Unable to find package ${pack} to star.`, - short: "Not Found", + short: "not_found", }; } @@ -1229,7 +1229,7 @@ async function updateIncrementStar(user, pack) { return { ok: false, content: `Failed to Star the Package`, - short: "Server Error", + short: "server_error", }; } @@ -1245,6 +1245,9 @@ async function updateIncrementStar(user, pack) { content: `Package Successfully Starred`, }; } catch (e) { + // TODO: While the comment below is accurate + // It's also worth noting that this catch will return success + // If the starring user does not exist. Resulting in a false positive // Catch the primary key violation on (package, userid), // Sinche the package is already starred by the user, we return ok. return { @@ -1256,7 +1259,7 @@ async function updateIncrementStar(user, pack) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -1280,7 +1283,7 @@ async function updateDecrementStar(user, pack) { return { ok: false, content: `Unable to find package ${pack} to unstar.`, - short: "Not Found", + short: "not_found", }; } @@ -1310,7 +1313,7 @@ async function updateDecrementStar(user, pack) { return { ok: false, content: "Failed to Unstar the Package", - short: "Server Error", + short: "server_error", }; } @@ -1329,7 +1332,7 @@ async function updateDecrementStar(user, pack) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -1366,7 +1369,7 @@ async function getStarredPointersByUserID(userid) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -1407,7 +1410,7 @@ async function getStarringUsersByPointer(pointer) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -1629,7 +1632,7 @@ async function getSortedPackages(opts, themes = false) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err.toString(), }; } @@ -1681,13 +1684,13 @@ async function authStoreStateKey(stateKey) { : { ok: false, content: `The state key has not been saved on the database.`, - short: "Server Error", + short: "server_error", }; } catch (err) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } @@ -1730,7 +1733,7 @@ async function authCheckAndDeleteStateKey(stateKey, timestamp = null) { return { ok: false, content: "The provided state key was not set for the auth login.", - short: "Not Found", + short: "not_found", }; } @@ -1742,7 +1745,7 @@ async function authCheckAndDeleteStateKey(stateKey, timestamp = null) { return { ok: false, content: "The provided state key is expired for the auth login.", - short: "Not Found", + short: "not_found", }; } @@ -1751,7 +1754,7 @@ async function authCheckAndDeleteStateKey(stateKey, timestamp = null) { return { ok: false, content: "Generic Error", - short: "Server Error", + short: "server_error", error: err, }; } diff --git a/test/post.packages.handler.integration.test.js b/test/post.packages.handler.integration.test.js deleted file mode 100644 index ac1e406e..00000000 --- a/test/post.packages.handler.integration.test.js +++ /dev/null @@ -1,303 +0,0 @@ -const request = require("supertest"); -const app = require("../src/main.js"); - -// Mock any webhooks that would be sent -const webhook = require("../src/webhook.js"); - -jest.mock("../src/webhook.js", () => { - return { - alertPublishPackage: jest.fn(), - alertPublishVersion: jest.fn(), - }; -}); - -const { authMock } = require("./httpMock.helper.jest.js"); - -let tmpMock; - -describe("Post /api/packages", () => { - afterEach(() => { - tmpMock.mockClear(); - }); - - test("Fails with 'Bad Auth' when bad token is passed.", async () => { - tmpMock = authMock({ - ok: false, - short: "Bad Auth", - content: "Bad Auth Mock Return for Dev User", - }); - - const res = await request(app) - .post("/api/packages") - .query({ repository: "pulsar-edit/langauge-css" }) - .set("Authorization", "invalid"); - expect(res.body.message).toEqual(msg.badAuth); - expect(res).toHaveHTTPCode(401); - }); - - test("Fails with 'badRepoJSON' when no repo is passed.", async () => { - tmpMock = authMock({ - ok: true, - content: { - token: "valid-token", - id: 9999, - node_id: "post-pkg-publish-test-user-node-id", - username: "post-pkg-publish-test-user", - avatar: "https://roadtonowhere.com", - }, - }); - - const res = await request(app) - .post("/api/packages") - .query({ repository: "" }) - .set("Authorization", "valid-token"); - expect(res.body.message).toEqual(msg.badRepoJSON); - expect(res).toHaveHTTPCode(400); - }); - - test("Fails with 'badRepoJSON' when bad repo is passed.", async () => { - tmpMock = authMock({ - ok: true, - content: { - token: "valid-token", - id: 9999, - node_id: "post-pkg-publish-test-user-node-id", - username: "post-pkg-publish-test-user", - avatar: "https://roadtonowhere.com", - }, - }); - - const res = await request(app) - .post("/api/packages") - .query({ repository: "not-exist" }) - .set("Authorization", "valid-token"); - expect(res.body.message).toEqual(msg.badRepoJSON); - expect(res).toHaveHTTPCode(400); - }); - test("Fails with 'badRepoJSON' when Repo with a space is passed", async () => { - tmpMock = authMock({ - ok: true, - content: { - token: "valid-token", - id: 9999, - node_id: "post-pkg-publish-test-user-node-id", - username: "post-pkg-publish-test-user", - avatar: "https://roadtonowhere.com", - }, - }); - - const res = await request(app) - .post("/api/packages") - .query({ repository: "pulsar-edit/language CSS" }) - .set("Authorization", "valid-token"); - expect(res.body.message).toEqual(msg.badRepoJSON); - expect(res).toHaveHTTPCode(400); - }); - - test("Fails with 'publishPackageExists' when existing package is passed", async () => { - tmpMock = authMock({ - ok: true, - content: { - token: "valid-token", - id: 9999, - node_id: "post-pkg-publish-test-user-node-id", - username: "post-pkg-publish-test-user", - avatar: "https://roadtonowhere.com", - }, - }); - - const res = await request(app) - .post("/api/packages") - .query({ repository: "pulsar-edit/language-pon" }) - .set("Authorization", "valid-token"); - expect(res.body.message).toEqual(msg.publishPackageExists); - expect(res).toHaveHTTPCode(409); - }); - - test.todo("Tests that actually modify data"); -}); - -describe("POST /api/packages/:packageName/versions", () => { - beforeEach(() => { - tmpMock = authMock({ - ok: false, - short: "Bad Auth", - content: "Bad Auth Mock Return for Dev user", - }); - }); - - afterEach(() => { - tmpMock.mockClear(); - }); - - test("Returns Bad Auth appropriately with Bad Package", async () => { - const res = await request(app).post( - "/api/packages/language-golang/versions" - ); - expect(res).toHaveHTTPCode(401); - expect(res.body.message).toEqual(msg.badAuth); - }); - - test.todo("Write all tests on this endpoint"); -}); - -describe("POST /api/packages/:packageName/star", () => { - afterEach(() => { - tmpMock.mockClear(); - }); - - test("Returns 401 with No Auth", async () => { - tmpMock = authMock({ - ok: false, - short: "Bad Auth", - content: "Bad Auth Mock Return for Dev User", - }); - - const res = await request(app).post("/api/packages/language-gfm/star"); - expect(res).toHaveHTTPCode(401); - }); - - test("Returns Bad Auth Msg with No Auth", async () => { - const res = await request(app).post("/api/packages/langauge-gfm/star"); - expect(res.body.message).toEqual(msg.badAuth); - }); - test("Returns 401 with Bad Auth", async () => { - tmpMock = authMock({ - ok: false, - short: "Bad Auth", - content: "Bad Auth Mock Return for Dev User", - }); - - const res = await request(app) - .post("/api/packages/language-gfm/star") - .set("Authorization", "invalid"); - expect(res).toHaveHTTPCode(401); - }); - test("Returns Bad Auth Msg with Bad Auth", async () => { - tmpMock = authMock({ - ok: false, - short: "Bad Auth", - content: "Bad Auth Mock Return for Dev User", - }); - - const res = await request(app) - .post("/api/packages/language-gfm/star") - .set("Authorization", "invalid"); - expect(res.body.message).toEqual(msg.badAuth); - }); - test("Returns not found with bad package", async () => { - tmpMock = authMock({ - ok: true, - content: { - token: "valid-token", - }, - }); - - const res = await request(app) - .post("/api/packages/no-exist/star") - .set("Authorization", "valid-token"); - - expect(res).toHaveHTTPCode(404); - expect(res.body.message).toEqual(msg.notFound); - }); - test("Returns proper data on Success", async () => { - const prev = await request(app).get("/api/packages/language-gfm"); - - tmpMock = authMock({ - ok: true, - content: { - token: "valid-token", - id: 999, - node_id: "post-star-test-user-node-id", - username: "post-star-test-user", - avatar: "https://roadtonowhere.com", - }, - }); - - const res = await request(app) - .post("/api/packages/language-gfm/star") - .set("Authorization", "valid-token"); - - tmpMock.mockClear(); - - tmpMock = authMock({ - ok: true, - content: { - token: "valid-token", - id: 999, - node_id: "post-star-test-user-node-id", - username: "post-star-test-user", - avatar: "https://roadtonowhere.com", - }, - }); - - const dup = await request(app) - .post("/api/packages/language-gfm/star") - .set("Authorization", "valid-token"); - // We are preforming multiple checks in the single check, - // because we want to test a star action when the package is already starred. - - // DESCRIBE: Returns Success Status Code - expect(res).toHaveHTTPCode(200); - // DESCRIBE: Returns same Package - expect(res.body.name).toEqual("language-gfm"); - // DESCRIBE: Properly Increases Star Count - expect(parseInt(res.body.stargazers_count, 10)).toEqual( - parseInt(prev.body.stargazers_count, 10) + 1 - ); - // DESCRIBE: A duplicate Request Returns Success Status Code - expect(dup).toHaveHTTPCode(200); - // DESCRIBE: A duplicate Request keeps the star, but does not increase the count - expect(parseInt(res.body.stargazers_count, 10)).toEqual( - parseInt(dup.body.stargazers_count, 10) - ); - }); -}); - -describe("POST /api/packages/:packageName/versions/:versionName/events/uninstall", () => { - // This endpoint is now being deprecated, so we will remove tests - // for handling any kind of actual functionality. - // Instead ensuring this returns as success to users are unaffected. - test.todo( - "This endpoint is deprecated, once it's fully removed, these tests should be too." - ); - - test("Returns 200 with Valid Package, Bad Version", async () => { - const res = await request(app) - .post("/api/packages/language-css/versions/1.0.0/events/uninstall") - .set("Authorization", "valid-token"); - expect(res).toHaveHTTPCode(200); - // Please note on Atom.io this would result in a 404. But the Pulsar Backend intentionally ignores the `version` - // of the query. This is due to changes in the database structure. - }); - test("Returns Json {ok: true } with Valid Package, Bad Version", async () => { - const res = await request(app) - .post("/api/packages/language-css/versions/1.0.0/events/uninstall") - .set("Authorization", "valid-token"); - expect(res.body.ok).toBeTruthy(); - // Please note on Atom.io this would result in a 404. But the Pulsar Backend intentionally ignores the `version` - // of the query. This is due to changes in the database structure. - }); - test("Returns 200 on Success", async () => { - const res = await request(app) - .post("/api/packages/language-css/versions/0.45.7/events/uninstall") - .set("Authorization", "valid-token"); - expect(res).toHaveHTTPCode(200); - }); - test("Returns Json { ok: true } on Success", async () => { - const res = await request(app) - .post("/api/packages/language-css/versions/0.45.7/events/uninstall") - .set("Authorization", "valid-token"); - expect(res.body.ok).toBeTruthy(); - }); - test("After deprecating endpoint, ensure the endpoint has no effect", async () => { - const orig = await request(app).get("/api/packages/language-css"); - const res = await request(app) - .post("/api/packages/language-css/versions/0.45.7/events/uninstall") - .set("Authorization", "valid-token"); - const after = await request(app).get("/api/packages/language-css"); - expect(parseInt(orig.body.downloads, 10)).toEqual( - parseInt(after.body.downloads, 10) - ); - }); -}); diff --git a/tests/database/applyFeatures.test.js b/tests/database/applyFeatures.test.js index 65fc9b43..18545616 100644 --- a/tests/database/applyFeatures.test.js +++ b/tests/database/applyFeatures.test.js @@ -16,7 +16,7 @@ describe("Exits properly", () => { expect(res.content).toBe( "Unable to find the pointer of this-name-doesn't-exist" ); - expect(res.short).toBe("Not Found"); + expect(res.short).toBe("not_found"); }); }); diff --git a/test/database.integration.test.js b/tests/database/database.test.js similarity index 95% rename from test/database.integration.test.js rename to tests/database/database.test.js index c93f7d67..4820b7c9 100644 --- a/test/database.integration.test.js +++ b/tests/database/database.test.js @@ -1,3 +1,6 @@ +// This file has been moved directly from the old testing method. +// Likely should be updated at one point + // This is our secondary integration test. // Due to the difficulty in testing some aspects as full integration tests, // namely tests for publishing and updating packages (due to the varried responses expected by github) @@ -6,8 +9,8 @@ // Or at the very least that if there is a failure within these, it will not result in // bad data being entered into the database in production. -let database = require("../src/database.js"); -let utils = require("../src/utils.js"); +let database = require("../../src/database.js"); +let utils = require("../../src/utils.js"); afterAll(async () => { await database.shutdownSQL(); @@ -39,7 +42,7 @@ describe("insertNewPackageName", () => { "notARepo-Reborn" ); expect(obj.ok).toBeFalsy(); - expect(obj.short).toEqual("Not Found"); + expect(obj.short).toEqual("not_found"); }); test("Should return Success for valid package", async () => { const obj = await database.insertNewPackageName( @@ -57,12 +60,12 @@ describe("getPackageByName", () => { test("Should return Server Error for Package that doesn't exist", async () => { const obj = await database.getPackageByName("language-golang"); expect(obj.ok).toBeFalsy(); - expect(obj.short).toEqual("Not Found"); + expect(obj.short).toEqual("not_found"); }); test("Should return Server Error for Package that doesn't exist, even with User", async () => { const obj = await database.getPackageByName("language-golang", true); expect(obj.ok).toBeFalsy(); - expect(obj.short).toEqual("Not Found"); + expect(obj.short).toEqual("not_found"); }); }); @@ -398,7 +401,7 @@ describe("Package Lifecycle Tests", () => { // === Can we get our now deleted package? const ghostPack = await database.getPackageByName(NEW_NAME); expect(ghostPack.ok).toBeFalsy(); - expect(ghostPack.short).toEqual("Not Found"); + expect(ghostPack.short).toEqual("not_found"); // === Is the name of the deleted package available? const deletedNameAvailable = await database.packageNameAvailability( @@ -412,7 +415,7 @@ describe("Package Lifecycle Tests", () => { // === Can we get our Non-Existant User? const noExistUser = await database.getUserByNodeID(user.userObj.node_id); expect(noExistUser.ok).toBeFalsy(); - expect(noExistUser.short).toEqual("Not Found"); + expect(noExistUser.short).toEqual("not_found"); // === Can we create our User? const createUser = await database.insertNewUser( @@ -469,10 +472,29 @@ describe("Package Lifecycle Tests", () => { expect(getFakeStars.content.length).toEqual(0); // === Can we star a package with our User? + // (After of course first creating the package to star) + await database.insertNewPackage({ + name: "language-css", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, + creation_method: "Test Package", + releases: { latest: "1.0.0" }, + readme: "This is a readme!", + metadata: { name: "language-css" }, + versions: { + "1.0.0": { + dist: { tarball: "download-url", sha: "1234" }, + name: "language-css" + } + } + }); const starPack = await database.updateIncrementStar( getUserID.content, "language-css" ); + expect(starPack.ok).toBeTruthy(); expect(starPack.content).toEqual("Package Successfully Starred"); @@ -513,6 +535,9 @@ describe("Package Lifecycle Tests", () => { // === Can we remove our User? // TODO: Currently there is no way to delete a user account. // There is no supported endpoint for this, but is something that should be implemented. + + // Lets cleanup by deleting the package we made + await database.removePackageByName("language-css", true); }); }); diff --git a/test/fixtures/git.createPackage_returns/valid_multi_version.js b/tests/database/fixtures/git.createPackage_returns/valid_multi_version.js similarity index 100% rename from test/fixtures/git.createPackage_returns/valid_multi_version.js rename to tests/database/fixtures/git.createPackage_returns/valid_multi_version.js diff --git a/test/fixtures/git.createPackage_returns/valid_one_version.js b/tests/database/fixtures/git.createPackage_returns/valid_one_version.js similarity index 100% rename from test/fixtures/git.createPackage_returns/valid_one_version.js rename to tests/database/fixtures/git.createPackage_returns/valid_one_version.js diff --git a/test/fixtures/lifetime/package-a.js b/tests/database/fixtures/lifetime/package-a.js similarity index 100% rename from test/fixtures/lifetime/package-a.js rename to tests/database/fixtures/lifetime/package-a.js diff --git a/test/fixtures/lifetime/user-a.js b/tests/database/fixtures/lifetime/user-a.js similarity index 100% rename from test/fixtures/lifetime/user-a.js rename to tests/database/fixtures/lifetime/user-a.js diff --git a/tests/http/postPackages.test.js b/tests/http/postPackages.test.js new file mode 100644 index 00000000..2a0b1191 --- /dev/null +++ b/tests/http/postPackages.test.js @@ -0,0 +1,122 @@ +const endpoint = require("../../src/controllers/postPackages.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("POST /api/packages Behaves as expected", () => { + + test("Fails with 'unauthorized' when bad token is passed", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: false, + short: "unauthorized", + content: "Bad Auth Mock Return" + }; + }; + + const sso = await endpoint.logic({}, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("unauthorized"); + }); + + test("Fails with 'bad repo' when no repo is passed", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + id: 9999, + node_id: "post-pkg-publish-test-user-node-id", + username: "post-pkg-publish-test-user", + avatar: "https://roadtonowhere.com" + } + }; + }; + + const sso = await endpoint.logic({ + repository: "", + auth: "valid-token" + }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.short).toBe("bad_repo"); + }); + + test("Fails when a bad repo format is passed", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + id: 9999, + node_id: "post-pkg-publish-test-user-node-id", + username: "post-pkg-publish-test-user", + avatar: "https://roadtonowhere.com" + } + }; + }; + + const sso = await endpoint.logic({ + repository: "bad-format", + auth: "valid-token" + }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.short).toBe("bad_repo"); + }); + + test("Fails if the package already exists", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + id: 9999, + node_id: "post-pkg-publish-test-user-node-id", + username: "post-pkg-publish-test-user", + avatar: "https://roadtonowhere.com" + } + }; + }; + + await database.insertNewPackage({ + name: "post-packages-test-package", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, + creation_method: "Test Package", + releases: { latest: "1.1.0" }, + readme: "This is a readme!", + metadata: { name: "post-packages-test-package" }, + versions: { + "1.1.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "post-packages-test-package" + } + } + }); + + const sso = await endpoint.logic({ + repository: "confused-Techie/post-packages-test-package", + auth: "valid-token" + }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.short).toBe("package_exists"); + + await database.removePackageByName("post-packages-test-package", true); + }); + + // This is the fully migrated test set that we previously had + // But with the changes made we should now be able to properly test + // everything here. + test.todo("post Packages test that actually modify data"); +}); diff --git a/tests/http/postPackagesPackageNameStar.test.js b/tests/http/postPackagesPackageNameStar.test.js new file mode 100644 index 00000000..6401cdc6 --- /dev/null +++ b/tests/http/postPackagesPackageNameStar.test.js @@ -0,0 +1,100 @@ +const endpoint = require("../../src/controllers/postPackagesPackageNameStar.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("POST /api/packages/:packageName/star", () => { + test("Fails with bad auth, with no auth", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: false, + short: "unauthorized", + content: "Bad Auth Mock Return" + }; + }; + + const sso = await endpoint.logic({}, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("unauthorized"); + }); + + test("Fails with not found with bad package", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + id: 99999, + node_id: "post-pkg-star-test-user-node-id", + username: "post-pkg-star-test-user-node-id", + avatar: "https://roadtonowhere.com" + } + }; + }; + + const sso = await endpoint.logic({ + auth: "valid-token", + packageName: "no-exist" + }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("not_found"); + }); + + test("Returns package and updates star count on success", async () => { + await database.insertNewPackage({ + name: "post-packages-star-test", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, + creation_method: "Test Package", + releases: { + latest: "1.0.0" + }, + readme: "This is a readme!", + metadata: { name: "post-packages-star-test" }, + versions: { + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "post-packages-star-test" + } + } + }); + + let addUser = await database.insertNewUser("post-pkg-star-test-user-node-id", "post-pkg-star-test-user-node-id", "https://roadtonowhere.com"); + + expect(addUser.ok).toBe(true); + + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + // The user data, specifically the ID must match for starring to work + id: addUser.content.id, + node_id: addUser.content.node_id, + username: addUser.content.username, + avatar: addUser.content.avatar + } + } + }; + + const sso = await endpoint.logic({ + auth: "valid-token", + packageName: "post-packages-star-test" + }, localContext); + + expect(sso.ok).toBe(true); + expect(sso.content.name).toBe("post-packages-star-test"); + expect(sso.content.stargazers_count).toBe("1"); + expect(sso).toMatchEndpointSuccessObject(endpoint); + await database.removePackageByName("post-packages-star-test", true); + }); +}); diff --git a/tests/http/postPackagesPackageNameVersions.test.js b/tests/http/postPackagesPackageNameVersions.test.js new file mode 100644 index 00000000..6b0624ec --- /dev/null +++ b/tests/http/postPackagesPackageNameVersions.test.js @@ -0,0 +1,24 @@ +const endpoint = require("../../src/controllers/postPackagesPackageNameVersions.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("POST /api/packages/:packageName/versions", () => { + test("Fails with bad auth if given a bad auth", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: false, + short: "unauthorized", + context: "Bad Auth Mock Return" + }; + }; + + const sso = await endpoint.logic({}, localContext); + + expect(sso.ok).toBe(false); + expect(sso.short).toBe("unauthorized"); + }); + + // This is where the original tests ended here + test.todo("Write the tests that are now possible"); +}); diff --git a/test/PackageObject.unit.test.js b/tests/unit/PackageObject.test.js similarity index 95% rename from test/PackageObject.unit.test.js rename to tests/unit/PackageObject.test.js index 60b77a1b..3ec1f1d0 100644 --- a/test/PackageObject.unit.test.js +++ b/tests/unit/PackageObject.test.js @@ -1,4 +1,4 @@ -const PackageObject = require("../src/PackageObject.js"); +const PackageObject = require("../../src/PackageObject.js"); describe("Building Objects with PackageObject Return as Expected", () => { test("Formal Usage", () => { diff --git a/test/ServerStatusObject.unit.test.js b/tests/unit/ServerStatusObject.test.js similarity index 94% rename from test/ServerStatusObject.unit.test.js rename to tests/unit/ServerStatusObject.test.js index f409dff7..fd538a0a 100644 --- a/test/ServerStatusObject.unit.test.js +++ b/tests/unit/ServerStatusObject.test.js @@ -1,4 +1,4 @@ -const ServerStatus = require("../src/ServerStatusObject.js"); +const ServerStatus = require("../../src/ServerStatusObject.js"); const Joi = require("joi"); describe("Building Objects with ServerStatus Return as Expected", () => { diff --git a/test/github.vcs.test.js b/tests/vcs/github.vcs.test.js similarity index 96% rename from test/github.vcs.test.js rename to tests/vcs/github.vcs.test.js index 7916e96f..fc4d3c8f 100644 --- a/test/github.vcs.test.js +++ b/tests/vcs/github.vcs.test.js @@ -1,5 +1,5 @@ -const GitHub = require("../src/vcs_providers/github.js"); -const httpMock = require("./httpMock.helper.jest.js"); +const GitHub = require("../../src/vcs_providers/github.js"); +const httpMock = require("../helpers/httpMock.helper.jest.js"); const webRequestMockHelper = (data) => { const tmpMock = jest diff --git a/test/vcs.unit.test.js b/tests/vcs/vcs.unit.test.js similarity index 98% rename from test/vcs.unit.test.js rename to tests/vcs/vcs.unit.test.js index 5c25006d..2a8a13c0 100644 --- a/test/vcs.unit.test.js +++ b/tests/vcs/vcs.unit.test.js @@ -1,4 +1,4 @@ -const vcs = require("../src/vcs.js"); +const vcs = require("../../src/vcs.js"); describe("determineProvider Returns as expected", () => { test("Returns null when no input is passed", () => { diff --git a/test/vcs.vcs.test.js b/tests/vcs/vcs.vcs.test.js similarity index 99% rename from test/vcs.vcs.test.js rename to tests/vcs/vcs.vcs.test.js index 54e6cc61..d0bbb473 100644 --- a/test/vcs.vcs.test.js +++ b/tests/vcs/vcs.vcs.test.js @@ -1,6 +1,6 @@ -const httpMock = require("./httpMock.helper.jest.js"); +const httpMock = require("../helpers/httpMock.helper.jest.js"); -const vcs = require("../src/vcs.js"); +const vcs = require("../../src/vcs.js"); let http_cache = { pack1: {}, // pack1 will be used for newPackageData tests From fce91b7d60ab56ecf1464d17c34eabcc9e94d283 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 22 Oct 2023 20:53:28 -0700 Subject: [PATCH 47/53] Properly ignore our new tests folder --- codeql-config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codeql-config.yml b/codeql-config.yml index deb8c738..dfec5e44 100644 --- a/codeql-config.yml +++ b/codeql-config.yml @@ -6,4 +6,5 @@ queries: paths-ignore: - ./test + - ./tests # https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors From 54b1f1c9280efa641eabd96b0220dfcbdacd367c Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Mon, 23 Oct 2023 17:08:46 -0700 Subject: [PATCH 48/53] Address code scanning alerts --- src/models/sso.js | 2 +- src/setupEndpoints.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/sso.js b/src/models/sso.js index 69d7dcd2..6f05b1e0 100644 --- a/src/models/sso.js +++ b/src/models/sso.js @@ -77,7 +77,7 @@ class SSO { addShort(enumValue) { if ( - !this.short?.length > 0 && + this.short?.length <= 0 && typeof enumValue === "string" && validEnums.includes(enumValue) ) { diff --git a/src/setupEndpoints.js b/src/setupEndpoints.js index 81908df6..c9a43a47 100644 --- a/src/setupEndpoints.js +++ b/src/setupEndpoints.js @@ -58,7 +58,7 @@ const endpointHandler = async function(node, req, res) { let obj; if (node.endpoint.endpointKind === "raw") { - obj = await node.logic(req, res, context); + await node.logic(req, res, context); // If it's a raw endpoint, they must handle all other steps manually return; From cc952211bb000d3abae342edcb677f2ffee39bed Mon Sep 17 00:00:00 2001 From: confused_techie Date: Wed, 8 Nov 2023 11:01:54 -0800 Subject: [PATCH 49/53] Add `DELETE /api/packages/:packageName` tests --- tests/http/deletePackagesPackageName.test.js | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tests/http/deletePackagesPackageName.test.js diff --git a/tests/http/deletePackagesPackageName.test.js b/tests/http/deletePackagesPackageName.test.js new file mode 100644 index 00000000..18aa8197 --- /dev/null +++ b/tests/http/deletePackagesPackageName.test.js @@ -0,0 +1,118 @@ +const endpoint = require("../../src/controllers/deletePackagesPackageName.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("DELETE /api/packages/:packageName", () => { + test("Fails with bad auth", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: false, + short: "unauthorized", + content: "Bad Auth Mock Return" + }; + }; + + const sso = await endpoint.logic({}, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("unauthorized"); + }); + test("Fails with not found with bad package", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + id: 9999, + node_id: "dlt-pkg-test-user-node-id", + username: "dlt-pkg-test-user-node-id", + avatar: "https://roadtonowhere.com" + } + }; + }; + + const sso = await endpoint.logic({ + auth: "valid-token", + packageName: "no-exist" + }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("not_found"); + }); + + test("Successfully deletes a package", async () => { + await database.insertNewPackage({ + name: "dlt-pkg-by-name-test", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, + creation_method: "Test Package", + releases: { + latest: "1.0.0" + }, + readme: "This is a readme!", + metadata: { name: "dlt-pkg-by-name-test" }, + versions: { + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "dlt-pkg-by-name-test" + } + } + }); + + let addUser = await database.insertNewUser( + "dlt-pkg-test-user-node-id", + "dlt-pkg-test-user-node-id", + "https://roadtonowhere.com" + ); + + expect(addUser.ok).toBe(true); + + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + // The user data must match whats in the db + id: addUser.content.id, + node_id: addUser.content.node_id, + username: addUser.content.username, + avatar: addUser.content.avatar + } + }; + }; + + localContext.vcs.ownership = () => { + return { + ok: true, + content: "admin" + }; + }; + + const sso = await endpoint.logic({ + auth: "valid-token", + packageName: "dlt-pkg-by-name-test" + }, localContext); + + expect(sso.ok).toBe(true); + expect(sso.content).toBe(false); + + let doesPackageStillExist = await database.getPackageByName("dlt-pkg-by-name-test"); + + expect(doesPackageStillExist.ok).toBe(false); + expect(doesPackageStillExist.short).toBe("not_found"); + + let isPackageNameAvailable = await database.packageNameAvailability("dlt-pkg-by-name-test"); + + expect(isPackageNameAvailable.ok).toBe(false); + expect(isPackageNameAvailable.short).toBe("not_found"); + expect(isPackageNameAvailable.content).toBe("dlt-pkg-by-name-test is not available to be used for a new package."); + }); +}); From 5248970b5c589f3db01bc0abb3ce15b1556e99f0 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 8 Nov 2023 20:55:31 -0800 Subject: [PATCH 50/53] Add in some more critical tests --- ...agesPackageNameVersionsVersionName.test.js | 129 ++++++++++++++++++ tests/http/postPackages.test.js | 89 +++++++++++- 2 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 tests/http/deletePackagesPackageNameVersionsVersionName.test.js diff --git a/tests/http/deletePackagesPackageNameVersionsVersionName.test.js b/tests/http/deletePackagesPackageNameVersionsVersionName.test.js new file mode 100644 index 00000000..6ef270a8 --- /dev/null +++ b/tests/http/deletePackagesPackageNameVersionsVersionName.test.js @@ -0,0 +1,129 @@ +const endpoint = require("../../src/controllers/deletePackagesPackageNameVersionsVersionName.js"); +const database = require("../../src/database.js"); +const context = require("../../src/context.js"); + +describe("DELETE /api/packages/:packageName/versions/:versionName", () => { + test("Fails with bad auth", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: false, + short: "unauthorized", + content: "Bad Auth Mock Return" + }; + }; + + const sso = await endpoint.logic({ versionName: "" }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("unauthorized"); + }); + test("Fails with not found with bad package", async () => { + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + id: 9999, + node_id: "dlt-pkg-ver-user-node-id", + username: "dlt-pkg-ver-user-node-id", + avatar: "https://roadtonowhere.com" + } + }; + }; + + const sso = await endpoint.logic({ + auth: "valid-token", + packageName: "no-exist", + versionName: "1.0.0" + }, localContext); + + expect(sso.ok).toBe(false); + expect(sso.content.short).toBe("not_found"); + }); + test("Successfully deletes a package version", async () => { + await database.insertNewPackage({ + name: "dlt-pkg-ver-by-name-test", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, + creation_method: "Test Package", + releases: { + latest: "1.0.1" + }, + readme: "This is a readme!", + metadata: { name: "dlt-pkg-ver-by-name-test" }, + versions: { + "1.0.1": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "dlt-pkg-ver-by-name-test" + }, + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "dlt-pkg-ver-by-name-test" + } + } + }); + + let addUser = await database.insertNewUser( + "dlt-pkg-ver-test-user-node-id", + "dlt-pkg-ver-test-user-node-id", + "https://roadotonowhere.com" + ); + + expect(addUser.ok).toBe(true); + + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + // The user data must match whats in the db + id: addUser.content.id, + node_id: addUser.content.node_id, + username: addUser.content.username, + avatar: addUser.content.avatar + } + }; + }; + localContext.vcs.ownership = () => { + return { + ok: true, + content: "admin" + }; + }; + + const sso = await endpoint.logic({ + auth: "valid-token", + packageName: "dlt-pkg-ver-by-name-test", + versionName: "1.0.1" + }, localContext); + + expect(sso.ok).toBe(true); + expect(sso.content).toBe(false); + + let currentPackageData = await database.getPackageByName("dlt-pkg-ver-by-name-test"); + + expect(currentPackageData.ok).toBe(true); + + currentPackageData = await context.utils.constructPackageObjectFull(currentPackageData.content); + + expect(currentPackageData.name).toBe("dlt-pkg-ver-by-name-test"); + // Does it modify the latest package version + expect(currentPackageData.releases.latest).toBe("1.0.0"); + expect(currentPackageData.versions["1.0.0"]).toBeTruthy(); + expect(currentPackageData.versions["1.0.1"]).toBeFalsy(); + + // cleanup + await database.removePackageByName("dlt-pkg-ver-by-name-test", true); + }); +}); diff --git a/tests/http/postPackages.test.js b/tests/http/postPackages.test.js index 2a0b1191..771b3e94 100644 --- a/tests/http/postPackages.test.js +++ b/tests/http/postPackages.test.js @@ -115,8 +115,89 @@ describe("POST /api/packages Behaves as expected", () => { await database.removePackageByName("post-packages-test-package", true); }); - // This is the fully migrated test set that we previously had - // But with the changes made we should now be able to properly test - // everything here. - test.todo("post Packages test that actually modify data"); + test("Successfully publishes a new package", async () => { + let addUser = await database.insertNewUser( + "post-pkg-test-user-node-id", + "post-pkg-test-user-node-id", + "https://roadtonowhere.com" + ); + + expect(addUser.ok).toBe(true); + + const localContext = context; + localContext.auth.verifyAuth = () => { + return { + ok: true, + content: { + token: "valid-token", + // The user data must match whats in the db + id: addUser.content.id, + node_id: addUser.content.node_id, + username: addUser.content.username, + avatar: addUser.content.avatar + } + }; + }; + localContext.vcs.ownership = () => { + return { + ok: true, + content: "admin" + }; + }; + localContext.vcs.newPackageData = () => { + return { + ok: true, + content: { + name: "post-pkg-test-pkg-name", + repository: { + url: "https://github.com/confused-Techie/package-backend", + type: "git" + }, + downloads: 0, + stargazers_count: 0, + creation_method: "Test Package", + releases: { + latest: "1.0.0" + }, + readme: "This is a readme!", + metadata: { name: "post-pkg-test-pkg-name" }, + versions: { + "1.0.0": { + dist: { + tarball: "download-url", + sha: "1234" + }, + name: "post-pkg-test-pkg-name" + } + } + } + }; + }; + + const sso = await endpoint.logic({ + repository: "confused-Techie/post-pkg-test-pkg-name", + auth: "valid-token" + }, localContext); + + expect(sso.ok).toBe(true); + expect(sso.content.name).toBe("post-pkg-test-pkg-name"); + expect(sso.content.releases.latest).toBe("1.0.0"); + + // Can we get the package by a specific version + let packByVer = await database.getPackageVersionByNameAndVersion( + "post-pkg-test-pkg-name", + "1.0.0" + ); + + expect(packByVer.ok).toBe(true); + + packByVer = await context.utils.constructPackageObjectJSON(packByVer.content); + + expect(packByVer.name).toBe("post-pkg-test-pkg-name"); + expect(packByVer.dist.tarball).toContain("/api/packages/post-pkg-test-pkg-name/versions/1.0.0"); + + // Cleanup + await database.removePackageByName("post-pkg-test-pkg-name", true); + }); + }); From a5d247a6c5e8c657fe0ca1ae28e8d39176dd3f22 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 8 Nov 2023 21:29:11 -0800 Subject: [PATCH 51/53] Major repo cleanup --- .compactignore | 2 + .gcloudignore | 4 +- docs/reference/API_Definition.md | 4 +- jest.OUTDATED.config.js | 54 - package.json | 4 +- {src => scripts/deprecated}/debug_utils.js | 0 {test => scripts/deprecated/test}/README.md | 0 ...elete.packages.handler.integration.test.js | 0 .../get.packages.handler.integration.test.js | 0 .../handlers/common_handler/auth_fail.test.js | 0 .../common_handler/bad_package_json.test.js | 0 .../common_handler/bad_repo_json.test.js | 0 .../handlers/common_handler/express_fakes.js | 0 .../handle_detailed_error.test.js | 0 .../common_handler/missing_auth_json.test.js | 0 .../handlers/common_handler/not_found.test.js | 0 .../common_handler/not_supported.test.js | 0 .../common_handler/package_exists.test.js | 0 .../common_handler/server_error.test.js | 0 .../site_wide_not_found.test.js | 0 .../get_package_handler/getPackages.test.js | 0 .../getPackagesSearch.test.js | 0 .../post_package_handler/postPackages.test.js | 0 .../test}/oauth.handler.integration.test.js | 0 .../test}/themes.handler.integration.test.js | 0 .../test}/users.handler.integration.test.js | 0 {src => scripts}/parameters/auth.js | 0 {src => scripts}/parameters/direction.js | 0 {src => scripts}/parameters/engine.js | 0 {src => scripts}/parameters/fileExtension.js | 0 {src => scripts}/parameters/login.js | 0 {src => scripts}/parameters/packageName.js | 0 {src => scripts}/parameters/page.js | 0 {src => scripts}/parameters/query.js | 0 {src => scripts}/parameters/service.js | 0 {src => scripts}/parameters/serviceType.js | 0 {src => scripts}/parameters/serviceVersion.js | 0 {src => scripts}/parameters/sort.js | 0 {src => scripts}/parameters/versionName.js | 0 src/context.js | 1 - src/dev_server.js | 2 +- src/handlers/common_handler.js | 290 --- src/handlers/delete_package_handler.js | 204 -- src/handlers/get_package_handler.js | 458 ----- src/handlers/oauth_handler.js | 267 --- src/handlers/package_handler.js | 25 - src/handlers/post_package_handler.js | 445 ----- src/handlers/star_handler.js | 82 - src/handlers/theme_handler.js | 187 -- src/handlers/update_handler.js | 26 - src/handlers/user_handler.js | 162 -- src/main.js | 1641 ----------------- tests/unit/debug_utils.test.js | 31 - 53 files changed, 9 insertions(+), 3880 deletions(-) delete mode 100644 jest.OUTDATED.config.js rename {src => scripts/deprecated}/debug_utils.js (100%) rename {test => scripts/deprecated/test}/README.md (100%) rename {test => scripts/deprecated/test}/delete.packages.handler.integration.test.js (100%) rename {test => scripts/deprecated/test}/get.packages.handler.integration.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/auth_fail.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/bad_package_json.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/bad_repo_json.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/express_fakes.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/handle_detailed_error.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/missing_auth_json.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/not_found.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/not_supported.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/package_exists.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/server_error.test.js (100%) rename {test => scripts/deprecated/test}/handlers/common_handler/site_wide_not_found.test.js (100%) rename {test => scripts/deprecated/test}/handlers/get_package_handler/getPackages.test.js (100%) rename {test => scripts/deprecated/test}/handlers/get_package_handler/getPackagesSearch.test.js (100%) rename {test => scripts/deprecated/test}/handlers/post_package_handler/postPackages.test.js (100%) rename {test => scripts/deprecated/test}/oauth.handler.integration.test.js (100%) rename {test => scripts/deprecated/test}/themes.handler.integration.test.js (100%) rename {test => scripts/deprecated/test}/users.handler.integration.test.js (100%) rename {src => scripts}/parameters/auth.js (100%) rename {src => scripts}/parameters/direction.js (100%) rename {src => scripts}/parameters/engine.js (100%) rename {src => scripts}/parameters/fileExtension.js (100%) rename {src => scripts}/parameters/login.js (100%) rename {src => scripts}/parameters/packageName.js (100%) rename {src => scripts}/parameters/page.js (100%) rename {src => scripts}/parameters/query.js (100%) rename {src => scripts}/parameters/service.js (100%) rename {src => scripts}/parameters/serviceType.js (100%) rename {src => scripts}/parameters/serviceVersion.js (100%) rename {src => scripts}/parameters/sort.js (100%) rename {src => scripts}/parameters/versionName.js (100%) delete mode 100644 src/handlers/common_handler.js delete mode 100644 src/handlers/delete_package_handler.js delete mode 100644 src/handlers/get_package_handler.js delete mode 100644 src/handlers/oauth_handler.js delete mode 100644 src/handlers/package_handler.js delete mode 100644 src/handlers/post_package_handler.js delete mode 100644 src/handlers/star_handler.js delete mode 100644 src/handlers/theme_handler.js delete mode 100644 src/handlers/update_handler.js delete mode 100644 src/handlers/user_handler.js delete mode 100644 src/main.js delete mode 100644 tests/unit/debug_utils.test.js diff --git a/.compactignore b/.compactignore index 062dbc8b..048a2ad6 100644 --- a/.compactignore +++ b/.compactignore @@ -14,6 +14,8 @@ coverage/ > gcloudignore .gcloudignore test/ +tests/ +scripts/ docs/reference docs/resources/ docs/build.md diff --git a/.gcloudignore b/.gcloudignore index f2ef0b89..1fedd7fd 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -5,13 +5,13 @@ node_modules/ data/ .gcloudignore test/ +tests/ +scripts/ docs/reference docs/resources/ docs/build.md docs/host_your_own.md docs/writingIntegrationTests.md -scripts/ -health-check-output.json # Just in case it's output and not deleted # Git Tooling / NPM Tooling .git/ .github/ diff --git a/docs/reference/API_Definition.md b/docs/reference/API_Definition.md index eb784f12..0014589b 100644 --- a/docs/reference/API_Definition.md +++ b/docs/reference/API_Definition.md @@ -1,3 +1,5 @@ +# WARNING This file is deprecated. And will soon be removed. Leaving our swagger instance to be the complete standard in the Pulsar backend API documentation. + # **[GET]** / A non-essential endpoint, returning a status message, and the server version. @@ -25,7 +27,7 @@ Parameters: --- -* page _(optional)_ `[integer]` | Location: `query` | Defaults: `1` +* page _(optional)_ `[integer]` | Location: `query` | Defaults: `1` - Indicate the page number to return. diff --git a/jest.OUTDATED.config.js b/jest.OUTDATED.config.js deleted file mode 100644 index 6f29be37..00000000 --- a/jest.OUTDATED.config.js +++ /dev/null @@ -1,54 +0,0 @@ -const config = { - setupFilesAfterEnv: ["/test/global.setup.jest.js"], - verbose: true, - collectCoverage: true, - coverageReporters: ["text", "clover"], - coveragePathIgnorePatterns: [ - "/src/tests_integration/fixtures/**", - "/test/fixtures/**", - "/node_modules/**", - ], - projects: [ - { - displayName: "Integration-Tests", - globalSetup: "/node_modules/@databases/pg-test/jest/globalSetup", - globalTeardown: - "/node_modules/@databases/pg-test/jest/globalTeardown", - setupFilesAfterEnv: [ - "/test/handlers.setup.jest.js", - "/test/global.setup.jest.js", - ], - testMatch: [ - "/test/*.integration.test.js", - "/test/database/**/**.js", - ], - }, - { - displayName: "Unit-Tests", - setupFilesAfterEnv: ["/test/global.setup.jest.js"], - testMatch: [ - "/test/*.unit.test.js", - "/test/handlers/**/**.test.js", - "/test/controllers/**.test.js" - ], - }, - { - displayName: "VCS-Tests", - setupFilesAfterEnv: ["/test/global.setup.jest.js"], - testMatch: ["/test/*.vcs.test.js"], - }, - { - displayName: "Handler-Tests", - globalSetup: "/node_modules/@databases/pg-test/jest/globalSetup", - globalTeardown: - "/node_modules/@databases/pg-test/jest/globalTeardown", - setupFilesAfterEnv: [ - "/test/handlers.setup.jest.js", - "/test/global.setup.jest.js", - ], - testMatch: ["/test/*.handler.integration.test.js"], - }, - ], -}; - -module.exports = config; diff --git a/package.json b/package.json index 89093698..c9e27c76 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,10 @@ }, "scripts": { "start": "node ./src/server.js", + "test": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests Unit-Tests --runInBand", "test:unit": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Unit-Tests", "test:integration": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests", "start:dev": "cross-env PULSAR_STATUS=dev node ./src/dev_server.js", - "test": "cross-env NODE_ENV=test PULSAR_STATUS=dev jest --selectProjects Integration-Tests Unit-Tests --runInBand", - "test:vcs": "cross-env NODE_ENV=test PULSAR_STATUS=dev MOCK_GH=false jest --selectProjects VCS-Tests", - "api-docs": "quick-webserver-docs -i ./src/main.js -o ./docs/reference/API_Definition.md", "lint": "prettier --check -u -w .", "complex": "cr --newmi --config .complexrc .", "js-docs": "jsdoc2md -c ./jsdoc.conf.js ./src/*.js ./src/handlers/*.js ./docs/resources/jsdoc_typedef.js > ./docs/reference/Source_Documentation.md", diff --git a/src/debug_utils.js b/scripts/deprecated/debug_utils.js similarity index 100% rename from src/debug_utils.js rename to scripts/deprecated/debug_utils.js diff --git a/test/README.md b/scripts/deprecated/test/README.md similarity index 100% rename from test/README.md rename to scripts/deprecated/test/README.md diff --git a/test/delete.packages.handler.integration.test.js b/scripts/deprecated/test/delete.packages.handler.integration.test.js similarity index 100% rename from test/delete.packages.handler.integration.test.js rename to scripts/deprecated/test/delete.packages.handler.integration.test.js diff --git a/test/get.packages.handler.integration.test.js b/scripts/deprecated/test/get.packages.handler.integration.test.js similarity index 100% rename from test/get.packages.handler.integration.test.js rename to scripts/deprecated/test/get.packages.handler.integration.test.js diff --git a/test/handlers/common_handler/auth_fail.test.js b/scripts/deprecated/test/handlers/common_handler/auth_fail.test.js similarity index 100% rename from test/handlers/common_handler/auth_fail.test.js rename to scripts/deprecated/test/handlers/common_handler/auth_fail.test.js diff --git a/test/handlers/common_handler/bad_package_json.test.js b/scripts/deprecated/test/handlers/common_handler/bad_package_json.test.js similarity index 100% rename from test/handlers/common_handler/bad_package_json.test.js rename to scripts/deprecated/test/handlers/common_handler/bad_package_json.test.js diff --git a/test/handlers/common_handler/bad_repo_json.test.js b/scripts/deprecated/test/handlers/common_handler/bad_repo_json.test.js similarity index 100% rename from test/handlers/common_handler/bad_repo_json.test.js rename to scripts/deprecated/test/handlers/common_handler/bad_repo_json.test.js diff --git a/test/handlers/common_handler/express_fakes.js b/scripts/deprecated/test/handlers/common_handler/express_fakes.js similarity index 100% rename from test/handlers/common_handler/express_fakes.js rename to scripts/deprecated/test/handlers/common_handler/express_fakes.js diff --git a/test/handlers/common_handler/handle_detailed_error.test.js b/scripts/deprecated/test/handlers/common_handler/handle_detailed_error.test.js similarity index 100% rename from test/handlers/common_handler/handle_detailed_error.test.js rename to scripts/deprecated/test/handlers/common_handler/handle_detailed_error.test.js diff --git a/test/handlers/common_handler/missing_auth_json.test.js b/scripts/deprecated/test/handlers/common_handler/missing_auth_json.test.js similarity index 100% rename from test/handlers/common_handler/missing_auth_json.test.js rename to scripts/deprecated/test/handlers/common_handler/missing_auth_json.test.js diff --git a/test/handlers/common_handler/not_found.test.js b/scripts/deprecated/test/handlers/common_handler/not_found.test.js similarity index 100% rename from test/handlers/common_handler/not_found.test.js rename to scripts/deprecated/test/handlers/common_handler/not_found.test.js diff --git a/test/handlers/common_handler/not_supported.test.js b/scripts/deprecated/test/handlers/common_handler/not_supported.test.js similarity index 100% rename from test/handlers/common_handler/not_supported.test.js rename to scripts/deprecated/test/handlers/common_handler/not_supported.test.js diff --git a/test/handlers/common_handler/package_exists.test.js b/scripts/deprecated/test/handlers/common_handler/package_exists.test.js similarity index 100% rename from test/handlers/common_handler/package_exists.test.js rename to scripts/deprecated/test/handlers/common_handler/package_exists.test.js diff --git a/test/handlers/common_handler/server_error.test.js b/scripts/deprecated/test/handlers/common_handler/server_error.test.js similarity index 100% rename from test/handlers/common_handler/server_error.test.js rename to scripts/deprecated/test/handlers/common_handler/server_error.test.js diff --git a/test/handlers/common_handler/site_wide_not_found.test.js b/scripts/deprecated/test/handlers/common_handler/site_wide_not_found.test.js similarity index 100% rename from test/handlers/common_handler/site_wide_not_found.test.js rename to scripts/deprecated/test/handlers/common_handler/site_wide_not_found.test.js diff --git a/test/handlers/get_package_handler/getPackages.test.js b/scripts/deprecated/test/handlers/get_package_handler/getPackages.test.js similarity index 100% rename from test/handlers/get_package_handler/getPackages.test.js rename to scripts/deprecated/test/handlers/get_package_handler/getPackages.test.js diff --git a/test/handlers/get_package_handler/getPackagesSearch.test.js b/scripts/deprecated/test/handlers/get_package_handler/getPackagesSearch.test.js similarity index 100% rename from test/handlers/get_package_handler/getPackagesSearch.test.js rename to scripts/deprecated/test/handlers/get_package_handler/getPackagesSearch.test.js diff --git a/test/handlers/post_package_handler/postPackages.test.js b/scripts/deprecated/test/handlers/post_package_handler/postPackages.test.js similarity index 100% rename from test/handlers/post_package_handler/postPackages.test.js rename to scripts/deprecated/test/handlers/post_package_handler/postPackages.test.js diff --git a/test/oauth.handler.integration.test.js b/scripts/deprecated/test/oauth.handler.integration.test.js similarity index 100% rename from test/oauth.handler.integration.test.js rename to scripts/deprecated/test/oauth.handler.integration.test.js diff --git a/test/themes.handler.integration.test.js b/scripts/deprecated/test/themes.handler.integration.test.js similarity index 100% rename from test/themes.handler.integration.test.js rename to scripts/deprecated/test/themes.handler.integration.test.js diff --git a/test/users.handler.integration.test.js b/scripts/deprecated/test/users.handler.integration.test.js similarity index 100% rename from test/users.handler.integration.test.js rename to scripts/deprecated/test/users.handler.integration.test.js diff --git a/src/parameters/auth.js b/scripts/parameters/auth.js similarity index 100% rename from src/parameters/auth.js rename to scripts/parameters/auth.js diff --git a/src/parameters/direction.js b/scripts/parameters/direction.js similarity index 100% rename from src/parameters/direction.js rename to scripts/parameters/direction.js diff --git a/src/parameters/engine.js b/scripts/parameters/engine.js similarity index 100% rename from src/parameters/engine.js rename to scripts/parameters/engine.js diff --git a/src/parameters/fileExtension.js b/scripts/parameters/fileExtension.js similarity index 100% rename from src/parameters/fileExtension.js rename to scripts/parameters/fileExtension.js diff --git a/src/parameters/login.js b/scripts/parameters/login.js similarity index 100% rename from src/parameters/login.js rename to scripts/parameters/login.js diff --git a/src/parameters/packageName.js b/scripts/parameters/packageName.js similarity index 100% rename from src/parameters/packageName.js rename to scripts/parameters/packageName.js diff --git a/src/parameters/page.js b/scripts/parameters/page.js similarity index 100% rename from src/parameters/page.js rename to scripts/parameters/page.js diff --git a/src/parameters/query.js b/scripts/parameters/query.js similarity index 100% rename from src/parameters/query.js rename to scripts/parameters/query.js diff --git a/src/parameters/service.js b/scripts/parameters/service.js similarity index 100% rename from src/parameters/service.js rename to scripts/parameters/service.js diff --git a/src/parameters/serviceType.js b/scripts/parameters/serviceType.js similarity index 100% rename from src/parameters/serviceType.js rename to scripts/parameters/serviceType.js diff --git a/src/parameters/serviceVersion.js b/scripts/parameters/serviceVersion.js similarity index 100% rename from src/parameters/serviceVersion.js rename to scripts/parameters/serviceVersion.js diff --git a/src/parameters/sort.js b/scripts/parameters/sort.js similarity index 100% rename from src/parameters/sort.js rename to scripts/parameters/sort.js diff --git a/src/parameters/versionName.js b/scripts/parameters/versionName.js similarity index 100% rename from src/parameters/versionName.js rename to scripts/parameters/versionName.js diff --git a/src/context.js b/src/context.js index 48fbb8e3..d9501389 100644 --- a/src/context.js +++ b/src/context.js @@ -9,7 +9,6 @@ module.exports = { query: require("./query.js"), vcs: require("./vcs.js"), config: require("./config.js").getConfig(), - common_handler: require("./handlers/common_handler.js"), utils: require("./utils.js"), auth: require("./auth.js"), sso: require("./models/sso.js"), diff --git a/src/dev_server.js b/src/dev_server.js index 3934fcc6..f1b98ad6 100644 --- a/src/dev_server.js +++ b/src/dev_server.js @@ -39,7 +39,7 @@ async function test() { process.env.PORT = 8080; } - const app = require("./main.js"); + const app = require("./setupEndpoints.js"); const logger = require("./logger.js"); const database = require("./database.js"); // We can only require these items after we have set our env variables diff --git a/src/handlers/common_handler.js b/src/handlers/common_handler.js deleted file mode 100644 index 5eeec3e2..00000000 --- a/src/handlers/common_handler.js +++ /dev/null @@ -1,290 +0,0 @@ -/** - * @module common_handler - * @desc Provides a simplistic way to refer to implement common endpoint returns. - * So these can be called as an async function without more complex functions, reducing - * verbosity, and duplication within the codebase. - * @implements {logger} - */ - -const logger = require("../logger.js"); - -/** - * @async - * @function handleError - * @desc Generic error handler mostly used to reduce the duplication of error handling in other modules. - * It checks the short error string and calls the relative endpoint. - * Note that it's designed to be called as the last async function before the return. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @param {object} obj - the Raw Status Object of the User, expected to return from `VerifyAuth`. - */ -async function handleError(req, res, obj, num) { - switch (obj.short) { - case "Not Found": - await notFound(req, res); - break; - - case "Bad Repo": - await badRepoJSON(req, res, num); - break; - - case "Bad Package": - await badPackageJSON(req, res, num); - break; - - case "No Repo Access": - case "Bad Auth": - await authFail(req, res, obj, num); - break; - - case "Package Exists": - await packageExists(req, res); - break; - - case "File Not Found": - case "Server Error": - default: - await serverError(req, res, obj.content, num); - break; - } -} - -/** - * @async - * @function handleDetailedError - * @desc Less generic error handler than `handleError()`. Used for returned the - * improved error messages to users. Where instead of only returning an error - * `message` it will return `message` and `details`. Providing better insight into - * what has gone wrong with the server. - * Additionally this will aim to simplify error handling by not handing off the - * handling to additional functions. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @param {object} obj - The Object provided to return the error message. - * @param {string} obj.short - The recognized Short Code string for error handling. - * @param {string} obj.content - The detailed user friendly content of what's gone wrong. - */ -async function handleDetailedError(req, res, obj) { - switch (obj.short) { - case "Not Found": - res.status(404).json({ - message: "Not Found", - details: obj.content, - }); - logger.httpLog(req, res); - break; - case "Bad Repo": - res.status(400).json({ - message: - "That repo does not exist, isn't a Pulsar package, or pulsarbot does not have access.", - details: obj.content, - }); - logger.httpLog(req, res); - break; - case "Bad Package": - res.status(400).json({ - message: "The package.json at owner/repo isn't valid.", - details: obj.content, - }); - logger.httpLog(req, res); - break; - case "No Repo Access": - case "Bad Auth": - res.status(401).json({ - message: - "Requires authentication. Please update your token if you haven't done so recently.", - details: obj.content, - }); - logger.httpLog(req, res); - break; - case "File Not Found": - case "Server Error": - default: - res.status(500).json({ - message: "Application Error", - details: obj.content, - }); - logger.httpLog(req, res); - break; - } - return; -} - -/** - * @async - * @function authFail - * @desc Will take the failed user object from VerifyAuth, and respond for the endpoint as - * either a "Server Error" or a "Bad Auth", whichever is correct based on the Error bubbled from VerifyAuth. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @param {object} user - The Raw Status Object of the User, expected to return from `VerifyAuth`. - * @implements {MissingAuthJSON} - * @implements {ServerErrorJSON} - * @implements {logger.HTTPLog} - */ -async function authFail(req, res, user, num) { - switch (user.short) { - case "Bad Auth": - case "Auth Fail": - case "No Repo Access": // support for being passed a git return. - await missingAuthJSON(req, res); - break; - default: - await serverError(req, res, user.content, num); - break; - } -} - -/** - * @async - * @function serverError - * @desc Returns a standard Server Error to the user as JSON. Logging the detailed error message to the server. - * ###### Setting: - * * Status Code: 500 - * * JSON Response Body: message: "Application Error" - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @param {string} err - The detailed error message to log server side. - * @implements {logger.HTTPLog} - * @implements {logger.generic} - */ -async function serverError(req, res, err, num) { - res.status(500).json({ message: "Application Error" }); - logger.httpLog(req, res); - logger.generic(3, "Returning Server Error in common", { - type: "http", - req: req, - res: res, - }); -} - -/** - * @async - * @function notFound - * @desc Standard endpoint to return the JSON Not Found error to the user. - * ###### Setting: - * * Status Code: 404 - * * JSON Respone Body: message: "Not Found" - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @implements {logger.HTTPLog} - */ -async function notFound(req, res) { - res.status(404).json({ message: "Not Found" }); - logger.httpLog(req, res); -} - -/** - * @async - * @function notSupported - * @desc Returns a Not Supported message to the user. - * ###### Setting: - * * Status Code: 501 - * * JSON Response Body: message: "While under development this feature is not supported." - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @implements {logger.HTTPLog} - */ -async function notSupported(req, res) { - const message = "While under development this feature is not supported."; - res.status(501).json({ message }); - logger.httpLog(req, res); -} - -/** - * @async - * @function siteWideNotFound - * @desc Returns the SiteWide 404 page to the end user. - * ###### Setting Currently: - * * Status Code: 404 - * * JSON Response Body: message: "This is a standin for the proper site wide 404 page." - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @implements {logger.HTTPLog} - */ -async function siteWideNotFound(req, res) { - res - .status(404) - .json({ message: "This is a standin for the proper site wide 404 page." }); - logger.httpLog(req, res); -} - -/** - * @async - * @function badRepoJSON - * @desc Returns the BadRepoJSON message to the user. - * ###### Setting: - * * Status Code: 400 - * * JSON Response Body: message: That repo does not exist, isn't an atom package, or atombot does not have access. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @implements {logger.HTTPLog} - */ -async function badRepoJSON(req, res, num) { - const message = - "That repo does not exist, isn't an atom package, or atombot does not have access."; - res.status(400).json({ message }); - logger.httpLog(req, res); -} - -/** - * @async - * @function badPackageJSON - * @desc Returns the BadPackageJSON message to the user. - * ###### Setting: - * * Status Code: 400 - * * JSON Response Body: message: The package.json at owner/repo isn't valid. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @implements {logger.HTTPLog} - */ -async function badPackageJSON(req, res, num) { - const message = "The package.json at owner/repo isn't valid."; - res.status(400).json({ message }); - logger.httpLog(req, res); -} - -/** - * @function packageExists - * @desc Returns the PackageExist message to the user. - * ###### Setting: - * * Status Code: 409 - * * JSON Response Body: message: "A Package by that name already exists." - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @implements {logger.HTTPLog} - */ -async function packageExists(req, res) { - res.status(409).json({ message: "A Package by that name already exists." }); - logger.httpLog(req, res); -} - -/** - * @function missingAuthJSON - * @desc Returns the MissingAuth message to the user. - * ###### Setting: - * * Status Code: 401 - * * JSON Response Body: message: "Requires authentication. Please update your token if you haven't done so recently." - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @implements {logger.HTTPLog} - */ -async function missingAuthJSON(req, res) { - const message = - "Requires authentication. Please update your token if you haven't done so recently."; - res.status(401).json({ message }); - logger.httpLog(req, res); -} - -module.exports = { - authFail, - badRepoJSON, - badPackageJSON, - handleError, - notFound, - notSupported, - packageExists, - serverError, - siteWideNotFound, - handleDetailedError, -}; diff --git a/src/handlers/delete_package_handler.js b/src/handlers/delete_package_handler.js deleted file mode 100644 index 37e0a998..00000000 --- a/src/handlers/delete_package_handler.js +++ /dev/null @@ -1,204 +0,0 @@ -/** - * @module delete_package_handler - * @desc Endpoint Handlers for every DELETE Request that relates to packages themselves - */ - -const logger = require("../logger.js"); -const utils = require("../utils.js"); - -/** - * @async - * @function deletePackagesName - * @desc Allows the user to delete a repo they have ownership of. - * @param {object} params - The query parameters - * @param {string} params.auth - The API key for the user - * @param {string} params.packageName - The name of the package - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @param {module} vcs - An instance of the `vcs.js` module - * @property {http_method} - DELETE - * @property {http_endpoint} - /api/packages/:packageName - */ -async function deletePackagesName(params, db, auth, vcs) { - const user = await auth.verifyAuth(params.auth, db); - - if (!user.ok) { - return { - ok: false, - content: user, - }; - } - - // Lets also first check to make sure the package exists. - const packageExists = await db.getPackageByName(params.packageName, true); - - if (!packageExists.ok) { - return { - ok: false, - content: packageExists, - }; - } - - logger.generic( - 6, - `${params.packageName} Successfully executed 'db.getPackageByName()'` - ); - - // Get `owner/repo` string format from package. - const ownerRepo = utils.getOwnerRepoFromPackage(packageExists.content.data); - - const gitowner = await vcs.ownership(user.content, ownerRepo); - - if (!gitowner.ok) { - return { - ok: false, - content: gitowner, - }; - } - - logger.generic( - 6, - `${params.packageName} Successfully executed 'vcs.ownership()'` - ); - - // Now they are logged in locally, and have permission over the GitHub repo. - const rm = await db.removePackageByName(params.packageName); - - if (!rm.ok) { - logger.generic( - 6, - `${params.packageName} FAILED to execute 'db.removePackageByName'`, - { - type: "error", - err: rm, - } - ); - return { - ok: false, - content: rm, - }; - } - - logger.generic( - 6, - `${params.packageName} Successfully executed 'db.removePackageByName'` - ); - - return { - ok: true, - }; -} - -/** - * @async - * @function deletePackageStar - * @desc Used to remove a star from a specific package for the authenticated usesr. - * @param {object} params - The query parameters - * @param {string} params.auth - The API Key of the user - * @param {string} params.packageName - The name of the package - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @property {http_method} - DELETE - * @property {http_endpoint} - /api/packages/:packageName/star - */ -async function deletePackagesStar(params, db, auth) { - const user = await auth.verifyAuth(params.auth, db); - - if (!user.ok) { - return { - ok: false, - content: user, - }; - } - - const unstar = await db.updateDecrementStar(user.content, params.packageName); - - if (!unstar.ok) { - return { - ok: false, - content: unstar, - }; - } - - // On a successful action here we will return an empty 201 - return { - ok: true, - }; -} - -/** - * @async - * @function deletePackageVersion - * @desc Allows a user to delete a specific version of their package. - * @param {object} params - The query parameters - * @param {string} params.auth - The API key of the user - * @param {string} params.packageName - The name of the package - * @param {string} params.versionName - The version of the package - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @param {module} vcs - An instance of the `vcs.js` module - * @property {http_method} - DELETE - * @property {http_endpoint} - /api/packages/:packageName/versions/:versionName - */ -async function deletePackageVersion(params, db, auth, vcs) { - // Moving this forward to do the least computationally expensive task first. - // Check version validity - if (params.versionName === false) { - return { - ok: false, - content: { - short: "Not Found", - }, - }; - } - - // Verify the user has local and remote permissions - const user = await auth.verifyAuth(params.auth, db); - - if (!user.ok) { - return { - ok: false, - content: user, - }; - } - - // Lets also first check to make sure the package exists. - const packageExists = await db.getPackageByName(params.packageName, true); - - if (!packageExists.ok) { - return { - ok: false, - content: packageExists, - }; - } - - const gitowner = await vcs.ownership(user.content, packageExists.content); - - if (!gitowner.ok) { - return { - ok: false, - content: gitowner, - }; - } - - // Mark the specified version for deletion, if version is valid - const removeVersion = await db.removePackageVersion( - params.packageName, - params.versionName - ); - - if (!removeVersion.ok) { - return { - ok: false, - content: removeVersion, - }; - } - - return { ok: true }; -} - -module.exports = { - deletePackagesName, - deletePackagesStar, - deletePackageVersion, -}; diff --git a/src/handlers/get_package_handler.js b/src/handlers/get_package_handler.js deleted file mode 100644 index 873a2354..00000000 --- a/src/handlers/get_package_handler.js +++ /dev/null @@ -1,458 +0,0 @@ -/** - * @module get_package_handler - * @desc Endpoint Handlers for every GET Request that relates to packages themselves - */ - -const logger = require("../logger.js"); -const { server_url } = require("../config.js").getConfig(); -const utils = require("../utils.js"); -const { URL } = require("node:url"); - -/** - * @async - * @function getPackages - * @desc Endpoint to return all packages to the user. Based on any filtering - * theyved applied via query parameters. - * @param {object} params - The query parameters for this endpoint. - * @param {integer} params.page - The page to retreive - * @param {string} params.sort - The method to sort by - * @param {string} params.direction - The direction to sort with - * @param {string} params.serviceType - The service type to display - * @param {string} params.service - The service to display - * @param {string} params.serviceVersion - The service version to show - * @param {string} params.fileExtension - File extension to only show compatible - * grammar package's of. - * @param {module} db - An instance of the database - * @property {http_method} - GET - * @property {http_endpoint} - /api/packages - */ -async function getPackages(params, db) { - const packages = await db.getSortedPackages(params); - - if (!packages.ok) { - logger.generic( - 3, - `getPackages-getSortedPackages Not OK: ${packages.content}` - ); - return { - ok: false, - content: packages, - }; - } - - const page = packages.pagination.page; - const totPage = packages.pagination.total; - const packObjShort = await utils.constructPackageObjectShort( - packages.content - ); - - // The endpoint using this function needs an array. - const packArray = Array.isArray(packObjShort) ? packObjShort : [packObjShort]; - - let link = `<${server_url}/api/packages?page=${page}&sort=${params.sort}&order=${params.direction}>; rel="self", <${server_url}/api/packages?page=${totPage}&sort=${params.sort}&order=${params.direction}>; rel="last"`; - - if (page !== totPage) { - link += `, <${server_url}/api/packages?page=${page + 1}&sort=${ - params.sort - }&order=${params.direction}>; rel="next"`; - } - - return { - ok: true, - link: link, - total: packages.pagination.count, - limit: packages.pagination.limit, - content: packArray, - }; -} - -/** - * @async - * @function getPackagesFeatured - * @desc Allows the user to retrieve the featured packages, as package object shorts. - * This endpoint was originally undocumented. The decision to return 200 is based off similar endpoints. - * Additionally for the time being this list is created manually, the same method used - * on Atom.io for now. Although there are plans to have this become automatic later on. - * @see {@link https://github.com/atom/apm/blob/master/src/featured.coffee|Source Code} - * @see {@link https://github.com/confused-Techie/atom-community-server-backend-JS/issues/23|Discussion} - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/packages/featured - */ -async function getPackagesFeatured(db) { - // Returns Package Object Short array. - // TODO: Does not support engine query parameter as of now - const packs = await db.getFeaturedPackages(); - - if (!packs.ok) { - logger.generic( - 3, - `getPackagesFeatured-getFeaturedPackages Not OK: ${packs.content}` - ); - return { - ok: false, - content: packs, - }; - } - - const packObjShort = await utils.constructPackageObjectShort(packs.content); - - // The endpoint using this function needs an array. - const packArray = Array.isArray(packObjShort) ? packObjShort : [packObjShort]; - - return { - ok: true, - content: packArray, - }; -} - -/** - * @async - * @function getPackagesSearch - * @desc Allows user to search through all packages. Using their specified - * query parameter. - * @param {object} params - The query parameters - * @param {integer} params.page - The page to retreive - * @param {string} params.sort - The method to sort by - * @param {string} params.direction - The direction to sort with - * @param {string} params.query - The search query - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/packages/search - * @todo Note: This **has** been migrated to the new DB, and is fully functional. - * The TODO here is to eventually move this to use the custom built in LCS search, - * rather than simple search. - */ -async function getPackagesSearch(params, db) { - // Because the task of implementing the custom search engine is taking longer - // than expected, this will instead use super basic text searching on the DB side. - // This is only an effort to get this working quickly and should be changed later. - // This also means for now, the default sorting method will be downloads, not relevance. - - const packs = await db.simpleSearch( - params.query, - params.page, - params.direction, - params.sort - ); - - if (!packs.ok) { - if (packs.short === "Not Found") { - logger.generic( - 4, - "getPackagesSearch-simpleSearch Responding with Empty Array for Not Found Status" - ); - // Because getting not found from the search, means the users - // search just had no matches, we will specially handle this to return - // an empty array instead. - // TODO: Re-evaluate if this is needed. The empty result - // returning 'Not Found' has been resolved via the DB. - // But this check still might come in handy, so it'll be left in. - return { - ok: true, - content: [], - }; - } - logger.generic( - 3, - `getPackagesSearch-simpleSearch Not OK: ${packs.content}` - ); - return { - ok: false, - content: packs, - }; - } - - const page = packs.pagination.page; - const totPage = packs.pagination.total; - const newPacks = await utils.constructPackageObjectShort(packs.content); - - let packArray = null; - - if (Array.isArray(newPacks)) { - packArray = newPacks; - } else if (Object.keys(newPacks).length < 1) { - packArray = []; - logger.generic( - 4, - "getPackagesSearch-simpleSearch Responding with Empty Array for 0 Key Length Object" - ); - // This also helps protect against misreturned searches. As in getting a 404 rather - // than empty search results. - // See: https://github.com/confused-Techie/atom-backend/issues/59 - } else { - packArray = [newPacks]; - } - - const safeQuery = encodeURIComponent( - params.query.replace(/[<>"':;\\/]+/g, "") - ); - // now to get headers. - let link = `<${server_url}/api/packages/search?q=${safeQuery}&page=${page}&sort=${params.sort}&order=${params.direction}>; rel="self", <${server_url}/api/packages/search?q=${safeQuery}&page=${totPage}&sort=${params.sort}&order=${params.direction}>; rel="last"`; - - if (page !== totPage) { - link += `, <${server_url}/api/packages/search?q=${safeQuery}&page=${ - page + 1 - }&sort=${params.sort}&order=${params.direction}>; rel="next"`; - } - - return { - ok: true, - link: link, - total: packs.pagination.count, - limit: packs.pagination.limit, - content: packArray, - }; -} - -/** - * @async - * @function getPackagesDetails - * @desc Allows the user to request a single package object full, depending - * on the package included in the path parameter. - * @param {object} param - The query parameters - * @param {string} param.engine - The version of Pulsar to check compatibility with - * @param {string} param.name - The package name - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/packages/:packageName - */ -async function getPackagesDetails(params, db) { - let pack = await db.getPackageByName(params.name, true); - - if (!pack.ok) { - logger.generic( - 3, - `getPackagesDetails-getPackageByName Not OK: ${pack.content}` - ); - return { - ok: false, - content: pack, - }; - } - - pack = await utils.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 utils.engineFilter(pack, params.engine); - } - - return { - ok: true, - content: pack, - }; -} - -/** - * @async - * @function getPackagesStargazers - * @desc Endpoint returns the array of `star_gazers` from a specified package. - * Taking only the package wanted, and returning it directly. - * @param {object} params - The query parameters - * @param {string} params.packageName - The name of the package - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/packages/:packageName/stargazers - */ -async function getPackagesStargazers(params, db) { - // The following can't be executed in user mode because we need the pointer - const pack = await db.getPackageByName(params.packageName); - - if (!pack.ok) { - return { - ok: false, - content: pack, - }; - } - - const stars = await db.getStarringUsersByPointer(pack.content); - - if (!stars.ok) { - return { - ok: false, - content: stars, - }; - } - - const gazers = await db.getUserCollectionById(stars.content); - - if (!gazers.ok) { - return { - ok: false, - content: gazers, - }; - } - - return { - ok: true, - content: gazers.content, - }; -} - -/** - * @async - * @function getPackagesVersion - * @desc Used to retrieve a specific version from a package. - * @param {object} params - The query parameters - * @param {string} params.packageName - The Package name we care about - * @param {string} params.versionName - The package version we care about - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/packages/:packageName/versions/:versionName - */ -async function getPackagesVersion(params, db) { - // Check the truthiness of the returned query engine. - if (params.versionName === false) { - // we return a 404 for the version, since its an invalid format - return { - ok: false, - content: { - short: "Not Found", - }, - }; - } - // Now we know the version is a valid semver. - - const pack = await db.getPackageVersionByNameAndVersion( - params.packageName, - params.versionName - ); - - if (!pack.ok) { - return { - ok: false, - content: pack, - }; - } - - const packRes = await utils.constructPackageObjectJSON(pack.content); - - return { - ok: true, - content: packRes, - }; -} - -/** - * @async - * @function getPackagesVersionTarball - * @desc Allows the user to get the tarball for a specific package version. - * Which should initiate a download of said tarball on their end. - * @param {object} params - The query parameters - * @param {string} params.packageName - The name of the package - * @param {string} params.versionName - The version of the package - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/packages/:packageName/versions/:versionName/tarball - */ -async function getPackagesVersionTarball(params, db) { - // Now that migration has began we know that each version will have - // a tarball_url key on it, linking directly to the tarball from gh for that version. - - // we initially want to ensure we have a valid version. - if (params.versionName === false) { - // since query.engine gives false if invalid, we can just check if its truthy - // additionally if its false, we know the version will never be found. - return { - ok: false, - content: { - short: "Not Found", - }, - }; - } - - // first lets get the package - const pack = await db.getPackageVersionByNameAndVersion( - params.packageName, - params.versionName - ); - - if (!pack.ok) { - return { - ok: false, - content: pack, - }; - } - - const save = await db.updatePackageIncrementDownloadByName( - params.packageName - ); - - if (!save.ok) { - logger.generic(3, "Failed to Update Downloads Count", { - type: "object", - obj: save.content, - }); - logger.generic(3, "Failed to Update Downloads Count", { - type: "http", - req: req, - res: res, - }); - // we don't want to exit on a failed to update downloads count, but should be logged. - } - - // For simplicity, we will redirect the request to gh tarball url, to allow - // the download to take place from their servers. - - // But right before, lets do a couple simple checks to make sure we are sending to a legit site. - const tarballURL = - pack.content.meta?.tarball_url ?? pack.content.meta?.dist?.tarball ?? ""; - let hostname = ""; - - // Try to extract the hostname - try { - const tbUrl = new URL(tarballURL); - hostname = tbUrl.hostname; - } catch (e) { - logger.generic( - 3, - `Malformed tarball URL for version ${params.versionName} of ${params.packageName}` - ); - return { - ok: false, - content: { - ok: false, - short: "Server Error", - content: e, - }, - }; - } - - const allowedHostnames = [ - "codeload.github.com", - "api.github.com", - "github.com", - "raw.githubusercontent.com", - ]; - - if ( - !allowedHostnames.includes(hostname) && - process.env.PULSAR_STATUS !== "dev" - ) { - return { - ok: false, - content: { - ok: false, - short: "Server Error", - content: `Invalid Domain for Download Redirect: ${hostname}`, - }, - }; - } - - return { - ok: true, - content: tarballURL, - }; -} - -module.exports = { - getPackages, - getPackagesFeatured, - getPackagesSearch, - getPackagesDetails, - getPackagesStargazers, - getPackagesVersion, - getPackagesVersionTarball, -}; diff --git a/src/handlers/oauth_handler.js b/src/handlers/oauth_handler.js deleted file mode 100644 index d04363da..00000000 --- a/src/handlers/oauth_handler.js +++ /dev/null @@ -1,267 +0,0 @@ -/** - * @module oauth_handler - * @desc Endpoint Handlers for Authentication URLs - * @implements {config} - * @implements {common_handler} - */ - -const { GH_CLIENTID, GH_REDIRECTURI, GH_CLIENTSECRET, GH_USERAGENT } = - require("../config.js").getConfig(); -const common = require("./common_handler.js"); -const utils = require("../utils.js"); -const logger = require("../logger.js"); -const superagent = require("superagent"); -const database = require("../database.js"); - -/** - * @async - * @function getLogin - * @desc Endpoint used to redirect users to login. Users will reach GitHub OAuth Page - * based on the backends client id. A key from crypto module is retrieved and used as - * state parameter for GH authentication. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @property {http_method} - GET - * @property {http_endpoint} - /api/lgoin - */ -async function getLogin(req, res) { - // The first point of contact to log into the app. - // Since this will be the endpoint for a user to login, we need to redirect to GH. - // @see https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps - logger.generic(4, "New Hit on api/login"); - - // Generate a random key. - const stateKey = utils.generateRandomString(64); - - // Before redirect, save the key into the database. - const saveStateKey = await database.authStoreStateKey(stateKey); - if (!saveStateKey.ok) { - await common.handleError(req, res, saveStateKey); - return; - } - - res - .status(302) - .redirect( - `https://github.com/login/oauth/authorize?client_id=${GH_CLIENTID}&redirect_uri=${GH_REDIRECTURI}&state=${stateKey}&scope=public_repo%20read:org` - ); - logger.generic(4, `Generated a new key and made the Redirect for: ${req.ip}`); - logger.httpLog(req, res); -} - -/** - * @async - * @function getOauth - * @desc Endpoint intended to use as the actual return from GitHub to login. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @property {http_method} - GET - * @property {http_endpoint} - /api/oath - */ -async function getOauth(req, res) { - let params = { - state: req.query.state ?? "", - code: req.query.code ?? "", - }; - logger.generic(4, "Get OAuth Hit!"); - - // First we want to ensure that the received state key is valid. - const validStateKey = await database.authCheckAndDeleteStateKey(params.state); - if (!validStateKey.ok) { - logger.generic(3, `Provided State Key is NOT Valid!`); - await common.handleError(req, res, validStateKey); - return; - } - - logger.generic(4, "The State Key is Valid and has been Removed"); - - // Retrieve access token - const initialAuth = await superagent - .post(`https://github.com/login/oauth/access_token`) - .query({ - code: params.code, - redirect_uri: GH_REDIRECTURI, - client_id: GH_CLIENTID, - client_secret: GH_CLIENTSECRET, - }); - - const accessToken = initialAuth.body?.access_token; - - if (accessToken === null || initialAuth.body?.token_type === null) { - logger.generic(2, "Auth Request to GitHub Failed!", { - type: "object", - obj: initialAuth, - }); - await common.handleError(req, res, { - ok: false, - short: "Server Error", - content: initialAuth, - }); - return; - } - - try { - // Request the user data using the access token - const userData = await superagent - .get("https://api.github.com/user") - .set({ Authorization: `Bearer ${accessToken}` }) - .set({ "User-Agent": GH_USERAGENT }); - - if (userData.status !== 200) { - logger.generic(2, "User Data Request to GitHub Failed!", { - type: "object", - obj: userData, - }); - await common.handleError(req, res, { - ok: false, - short: "Server Error", - content: userData, - }); - return; - } - - // Now retrieve the user data thet we need to store into the DB. - const username = userData.body.login; - const userId = userData.body.node_id; - const userAvatar = userData.body.avatar_url; - - const userExists = await database.getUserByNodeID(userId); - - if (userExists.ok) { - logger.generic(4, `User Check Says User Exists: ${username}`); - // This means that the user does in fact already exist. - // And from there they are likely reauthenticating, - // But since we don't save any type of auth tokens, the user just needs a new one - // and we should return their new one to them. - - // Now we redirect to the frontend site. - res.redirect(`https://web.pulsar-edit.dev/users?token=${accessToken}`); - logger.httpLog(req, res); - return; - } - - // The user does not exist, so we save its data into the DB. - let createdUser = await database.insertNewUser( - username, - userId, - userAvatar - ); - - if (!createdUser.ok) { - logger.generic(2, `Creating User Failed! ${userObj.username}`); - await common.handleError(req, res, createdUser); - return; - } - - // Before returning, lets append their access token - createdUser.content.token = accessToken; - - // Now we redirect to the frontend site. - res.redirect( - `https://web.pulsar-edit.dev/users?token=${createdUser.content.token}` - ); - logger.httpLog(req, res); - } catch (err) { - logger.generic(2, "/api/oauth Caught an Error!", { - type: "error", - err: err, - }); - await common.handleError(req, res, err); - return; - } -} - -/** - * @async - * @function getPat - * @desc Endpoint intended to Allow users to sign up with a Pat Token. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {object} res - The `Response` object inherited from the Express endpoint. - * @property {http_method} - GET - * @property {http_endpoint} - /api/pat - */ -async function getPat(req, res) { - let params = { - token: req.query.token ?? "", - }; - - logger.generic(4, `Get Pat Hit!`); - - if (params.pat === "") { - logger.generic(3, "Pat Empty on Request"); - await common.handleError(req, res, { - ok: false, - short: "Not Found", - content: "Pat Empty on Request", - }); - return; - } - - try { - const userData = await superagent - .get("https://api.github.com/user") - .set({ Authorization: `Bearer ${params.token}` }) - .set({ "User-Agent": GH_USERAGENT }); - - if (userData.status !== 200) { - logger.generic(2, "User Data Request to GitHub Failed!", { - type: "object", - obj: userData, - }); - await common.handleError(req, res, { - ok: false, - short: "Server Error", - content: userData, - }); - return; - } - - // Now to build a valid user object - const username = userData.body.login; - const userId = userData.body.node_id; - const userAvatar = userData.body.avatar_url; - - const userExists = await database.getUserByNodeID(userId); - - if (userExists.ok) { - logger.generic(4, `User Check Says User Exists: ${username}`); - - // If we plan to allow updating the user name or image, we would do so here - - // Now we redirect to the frontend site. - res.redirect(`https://web.pulsar-edit.dev/users?token=${params.token}`); - logger.httpLog(req, res); - return; - } - - let createdUser = await database.insertNewUser( - username, - userId, - userAvatar - ); - - if (!createdUser.ok) { - logger.generic(2, `Creating User Failed! ${username}`); - await common.handleError(req, res, createdUser); - return; - } - - // Before returning, lets append their PAT token - createdUser.content.token = params.token; - - res.redirect( - `https://web.pulsar-edit.dev/users?token=${createdUser.content.token}` - ); - logger.httpLog(req, res); - } catch (err) { - logger.generic(2, "/api/pat Caught an Error!", { type: "error", err: err }); - await common.handleError(req, res, err); - return; - } -} - -module.exports = { - getLogin, - getOauth, - getPat, -}; diff --git a/src/handlers/package_handler.js b/src/handlers/package_handler.js deleted file mode 100644 index cbddbd71..00000000 --- a/src/handlers/package_handler.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @module package_handler - * @desc Exports individual files handling endpoints relating to Packages - */ - -const getPackageHandler = require("./get_package_handler.js"); -const postPackageHandler = require("./post_package_handler.js"); -const deletePackageHandler = require("./delete_package_handler.js"); - -module.exports = { - getPackages: getPackageHandler.getPackages, - postPackages: postPackageHandler.postPackages, - getPackagesFeatured: getPackageHandler.getPackagesFeatured, - getPackagesSearch: getPackageHandler.getPackagesSearch, - getPackagesDetails: getPackageHandler.getPackagesDetails, - deletePackagesName: deletePackageHandler.deletePackagesName, - postPackagesStar: postPackageHandler.postPackagesStar, - deletePackagesStar: deletePackageHandler.deletePackagesStar, - getPackagesStargazers: getPackageHandler.getPackagesStargazers, - postPackagesVersion: postPackageHandler.postPackagesVersion, - getPackagesVersion: getPackageHandler.getPackagesVersion, - getPackagesVersionTarball: getPackageHandler.getPackagesVersionTarball, - deletePackageVersion: deletePackageHandler.deletePackageVersion, - postPackagesEventUninstall: postPackageHandler.postPackagesEventUninstall, -}; diff --git a/src/handlers/post_package_handler.js b/src/handlers/post_package_handler.js deleted file mode 100644 index ad35afc0..00000000 --- a/src/handlers/post_package_handler.js +++ /dev/null @@ -1,445 +0,0 @@ -/** - * @module post_package_handler - * @desc Endpoint Handlers for every POST Request that relates to packages themselves - */ - -const logger = require("../logger.js"); -const utils = require("../utils.js"); - -/** - * @async - * @function postPackages - * @desc This endpoint is used to publish a new package to the backend server. - * Taking the repo, and your authentication for it, determines if it can be published, - * then goes about doing so. - * @param {object} params - The query parameters - * @param {string} params.repository - The `owner/repo` combo of the remote package - * @param {string} params.auth - The API key of the user - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @param {module} vcs - An instance of the `vcs.js` module - * @return {string} JSON object of new data pushed into the database, but stripped of - * sensitive informations like primary and foreign keys. - * @property {http_method} - POST - * @property {http_endpoint} - /api/packages - */ -async function postPackages(params, db, auth, vcs) { - const user = await auth.verifyAuth(params.auth, db); - logger.generic( - 6, - `${user.content.username} Attempting to Publish new package` - ); - // Check authentication. - if (!user.ok) { - logger.generic(3, `postPackages-verifyAuth Not OK: ${user.content}`); - return { - ok: false, - content: user, - }; - } - - // Check repository format validity. - if (params.repository === "") { - logger.generic(6, "Repository Format Invalid, returning error"); - // The repository format is invalid. - return { - ok: false, - content: { - short: "Bad Repo", - }, - }; - } - - // Currently though the repository is in `owner/repo` format, - // meanwhile needed functions expects just `repo` - - const repo = params.repository.split("/")[1]?.toLowerCase(); - - if (repo === undefined) { - logger.generic(6, "Repository determined invalid after failed split"); - // The repository format is invalid. - return { - ok: false, - content: { - short: "Bad Repo", - }, - }; - } - - // Now check if the name is banned. - const isBanned = await utils.isPackageNameBanned(repo); - - if (isBanned.ok) { - logger.generic(3, `postPackages Blocked by banned package name: ${repo}`); - // The package name is banned - return { - ok: false, - type: "detailed", - content: { - ok: false, - short: "Server Error", - content: "Package Name is banned.", - }, - }; - // ^^^ Replace with a more specific error handler once supported TODO - } - - // Check the package does NOT exists. - // We will utilize our database.packageNameAvailability to see if the name is available. - - const nameAvailable = await db.packageNameAvailability(repo); - - if (!nameAvailable.ok) { - // Even further though we need to check that the error is not "Not Found", - // since an exception could have been caught. - if (nameAvailable.short !== "Not Found") { - logger.generic( - 3, - `postPackages-getPackageByName Not OK: ${nameAvailable.content}` - ); - // The server failed for some other bubbled reason, and is now encountering an error - return { - ok: false, - content: nameAvailable, - }; - } - // But if the short is then only "Not Found" we can report it as not being available - logger.generic( - 6, - "The name for the package is not available: aborting publish" - ); - // The package exists. - return { - ok: false, - content: { - short: "Package Exists", - }, - }; - } - - // Now we know the package doesn't exist. And we want to check that the user owns this repo on git. - const gitowner = await vcs.ownership(user.content, params.repository); - - if (!gitowner.ok) { - logger.generic(3, `postPackages-ownership Not OK: ${gitowner.content}`); - return { - ok: false, - content: gitowner, - }; - } - - // Now knowing they own the git repo, and it doesn't exist here, lets publish. - // TODO: Stop hardcoding `git` as service - const newPack = await vcs.newPackageData( - user.content, - params.repository, - "git" - ); - - if (!newPack.ok) { - logger.generic(3, `postPackages-createPackage Not OK: ${newPack.content}`); - return { - ok: false, - type: "detailed", - content: newPack, - }; - } - - // Now with valid package data, we can insert them into the DB. - const insertedNewPack = await db.insertNewPackage(newPack.content); - - if (!insertedNewPack.ok) { - logger.generic( - 3, - `postPackages-insertNewPackage Not OK: ${insertedNewPack.content}` - ); - return { - ok: false, - content: insertedNewPack, - }; - } - - // Finally we can return what was actually put into the database. - // Retrieve the data from database.getPackageByName() and - // convert it into Package Object Full format. - const newDbPack = await db.getPackageByName(repo, true); - - if (!newDbPack.ok) { - logger.generic( - 3, - `postPackages-getPackageByName (After Pub) Not OK: ${newDbPack.content}` - ); - return { - ok: false, - content: newDbPack, - }; - } - - const packageObjectFull = await utils.constructPackageObjectFull( - newDbPack.content - ); - - // Since this is a webhook call, we will return with some extra data - return { - ok: true, - content: packageObjectFull, - webhook: { - pack: packageObjectFull, - user: user.content, - }, - featureDetection: { - user: user.content, - service: "git", // TODO Stop hardcoding Git - ownerRepo: params.repository, - }, - }; -} - -/** - * @async - * @function postPackagesStar - * @desc Used to submit a new star to a package from the authenticated user. - * @param {object} params - The query parameters - * @param {string} params.auth - The API key of the user - * @param {string} params.packageName - The name of the package - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @property {http_method} - POST - * @property {http_endpoint} - /api/packages/:packageName/star - */ -async function postPackagesStar(params, db, auth) { - const user = await auth.verifyAuth(params.auth, db); - - if (!user.ok) { - return { - ok: false, - content: user, - }; - } - - const star = await db.updateIncrementStar(user.content, params.packageName); - - if (!star.ok) { - return { - ok: false, - content: star, - }; - } - - // Now with a success we want to return the package back in this query - let pack = await db.getPackageByName(params.packageName, true); - - if (!pack.ok) { - return { - ok: false, - content: pack, - }; - } - - pack = await utils.constructPackageObjectFull(pack.content); - - return { - ok: true, - content: pack, - }; -} - -/** - * @async - * @function postPackagesVersion - * @desc Allows a new version of a package to be published. But also can allow - * a user to rename their application during this process. - * @param {object} params - The query parameters - * @param {boolean} params.rename - Whether or not to preform a rename - * @param {string} params.auth - The API key of the user - * @param {string} params.packageName - The name of the package - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @param {module} vcs - An instance of the `vcs.js` module - * @property {http_method} - POST - * @property {http_endpoint} - /api/packages/:packageName/versions - */ -async function postPackagesVersion(params, db, auth, vcs) { - // On renaming: - // When a package is being renamed, we will expect that packageName will - // match a previously published package. - // But then the `name` of their `package.json` will be different. - // And if they are, we expect that `rename` is true. Because otherwise it will fail. - // That's the methodology, the logic here just needs to catch up. - - const user = await auth.verifyAuth(params.auth, db); - - if (!user.ok) { - logger.generic( - 6, - "User Authentication Failed when attempting to publish package version!" - ); - - return { - ok: false, - type: "detailed", - content: user, - }; - } - logger.generic( - 6, - `${user.content.username} Attempting to publish a new package version - ${params.packageName}` - ); - - // To support a rename, we need to check if they have permissions over this packages new name. - // Which means we have to check if they have ownership AFTER we collect it's data. - - const packExists = await db.getPackageByName(params.packageName, true); - - if (!packExists.ok) { - logger.generic( - 6, - `Seems Package does not exist when trying to publish new version - ${packExists.content}` - ); - return { - ok: false, - type: "detailed", - content: { - ok: false, - short: packExists.short, - content: - "The server was unable to locate your package when publishing a new version.", - }, - }; - } - - // Get `owner/repo` string format from package. - let ownerRepo = utils.getOwnerRepoFromPackage(packExists.content.data); - - // Using our new VCS Service - // TODO: The "git" Service shouldn't always be hardcoded. - let packMetadata = await vcs.newVersionData(user.content, ownerRepo, "git"); - - if (!packMetadata.ok) { - logger.generic(6, packMetadata.content); - return { - ok: false, - content: packMetadata, - }; - } - - const newName = packMetadata.content.name; - - const currentName = packExists.content.name; - if (newName !== currentName && !params.rename) { - logger.generic( - 6, - "Package JSON and Params Package Names don't match, with no rename flag" - ); - // Only return error if the names don't match, and rename isn't enabled. - return { - ok: false, - content: { - ok: false, - short: "Bad Repo", - content: "Package name doesn't match local name, with rename false", - }, - }; - } - - // Else we will continue, and trust the name provided from the package as being accurate. - // And now we can ensure the user actually owns this repo, with our updated name. - - // By passing `packMetadata` explicitely, it ensures that data we use to check - // ownership is fresh, allowing for things like a package rename. - - const gitowner = await vcs.ownership(user.content, packMetadata.content); - - if (!gitowner.ok) { - logger.generic(6, `User Failed Git Ownership Check: ${gitowner.content}`); - return { - ok: false, - content: gitowner, - }; - } - - // Now the only thing left to do, is add this new version with the name from the package. - // And check again if the name is incorrect, since it'll need a new entry onto the names. - - const rename = newName !== currentName && params.rename; - if (rename) { - // Before allowing the rename of a package, ensure the new name isn't banned. - - const isBanned = await utils.isPackageNameBanned(newName); - - if (isBanned.ok) { - logger.generic( - 3, - `postPackages Blocked by banned package name: ${newName}` - ); - return { - ok: false, - type: "detailed", - content: { - ok: false, - short: "Server Error", - content: "This Package Name is Banned on the Pulsar Registry.", - }, - }; - } - - const isAvailable = await db.packageNameAvailability(newName); - - if (isAvailable.ok) { - logger.generic( - 3, - `postPackages Blocked by new name ${newName} not available` - ); - return { - ok: false, - type: "detailed", - content: { - ok: false, - short: "Server Error", - content: `The Package Name: ${newName} is not available.`, - }, - }; - } - } - - // Now add the new Version key. - - const addVer = await db.insertNewPackageVersion( - packMetadata.content, - rename ? currentName : null - ); - - if (!addVer.ok) { - // TODO: Use hardcoded message until we can verify messages from the db are safe - // to pass directly to end users. - return { - ok: false, - type: "detailed", - content: { - ok: addVer.ok, - short: addVer.short, - content: "Failed to add the new package version to the database.", - }, - }; - } - - return { - ok: true, - content: addVer.content, - webhook: { - pack: packMetadata.content, - user: user.content, - }, - featureDetection: { - user: user.content, - service: "git", // TODO Stop hardcoding git - ownerRepo: ownerRepo, - }, - }; -} - -module.exports = { - postPackages, - postPackagesStar, - postPackagesVersion, -}; diff --git a/src/handlers/star_handler.js b/src/handlers/star_handler.js deleted file mode 100644 index 7781edb7..00000000 --- a/src/handlers/star_handler.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @module star_handler - * @desc Handler for any endpoints whose slug after `/api/` is `star`. - */ - -const logger = require("../logger.js"); -const utils = require("../utils.js"); - -/** - * @async - * @function getStars - * @desc Endpoint for `GET /api/stars`. Whose endgoal is to return an array of all packages - * the authenticated user has stared. - * @param {object} param - The supported query parameters. - * @param {string} param.auth - The authentication API token - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/stars - */ -async function getStars(params, db, auth) { - let user = await auth.verifyAuth(params.auth, db); - - if (!user.ok) { - logger.generic(3, "getStars auth.verifyAuth() Not OK", { - type: "object", - obj: user, - }); - return { - ok: false, - content: user, - }; - } - - let userStars = await db.getStarredPointersByUserID(user.content.id); - - if (!userStars.ok) { - logger.generic(3, "getStars database.getStarredPointersByUserID() Not OK", { - type: "object", - obj: userStars, - }); - return { - ok: false, - content: userStars, - }; - } - - if (userStars.content.length === 0) { - logger.generic(6, "getStars userStars Has Length of 0. Returning empty"); - // If we have a return with no items, means the user has no stars. - // And this will error out later when attempting to collect the data for the stars. - // So we will reutrn here - return { - ok: true, - content: [], - }; - } - - let packCol = await db.getPackageCollectionByID(userStars.content); - - if (!packCol.ok) { - logger.generic(3, "getStars database.getPackageCollectionByID() Not OK", { - type: "object", - obj: packCol, - }); - return { - ok: false, - content: packCol, - }; - } - - let newCol = await utils.constructPackageObjectShort(packCol.content); - - return { - ok: true, - content: newCol, - }; -} - -module.exports = { - getStars, -}; diff --git a/src/handlers/theme_handler.js b/src/handlers/theme_handler.js deleted file mode 100644 index 36dc2247..00000000 --- a/src/handlers/theme_handler.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @module theme_handler - * @desc Endpoint Handlers relating to themes only. - * @implements {database} - * @implements {utils} - * @implements {logger} - * @implements {config} - */ - -const utils = require("../utils.js"); -const logger = require("../logger.js"); -const { server_url } = require("../config.js").getConfig(); - -/** - * @async - * @function getThemeFeatured - * @desc Used to retrieve all Featured Packages that are Themes. Originally an undocumented - * endpoint. Returns a 200 response based on other similar responses. - * Additionally for the time being this list is created manually, the same method used - * on Atom.io for now. Although there are plans to have this become automatic later on. - * @see {@link https://github.com/atom/apm/blob/master/src/featured.coffee|Source Code} - * @see {@link https://github.com/confused-Techie/atom-community-server-backend-JS/issues/23|Discussion} - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/themes/featured - */ -async function getThemeFeatured(db) { - // Returns Package Object Short Array - - let col = await db.getFeaturedThemes(); - - if (!col.ok) { - return { - ok: false, - content: col, - }; - } - - let newCol = await utils.constructPackageObjectShort(col.content); - - return { - ok: true, - content: newCol, - }; -} - -/** - * @async - * @function getThemes - * @desc Endpoint to return all Themes to the user. Based on any filtering - * they'ved applied via query parameters. - * @param {object} params - The query parameters that can operate on this endpoint. - * @param {integer} params.page - The page of results to retreive. - * @param {string} params.sort - The sort method to use. - * @param {string} params.direction - The direction to sort results. - * @param {module} db - An instance of the `database.js` module - * @returns {object} An HTTP ServerStatus. - * @property {http_method} - GET - * @property {http_endpoint} - /api/themes - */ -async function getThemes(params, db) { - const packages = await db.getSortedPackages(params, true); - - if (!packages.ok) { - logger.generic( - 3, - `getThemes-getSortedPackages Not OK: ${packages.content}` - ); - return { - ok: false, - content: packages, - }; - } - - const page = packages.pagination.page; - const totPage = packages.pagination.total; - const packObjShort = await utils.constructPackageObjectShort( - packages.content - ); - - const packArray = Array.isArray(packObjShort) ? packObjShort : [packObjShort]; - - let link = `<${server_url}/api/themes?page=${page}&sort=${params.sort}&order=${params.direction}>; rel="self", <${server_url}/api/themes?page=${totPage}&sort=${params.sort}&order=${params.direction}>; rel="last"`; - - if (page !== totPage) { - link += `, <${server_url}/api/themes?page=${page + 1}&sort=${ - params.sort - }&order=${params.direction}>; rel="next"`; - } - - return { - ok: true, - link: link, - total: packages.pagination.count, - limit: packages.pagination.limit, - content: packArray, - }; -} - -/** - * @async - * @function getThemesSearch - * @desc Endpoint to Search from all themes on the registry. - * @param {object} params - The query parameters from the initial request. - * @param {integer} params.page - The page number to return - * @param {string} params.sort - The method to use to sort - * @param {string} params.direction - The direction to sort - * @param {string} params.query - The search query to use - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/themes/search - */ -async function getThemesSearch(params, db) { - const packs = await db.simpleSearch( - params.query, - params.page, - params.direction, - params.sort, - true - ); - - if (!packs.ok) { - if (packs.short === "Not Found") { - logger.generic( - 4, - "getThemesSearch-simpleSearch Responding with Empty Array for Not Found Status" - ); - return { - ok: true, - content: [], - link: "", - total: 0, - limit: 0, - }; - } - - logger.generic(3, `getThemesSearch-simpleSearch Not OK: ${packs.content}`); - return { - ok: false, - content: packs, - }; - } - - const page = packs.pagination.page; - const totPage = packs.pagination.total; - const newPacks = await utils.constructPackageObjectShort(packs.content); - - let packArray = null; - - if (Array.isArray(newPacks)) { - packArray = newPacks; - } else if (Object.keys(newPacks).length < 1) { - packArray = []; - logger.generic( - 4, - "getThemesSearch-simpleSearch Responding with Empty Array for 0 key Length Object" - ); - } else { - packArray = [newPacks]; - } - - const safeQuery = encodeURIComponent( - params.query.replace(/[<>"':;\\/]+/g, "") - ); - // now to get headers. - let link = `<${server_url}/api/themes/search?q=${safeQuery}&page=${page}&sort=${params.sort}&order=${params.direction}>; rel="self", <${server_url}/api/themes/search?q=${safeQuery}&page=${totPage}&sort=${params.sort}&order=${params.direction}>; rel="last"`; - - if (page !== totPage) { - link += `, <${server_url}/api/themes/search?q=${safeQuery}&page=${ - page + 1 - }&sort=${params.sort}&order=${params.direction}>; rel="next"`; - } - - return { - ok: true, - content: packArray, - link: link, - total: packs.pagination.count, - limit: packs.pagination.limit, - }; -} - -module.exports = { - getThemeFeatured, - getThemes, - getThemesSearch, -}; diff --git a/src/handlers/update_handler.js b/src/handlers/update_handler.js deleted file mode 100644 index 05b26b7e..00000000 --- a/src/handlers/update_handler.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @module update_handler - * @desc Endpoint Handlers relating to updating the editor. - * @implments {command_handler} - */ - -const common = require("./common_handler.js"); - -/** - * @async - * @function getUpdates - * @desc Used to retrieve new editor update information. - * @property {http_method} - GET - * @property {http_endpoint} - /api/updates - * @todo This function has never been implemented on this system. Since there is currently no - * update methodology. - */ -async function getUpdates() { - return { - ok: false, - }; -} - -module.exports = { - getUpdates, -}; diff --git a/src/handlers/user_handler.js b/src/handlers/user_handler.js deleted file mode 100644 index 5eecf6e8..00000000 --- a/src/handlers/user_handler.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @module user_handler - * @desc Handler for endpoints whose slug after `/api/` is `user`. - */ - -const logger = require("../logger.js"); -const utils = require("../utils.js"); - -/** - * @async - * @function getLoginStars - * @desc Endpoint that returns another users Star Gazers List. - * @param {object} params - The query parameters for the request - * @param {string} params.login - The username - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/users/:login/stars - */ -async function getLoginStars(params, db) { - let user = await db.getUserByName(params.login); - - if (!user.ok) { - return { - ok: false, - content: user, - }; - } - - let pointerCollection = await db.getStarredPointersByUserID(user.content.id); - - if (!pointerCollection.ok) { - return { - ok: false, - content: pointerCollection, - }; - } - - // Since even if the pointerCollection is okay, it could be empty. Meaning the user - // has no stars. This is okay, but getPackageCollectionByID will fail, and result - // in a not found when discovering no packages by the ids passed, which is none. - // So we will catch the exception of pointerCollection being an empty array. - - if ( - Array.isArray(pointerCollection.content) && - pointerCollection.content.length === 0 - ) { - // Check for array to protect from an unexpected return - return { - ok: true, - content: [], - }; - } - - let packageCollection = await db.getPackageCollectionByID( - pointerCollection.content - ); - - if (!packageCollection.ok) { - return { - ok: false, - content: packageCollection, - }; - } - - packageCollection = await utils.constructPackageObjectShort( - packageCollection.content - ); - - return { - ok: true, - content: packageCollection, - }; -} - -/** - * @async - * @function getAuthUser - * @desc Endpoint that returns the currently authenticated Users User Details - * @param {object} params - The query parameters for this endpoint - * @param {string} params.auth - The API Key - * @param {module} db - An instance of the `database.js` module - * @param {module} auth - An instance of the `auth.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/users - */ -async function getAuthUser(params, db, auth) { - const user = await auth.verifyAuth(params.auth, db); - - if (!user.ok) { - return { - ok: false, - content: user, - }; - } - - // TODO We need to find a way to add the users published pacakges here - // When we do we will want to match the schema in ./docs/returns.md#userobjectfull - // Until now we will return the public details of their account. - const returnUser = { - username: user.content.username, - avatar: user.content.avatar, - created_at: user.content.created_at, - data: user.content.data, - node_id: user.content.node_id, - token: user.content.token, // Since this is for the auth user we can provide token - packages: [], // Included as it should be used in the future - }; - - // Now with the user, since this is the authenticated user we can return all account details. - - return { - ok: true, - content: returnUser, - }; -} - -/** - * @async - * @function getUser - * @desc Endpoint that returns the user account details of another user. Including all packages - * published. - * @param {object} params - The query parameters - * @param {string} params.login - The Username we want to look for - * @param {module} db - An instance of the `database.js` module - * @property {http_method} - GET - * @property {http_endpoint} - /api/users/:login - */ -async function getUser(params, db) { - let user = await db.getUserByName(params.login); - - if (!user.ok) { - return { - ok: false, - content: user, - }; - } - - // TODO We need to find a way to add the users published pacakges here - // When we do we will want to match the schema in ./docs/returns.md#userobjectfull - // Until now we will return the public details of their account. - - // Although now we have a user to return, but we need to ensure to strip any sensitive details - // since this return will go to any user. - const returnUser = { - username: user.content.username, - avatar: user.content.avatar, - created_at: user.content.created_at, - data: user.content.data, - packages: [], // included as it should be used in the future - }; - - return { - ok: true, - content: returnUser, - }; -} - -module.exports = { - getLoginStars, - getAuthUser, - getUser, -}; diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 6d1fd793..00000000 --- a/src/main.js +++ /dev/null @@ -1,1641 +0,0 @@ -/** - * @module main - * @desc The Main functionality for the entire server. Sets up the Express server, providing - * all endpoints it listens on. With those endpoints being further documented in `api.md`. - */ - -const express = require("express"); -const app = express(); - -const update_handler = require("./handlers/update_handler.js"); -const star_handler = require("./handlers/star_handler.js"); -const user_handler = require("./handlers/user_handler.js"); -const theme_handler = require("./handlers/theme_handler.js"); -const package_handler = require("./handlers/package_handler.js"); -const common_handler = require("./handlers/common_handler.js"); -const oauth_handler = require("./handlers/oauth_handler.js"); -const webhook = require("./webhook.js"); -const database = require("./database.js"); -const auth = require("./auth.js"); -const server_version = require("../package.json").version; -const logger = require("./logger.js"); -const query = require("./query.js"); -const vcs = require("./vcs.js"); -const rateLimit = require("express-rate-limit"); -const { MemoryStore } = require("express-rate-limit"); -const { RATE_LIMIT_AUTH, RATE_LIMIT_GENERIC } = - require("./config.js").getConfig(); - -// Define our Basic Rate Limiters -const genericLimit = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: process.env.PULSAR_STATUS === "dev" ? 0 : RATE_LIMIT_GENERIC, // Limit each IP per window, 0 disables rate limit. - standardHeaders: true, // Return rate limit info in headers - legacyHeaders: true, // Legacy rate limit info in headers - store: new MemoryStore(), // Use default memory store - message: "Too many requests, please try again later.", // Message once limit is reached. - statusCode: 429, // HTTP Status Code once limit is reached. - handler: (request, response, next, options) => { - response.status(options.statusCode).json({ message: options.message }); - logger.httpLog(request, response); - }, -}); - -const authLimit = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: process.env.PULSAR_STATUS === "dev" ? 0 : RATE_LIMIT_AUTH, // Limit each IP per window, 0 disables rate limit. - standardHeaders: true, // Return rate limit info on headers - legacyHeaders: true, // Legacy rate limit info in headers - store: new MemoryStore(), // use default memory store - message: "Too many requests, please try again later.", // message once limit is reached - statusCode: 429, // HTTP Status code once limit is reached. - handler: (request, response, next, options) => { - response.status(options.statusCode).json({ message: options.message }); - logger.httpLog(request, response); - }, -}); - -// ^^ Our two Rate Limiters ^^ these are essentially currently disabled. -// The reason being, the original API spec made no mention of rate limiting, so nor will we. -// But once we have surpassed feature parity, we will instead enable these limits, to help -// prevent overusage of the api server. With Auth having a lower limit, then non-authed requests. - -app.set("trust proxy", true); -// ^^^ Used to determine the true IP address behind the Google App Engine Load Balancer. -// This is need for the Authentication features to proper maintain their StateStore -// Hashmap. https://cloud.google.com/appengine/docs/flexible/nodejs/runtime#https_and_forwarding_proxies - -app.use("/swagger-ui", express.static("docs/swagger")); - -app.use((req, res, next) => { - // This adds a start to the request, logging the exact time a request was received. - req.start = Date.now(); - next(); -}); - -/** - * @web - * @ignore - * @path / - * @desc A non-essential endpoint, returning a status message, and the server version. - * @method GET - * @auth FALSE - */ -app.get("/", genericLimit, (req, res) => { - // While originally here in case this became the endpoint to host the - // frontend website, now that that is no longer planned, it can be used - // as a way to check the version of the server. Not needed, but may become helpful. - res.status(200).send(` -

Server is up and running Version ${server_version}


- Swagger UI
- Documentation - `); -}); - -app.options("/", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/oauth - * @desc OAuth Callback URL. Other details TBD. - * @method GET - * @auth FALSE - */ -app.get("/api/login", authLimit, async (req, res) => { - await oauth_handler.getLogin(req, res); -}); - -app.options("/api/login", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/oauth - * @desc OAuth Callback URL. Other details TDB. - * @method GET - * @auth FALSE - */ -app.get("/api/oauth", authLimit, async (req, res) => { - await oauth_handler.getOauth(req, res); -}); - -app.options("/api/oauth", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/pat - * @desc Pat Token Signup URL. - * @method GET - * @auth FALSE - */ -app.get("/api/pat", authLimit, async (req, res) => { - await oauth_handler.getPat(req, res); -}); - -app.options("/api/pat", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/:packType - * @desc List all packages. - * @method GET - * @auth false - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @name page - * @location query - * @Ptype integer - * @default 1 - * @required false - * @Pdesc Indicate the page number to return. - * @param - * @name sort - * @Ptype string - * @location query - * @default downloads - * @valid downloads, created_at, updated_at, stars - * @required false - * @Pdesc The method to sort the returned pacakges by. - * @param - * @name direction - * @Ptype string - * @default desc - * @valid desc, asc - * @required false - * @Pdesc Which direction to list the results. If sorting by stars, can only be sorted by desc. - * @param - * @name service - * @Ptype string - * @required false - * @Pdesc A service to filter results by. - * @param - * @name serviceType - * @Ptype string - * @required false - * @valid provided, consumed - * @Pdesc The service type to filter results by. Must be supplied if a service is provided. - * @param - * @name serviceVersion - * @Ptype string - * @required false - * @Pdesc An optional (when providing a service) version to filter results by. - * @param - * @name fileExtension - * @Ptype string - * @required false - * @Pdesc The file extension to filter all results by. Must be just the file extension without any `.` - * @response - * @status 200 - * @Rtype application/json - * @Rdesc Returns a list of all packages. Paginated 30 at a time. Links to the next and last pages are in the 'Link' Header. - */ -app.get("/api/:packType", genericLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": { - let ret = await package_handler.getPackages( - { - page: query.page(req), - sort: query.sort(req), - direction: query.dir(req), - serviceType: query.serviceType(req), - service: query.service(req), - serviceVersion: query.serviceVersion(req), - fileExtension: query.fileExtension(req), - }, - database - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // Since we know this is a paginated endpoint we will handle that here - res.append("Link", ret.link); - res.append("Query-Total", ret.total); - res.append("Query-Limit", ret.limit); - - res.status(200).json(ret.content); - logger.httpLog(req, res); - - break; - } - case "themes": { - let ret = await theme_handler.getThemes( - { - page: query.page(req), - sort: query.sort(req), - direction: query.dir(req), - }, - database - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // Since we know this is a paginated endpoint we will handle that here - res.append("Link", ret.link); - res.append("Query-Total", ret.total); - res.append("Query-Limit", ret.limit); - - res.status(200).json(ret.content); - logger.httpLog(req, res); - - break; - } - default: { - next(); - break; - } - } -}); - -/** - * @web - * @ignore - * @path /api/packages - * @desc Publishes a new Package. - * @method POST - * @auth true - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @name repository - * @Ptype string - * @location query - * @required true - * @Pdesc The repository containing the plugin, in the form 'owner/repo'. - * @param - * @name Authentication - * @Ptype string - * @location header - * @required true - * @Pdesc A valid Atom.io token, in the 'Authorization' Header. - * @response - * @status 201 - * @Rtype application/json - * @Rdesc Successfully created, return created package. - * @response - * @status 400 - * @Rtype application/json - * @Rdesc Repository is inaccessible, nonexistant, not an atom package. Could be different errors returned. - * @Rexample { "message": "That repo does not exist, ins't an atom package, or atombot does not have access." }, { "message": "The package.json at owner/repo isn't valid." } - * @response - * @status 409 - * @Rtype application/json - * @Rdesc A package by that name already exists. - */ -app.post("/api/:packType", authLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - repository: query.repo(req), - auth: query.auth(req), - }; - - let ret = await package_handler.postPackages(params, database, auth, vcs); - - if (!ret.ok) { - if (ret.type === "detailed") { - await common_handler.handleDetailedError(req, res, ret.content); - return; - } else { - await common_handler.handleError(req, res, ret.content); - return; - } - } - - res.status(201).json(ret.content); - - // Return to user before webhook call, so user doesn't wait on it - await webhook.alertPublishPackage(ret.webhook.pack, ret.webhook.user); - // Now to call for feature detection - let features = await vcs.featureDetection( - ret.featureDetection.user, - ret.featureDetection.ownerRepo, - ret.featureDetection.service - ); - - if (!features.ok) { - logger.generic(3, features); - return; - } - - // Then we know we don't need to apply any special features for a standard - // package, so we will check that early - if (features.content.standard) { - return; - } - - let featureApply = await database.applyFeatures( - features.content, - ret.webhook.pack.name, - ret.wehbook.pack.version - ); - - if (!featureApply.ok) { - logger.generic(3, featureApply); - return; - } - - // Now everything has completed successfully - return; - - break; - default: - next(); - break; - } -}); - -app.options("/api/:packType", genericLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "POST, GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } -}); - -/** - * @web - * @ignore - * @path /api/:packType/featured - * @desc Previously Undocumented endpoint. Used to return featured packages from all existing packages. - * @method GET - * @auth false - * @param - * @name packType - * @location path - * @Ptype string - * @valid packages, themes - * @required true - * @Pdesc The Package Type you want to request. - * @response - * @status 200 - * @Rdesc An array of packages similar to /api/packages endpoint. - */ -app.get("/api/:packType/featured", genericLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": { - let ret = await package_handler.getPackagesFeatured(database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - } - case "themes": { - let ret = await theme_handler.getThemeFeatured(database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - } - default: { - next(); - break; - } - } -}); - -app.options("/api/:packType/featured", genericLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } -}); - -/** - * @web - * @ignore - * @path /api/:packType/search - * @desc Searches all Packages. - * @method GET - * @auth false - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @valid packages, themes - * @Pdesc The Package Type you want. - * @param - * @name q - * @Ptype string - * @required true - * @location query - * @Pdesc Search query. - * @param - * @name page - * @Ptype integer - * @required false - * @location query - * @Pdesc The page of search results to return. - * @param - * @name sort - * @Ptype string - * @required false - * @valid downloads, created_at, updated_at, stars - * @default relevance - * @location query - * @Pdesc Method to sort the results. - * @param - * @name direction - * @Ptype string - * @required false - * @valid asc, desc - * @default desc - * @location query - * @Pdesc Direction to list search results. - * @response - * @status 200 - * @Rtype application/json - * @Rdesc Same format as listing packages, additionally paginated at 30 items. - */ -app.get("/api/:packType/search", genericLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": { - let ret = await package_handler.getPackagesSearch( - { - sort: query.sort(req), - page: query.page(req), - direction: query.dir(req), - query: query.query(req), - }, - database - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // Since we know this is a paginated endpoint we must handle that here - res.append("Link", ret.link); - res.append("Query-Total", ret.total); - res.append("Query-Limit", ret.limit); - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - } - case "themes": { - const params = { - sort: query.sort(req), - page: query.page(req), - direction: query.dir(req), - query: query.query(req), - }; - - let ret = await theme_handler.getThemesSearch(params, database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // Since we know this is a paginated endpoint we must handle that here - res.append("Link", ret.link); - res.append("Query-Total", ret.total); - res.append("Query-Limit", ret.limit); - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - } - default: { - next(); - break; - } - } -}); - -app.options("/api/:packType/search", genericLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } -}); - -/** - * @web - * @ignore - * @path /api/packages/:packageName - * @desc Show package details. - * @method GET - * @auth false - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @name packageName - * @location path - * @Ptype string - * @Pdesc The name of the package to return details for. URL escaped. - * @required true - * @param - * @name engine - * @location query - * @Ptype string - * @Pdesc Only show packages compatible with this Atom version. Must be valid SemVer. - * @required false - * @response - * @status 200 - * @Rtype application/json - * @Rdesc Returns package details and versions for a single package. - */ -app.get("/api/:packType/:packageName", genericLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - // We can use the same handler here because the logic of the return - // Will be identical no matter what type of package it is. - const params = { - engine: query.engine(req.query.engine), - name: query.packageName(req), - }; - - let ret = await package_handler.getPackagesDetails(params, database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - default: - next(); - break; - } -}); - -/** - * @web - * @ignore - * @path /api/packages/:packageName - * @method DELETE - * @auth true - * @desc Delete a package. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @name packageName - * @location path - * @Ptype string - * @Pdesc The name of the package to delete. - * @required true - * @param - * @name Authorization - * @location header - * @Ptype string - * @Pdesc A valid Atom.io token, in the 'Authorization' Header. - * @required true - * @response - * @status 204 - * @Rtype application/json - * @Rdesc Successfully deleted package. Returns No Content. - * @response - * @status 400 - * @Rtype application/json - * @Rdesc Repository is inaccessible. - * @Rexample { "message": "Respository is inaccessible." } - * @response - * @status 401 - * @Rtype application/json - * @Rdesc Unauthorized. - */ -app.delete("/api/:packType/:packageName", authLimit, async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - auth: query.auth(req), - packageName: query.packageName(req), - }; - - let ret = await package_handler.deletePackagesName( - params, - database, - auth, - vcs - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // We know on success we should just return a statuscode - res.status(204).send(); - logger.httpLog(req, res); - - break; - default: - next(); - break; - } -}); - -app.options( - "/api/:packType/:packageName", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "DELETE, GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/star - * @method POST - * @auth true - * @desc Star a package. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @name packageName - * @location path - * @Ptype string - * @Pdesc The name of the package to star. - * @required true - * @param - * @name Authorization - * @location header - * @Ptype string - * @Pdesc A valid Atom.io token, in the 'Authorization' Header - * @required true - * @response - * @status 200 - * @Rtype application/json - * @Rdesc Returns the package that was stared. - */ -app.post( - "/api/:packType/:packageName/star", - authLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - auth: query.auth(req), - packageName: query.packageName(req), - }; - - let ret = await package_handler.postPackagesStar( - params, - database, - auth - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/star - * @method DELETE - * @auth true - * @desc Unstar a package, requires authentication. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @location header - * @Ptype string - * @name Authentication - * @required true - * @Pdesc Atom Token, in the Header Authentication Item - * @param - * @location path - * @Ptype string - * @name packageName - * @required true - * @Pdesc The package name to unstar. - * @response - * @status 201 - * @Rdesc An empty response to convey successfully unstaring a package. - */ -app.delete( - "/api/:packType/:packageName/star", - authLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - auth: query.auth(req), - packageName: query.packageName(req), - }; - - let ret = await package_handler.deletePackagesStar( - params, - database, - auth - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // On success we just return status code - res.status(201).send(); - logger.httpLog(req, res); - - break; - default: - next(); - break; - } - } -); - -app.options( - "/api/:packType/:packageName/star", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "DELETE, POST", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/stargazers - * @method GET - * @desc List the users that have starred a package. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @location path - * @required true - * @name packageName - * @Pdesc The package name to check for users stars. - * @response - * @status 200 - * @Rdesc A list of user Objects. - * @Rexample [ { "login": "aperson" }, { "login": "anotherperson" } ] - */ -app.get( - "/api/:packType/:packageName/stargazers", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - packageName: query.packageName(req), - }; - let ret = await package_handler.getPackagesStargazers(params, database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - default: - next(); - break; - } - } -); - -app.options( - "/api/:packType/:packageName/stargazers", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/versions - * @auth true - * @method POST - * @desc Creates a new package version. If `rename` is not `true`, the `name` field in `package.json` _must_ match the current package name. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @location path - * @name packageName - * @required true - * @Pdesc The Package to modify. - * @param - * @location query - * @name rename - * @required false - * @Pdesc Boolean indicating whether this version contains a new name for the package. - * @param - * @location header - * @name auth - * @required true - * @Pdesc A valid Atom.io API token, to authenticate with Github. - * @response - * @status 201 - * @Rdesc Successfully created. Returns created version. - * @response - * @status 400 - * @Rdesc Git tag not found / Repository inaccessible / package.json invalid. - */ -app.post( - "/api/:packType/:packageName/versions", - authLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - rename: query.rename(req), - auth: query.auth(req), - packageName: query.packageName(req), - }; - - let ret = await package_handler.postPackagesVersion( - params, - database, - auth, - vcs - ); - - if (!ret.ok) { - if (ret.type === "detailed") { - await common_handler.handleDetailedError(req, res, ret.content); - return; - } else { - await common_handler.handleError(req, res, ret.content); - return; - } - } - - res.status(201).json(ret.content); - - // Return to user before webhook call, so user doesn't wait on it - await webhook.alertPublishVersion(ret.webhook.pack, ret.webhook.user); - // Now to call for feature detection - let features = await vcs.featureDetection( - ret.featureDetection.user, - ret.featureDetection.ownerRepo, - ret.featureDetection.service - ); - - if (!features.ok) { - logger.generic(3, features); - return; - } - - // Then we know we don't need to apply any special features for a standard - // package, so we will check that early - if (features.content.standard) { - return; - } - - let featureApply = await database.applyFeatures( - features.content, - ret.webhook.pack.name, - ret.webhook.pack.version - ); - - if (!featureApply.ok) { - logger.generic(3, featureApply); - return; - } - - // Otherwise we have completed successfully. - // We could log this, but will just return - return; - break; - default: - next(); - break; - } - } -); - -app.options( - "/api/:packType/:packageName/versions", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "POST", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/versions/:versionName - * @method GET - * @auth false - * @desc Returns `package.json` with `dist` key added for tarball download. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @location path - * @name packageName - * @required true - * @Pdesc The package name we want to access - * @param - * @location path - * @name versionName - * @required true - * @Pdesc The Version we want to access. - * @response - * @status 200 - * @Rdesc The `package.json` modified as explainged in the endpoint description. - */ -app.get( - "/api/:packType/:packageName/versions/:versionName", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - packageName: query.packageName(req), - versionName: query.engine(req.params.versionName), - }; - - let ret = await package_handler.getPackagesVersion(params, database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/versions/:versionName - * @method DELETE - * @auth true - * @desc Deletes a package version. Note once a version is deleted, that same version should not be reused again. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @location header - * @name Authentication - * @required true - * @Pdesc The Authentication header containing a valid Atom Token - * @param - * @location path - * @name packageName - * @required true - * @Pdesc The package name to check for the version to delete. - * @param - * @location path - * @name versionName - * @required true - * @Pdesc The Package Version to actually delete. - * @response - * @status 204 - * @Rdesc Indicates a successful deletion. - */ -app.delete( - "/api/:packType/:packageName/versions/:versionName", - authLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - auth: query.auth(req), - packageName: query.packageName(req), - versionName: query.engine(req.params.versionName), - }; - - let ret = await package_handler.deletePackageVersion( - params, - database, - auth, - vcs - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // This is, on success, and empty return - res.status(204).send(); - logger.httpLog(req, res); - - break; - default: - next(); - break; - } - } -); - -app.options( - "/api/:packType/:packageName/versions/:versionName", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "GET, DELETE", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/versions/:versionName/tarball - * @method GET - * @auth false - * @desc Previously undocumented endpoint. Seems to allow for installation of a package. This is not currently implemented. - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @location path - * @name packageName - * @required true - * @Pdesc The package we want to download. - * @param - * @location path - * @name versionName - * @required true - * @Pdesc The package version we want to download. - * @response - * @status 200 - * @Rdesc The tarball data for the user to then be able to install. - */ -app.get( - "/api/:packType/:packageName/versions/:versionName/tarball", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - const params = { - packageName: query.packageName(req), - versionName: query.engine(req.params.versionName), - }; - - let ret = await package_handler.getPackagesVersionTarball( - params, - database - ); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // We know this endpoint, if successful will redirect, so that must be handled here - res.redirect(ret.content); - logger.httpLog(req, res); - - break; - default: - next(); - break; - } - } -); - -app.options( - "/api/:packType/:packageName/versions/:versionName/tarball", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/packages/:packageName/versions/:versionName/events/uninstall - * @desc Previously undocumented endpoint. BETA: Decreases the packages download count, by one. Indicating an uninstall. - * v1.0.2 - Now has no effect. Being deprecated, but presents no change to end users. - * @method POST - * @auth true - * @param - * @name packType - * @location path - * @Ptype string - * @required true - * @Pdesc The Package Type you want to request. - * @valid packages, themes - * @param - * @name packageName - * @location path - * @required true - * @Pdesc The name of the package to modify. - * @param - * @name versionName - * @location path - * @required true - * @Pdesc This value is within the original spec. But has no use in its current implementation. - * @param - * @name auth - * @location header - * @required true - * @Pdesc Valid Atom.io token. - * @response - * @status 200 - * @Rdesc Returns JSON ok: true - */ -app.post( - "/api/:packType/:packageName/versions/:versionName/events/uninstall", - authLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - /** - Used when a package is uninstalled, decreases the download count by 1. - Originally an undocumented endpoint. - The decision to return a '201' is based on how other POST endpoints return, - during a successful event. - This endpoint has now been deprecated, as it serves no useful features, - and on further examination may have been intended as a way to collect - data on users, which is not something we implement. - * Deprecated since v1.0.2 - * see: https://github.com/atom/apm/blob/master/src/uninstall.coffee - * While decoupling HTTP handling from logic, the function has been removed - entirely: https://github.com/pulsar-edit/package-backend/pull/171 - */ - res.status(200).json({ ok: true }); - logger.httpLog(req, res); - break; - default: - next(); - break; - } - } -); - -app.options( - "/api/:packType/:packageName/versions/:versionName/events/uninstall", - genericLimit, - async (req, res, next) => { - switch (req.params.packType) { - case "packages": - case "themes": - res.header({ - Allow: "POST", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); - break; - default: - next(); - break; - } - } -); - -/** - * @web - * @ignore - * @path /api/users/:login/stars - * @method GET - * @auth false - * @desc List a user's starred packages. - * @param - * @name login - * @Ptype string - * @required true - * @Pdesc The username of who to list their stars. - * @response - * @status 200 - * @Rdesc Return value is similar to GET /api/packages - * @response - * @status 404 - * @Rdesc If the login does not exist, a 404 is returned. - */ -app.get("/api/users/:login/stars", genericLimit, async (req, res) => { - const params = { - login: query.login(req), - }; - - let ret = await user_handler.getLoginStars(params, database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); -}); - -app.options("/api/users/:login/stars", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/users - * @method GET - * @desc Display details of the currently authenticated user. - * This endpoint is undocumented and technically doesn't exist. - * This is a strange endpoint that only exists on the Web version of the upstream - * API. Having no equivalent on the backend. This is an inferred implementation. - * @auth true - * @param - * @name auth - * @location header - * @Ptype string - * @required true - * @Pdesc Authorization Header of valid User Account Token. - * @response - * @status 200 - * @Rdesc The return Details of the User Account. - * @Rtype application/json - */ -app.get("/api/users", authLimit, async (req, res) => { - res.header("Access-Control-Allow-Methods", "GET"); - res.header( - "Access-Control-Allow-Headers", - "Content-Type, Authorization, Access-Control-Allow-Credentials" - ); - res.header("Access-Control-Allow-Origin", "https://web.pulsar-edit.dev"); - res.header("Access-Control-Allow-Credentials", true); - - const params = { - auth: query.auth(req), - }; - - let ret = await user_handler.getAuthUser(params, database, auth); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - // TODO: This was set within the function previously, needs to be determined if this is needed - res.set({ "Access-Control-Allow-Credentials": true }); - - res.status(200).json(ret.content); - logger.httpLog(req, res); -}); - -app.options("/api/users", async (req, res) => { - res.header({ - Allow: "GET", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": - "Content-Type, Authorization, Access-Control-Allow-Credentials", - "Access-Control-Allow-Origin": "https://web.pulsar-edit.dev", - "Access-Control-Allow-Credentials": true, - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/users/:login - * @method GET - * @desc Display the details of any user, as well as the packages they have published. - * @auth false - * @param - * @name login - * @location path - * @Ptype string - * @required true - * @Pdesc The User of which to collect the details of. - * @response - * @status 200 - * @Rdesc The returned details of a specific user, along with the packages they have published. - * @Rtype application/json - */ -app.get("/api/users/:login", genericLimit, async (req, res) => { - const params = { - login: query.login(req), - }; - - let ret = await user_handler.getUser(params, database); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); -}); - -app.options("/api/users/:login", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/stars - * @method GET - * @desc List the authenticated user's starred packages. - * @auth true - * @param - * @name auth - * @location header - * @Ptype string - * @required true - * @Pdesc Authorization Header of valid Atom.io Token. - * @response - * @status 200 - * @Rdesc Return value similar to GET /api/packages, an array of package objects. - * @Rtype application/json - */ -app.get("/api/stars", authLimit, async (req, res) => { - const params = { - auth: query.auth(req), - }; - - let ret = await star_handler.getStars(params, database, auth); - - if (!ret.ok) { - await common_handler.handleError(req, res, ret.content); - return; - } - - res.status(200).json(ret.content); - logger.httpLog(req, res); -}); - -app.options("/api/stars", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -/** - * @web - * @ignore - * @path /api/updates - * @method GET - * @desc List Atom Updates. - * @response - * @status 200 - * @Rtype application/json - * @Rdesc Atom update feed, following the format expected by Squirrel. - */ -app.get("/api/updates", genericLimit, async (req, res) => { - let ret = await update_handler.getUpdates(); - - if (!ret.ok) { - await common_handler.notSupported(req, res); - return; - } - - // TODO: There is no else until this endpoint is implemented. -}); - -app.options("/api/updates", genericLimit, async (req, res) => { - res.header({ - Allow: "GET", - "X-Content-Type-Options": "nosniff", - }); - res.sendStatus(204); -}); - -app.use(async (err, req, res, next) => { - // Having this as the last route, will handle all other unknown routes. - // Ensure to leave this at the very last position to handle properly. - // We can also check for any unhandled errors passed down the endpoint chain - - if (err) { - console.error( - `An error was encountered handling the request: ${err.toString()}` - ); - await common_handler.serverError(req, res, err); - return; - } - - common_handler.siteWideNotFound(req, res); -}); - -module.exports = app; diff --git a/tests/unit/debug_utils.test.js b/tests/unit/debug_utils.test.js deleted file mode 100644 index 0823b2a1..00000000 --- a/tests/unit/debug_utils.test.js +++ /dev/null @@ -1,31 +0,0 @@ -const debug_utils = require("../../src/debug_utils.js"); - -describe("Test lengths Returned by different Variables", () => { - const objectCases = [ - [ - { - value: "Hello World", - }, - 22, - ], - [ - { - boolean: true, - }, - 4, - ], - [ - { - obj: { - boolean: false, - value: "H", - }, - }, - 6, - ], - ]; - - test.each(objectCases)("Given %o Returns %p", (arg, expectedResult) => { - expect(debug_utils.roughSizeOfObject(arg)).toBe(expectedResult); - }); -}); From d17bd0dd49dad22735d9d8330562a5ae4fa7f2d8 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Wed, 8 Nov 2023 21:34:47 -0800 Subject: [PATCH 52/53] Ensure Codacy ignores migration files --- .codacy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codacy.yaml b/.codacy.yaml index 23259c5d..0a440619 100644 --- a/.codacy.yaml +++ b/.codacy.yaml @@ -4,4 +4,4 @@ engines: minTokenMatch: 80 exclude_paths: - "./docs/*" - - "./scripts/**" + - "./scripts/**/**" From 004005ed34410672ec7b5f56d4dddc754ca823bf Mon Sep 17 00:00:00 2001 From: confused_techie Date: Sat, 9 Dec 2023 17:04:41 -0800 Subject: [PATCH 53/53] Rename refactor-DONT_KEEP_THIS_FILE.md to refactor-docs.md --- .../{refactor-DONT_KEEP_THIS_FILE.md => refactor-docs.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/resources/{refactor-DONT_KEEP_THIS_FILE.md => refactor-docs.md} (100%) diff --git a/docs/resources/refactor-DONT_KEEP_THIS_FILE.md b/docs/resources/refactor-docs.md similarity index 100% rename from docs/resources/refactor-DONT_KEEP_THIS_FILE.md rename to docs/resources/refactor-docs.md