Skip to content

Commit

Permalink
Add profile service tests & fix profile service (#102)
Browse files Browse the repository at this point in the history
* added more fields to test tools

* added profile router testing file

* profile router bug fixes

* added leaderboard query limit

* changed all status codes to constants
  • Loading branch information
akulsharma1 authored Oct 25, 2023
1 parent ceb2a2d commit 968ff96
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ abstract class Constants {
static readonly MILLISECONDS_PER_SECOND: number = 1000;
static readonly DEFAULT_POINT_VALUE: number = 0;
static readonly DEFAULT_FOOD_WAVE: number = 0;
static readonly LEADERBOARD_QUERY_LIMIT: number = 25;
static readonly QR_EXPIRY_TIME: string = "20s";
}

Expand Down
173 changes: 173 additions & 0 deletions src/services/profile/profile-router.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { describe, expect, it, beforeEach } from "@jest/globals";
import Models from "../../database/models.js";
import { TESTER, delAsUser, getAsAdmin, getAsUser, postAsAttendee, postAsUser } from "../../testTools.js";
import { ProfileFormat } from "./profile-formats.js";
import Constants from "../../constants.js";
import { AttendeeMetadata, AttendeeProfile } from "database/attendee-db.js";
import { StatusCode } from "status-code-enum";

const TESTER_USER = {
userId: TESTER.id,
displayName: TESTER.name,
avatarUrl: TESTER.avatarUrl,
discordTag: TESTER.discordTag,
points: 0,
} satisfies AttendeeProfile;

const TESTER_METADATA = {
userId: TESTER.id,
foodWave: 0,
} satisfies AttendeeMetadata;

const TESTER_USER_2 = {
userId: "tester2",
displayName: TESTER.name + "2",
avatarUrl: TESTER.avatarUrl,
discordTag: TESTER.discordTag,
points: 12,
} satisfies AttendeeProfile;

const TESTER_USER_3 = {
userId: "tester3",
displayName: TESTER.name + "3",
avatarUrl: TESTER.avatarUrl,
discordTag: TESTER.discordTag,
points: 12,
} satisfies AttendeeProfile;

const profile: ProfileFormat = {
userId: TESTER.id,
displayName: TESTER.name,
avatarUrl: TESTER.avatarUrl,
discordTag: TESTER.discordTag,
points: 0,
};

beforeEach(async () => {
Models.initialize();
await Models.AttendeeProfile.create(TESTER_USER);
await Models.AttendeeMetadata.create(TESTER_METADATA);
await Models.AttendeeProfile.create(TESTER_USER_2);
await Models.AttendeeProfile.create(TESTER_USER_3);
});

describe("POST /profile", () => {
it("works for an attendee", async () => {
await Models.AttendeeProfile.deleteOne({ userId: TESTER_USER.userId });
const response = await postAsAttendee("/profile/").send(profile).expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toHaveProperty("displayName", TESTER.name);
});

it("fails when a profile is already created", async () => {
await Models.AttendeeProfile.deleteOne({ userId: TESTER_USER.userId });
const response = await postAsUser("/profile/").send(profile).expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toHaveProperty("displayName", TESTER.name);

// to verify they can't double create
const response2 = await postAsUser("/profile/").send(profile).expect(StatusCode.ClientErrorBadRequest);
expect(JSON.parse(response2.text)).toHaveProperty("error", "UserAlreadyExists");
});

it("fails when invalid data is provided", async () => {
const response = await postAsUser("/profile/")
.send({
displayName: 123,
avatarUrl: 1,
discordTag: "test",
})
.expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(response.text)).toHaveProperty("error", "InvalidParams");
});
});

describe("GET /profile", () => {
it("fails to get a profile that doesn't exist", async () => {
await Models.AttendeeProfile.deleteOne({ userId: TESTER_USER.userId });

const response = await getAsUser("/profile/").expect(StatusCode.ClientErrorNotFound);

expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
});

it("gets a profile that exists", async () => {
const response = await getAsUser("/profile/").expect(StatusCode.SuccessOK);
expect(JSON.parse(response.text)).toHaveProperty("displayName", TESTER.name);
});
});

describe("GET /profile/id/:USERID", () => {
it("redirects with no id provided", async () => {
await getAsUser("/profile/id").expect(StatusCode.RedirectFound);
});

it("fails to get a profile as a user", async () => {
const response = await getAsUser("/profile/id/" + TESTER.id).expect(StatusCode.ClientErrorForbidden);

expect(JSON.parse(response.text)).toHaveProperty("error", "Forbidden");
});

it("gets a profile as an admin", async () => {
const response = await getAsAdmin("/profile/id/" + TESTER.id).expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toHaveProperty("displayName", TESTER.name);
});

it("gets a user that doesnt exist", async () => {
const response = await getAsAdmin("/profile/id/doesnotexist").expect(StatusCode.ClientErrorNotFound);

expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
});
});

describe("DELETE /profile/", () => {
it("fails to delete a profile that doesn't exist", async () => {
await Models.AttendeeProfile.deleteOne({ userId: TESTER_USER.userId });
const response = await delAsUser("/profile").expect(StatusCode.ClientErrorNotFound);
expect(JSON.parse(response.text)).toHaveProperty("error", "AttendeeNotFound");
});

it("deletes a profile", async () => {
const response = await delAsUser("/profile").expect(StatusCode.SuccessOK);
expect(JSON.parse(response.text)).toHaveProperty("success", true);
});
});

describe("GET /profile/leaderboard", () => {
it("gets 3 entries when no limit is set", async () => {
await getAsUser("/profile/leaderboard").expect(StatusCode.SuccessOK);
});

it("gets with a limit of 2", async () => {
const response = await getAsUser("/profile/leaderboard?limit=2").expect(StatusCode.SuccessOK);

const responseArray = JSON.parse(response.text);
expect(responseArray.profiles.length).toBeLessThan(3);
});

it("only gets the max limit when no limit is set", async () => {
for (let i = 0; i < Constants.LEADERBOARD_QUERY_LIMIT + 15; i++) {
await Models.AttendeeProfile.create({
userId: TESTER.id + " " + i,
displayName: TESTER.name + " " + i,
avatarUrl: TESTER.avatarUrl,
discordTag: TESTER.discordTag,
points: i,
});
}

const response = await getAsUser("/profile/leaderboard").expect(StatusCode.SuccessOK);

const responseArray = JSON.parse(response.text);

expect(responseArray.profiles.length).toBeLessThan(Constants.LEADERBOARD_QUERY_LIMIT + 1);
});

it("fails when an invalid limit is set", async () => {
const response = await getAsUser("/profile/leaderboard?limit=0").expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(response.text)).toHaveProperty("error", "InvalidLimit");
});
});
40 changes: 29 additions & 11 deletions src/services/profile/profile-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { JwtPayload } from "../auth/auth-models.js";
import { strongJwtVerification } from "../../middleware/verify-jwt.js";
import { ProfileFormat, isValidProfileFormat } from "./profile-formats.js";
import { hasElevatedPerms } from "../auth/auth-lib.js";
import { DeleteResult } from "mongodb";
import { StatusCode } from "status-code-enum";

const profileRouter: Router = Router();
Expand Down Expand Up @@ -57,13 +58,20 @@ profileRouter.get("/leaderboard/", async (req: Request, res: Response) => {

// Returns NaN if invalid input is passed in
if (limitString) {
const limit = parseInt(limitString);
let limit = parseInt(limitString);

// Check for limit validity
if (!limit || !isValidLimit) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidLimit" });
}

// if the limit is above the leaderboard query limit, set it to the query limit
limit = Math.min(limit, Constants.LEADERBOARD_QUERY_LIMIT);

leaderboardQuery = leaderboardQuery.limit(limit);
} else {
const limit = Constants.LEADERBOARD_QUERY_LIMIT;

leaderboardQuery = leaderboardQuery.limit(limit);
}
// Perform the actual query, filter, and return the results
Expand Down Expand Up @@ -113,11 +121,11 @@ profileRouter.get("/", strongJwtVerification, async (_: Request, res: Response)
const payload: JwtPayload = res.locals.payload as JwtPayload;

const userId: string = payload.id;
console.log(userId);

const user: AttendeeProfile | null = await Models.AttendeeProfile.findOne({ userId: userId });

if (!user) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" });
}

return res.status(StatusCode.SuccessOK).send(user);
Expand Down Expand Up @@ -162,24 +170,25 @@ profileRouter.get("/id/:USERID", strongJwtVerification, async (req: Request, res
const userId: string | undefined = req.params.USERID;
const payload: JwtPayload = res.locals.payload as JwtPayload;

if (!userId) {
return res.redirect("/user/");
}

// Trying to perform elevated operation (getting someone else's profile without elevated perms)
if (userId != payload.id && !hasElevatedPerms(payload)) {
if (!hasElevatedPerms(payload)) {
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
}

const user: AttendeeProfile | null = await Models.AttendeeProfile.findOne({ userId: userId });

if (!user) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" });
}

return res.status(StatusCode.SuccessOK).send(user);
});

profileRouter.get("/id", (_: Request, res: Response) => {
// Redirect to the root URL
return res.redirect("/user");
});

/**
* @api {post} /profile POST /profile
* @apiGroup Profile
Expand Down Expand Up @@ -221,6 +230,9 @@ profileRouter.post("/", strongJwtVerification, async (req: Request, res: Respons
const profile: ProfileFormat = req.body as ProfileFormat;
profile.points = Constants.DEFAULT_POINT_VALUE;

const payload: JwtPayload = res.locals.payload as JwtPayload;
profile.userId = payload.id;

if (!isValidProfileFormat(profile)) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" });
}
Expand Down Expand Up @@ -264,9 +276,15 @@ profileRouter.post("/", strongJwtVerification, async (req: Request, res: Respons
profileRouter.delete("/", strongJwtVerification, async (_: Request, res: Response) => {
const decodedData: JwtPayload = res.locals.payload as JwtPayload;

await Models.AttendeeProfile.deleteOne({ userId: decodedData.id });
await Models.AttendeeMetadata.deleteOne({ userId: decodedData.id });
const attendeeProfileDeleteResponse: DeleteResult = await Models.AttendeeProfile.deleteOne({ userId: decodedData.id });
const attendeeMetadataDeleteResponse: DeleteResult = await Models.AttendeeMetadata.deleteOne({ userId: decodedData.id });

if (
attendeeMetadataDeleteResponse.deletedCount == Constants.ZERO ||
attendeeProfileDeleteResponse.deletedCount == Constants.ZERO
) {
return res.status(StatusCode.ClientErrorNotFound).send({ success: false, error: "AttendeeNotFound" });
}
return res.status(StatusCode.SuccessOK).send({ success: true });
});

Expand Down
2 changes: 2 additions & 0 deletions src/testTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const TESTER = {
id: "bob-the-tester101010101011",
email: "bob-the-tester@hackillinois.org",
name: "Bob Tester",
avatarUrl: "https://www.hackillinois.org",
discordTag: "hackillinoistest",
};

// A mapping of role to roles they have, used for JWT generation
Expand Down

0 comments on commit 968ff96

Please sign in to comment.