Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion controllers/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ const nudgeApplication = async (req: CustomRequest, res: CustomResponse) => {
const lastNudgeTimestamp = new Date(lastNudgeAt).getTime();
const timeDifference = currentTime - lastNudgeTimestamp;

if (timeDifference < twentyFourHoursInMilliseconds) {
if (timeDifference <= twentyFourHoursInMilliseconds) {
return res.boom.tooManyRequests(APPLICATION_ERROR_MESSAGES.NUDGE_TOO_SOON);
}
}
Expand Down
2 changes: 2 additions & 0 deletions services/discordService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const firestore = require("../utils/firestore");
const { fetchAllUsers } = require("../models/users");
const { generateAuthTokenForCloudflare, generateCloudFlareHeaders } = require("../utils/discord-actions");
const logger = require("../utils/logger");
const config = require("config");
const userModel = firestore.collection("users");
const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl");

Expand Down
1 change: 1 addition & 0 deletions services/logService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import firestore from "../utils/firestore";
const logsModel = firestore.collection("logs");
import admin from "firebase-admin";
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
const logger = require("../utils/logger");

interface LogMeta {
userId?: string;
Expand Down
132 changes: 132 additions & 0 deletions test/integration/application.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chai from "chai";
import chaiHttp from "chai-http";
const { expect } = chai;
import config from "config";
import sinon from "sinon";
const app = require("../../server");
const addUser = require("../utils/addUser");
const cleanDb = require("../utils/cleanDb");
Expand Down Expand Up @@ -65,6 +66,7 @@ describe("Application", function () {

after(async function () {
await cleanDb();
sinon.restore();
});

describe("GET /applications", function () {
Expand Down Expand Up @@ -488,4 +490,134 @@ describe("Application", function () {
});
});
});

describe("PATCH /applications/:applicationId/nudge", function () {
let nudgeApplicationId: string;

beforeEach(async function () {
const applicationData = { ...applicationsData[0], userId };
nudgeApplicationId = await applicationModel.addApplication(applicationData);
});

afterEach(async function () {
sinon.restore();
});

it("should successfully nudge an application when user owns it and no previous nudge exists", function (done) {
chai
.request(app)
.patch(`/applications/${nudgeApplicationId}/nudge`)
.set("cookie", `${cookieName}=${jwt}`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(200);
expect(res.body.message).to.be.equal("Application nudged successfully");
expect(res.body.nudgeCount).to.be.equal(1);
expect(res.body.lastNudgeAt).to.be.a("string");
done();
});
});

it("should successfully nudge an application when 24 hours have passed since last nudge", function (done) {
chai
.request(app)
.patch(`/applications/${nudgeApplicationId}/nudge`)
.set("cookie", `${cookieName}=${jwt}`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(200);
expect(res.body.nudgeCount).to.be.equal(1);

const twentyFiveHoursAgo = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
applicationModel.updateApplication({ lastNudgeAt: twentyFiveHoursAgo }, nudgeApplicationId).then(() => {
chai
.request(app)
.patch(`/applications/${nudgeApplicationId}/nudge`)
.set("cookie", `${cookieName}=${jwt}`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(200);
expect(res.body.message).to.be.equal("Application nudged successfully");
expect(res.body.nudgeCount).to.be.equal(2);
expect(res.body.lastNudgeAt).to.be.a("string");
done();
});
});
});
});

it("should return 404 if the application doesn't exist", function (done) {
chai
.request(app)
.patch(`/applications/non-existent-id/nudge`)
.set("cookie", `${cookieName}=${jwt}`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(404);
expect(res.body.error).to.be.equal("Not Found");
expect(res.body.message).to.be.equal("Application not found");
done();
});
});

it("should return 401 if user is not authenticated", function (done) {
chai
.request(app)
.patch(`/applications/${nudgeApplicationId}/nudge`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(401);
expect(res.body.error).to.be.equal("Unauthorized");
expect(res.body.message).to.be.equal("Unauthenticated User");
done();
});
});

it("should return 401 if user does not own the application", function (done) {
chai
.request(app)
.patch(`/applications/${nudgeApplicationId}/nudge`)
.set("cookie", `${cookieName}=${secondUserJwt}`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(401);
expect(res.body.error).to.be.equal("Unauthorized");
expect(res.body.message).to.be.equal("You are not authorized to nudge this application");
done();
});
});

it("should return 429 when trying to nudge within 24 hours", function (done) {
chai
.request(app)
.patch(`/applications/${nudgeApplicationId}/nudge`)
.set("cookie", `${cookieName}=${jwt}`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(200);

chai
.request(app)
.patch(`/applications/${nudgeApplicationId}/nudge`)
.set("cookie", `${cookieName}=${jwt}`)
.end(function (err, res) {
if (err) return done(err);

expect(res).to.have.status(429);
expect(res.body.error).to.be.equal("Too Many Requests");
expect(res.body.message).to.be.equal(
"Cannot nudge application. Please wait 24 hours since the last nudge."
);
done();
});
});
});
});
});
210 changes: 210 additions & 0 deletions test/unit/controllers/applications.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { expect } from "chai";
import sinon from "sinon";
import { CustomRequest, CustomResponse } from "../../../types/global";
const applicationsController = require("../../../controllers/applications");
const ApplicationModel = require("../../../models/applications");
const { API_RESPONSE_MESSAGES, APPLICATION_ERROR_MESSAGES } = require("../../../constants/application");
const { convertDaysToMilliseconds } = require("../../../utils/time");

describe("nudgeApplication", () => {
let req: Partial<CustomRequest>;
let res: Partial<CustomResponse> & {
json: sinon.SinonSpy;
boom: {
notFound: sinon.SinonSpy;
unauthorized: sinon.SinonSpy;
tooManyRequests: sinon.SinonSpy;
badImplementation: sinon.SinonSpy;
};
};
let jsonSpy: sinon.SinonSpy;
let boomNotFound: sinon.SinonSpy;
let boomUnauthorized: sinon.SinonSpy;
let boomTooManyRequests: sinon.SinonSpy;
let boomBadImplementation: sinon.SinonSpy;

const mockApplicationId = "test-application-id-123";
const mockUserId = "test-user-id-456";
const mockOtherUserId = "other-user-id-789";

beforeEach(() => {
jsonSpy = sinon.spy();
boomNotFound = sinon.spy();
boomUnauthorized = sinon.spy();
boomTooManyRequests = sinon.spy();
boomBadImplementation = sinon.spy();

req = {
params: {
applicationId: mockApplicationId,
},
userData: {
id: mockUserId,
username: "testuser",
},
};

res = {
json: jsonSpy,
boom: {
notFound: boomNotFound,
unauthorized: boomUnauthorized,
tooManyRequests: boomTooManyRequests,
badImplementation: boomBadImplementation,
},
};
});

afterEach(() => {
sinon.restore();
});

describe("Success cases", () => {
it("should successfully nudge an application when no previous nudge exists", async () => {
const mockApplication = {
id: mockApplicationId,
userId: mockUserId,
notFound: false,
lastNudgeAt: null,
nudgeCount: 0,
};

const getApplicationByIdStub = sinon.stub(ApplicationModel, "getApplicationById").resolves(mockApplication);
const updateApplicationStub = sinon.stub(ApplicationModel, "updateApplication").resolves();

await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse);

expect(getApplicationByIdStub.calledOnce).to.be.true;
expect(getApplicationByIdStub.calledWith(mockApplicationId)).to.be.true;
expect(updateApplicationStub.calledOnce).to.be.true;

const updateData = updateApplicationStub.firstCall.args[0];
expect(updateData.nudgeCount).to.equal(1);
expect(updateData.lastNudgeAt).to.be.a("string");
expect(new Date(updateData.lastNudgeAt).getTime()).to.be.closeTo(Date.now(), 1000);

expect(jsonSpy.calledOnce).to.be.true;
expect(jsonSpy.firstCall.args[0].message).to.equal(API_RESPONSE_MESSAGES.NUDGE_SUCCESS);
expect(jsonSpy.firstCall.args[0].nudgeCount).to.equal(1);
expect(jsonSpy.firstCall.args[0].lastNudgeAt).to.be.a("string");
});

it("should successfully nudge an application when 24 hours have passed since last nudge", async () => {
const twentyFourHoursAgo = new Date(Date.now() - convertDaysToMilliseconds(1) - 1000).toISOString();
const mockApplication = {
id: mockApplicationId,
userId: mockUserId,
notFound: false,
lastNudgeAt: twentyFourHoursAgo,
nudgeCount: 2,
};

const getApplicationByIdStub = sinon.stub(ApplicationModel, "getApplicationById").resolves(mockApplication);
const updateApplicationStub = sinon.stub(ApplicationModel, "updateApplication").resolves();

await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse);

expect(getApplicationByIdStub.calledOnce).to.be.true;
expect(getApplicationByIdStub.calledWith(mockApplicationId)).to.be.true;
expect(updateApplicationStub.calledOnce).to.be.true;

const updateData = updateApplicationStub.firstCall.args[0];
expect(updateData.nudgeCount).to.equal(3);
expect(updateData.lastNudgeAt).to.be.a("string");

expect(jsonSpy.calledOnce).to.be.true;
expect(jsonSpy.firstCall.args[0].message).to.equal(API_RESPONSE_MESSAGES.NUDGE_SUCCESS);
expect(jsonSpy.firstCall.args[0].nudgeCount).to.equal(3);
});

it("should increment nudgeCount correctly when nudgeCount is undefined", async () => {
const mockApplication = {
id: mockApplicationId,
userId: mockUserId,
notFound: false,
lastNudgeAt: null,
nudgeCount: undefined,
};

sinon.stub(ApplicationModel, "getApplicationById").resolves(mockApplication);
const updateApplicationStub = sinon.stub(ApplicationModel, "updateApplication").resolves();

await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse);

const updateData = updateApplicationStub.firstCall.args[0];
expect(updateData.nudgeCount).to.equal(1);
});
});

describe("Error cases", () => {
it("should return 404 when application is not found", async () => {
const mockApplication = {
notFound: true,
};

sinon.stub(ApplicationModel, "getApplicationById").resolves(mockApplication);

await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse);

expect(boomNotFound.calledOnce).to.be.true;
expect(boomNotFound.firstCall.args[0]).to.equal("Application not found");
expect(jsonSpy.notCalled).to.be.true;
});

it("should return 401 when user is not authorized (not the owner)", async () => {
const mockApplication = {
id: mockApplicationId,
userId: mockOtherUserId,
notFound: false,
lastNudgeAt: null,
nudgeCount: 0,
};

sinon.stub(ApplicationModel, "getApplicationById").resolves(mockApplication);

await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse);

expect(boomUnauthorized.calledOnce).to.be.true;
expect(boomUnauthorized.firstCall.args[0]).to.equal("You are not authorized to nudge this application");
expect(jsonSpy.notCalled).to.be.true;
});

it("should return 429 when trying to nudge within 24 hours", async () => {
const oneHourAgo = new Date(Date.now() - convertDaysToMilliseconds(1) / 24).toISOString();
const mockApplication = {
id: mockApplicationId,
userId: mockUserId,
notFound: false,
lastNudgeAt: oneHourAgo,
nudgeCount: 1,
};

sinon.stub(ApplicationModel, "getApplicationById").resolves(mockApplication);

await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse);

expect(boomTooManyRequests.calledOnce).to.be.true;
expect(boomTooManyRequests.firstCall.args[0]).to.equal(APPLICATION_ERROR_MESSAGES.NUDGE_TOO_SOON);
expect(jsonSpy.notCalled).to.be.true;
});

it("should return 429 when trying to nudge exactly at 24 hours", async () => {
const exactlyTwentyFourHoursAgo = new Date(Date.now() - convertDaysToMilliseconds(1)).toISOString();
const mockApplication = {
id: mockApplicationId,
userId: mockUserId,
notFound: false,
lastNudgeAt: exactlyTwentyFourHoursAgo,
nudgeCount: 1,
};

sinon.stub(ApplicationModel, "getApplicationById").resolves(mockApplication);

await applicationsController.nudgeApplication(req as CustomRequest, res as CustomResponse);

expect(boomTooManyRequests.calledOnce).to.be.true;
expect(boomTooManyRequests.firstCall.args[0]).to.equal(APPLICATION_ERROR_MESSAGES.NUDGE_TOO_SOON);
expect(jsonSpy.notCalled).to.be.true;
});
});
});