mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-05-15 14:38:03 +02:00
add tests
This commit is contained in:
Generated
+987
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --reporter spec",
|
"test": "mocha --reporter spec",
|
||||||
|
"coverage": "c8 --all --reporter=text --reporter=html --include='src/core/**/*.ts' --include='src/server/routes/**/*.ts' --include='src/config.ts' --exclude='src/**/*.d.ts' --exclude='src/core/model/**' npm test",
|
||||||
|
"coverage:lcov": "c8 --all --reporter=lcov --include='src/core/**/*.ts' --include='src/server/routes/**/*.ts' --include='src/config.ts' --exclude='src/**/*.d.ts' --exclude='src/core/model/**' npm test",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"start": "node --inspect=5858 -r ts-node/register ./src/server/index.ts",
|
"start": "node --inspect=5858 -r ts-node/register ./src/server/index.ts",
|
||||||
"dev": "nodemon --transpile-only ./src/server/index.ts",
|
"dev": "nodemon --transpile-only ./src/server/index.ts",
|
||||||
@@ -85,6 +87,7 @@
|
|||||||
"@types/passport-github2": "^1.2.9",
|
"@types/passport-github2": "^1.2.9",
|
||||||
"@types/sanitize-html": "^2.16.1",
|
"@types/sanitize-html": "^2.16.1",
|
||||||
"@types/unzip-stream": "^0.3.4",
|
"@types/unzip-stream": "^0.3.4",
|
||||||
|
"c8": "^10.1.3",
|
||||||
"chai": "^4.5.0",
|
"chai": "^4.5.0",
|
||||||
"eslint": "^10.2.0",
|
"eslint": "^10.2.0",
|
||||||
"gulp": "^5.0.0",
|
"gulp": "^5.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
const { expect } = require("chai");
|
||||||
|
require("ts-node/register/transpile-only");
|
||||||
|
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const {
|
||||||
|
isOwnerOrAdmin,
|
||||||
|
isCoauthor,
|
||||||
|
isOwnerCoauthorOrAdmin,
|
||||||
|
} = require("../src/server/routes/route-utils");
|
||||||
|
const AnonymousError = require("../src/core/AnonymousError").default;
|
||||||
|
const User = require("../src/core/User").default;
|
||||||
|
const UserModel = require("../src/core/model/users/users.model").default;
|
||||||
|
const Repository = require("../src/core/Repository").default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the authorization helpers in src/server/routes/route-utils.ts.
|
||||||
|
* These functions are pure (read-only on the User/Repository instances)
|
||||||
|
* so they can be exercised directly with hand-built mongoose-backed
|
||||||
|
* model objects without a live DB.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function makeUser({ id, username, isAdmin = false } = {}) {
|
||||||
|
const _id = id || new mongoose.Types.ObjectId();
|
||||||
|
return new User(
|
||||||
|
new UserModel({
|
||||||
|
_id,
|
||||||
|
id: _id.toString(),
|
||||||
|
username,
|
||||||
|
isAdmin,
|
||||||
|
accessTokens: { github: "tok" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRepo({ ownerId, coauthors = [] } = {}) {
|
||||||
|
return new Repository({
|
||||||
|
owner: ownerId || new mongoose.Types.ObjectId(),
|
||||||
|
repoId: "r1",
|
||||||
|
source: {},
|
||||||
|
options: {},
|
||||||
|
coauthors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("route-utils.isOwnerOrAdmin", function () {
|
||||||
|
it("returns silently when user id is in the authorized list", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
expect(() =>
|
||||||
|
isOwnerOrAdmin([user.model.id, "other"], user)
|
||||||
|
).to.not.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns silently when user is admin even if not listed", function () {
|
||||||
|
const user = makeUser({ username: "alice", isAdmin: true });
|
||||||
|
expect(() => isOwnerOrAdmin(["someone-else"], user)).to.not.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws not_authorized AnonymousError with httpStatus 401 otherwise", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
let caught;
|
||||||
|
try {
|
||||||
|
isOwnerOrAdmin(["someone-else"], user);
|
||||||
|
} catch (e) {
|
||||||
|
caught = e;
|
||||||
|
}
|
||||||
|
expect(caught).to.be.instanceOf(AnonymousError);
|
||||||
|
expect(caught.message).to.equal("not_authorized");
|
||||||
|
expect(caught.httpStatus).to.equal(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats an empty authorized list as unauthorized for non-admin", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
expect(() => isOwnerOrAdmin([], user)).to.throw(AnonymousError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("route-utils.isCoauthor", function () {
|
||||||
|
it("returns true when the user's username matches a coauthor entry", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
const repo = makeRepo({ coauthors: [{ username: "alice" }] });
|
||||||
|
expect(isCoauthor(repo, user)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when no coauthor matches the username", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
const repo = makeRepo({ coauthors: [{ username: "bob" }] });
|
||||||
|
expect(isCoauthor(repo, user)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when coauthors is undefined", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
const repo = makeRepo({ coauthors: undefined });
|
||||||
|
expect(isCoauthor(repo, user)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when coauthors is an empty list", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
const repo = makeRepo({ coauthors: [] });
|
||||||
|
expect(isCoauthor(repo, user)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the user has no username (early return)", function () {
|
||||||
|
const user = makeUser({ username: undefined });
|
||||||
|
const repo = makeRepo({ coauthors: [{ username: "alice" }] });
|
||||||
|
expect(isCoauthor(repo, user)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches case-sensitively (alice !== Alice)", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
const repo = makeRepo({ coauthors: [{ username: "Alice" }] });
|
||||||
|
expect(isCoauthor(repo, user)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("route-utils.isOwnerCoauthorOrAdmin", function () {
|
||||||
|
it("admin short-circuits regardless of ownership", function () {
|
||||||
|
const user = makeUser({ username: "carol", isAdmin: true });
|
||||||
|
const repo = makeRepo();
|
||||||
|
expect(() => isOwnerCoauthorOrAdmin(repo, user)).to.not.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("owner is allowed when user.id matches repo.owner.id", function () {
|
||||||
|
const id = new mongoose.Types.ObjectId();
|
||||||
|
const user = makeUser({ id, username: "alice" });
|
||||||
|
const repo = makeRepo({ ownerId: id });
|
||||||
|
expect(repo.owner.model.id).to.equal(user.model.id);
|
||||||
|
expect(() => isOwnerCoauthorOrAdmin(repo, user)).to.not.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("coauthor is allowed", function () {
|
||||||
|
const user = makeUser({ username: "alice" });
|
||||||
|
const repo = makeRepo({ coauthors: [{ username: "alice" }] });
|
||||||
|
expect(() => isOwnerCoauthorOrAdmin(repo, user)).to.not.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws not_authorized with httpStatus 401 for an unrelated user", function () {
|
||||||
|
const user = makeUser({ username: "stranger" });
|
||||||
|
const repo = makeRepo({ coauthors: [{ username: "alice" }] });
|
||||||
|
let caught;
|
||||||
|
try {
|
||||||
|
isOwnerCoauthorOrAdmin(repo, user);
|
||||||
|
} catch (e) {
|
||||||
|
caught = e;
|
||||||
|
}
|
||||||
|
expect(caught).to.be.instanceOf(AnonymousError);
|
||||||
|
expect(caught.message).to.equal("not_authorized");
|
||||||
|
expect(caught.httpStatus).to.equal(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
const { expect } = require("chai");
|
||||||
|
require("ts-node/register/transpile-only");
|
||||||
|
|
||||||
|
const { handleError } = require("../src/server/routes/route-utils");
|
||||||
|
const AnonymousError = require("../src/core/AnonymousError").default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct tests against the real handleError implementation. Each test
|
||||||
|
* builds a fake express response and asserts the status code + JSON
|
||||||
|
* body that handleError produced.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function makeRes() {
|
||||||
|
const res = {
|
||||||
|
statusCode: undefined,
|
||||||
|
body: undefined,
|
||||||
|
headersSent: false,
|
||||||
|
status(code) {
|
||||||
|
this.statusCode = code;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
json(body) {
|
||||||
|
this.body = body;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Silence the console.error noise that printError emits during tests.
|
||||||
|
let originalErr;
|
||||||
|
before(function () {
|
||||||
|
originalErr = console.error;
|
||||||
|
console.error = () => {};
|
||||||
|
});
|
||||||
|
after(function () {
|
||||||
|
console.error = originalErr;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("route-utils.handleError", function () {
|
||||||
|
it("uses error.httpStatus when present", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
const err = new AnonymousError("boom", { httpStatus: 418 });
|
||||||
|
handleError(err, res);
|
||||||
|
expect(res.statusCode).to.equal(418);
|
||||||
|
expect(res.body).to.deep.equal({ error: "boom" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to error.$metadata.httpStatusCode (S3-style errors)", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
const err = Object.assign(new Error("S3 down"), {
|
||||||
|
$metadata: { httpStatusCode: 503 },
|
||||||
|
});
|
||||||
|
handleError(err, res);
|
||||||
|
expect(res.statusCode).to.equal(503);
|
||||||
|
expect(res.body).to.deep.equal({ error: "S3 down" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps messages containing 'not_found' to 404", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
handleError(new Error("repo_not_found"), res);
|
||||||
|
expect(res.statusCode).to.equal(404);
|
||||||
|
expect(res.body).to.deep.equal({ error: "repo_not_found" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps messages containing '(Not Found)' (got HTTPError style) to 404", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
handleError(new Error("Response code 404 (Not Found)"), res);
|
||||||
|
expect(res.statusCode).to.equal(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps messages containing 'not_connected' to 401", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
handleError(new Error("user_not_connected"), res);
|
||||||
|
expect(res.statusCode).to.equal(401);
|
||||||
|
expect(res.body).to.deep.equal({ error: "user_not_connected" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to 500 when nothing matches", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
handleError(new Error("kaboom"), res);
|
||||||
|
expect(res.statusCode).to.equal(500);
|
||||||
|
expect(res.body).to.deep.equal({ error: "kaboom" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts a string error and stringifies it in the body", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
handleError("something_bad", res);
|
||||||
|
expect(res.statusCode).to.equal(500);
|
||||||
|
expect(res.body).to.deep.equal({ error: "something_bad" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call res when headersSent is true", function () {
|
||||||
|
const res = makeRes();
|
||||||
|
res.headersSent = true;
|
||||||
|
handleError(new Error("late"), res);
|
||||||
|
expect(res.statusCode).to.equal(undefined);
|
||||||
|
expect(res.body).to.equal(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is a no-op-on-res when no res is passed", function () {
|
||||||
|
expect(() => handleError(new Error("noop"))).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user