2219 lines
58 KiB
TypeScript
2219 lines
58 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { CustomThemeColors } from "@monkeytype/schemas/configs";
|
|
import { PersonalBest, PersonalBests } from "@monkeytype/schemas/shared";
|
|
import { MonkeyMail, ResultFilters } from "@monkeytype/schemas/users";
|
|
import { ObjectId } from "mongodb";
|
|
import * as UserDAL from "../../../src/dal/user";
|
|
import { createConnection as createFriend } from "../../__testData__/connections";
|
|
import * as UserTestData from "../../__testData__/users";
|
|
|
|
const mockPersonalBest: PersonalBest = {
|
|
acc: 1,
|
|
consistency: 1,
|
|
difficulty: "normal" as const,
|
|
lazyMode: true,
|
|
language: "polish",
|
|
punctuation: false,
|
|
raw: 230,
|
|
wpm: 215,
|
|
timestamp: 13123123,
|
|
};
|
|
|
|
const mockResultFilter: ResultFilters = {
|
|
_id: "id",
|
|
name: "sfdkjhgdf",
|
|
pb: {
|
|
no: true,
|
|
yes: true,
|
|
},
|
|
difficulty: {
|
|
normal: true,
|
|
expert: false,
|
|
master: false,
|
|
},
|
|
mode: {
|
|
words: false,
|
|
time: true,
|
|
quote: false,
|
|
zen: false,
|
|
custom: false,
|
|
},
|
|
words: {
|
|
"10": false,
|
|
"25": false,
|
|
"50": false,
|
|
"100": false,
|
|
custom: false,
|
|
},
|
|
time: {
|
|
"15": false,
|
|
"30": true,
|
|
"60": false,
|
|
"120": false,
|
|
custom: false,
|
|
},
|
|
quoteLength: {
|
|
short: false,
|
|
medium: false,
|
|
long: false,
|
|
thicc: false,
|
|
},
|
|
punctuation: {
|
|
on: false,
|
|
off: true,
|
|
},
|
|
numbers: {
|
|
on: false,
|
|
off: true,
|
|
},
|
|
date: {
|
|
last_day: false,
|
|
last_week: false,
|
|
last_month: false,
|
|
last_3months: false,
|
|
all: true,
|
|
},
|
|
tags: {
|
|
none: true,
|
|
},
|
|
language: {
|
|
english: true,
|
|
},
|
|
funbox: {
|
|
none: true,
|
|
},
|
|
};
|
|
|
|
const mockDbResultFilter = { ...mockResultFilter, _id: new ObjectId() };
|
|
|
|
describe("UserDal", () => {
|
|
it("should be able to insert users", async () => {
|
|
// given
|
|
const uid = new ObjectId().toHexString();
|
|
const newUser = {
|
|
name: "Test",
|
|
email: "mockemail@email.com",
|
|
uid,
|
|
};
|
|
|
|
// when
|
|
await UserDAL.addUser(newUser.name, newUser.email, newUser.uid);
|
|
const insertedUser = await UserDAL.getUser(newUser.uid, "test");
|
|
|
|
// then
|
|
expect(insertedUser.email).toBe(newUser.email);
|
|
expect(insertedUser.uid).toBe(newUser.uid);
|
|
expect(insertedUser.name).toBe(newUser.name);
|
|
});
|
|
|
|
it("should error if the user already exists", async () => {
|
|
// given
|
|
const uid = new ObjectId().toHexString();
|
|
const newUser = {
|
|
name: "Test",
|
|
email: "mockemail@email.com",
|
|
uid: uid,
|
|
};
|
|
|
|
// when
|
|
await UserDAL.addUser(newUser.name, newUser.email, newUser.uid);
|
|
|
|
// then
|
|
// should error because user already exists
|
|
await expect(
|
|
UserDAL.addUser(newUser.name, newUser.email, newUser.uid),
|
|
).rejects.toThrow("User document already exists");
|
|
});
|
|
|
|
it("isNameAvailable should correctly check if a username is available", async () => {
|
|
// given
|
|
const name1 = "user" + new ObjectId().toHexString();
|
|
const name2 = "user" + new ObjectId().toHexString();
|
|
const { uid: user1 } = await UserTestData.createUser({ name: name1 });
|
|
await UserTestData.createUser({ name: name2 });
|
|
|
|
const testCases = [
|
|
{
|
|
name: name1,
|
|
whosChecking: user1,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: name1.toUpperCase(),
|
|
whosChecking: user1,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: name2,
|
|
whosChecking: user1,
|
|
expected: false,
|
|
},
|
|
];
|
|
|
|
// when, then
|
|
for (const { name, expected, whosChecking } of testCases) {
|
|
const isAvailable = await UserDAL.isNameAvailable(name, whosChecking);
|
|
expect(isAvailable).toBe(expected);
|
|
}
|
|
});
|
|
|
|
it("updatename should not allow unavailable usernames", async () => {
|
|
// given
|
|
const name1 = "user" + new ObjectId().toHexString();
|
|
const name2 = "user" + new ObjectId().toHexString();
|
|
const user1 = await UserTestData.createUser({ name: name1 });
|
|
const user2 = await UserTestData.createUser({ name: name2 });
|
|
const _decoy = await UserTestData.createUser();
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.updateName(user1.uid, user2.name, user1.name),
|
|
).rejects.toThrow("Username already taken");
|
|
});
|
|
|
|
it("same usernames (different casing) should be available only for the same user", async () => {
|
|
const name1 = "user" + new ObjectId().toHexString();
|
|
const name2 = "user" + new ObjectId().toHexString();
|
|
const user1 = await UserTestData.createUser({ name: name1 });
|
|
const user2 = await UserTestData.createUser({ name: name2 });
|
|
|
|
await UserDAL.updateName(user1.uid, name1.toUpperCase(), user1.name);
|
|
|
|
const updatedUser1 = await UserDAL.getUser(user1.uid, "test");
|
|
|
|
// when, then
|
|
expect(updatedUser1.name).toBe(name1.toUpperCase());
|
|
|
|
await expect(
|
|
UserDAL.updateName(user2.uid, name1, user2.name),
|
|
).rejects.toThrow("Username already taken");
|
|
});
|
|
|
|
it("UserDAL.updateName should change the name of a user", async () => {
|
|
// given
|
|
const name = "user" + new ObjectId().toHexString();
|
|
const renamed = "renamed" + new ObjectId().toHexString();
|
|
const testUser = await UserTestData.createUser({ name: name });
|
|
|
|
// when
|
|
await UserDAL.updateName(testUser.uid, renamed, testUser.name);
|
|
|
|
// then
|
|
const updatedUser = await UserDAL.getUser(testUser.uid, "test");
|
|
expect(updatedUser.name).toBe(renamed);
|
|
});
|
|
|
|
it("clearPb should clear the personalBests of a user", async () => {
|
|
// given
|
|
const testUser = await UserTestData.createUser();
|
|
await UserDAL.getUsersCollection().updateOne(
|
|
{ uid: testUser.uid },
|
|
{
|
|
$set: {
|
|
personalBests: {
|
|
time: { 20: [mockPersonalBest] },
|
|
words: {},
|
|
quote: {},
|
|
zen: {},
|
|
custom: {},
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
const { personalBests } =
|
|
(await UserDAL.getUser(testUser.uid, "test")) ?? {};
|
|
expect(personalBests).toStrictEqual({
|
|
time: { 20: [mockPersonalBest] },
|
|
words: {},
|
|
quote: {},
|
|
custom: {},
|
|
zen: {},
|
|
});
|
|
// when
|
|
await UserDAL.clearPb(testUser.uid);
|
|
|
|
// then
|
|
const updatedUser = (await UserDAL.getUser(testUser.uid, "test")) ?? {};
|
|
expect(updatedUser.personalBests).toStrictEqual({
|
|
time: {},
|
|
words: {},
|
|
quote: {},
|
|
custom: {},
|
|
zen: {},
|
|
});
|
|
});
|
|
|
|
it("autoBan should automatically ban after configured anticheat triggers", async () => {
|
|
// given
|
|
const testUser = await UserTestData.createUser();
|
|
|
|
// when
|
|
Date.now = vi.fn(() => 0);
|
|
await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1);
|
|
await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1);
|
|
await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1);
|
|
|
|
// then
|
|
const updatedUser = await UserDAL.getUser(testUser.uid, "test");
|
|
expect(updatedUser.banned).toBe(true);
|
|
expect(updatedUser.autoBanTimestamps).toEqual([0, 0, 0]);
|
|
});
|
|
|
|
it("autoBan should not ban ban if triggered once", async () => {
|
|
// given
|
|
const testUser = await UserTestData.createUser();
|
|
|
|
// when
|
|
Date.now = vi.fn(() => 0);
|
|
await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1);
|
|
|
|
// then
|
|
const updatedUser = await UserDAL.getUser(testUser.uid, "test");
|
|
expect(updatedUser.banned).toBe(undefined);
|
|
expect(updatedUser.autoBanTimestamps).toEqual([0]);
|
|
});
|
|
|
|
it("autoBan should correctly remove old anticheat triggers", async () => {
|
|
// given
|
|
const testUser = await UserTestData.createUser();
|
|
|
|
// when
|
|
Date.now = vi.fn(() => 0);
|
|
await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1);
|
|
await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1);
|
|
|
|
Date.now = vi.fn(() => 36000000);
|
|
|
|
await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1);
|
|
|
|
// then
|
|
const updatedUser = await UserDAL.getUser(testUser.uid, "test");
|
|
expect(updatedUser.banned).toBe(undefined);
|
|
expect(updatedUser.autoBanTimestamps).toEqual([36000000]);
|
|
});
|
|
|
|
describe("addResultFilterPreset", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addResultFilterPreset("non existing uid", mockResultFilter, 5),
|
|
).rejects.toThrow(
|
|
"Maximum number of custom filters reached\nStack: add result filter preset",
|
|
);
|
|
});
|
|
|
|
it("should return error if user has reached maximum", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
resultFilterPresets: [mockDbResultFilter],
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addResultFilterPreset(uid, mockResultFilter, 1),
|
|
).rejects.toThrow(
|
|
"Maximum number of custom filters reached\nStack: add result filter preset",
|
|
);
|
|
});
|
|
|
|
it("should handle zero maximum", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser();
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addResultFilterPreset(uid, mockResultFilter, 0),
|
|
).rejects.toThrow(
|
|
"Maximum number of custom filters reached\nStack: add result filter preset",
|
|
);
|
|
});
|
|
|
|
it("addResultFilterPreset success", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
resultFilterPresets: [mockDbResultFilter],
|
|
});
|
|
|
|
// when
|
|
const result = await UserDAL.addResultFilterPreset(
|
|
uid,
|
|
{ ...mockResultFilter },
|
|
2,
|
|
);
|
|
|
|
// then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
const createdFilter = read.resultFilterPresets ?? [];
|
|
|
|
expect(result).toStrictEqual(createdFilter[1]?._id);
|
|
});
|
|
});
|
|
|
|
describe("removeResultFilterPreset", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeResultFilterPreset(
|
|
"non existing uid",
|
|
new ObjectId().toHexString(),
|
|
),
|
|
).rejects.toThrow("Custom filter not found\nStack: remove result filter");
|
|
});
|
|
|
|
it("should return error if filter is unknown", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
resultFilterPresets: [mockDbResultFilter],
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeResultFilterPreset(uid, new ObjectId().toHexString()),
|
|
).rejects.toThrow("Custom filter not found\nStack: remove result filter");
|
|
});
|
|
it("should remove filter", async () => {
|
|
// given
|
|
const filterOne = { ...mockDbResultFilter, _id: new ObjectId() };
|
|
const filterTwo = { ...mockDbResultFilter, _id: new ObjectId() };
|
|
const filterThree = { ...mockDbResultFilter, _id: new ObjectId() };
|
|
const { uid } = await UserTestData.createUser({
|
|
resultFilterPresets: [filterOne, filterTwo, filterThree],
|
|
});
|
|
|
|
// when, then
|
|
await UserDAL.removeResultFilterPreset(uid, filterTwo._id.toHexString());
|
|
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.resultFilterPresets).toStrictEqual([filterOne, filterThree]);
|
|
});
|
|
});
|
|
|
|
describe("addTag", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addTag("non existing uid", "tagName"),
|
|
).rejects.toThrow("Maximum number of tags reached\nStack: add tag");
|
|
});
|
|
|
|
it("should return error if user has reached maximum", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: new Array(15).fill(0).map(() => ({
|
|
_id: new ObjectId(),
|
|
name: "any",
|
|
personalBests: {} as any,
|
|
})),
|
|
});
|
|
|
|
// when, then
|
|
await expect(UserDAL.addTag(uid, "new")).rejects.toThrow(
|
|
"Maximum number of tags reached\nStack: add tag",
|
|
);
|
|
});
|
|
|
|
it("addTag success", async () => {
|
|
// given
|
|
const emptyPb: PersonalBests = {
|
|
time: {},
|
|
words: {},
|
|
quote: {},
|
|
zen: {},
|
|
custom: {},
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: [
|
|
{
|
|
_id: new ObjectId(),
|
|
name: "first",
|
|
personalBests: emptyPb,
|
|
},
|
|
],
|
|
});
|
|
|
|
// when
|
|
await UserDAL.addTag(uid, "newTag");
|
|
|
|
// then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ name: "first", personalBests: emptyPb }),
|
|
expect.objectContaining({ name: "newTag", personalBests: emptyPb }),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("editTag", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.editTag(
|
|
"non existing uid",
|
|
new ObjectId().toHexString(),
|
|
"newName",
|
|
),
|
|
).rejects.toThrow("Tag not found\nStack: edit tag");
|
|
});
|
|
|
|
it("should fail if tag not found", async () => {
|
|
// given
|
|
const tagOne: UserDAL.DBUserTag = {
|
|
_id: new ObjectId(),
|
|
name: "one",
|
|
personalBests: {} as any,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: [tagOne],
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.editTag(uid, new ObjectId().toHexString(), "newName"),
|
|
).rejects.toThrow("Tag not found\nStack: edit tag");
|
|
});
|
|
|
|
it("editTag success", async () => {
|
|
// given
|
|
const tagOne: UserDAL.DBUserTag = {
|
|
_id: new ObjectId(),
|
|
name: "one",
|
|
personalBests: {} as any,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: [tagOne],
|
|
});
|
|
|
|
// when
|
|
await UserDAL.editTag(uid, tagOne._id.toHexString(), "newTagName");
|
|
|
|
// then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.tags ?? [][0]).toStrictEqual([
|
|
{ ...tagOne, name: "newTagName" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("removeTag", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeTag("non existing uid", new ObjectId().toHexString()),
|
|
).rejects.toThrow("Tag not found\nStack: remove tag");
|
|
});
|
|
|
|
it("should return error if tag is unknown", async () => {
|
|
// given
|
|
const tagOne: UserDAL.DBUserTag = {
|
|
_id: new ObjectId(),
|
|
name: "one",
|
|
personalBests: {} as any,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: [tagOne],
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeTag(uid, new ObjectId().toHexString()),
|
|
).rejects.toThrow("Tag not found\nStack: remove tag");
|
|
});
|
|
it("should remove tag", async () => {
|
|
// given
|
|
const tagOne = {
|
|
_id: new ObjectId(),
|
|
name: "tagOne",
|
|
personalBests: {} as any,
|
|
};
|
|
const tagTwo = {
|
|
_id: new ObjectId(),
|
|
name: "tagTwo",
|
|
personalBests: {} as any,
|
|
};
|
|
const tagThree = {
|
|
_id: new ObjectId(),
|
|
name: "tagThree",
|
|
personalBests: {} as any,
|
|
};
|
|
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: [tagOne, tagTwo, tagThree],
|
|
});
|
|
|
|
// when, then
|
|
await UserDAL.removeTag(uid, tagTwo._id.toHexString());
|
|
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.tags).toStrictEqual([tagOne, tagThree]);
|
|
});
|
|
});
|
|
|
|
describe("removeTagPb", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeTagPb("non existing uid", new ObjectId().toHexString()),
|
|
).rejects.toThrow("Tag not found\nStack: remove tag pb");
|
|
});
|
|
|
|
it("should return error if tag is unknown", async () => {
|
|
// given
|
|
const tagOne: UserDAL.DBUserTag = {
|
|
_id: new ObjectId(),
|
|
name: "one",
|
|
personalBests: {} as any,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: [tagOne],
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeTagPb(uid, new ObjectId().toHexString()),
|
|
).rejects.toThrow("Tag not found\nStack: remove tag pb");
|
|
});
|
|
it("should remove tag pb", async () => {
|
|
// given
|
|
const tagOne = {
|
|
_id: new ObjectId(),
|
|
name: "tagOne",
|
|
personalBests: {
|
|
custom: { custom: [mockPersonalBest] },
|
|
} as PersonalBests,
|
|
};
|
|
const tagTwo = {
|
|
_id: new ObjectId(),
|
|
name: "tagTwo",
|
|
personalBests: {
|
|
custom: { custom: [mockPersonalBest] },
|
|
} as PersonalBests,
|
|
};
|
|
const tagThree = {
|
|
_id: new ObjectId(),
|
|
name: "tagThree",
|
|
personalBests: {
|
|
custom: { custom: [mockPersonalBest] },
|
|
} as PersonalBests,
|
|
};
|
|
|
|
const { uid } = await UserTestData.createUser({
|
|
tags: [tagOne, tagTwo, tagThree],
|
|
});
|
|
|
|
// when, then
|
|
await UserDAL.removeTagPb(uid, tagTwo._id.toHexString());
|
|
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.tags).toStrictEqual([
|
|
tagOne,
|
|
{
|
|
...tagTwo,
|
|
personalBests: {
|
|
time: {},
|
|
words: {},
|
|
quote: {},
|
|
zen: {},
|
|
custom: {},
|
|
},
|
|
},
|
|
tagThree,
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("updateProfile", () => {
|
|
it("updateProfile should appropriately handle multiple profile updates", async () => {
|
|
const uid = new ObjectId().toHexString();
|
|
await UserDAL.addUser("test name", "test email", uid);
|
|
|
|
await UserDAL.updateProfile(
|
|
uid,
|
|
{
|
|
bio: "test bio",
|
|
},
|
|
{
|
|
badges: [],
|
|
},
|
|
);
|
|
|
|
const user = await UserDAL.getUser(uid, "test add result filters");
|
|
expect(user.profileDetails).toStrictEqual({
|
|
bio: "test bio",
|
|
});
|
|
expect(user.inventory).toStrictEqual({
|
|
badges: [],
|
|
});
|
|
|
|
await UserDAL.updateProfile(
|
|
uid,
|
|
{
|
|
keyboard: "test keyboard",
|
|
socialProfiles: {
|
|
twitter: "test twitter",
|
|
},
|
|
},
|
|
{
|
|
badges: [
|
|
{
|
|
id: 1,
|
|
selected: true,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
|
|
const updatedUser = await UserDAL.getUser(uid, "test add result filters");
|
|
expect(updatedUser.profileDetails).toStrictEqual({
|
|
bio: "test bio",
|
|
keyboard: "test keyboard",
|
|
socialProfiles: {
|
|
twitter: "test twitter",
|
|
},
|
|
});
|
|
expect(updatedUser.inventory).toStrictEqual({
|
|
badges: [
|
|
{
|
|
id: 1,
|
|
selected: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
await UserDAL.updateProfile(
|
|
uid,
|
|
{
|
|
bio: "test bio 2",
|
|
socialProfiles: {
|
|
github: "test github",
|
|
website: "test website",
|
|
},
|
|
},
|
|
{
|
|
badges: [
|
|
{
|
|
id: 1,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
|
|
const updatedUser2 = await UserDAL.getUser(
|
|
uid,
|
|
"test add result filters",
|
|
);
|
|
expect(updatedUser2.profileDetails).toStrictEqual({
|
|
bio: "test bio 2",
|
|
keyboard: "test keyboard",
|
|
socialProfiles: {
|
|
twitter: "test twitter",
|
|
github: "test github",
|
|
website: "test website",
|
|
},
|
|
});
|
|
expect(updatedUser2.inventory).toStrictEqual({
|
|
badges: [
|
|
{
|
|
id: 1,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
it("should omit undefined or empty object values", async () => {
|
|
//GIVEN
|
|
const givenUser = await UserTestData.createUser({
|
|
profileDetails: {
|
|
bio: "test bio",
|
|
keyboard: "test keyboard",
|
|
socialProfiles: {
|
|
twitter: "test twitter",
|
|
github: "test github",
|
|
},
|
|
},
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.updateProfile(givenUser.uid, {
|
|
bio: undefined, //ignored
|
|
keyboard: "updates",
|
|
socialProfiles: {}, //ignored
|
|
});
|
|
|
|
//THEN
|
|
const read = await UserDAL.getUser(givenUser.uid, "read");
|
|
expect(read.profileDetails).toStrictEqual({
|
|
...givenUser.profileDetails,
|
|
keyboard: "updates",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("resetUser should reset user", async () => {
|
|
const uid = new ObjectId().toHexString();
|
|
await UserDAL.addUser("test name", "test email", uid);
|
|
|
|
await UserDAL.updateProfile(
|
|
uid,
|
|
{
|
|
bio: "test bio",
|
|
keyboard: "test keyboard",
|
|
socialProfiles: {
|
|
twitter: "test twitter",
|
|
github: "test github",
|
|
},
|
|
},
|
|
{
|
|
badges: [],
|
|
},
|
|
);
|
|
|
|
await UserDAL.incrementBananas(uid, 100);
|
|
await UserDAL.incrementXp(uid, 15);
|
|
|
|
await UserDAL.resetUser(uid);
|
|
const resetUser = await UserDAL.getUser(uid, "test add result filters");
|
|
|
|
expect(resetUser.profileDetails).toStrictEqual({
|
|
bio: "",
|
|
keyboard: "",
|
|
socialProfiles: {},
|
|
});
|
|
|
|
expect(resetUser.inventory).toStrictEqual({
|
|
badges: [],
|
|
});
|
|
|
|
expect(resetUser.bananas).toStrictEqual(0);
|
|
expect(resetUser.xp).toStrictEqual(0);
|
|
expect(resetUser.streak).toStrictEqual({
|
|
length: 0,
|
|
lastResultTimestamp: 0,
|
|
maxLength: 0,
|
|
});
|
|
});
|
|
|
|
it("getInbox should return the user's inbox", async () => {
|
|
const uid = new ObjectId().toHexString();
|
|
await UserDAL.addUser("test name", "test email", uid);
|
|
|
|
const emptyInbox = await UserDAL.getInbox(uid);
|
|
|
|
expect(emptyInbox).toStrictEqual([]);
|
|
|
|
await UserDAL.addToInbox(
|
|
uid,
|
|
[
|
|
{
|
|
subject: `Hello!`,
|
|
} as any,
|
|
],
|
|
{
|
|
enabled: true,
|
|
maxMail: 100,
|
|
},
|
|
);
|
|
|
|
const inbox = await UserDAL.getInbox(uid);
|
|
|
|
expect(inbox).toStrictEqual([
|
|
{
|
|
subject: "Hello!",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("addToInbox discards mail if inbox is full", async () => {
|
|
const uid = new ObjectId().toHexString();
|
|
await UserDAL.addUser("test name", "test email", uid);
|
|
|
|
const config = {
|
|
enabled: true,
|
|
maxMail: 1,
|
|
};
|
|
|
|
await UserDAL.addToInbox(
|
|
uid,
|
|
[
|
|
{
|
|
subject: "Hello 1!",
|
|
} as any,
|
|
],
|
|
config,
|
|
);
|
|
|
|
await UserDAL.addToInbox(
|
|
uid,
|
|
[
|
|
{
|
|
subject: "Hello 2!",
|
|
} as any,
|
|
],
|
|
config,
|
|
);
|
|
|
|
const inbox = await UserDAL.getInbox(uid);
|
|
|
|
expect(inbox).toStrictEqual([
|
|
{
|
|
subject: "Hello 2!",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("addToInboxBulk should add mail to multiple users", async () => {
|
|
const { uid: user1 } = await UserTestData.createUser();
|
|
const { uid: user2 } = await UserTestData.createUser();
|
|
|
|
await UserDAL.addToInboxBulk(
|
|
[
|
|
{
|
|
uid: user1,
|
|
mail: [
|
|
{
|
|
subject: `Hello!`,
|
|
} as any,
|
|
],
|
|
},
|
|
{
|
|
uid: user2,
|
|
mail: [
|
|
{
|
|
subject: `Hello 2!`,
|
|
} as any,
|
|
],
|
|
},
|
|
],
|
|
{
|
|
enabled: true,
|
|
maxMail: 100,
|
|
},
|
|
);
|
|
|
|
const inbox = await UserDAL.getInbox(user1);
|
|
const inbox2 = await UserDAL.getInbox(user2);
|
|
|
|
expect(inbox).toStrictEqual([
|
|
{
|
|
subject: "Hello!",
|
|
},
|
|
]);
|
|
|
|
expect(inbox2).toStrictEqual([
|
|
{
|
|
subject: "Hello 2!",
|
|
},
|
|
]);
|
|
});
|
|
|
|
describe("updateStreak", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(UserDAL.updateStreak("non existing uid", 0)).rejects.toThrow(
|
|
"User not found\nStack: calculate streak",
|
|
);
|
|
});
|
|
|
|
it("updateStreak should update streak", async () => {
|
|
const { uid } = await UserTestData.createUser();
|
|
|
|
const testSteps = [
|
|
{
|
|
date: "2023/06/07 21:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
{
|
|
date: "2023/06/07 23:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
{
|
|
date: "2023/06/08 00:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/08 23:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/09 00:00:00 UTC",
|
|
expectedStreak: 3,
|
|
},
|
|
{
|
|
date: "2023/06/11 00:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
];
|
|
|
|
for (const { date, expectedStreak } of testSteps) {
|
|
const milis = new Date(date).getTime();
|
|
Date.now = vi.fn(() => milis);
|
|
|
|
const streak = await UserDAL.updateStreak(uid, milis);
|
|
|
|
expect(streak).toBe(expectedStreak);
|
|
}
|
|
});
|
|
|
|
it("positive streak offset should award streak correctly", async () => {
|
|
const { uid } = await UserTestData.createUser({
|
|
streak: { hourOffset: 10 } as any,
|
|
});
|
|
|
|
const testSteps = [
|
|
{
|
|
date: "2023/06/06 21:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
{
|
|
date: "2023/06/07 01:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
{
|
|
date: "2023/06/07 09:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
{
|
|
date: "2023/06/07 10:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/07 23:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/08 00:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/08 01:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/08 09:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/08 10:00:00 UTC",
|
|
expectedStreak: 3,
|
|
},
|
|
{
|
|
date: "2023/06/10 10:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
];
|
|
|
|
for (const { date, expectedStreak } of testSteps) {
|
|
const milis = new Date(date).getTime();
|
|
Date.now = vi.fn(() => milis);
|
|
|
|
const streak = await UserDAL.updateStreak(uid, milis);
|
|
|
|
expect(streak).toBe(expectedStreak);
|
|
}
|
|
});
|
|
|
|
it("negative streak offset should award streak correctly", async () => {
|
|
const { uid } = await UserTestData.createUser({
|
|
streak: { hourOffset: -4 } as any,
|
|
});
|
|
|
|
const testSteps = [
|
|
{
|
|
date: "2023/06/06 19:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
{
|
|
date: "2023/06/06 20:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/07 01:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/07 19:00:00 UTC",
|
|
expectedStreak: 2,
|
|
},
|
|
{
|
|
date: "2023/06/07 20:00:00 UTC",
|
|
expectedStreak: 3,
|
|
},
|
|
{
|
|
date: "2023/06/09 23:00:00 UTC",
|
|
expectedStreak: 1,
|
|
},
|
|
];
|
|
|
|
for (const { date, expectedStreak } of testSteps) {
|
|
const milis = new Date(date).getTime();
|
|
Date.now = vi.fn(() => milis);
|
|
|
|
const streak = await UserDAL.updateStreak(uid, milis);
|
|
|
|
expect(streak).toBe(expectedStreak);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("incrementTestActivity", () => {
|
|
it("ignores user without migration", async () => {
|
|
// given
|
|
const user = await UserTestData.createUserWithoutMigration();
|
|
|
|
//when
|
|
await UserDAL.incrementTestActivity(user, 1712102400000);
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(user.uid, "");
|
|
expect(read.testActivity).toBeUndefined();
|
|
});
|
|
it("increments for new year", async () => {
|
|
// given
|
|
const user = await UserTestData.createUser({
|
|
testActivity: { "2023": [null, 1] },
|
|
});
|
|
|
|
//when
|
|
await UserDAL.incrementTestActivity(user, 1712102400000);
|
|
|
|
//then
|
|
const read = (await UserDAL.getUser(user.uid, "")).testActivity || {};
|
|
expect(read).toHaveProperty("2024");
|
|
const year2024 = read["2024"] as any;
|
|
expect(year2024).toHaveLength(94);
|
|
//fill previous days with null
|
|
expect(year2024.slice(0, 93)).toEqual(new Array(93).fill(null));
|
|
expect(year2024[93]).toEqual(1);
|
|
});
|
|
it("increments for existing year", async () => {
|
|
// given
|
|
const user = await UserTestData.createUser({
|
|
testActivity: { "2024": [null, 5] },
|
|
});
|
|
|
|
//when
|
|
await UserDAL.incrementTestActivity(user, 1712102400000);
|
|
|
|
//then
|
|
const read = (await UserDAL.getUser(user.uid, "")).testActivity || {};
|
|
expect(read).toHaveProperty("2024");
|
|
const year2024 = read["2024"] as any;
|
|
expect(year2024).toHaveLength(94);
|
|
|
|
expect(year2024[0]).toBeNull();
|
|
expect(year2024[1]).toEqual(5);
|
|
expect(year2024.slice(2, 91)).toEqual(new Array(89).fill(null));
|
|
expect(year2024[93]).toEqual(1);
|
|
});
|
|
it("increments for existing day", async () => {
|
|
// given
|
|
let user = await UserTestData.createUser({ testActivity: {} });
|
|
await UserDAL.incrementTestActivity(user, 1712102400000);
|
|
user = await UserDAL.getUser(user.uid, "");
|
|
|
|
//when
|
|
await UserDAL.incrementTestActivity(user, 1712102400000);
|
|
|
|
//then
|
|
const read = (await UserDAL.getUser(user.uid, "")).testActivity || {};
|
|
const year2024 = read["2024"] as any;
|
|
expect(year2024[93]).toEqual(2);
|
|
});
|
|
});
|
|
|
|
describe("getUser", () => {
|
|
it("should get with missing personalBests", async () => {
|
|
//GIVEN
|
|
let user = await UserTestData.createUser({ personalBests: undefined });
|
|
|
|
//WHEN
|
|
const read = await UserDAL.getUser(user.uid, "read");
|
|
|
|
expect(read.personalBests).toEqual({
|
|
custom: {},
|
|
quote: {},
|
|
time: {},
|
|
words: {},
|
|
zen: {},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getUserByName", () => {
|
|
it("should get with missing personalBests", async () => {
|
|
//GIVEN
|
|
let user = await UserTestData.createUser({ personalBests: undefined });
|
|
|
|
//WHEN
|
|
const read = await UserDAL.getUserByName(user.name, "read");
|
|
|
|
expect(read.personalBests).toEqual({
|
|
custom: {},
|
|
quote: {},
|
|
time: {},
|
|
words: {},
|
|
zen: {},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getPersonalBests", () => {
|
|
it("should get with missing personalBests", async () => {
|
|
//GIVEN
|
|
let user = await UserTestData.createUser({ personalBests: undefined });
|
|
|
|
//WHEN
|
|
const read = await UserDAL.getPersonalBests(user.uid, "time", "15");
|
|
|
|
expect(read).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("getPartialUser", () => {
|
|
it("should throw for unknown user", async () => {
|
|
await expect(async () =>
|
|
UserDAL.getPartialUser("1234", "stack", []),
|
|
).rejects.toThrow("User not found\nStack: stack");
|
|
});
|
|
|
|
it("should get streak", async () => {
|
|
//GIVEN
|
|
let user = await UserTestData.createUser({
|
|
streak: {
|
|
hourOffset: 1,
|
|
length: 5,
|
|
lastResultTimestamp: 4711,
|
|
maxLength: 23,
|
|
},
|
|
});
|
|
|
|
//WHEN
|
|
const partial = await UserDAL.getPartialUser(user.uid, "streak", [
|
|
"streak",
|
|
]);
|
|
|
|
//THEN
|
|
expect(partial).toStrictEqual({
|
|
_id: user._id,
|
|
streak: {
|
|
hourOffset: 1,
|
|
length: 5,
|
|
lastResultTimestamp: 4711,
|
|
maxLength: 23,
|
|
},
|
|
});
|
|
});
|
|
it("should get with missing personalBests", async () => {
|
|
//GIVEN
|
|
let user = await UserTestData.createUser({ personalBests: undefined });
|
|
|
|
//WHEN
|
|
const read = await UserDAL.getPartialUser(user.uid, "read", [
|
|
"uid",
|
|
"personalBests",
|
|
]);
|
|
|
|
expect(read.personalBests).toEqual({
|
|
custom: {},
|
|
quote: {},
|
|
time: {},
|
|
words: {},
|
|
zen: {},
|
|
});
|
|
});
|
|
});
|
|
describe("updateEmail", () => {
|
|
it("throws for nonexisting user", async () => {
|
|
await expect(async () =>
|
|
UserDAL.updateEmail("unknown", "test@example.com"),
|
|
).rejects.toThrow("User not found\nStack: update email");
|
|
});
|
|
it("should update", async () => {
|
|
//given
|
|
const { uid } = await UserTestData.createUser({ email: "init" });
|
|
|
|
//when
|
|
await expect(UserDAL.updateEmail(uid, "next")).resolves.toBe(true);
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.email).toEqual("next");
|
|
});
|
|
});
|
|
describe("resetPb", () => {
|
|
it("throws for nonexisting user", async () => {
|
|
await expect(async () => UserDAL.resetPb("unknown")).rejects.toThrow(
|
|
"User not found\nStack: reset pb",
|
|
);
|
|
});
|
|
it("should reset", async () => {
|
|
//given
|
|
const { uid } = await UserTestData.createUser({
|
|
personalBests: { custom: { custom: [{ acc: 1 } as any] } } as any,
|
|
});
|
|
|
|
//when
|
|
await UserDAL.resetPb(uid);
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.personalBests).toStrictEqual({
|
|
time: {},
|
|
words: {},
|
|
quote: {},
|
|
zen: {},
|
|
custom: {},
|
|
});
|
|
});
|
|
});
|
|
describe("linkDiscord", () => {
|
|
it("throws for nonexisting user", async () => {
|
|
await expect(async () =>
|
|
UserDAL.linkDiscord("unknown", "", ""),
|
|
).rejects.toThrow("User not found\nStack: link discord");
|
|
});
|
|
it("should update", async () => {
|
|
//given
|
|
const { uid } = await UserTestData.createUser({
|
|
discordId: "discordId",
|
|
discordAvatar: "discordAvatar",
|
|
});
|
|
//when
|
|
await UserDAL.linkDiscord(uid, "newId", "newAvatar");
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.discordId).toEqual("newId");
|
|
expect(read.discordAvatar).toEqual("newAvatar");
|
|
});
|
|
it("should update without avatar", async () => {
|
|
//given
|
|
const { uid } = await UserTestData.createUser({
|
|
discordId: "discordId",
|
|
discordAvatar: "discordAvatar",
|
|
});
|
|
|
|
//when
|
|
await UserDAL.linkDiscord(uid, "newId");
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.discordId).toEqual("newId");
|
|
expect(read.discordAvatar).toEqual("discordAvatar");
|
|
});
|
|
});
|
|
describe("unlinkDiscord", () => {
|
|
it("throws for nonexisting user", async () => {
|
|
await expect(async () =>
|
|
UserDAL.unlinkDiscord("unknown"),
|
|
).rejects.toThrow("User not found\nStack: unlink discord");
|
|
});
|
|
it("should update", async () => {
|
|
//given
|
|
const { uid } = await UserTestData.createUser({
|
|
discordId: "discordId",
|
|
discordAvatar: "discordAvatar",
|
|
});
|
|
|
|
//when
|
|
await UserDAL.unlinkDiscord(uid);
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.discordId).toBeUndefined();
|
|
expect(read.discordAvatar).toBeUndefined();
|
|
});
|
|
});
|
|
describe("updateInbox", () => {
|
|
it("claims rewards on read", async () => {
|
|
//GIVEN
|
|
const rewardOne: MonkeyMail = {
|
|
id: "b5866d4c-0749-41b6-b101-3656249d39b9",
|
|
body: "test",
|
|
subject: "reward one",
|
|
timestamp: 1,
|
|
read: false,
|
|
rewards: [
|
|
{ type: "xp", item: 400 },
|
|
{ type: "xp", item: 600 },
|
|
{ type: "badge", item: { id: 4 } },
|
|
],
|
|
};
|
|
const rewardTwo: MonkeyMail = {
|
|
id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a",
|
|
body: "test",
|
|
subject: "reward two",
|
|
timestamp: 2,
|
|
read: false,
|
|
rewards: [{ type: "xp", item: 2000 }],
|
|
};
|
|
const rewardThree: MonkeyMail = {
|
|
id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a",
|
|
body: "test",
|
|
subject: "reward three",
|
|
timestamp: 3,
|
|
read: true,
|
|
rewards: [{ type: "xp", item: 3000 }],
|
|
};
|
|
const rewardFour: MonkeyMail = {
|
|
id: "d852d2cf-1802-4cd0-9fb4-336650fc470a",
|
|
body: "test",
|
|
subject: "reward four",
|
|
timestamp: 4,
|
|
read: false,
|
|
rewards: [{ type: "xp", item: 4000 }],
|
|
};
|
|
|
|
let user = await UserTestData.createUser({
|
|
xp: 100,
|
|
inbox: [rewardOne, rewardTwo, rewardThree, rewardFour],
|
|
});
|
|
|
|
//WNEN
|
|
|
|
await UserDAL.updateInbox(
|
|
user.uid,
|
|
[rewardOne.id, rewardTwo.id, rewardThree.id],
|
|
[],
|
|
);
|
|
|
|
//THEN
|
|
const read = await UserDAL.getUser(user.uid, "");
|
|
expect(read).not.toHaveProperty("tmp");
|
|
|
|
const { xp, inbox } = read;
|
|
expect(xp).toEqual(3100); //100 existing + 1000 from rewardOne, 2000 from rewardTwo
|
|
|
|
//inbox is sorted by timestamp
|
|
expect(inbox).toStrictEqual([
|
|
{ ...rewardFour },
|
|
{ ...rewardThree },
|
|
{ ...rewardTwo, read: true, rewards: [] },
|
|
{ ...rewardOne, read: true, rewards: [] },
|
|
]);
|
|
});
|
|
|
|
it("claims rewards on delete", async () => {
|
|
//GIVEN
|
|
//GIVEN
|
|
const rewardOne: MonkeyMail = {
|
|
id: "b5866d4c-0749-41b6-b101-3656249d39b9",
|
|
body: "test",
|
|
subject: "reward one",
|
|
timestamp: 1,
|
|
read: false,
|
|
rewards: [
|
|
{ type: "xp", item: 400 },
|
|
{ type: "xp", item: 600 },
|
|
{ type: "badge", item: { id: 4 } },
|
|
],
|
|
};
|
|
const rewardTwo: MonkeyMail = {
|
|
id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a",
|
|
body: "test",
|
|
subject: "reward two",
|
|
timestamp: 2,
|
|
read: true,
|
|
rewards: [{ type: "xp", item: 2000 }],
|
|
};
|
|
|
|
const rewardThree: MonkeyMail = {
|
|
id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a",
|
|
body: "test",
|
|
subject: "reward three",
|
|
timestamp: 4,
|
|
read: false,
|
|
rewards: [{ type: "xp", item: 3000 }],
|
|
};
|
|
|
|
let user = await UserTestData.createUser({
|
|
xp: 100,
|
|
inbox: [rewardOne, rewardTwo, rewardThree],
|
|
});
|
|
|
|
//WNEN
|
|
await UserDAL.updateInbox(user.uid, [], [rewardOne.id, rewardTwo.id]);
|
|
|
|
//THEN
|
|
const { xp, inbox } = await UserDAL.getUser(user.uid, "");
|
|
expect(xp).toBe(1100);
|
|
expect(inbox).toStrictEqual([rewardThree]);
|
|
});
|
|
|
|
it("updates badge", async () => {
|
|
//GIVEN
|
|
const rewardOne: MonkeyMail = {
|
|
id: "b5866d4c-0749-41b6-b101-3656249d39b9",
|
|
body: "test",
|
|
subject: "reward one",
|
|
timestamp: 2,
|
|
read: false,
|
|
rewards: [
|
|
{ type: "xp", item: 400 },
|
|
{ type: "badge", item: { id: 4 } },
|
|
],
|
|
};
|
|
const rewardTwo: MonkeyMail = {
|
|
id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a",
|
|
body: "test",
|
|
subject: "reward two",
|
|
timestamp: 1,
|
|
read: false,
|
|
rewards: [
|
|
{ type: "badge", item: { id: 3 } },
|
|
{ type: "badge", item: { id: 4 } },
|
|
{ type: "badge", item: { id: 5 } },
|
|
],
|
|
};
|
|
const rewardThree: MonkeyMail = {
|
|
id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a",
|
|
body: "test",
|
|
subject: "reward three",
|
|
timestamp: 0,
|
|
read: true,
|
|
rewards: [{ type: "badge", item: { id: 6 } }],
|
|
};
|
|
|
|
let user = await UserTestData.createUser({
|
|
inbox: [rewardOne, rewardTwo, rewardThree],
|
|
inventory: {
|
|
badges: [
|
|
{ id: 1, selected: true },
|
|
{ id: 3, selected: false },
|
|
],
|
|
},
|
|
});
|
|
|
|
//WNEN
|
|
await UserDAL.updateInbox(
|
|
user.uid,
|
|
[rewardOne.id, rewardTwo.id, rewardThree.id, rewardOne.id],
|
|
[],
|
|
);
|
|
|
|
//THEN
|
|
const { inbox, inventory } = await UserDAL.getUser(user.uid, "");
|
|
expect(inbox).toStrictEqual([
|
|
{ ...rewardOne, read: true, rewards: [] },
|
|
{ ...rewardTwo, read: true, rewards: [] },
|
|
{ ...rewardThree },
|
|
]);
|
|
expect(inventory?.badges).toStrictEqual([
|
|
{ id: 1, selected: true }, //previously owned
|
|
{ id: 3, selected: false }, // previously owned, no duplicate
|
|
{ id: 4 }, // gets only added once
|
|
{ id: 5 },
|
|
]);
|
|
});
|
|
it("read and delete the same message does not claim reward twice", async () => {
|
|
//GIVEN
|
|
const rewardOne: MonkeyMail = {
|
|
id: "b5866d4c-0749-41b6-b101-3656249d39b9",
|
|
body: "test",
|
|
subject: "reward one",
|
|
timestamp: 0,
|
|
read: false,
|
|
rewards: [{ type: "xp", item: 1000 }],
|
|
};
|
|
const rewardTwo: MonkeyMail = {
|
|
id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a",
|
|
body: "test",
|
|
subject: "reward two",
|
|
timestamp: 0,
|
|
read: false,
|
|
rewards: [{ type: "xp", item: 2000 }],
|
|
};
|
|
let user = await UserTestData.createUser({
|
|
xp: 100,
|
|
inbox: [rewardOne, rewardTwo],
|
|
});
|
|
|
|
await UserDAL.updateInbox(
|
|
user.uid,
|
|
[rewardOne.id, rewardTwo.id],
|
|
[rewardOne.id, rewardTwo.id],
|
|
);
|
|
|
|
//THEN
|
|
|
|
const { xp } = await UserDAL.getUser(user.uid, "");
|
|
expect(xp).toEqual(3100);
|
|
});
|
|
|
|
it("concurrent calls dont claim a reward multiple times", async () => {
|
|
//GIVEN
|
|
const rewardOne: MonkeyMail = {
|
|
id: "b5866d4c-0749-41b6-b101-3656249d39b9",
|
|
body: "test",
|
|
subject: "reward one",
|
|
timestamp: 0,
|
|
read: false,
|
|
rewards: [
|
|
{ type: "xp", item: 400 },
|
|
{ type: "xp", item: 600 },
|
|
{ type: "badge", item: { id: 4 } },
|
|
],
|
|
};
|
|
const rewardTwo: MonkeyMail = {
|
|
id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a",
|
|
body: "test",
|
|
subject: "reward two",
|
|
timestamp: 0,
|
|
read: false,
|
|
rewards: [{ type: "xp", item: 2000 }],
|
|
};
|
|
const rewardThree: MonkeyMail = {
|
|
id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a",
|
|
body: "test",
|
|
subject: "reward three",
|
|
timestamp: 0,
|
|
read: true,
|
|
rewards: [{ type: "xp", item: 2000 }],
|
|
};
|
|
|
|
let user = await UserTestData.createUser({
|
|
xp: 100,
|
|
inbox: [rewardOne, rewardTwo, rewardThree],
|
|
});
|
|
|
|
const count = 100;
|
|
const calls = new Array(count)
|
|
.fill(0)
|
|
.map(() =>
|
|
UserDAL.updateInbox(
|
|
user.uid,
|
|
[rewardOne.id, rewardTwo.id, rewardThree.id],
|
|
[],
|
|
),
|
|
);
|
|
|
|
await Promise.all(calls);
|
|
|
|
//THEN
|
|
|
|
const { xp } = await UserDAL.getUser(user.uid, "");
|
|
expect(xp).toEqual(3100);
|
|
});
|
|
});
|
|
describe("isDiscordIdAvailable", () => {
|
|
it("should return true for available discordId", async () => {
|
|
const discordId = new ObjectId().toHexString();
|
|
await expect(UserDAL.isDiscordIdAvailable(discordId)).resolves.toBe(true);
|
|
});
|
|
|
|
it("should return false if discordId is taken", async () => {
|
|
// given
|
|
const discordId = new ObjectId().toHexString();
|
|
await UserTestData.createUser({
|
|
discordId: discordId,
|
|
});
|
|
|
|
// when, then
|
|
await expect(UserDAL.isDiscordIdAvailable(discordId)).resolves.toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
describe("updateLbMemory", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.updateLbMemory(
|
|
"non existing uid",
|
|
"time",
|
|
"15",
|
|
"english",
|
|
4711,
|
|
),
|
|
).rejects.toThrow("User not found\nStack: update lb memory");
|
|
});
|
|
|
|
it("updates on empty lbMemory", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({});
|
|
|
|
//WHEN
|
|
await UserDAL.updateLbMemory(uid, "time", "15", "english", 4711);
|
|
|
|
//THEN
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.lbMemory).toStrictEqual({
|
|
time: {
|
|
"15": {
|
|
english: 4711,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
it("updates on empty lbMemory.mode", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({
|
|
lbMemory: { custom: {} },
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.updateLbMemory(uid, "time", "15", "english", 4711);
|
|
|
|
//THEN
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.lbMemory).toStrictEqual({
|
|
custom: {},
|
|
time: {
|
|
"15": {
|
|
english: 4711,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
it("updates on empty lbMemory.mode.mode2", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({
|
|
lbMemory: { time: { "30": {} } },
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.updateLbMemory(uid, "time", "15", "english", 4711);
|
|
|
|
//THEN
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.lbMemory).toStrictEqual({
|
|
time: {
|
|
"15": {
|
|
english: 4711,
|
|
},
|
|
"30": {},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
describe("incrementBananas", () => {
|
|
it("should not return error if uuid not found", async () => {
|
|
// when, then
|
|
await UserDAL.incrementBananas("non existing uid", 60);
|
|
});
|
|
|
|
it("increments bananas", async () => {
|
|
//GIVEN
|
|
const name = "user" + new ObjectId().toHexString();
|
|
const { uid } = await UserTestData.createUser({
|
|
name,
|
|
bananas: 1,
|
|
personalBests: {
|
|
time: {
|
|
"60": [
|
|
{ wpm: 100 } as PersonalBest,
|
|
{ wpm: 30 } as PersonalBest, //highest PB should be used
|
|
],
|
|
},
|
|
} as any,
|
|
});
|
|
|
|
//within 25% of PB
|
|
|
|
await UserDAL.incrementBananas(uid, 75);
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.bananas).toEqual(2);
|
|
expect(read.name).toEqual(name);
|
|
|
|
//NOT within 25% of PB
|
|
await UserDAL.incrementBananas(uid, 74);
|
|
expect((await UserDAL.getUser(uid, "read")).bananas).toEqual(2);
|
|
});
|
|
|
|
it("ignores missing personalBests", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({
|
|
bananas: 1,
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.incrementBananas(uid, 75);
|
|
|
|
//THEM
|
|
expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1);
|
|
});
|
|
|
|
it("ignores missing personalBests time", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({
|
|
bananas: 1,
|
|
personalBests: {} as any,
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.incrementBananas(uid, 75);
|
|
|
|
//THEM
|
|
expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1);
|
|
});
|
|
it("ignores missing personalBests time 60", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({
|
|
bananas: 1,
|
|
personalBests: { time: {} } as any,
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.incrementBananas(uid, 75);
|
|
|
|
//THEM
|
|
expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1);
|
|
});
|
|
it("ignores empty personalBests time 60", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({
|
|
bananas: 1,
|
|
personalBests: { time: { "60": [] } } as any,
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.incrementBananas(uid, 75);
|
|
|
|
//THEM
|
|
expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1);
|
|
});
|
|
it("should increment missing bananas", async () => {
|
|
//GIVEN
|
|
const { uid } = await UserTestData.createUser({
|
|
personalBests: { time: { "60": [{ wpm: 100 }] } } as any,
|
|
});
|
|
|
|
//WHEN
|
|
await UserDAL.incrementBananas(uid, 75);
|
|
|
|
//THEM
|
|
expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("addTheme", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addTheme("non existing uid", {
|
|
name: "new",
|
|
colors: [] as any,
|
|
}),
|
|
).rejects.toThrow(
|
|
"Maximum number of custom themes reached\nStack: add theme",
|
|
);
|
|
});
|
|
|
|
it("should return error if user has reached maximum", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
customThemes: new Array(20).fill(0).map(() => ({
|
|
_id: new ObjectId(),
|
|
name: "any",
|
|
colors: [] as any,
|
|
})),
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addTheme(uid, { name: "new", colors: [] as any }),
|
|
).rejects.toThrow(
|
|
"Maximum number of custom themes reached\nStack: add theme",
|
|
);
|
|
});
|
|
|
|
it("addTheme success", async () => {
|
|
// given
|
|
const themeOne = {
|
|
_id: new ObjectId(),
|
|
name: "first",
|
|
colors: new Array(10).fill("#123456") as CustomThemeColors,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
customThemes: [themeOne],
|
|
});
|
|
|
|
const newTheme = {
|
|
name: "newTheme",
|
|
colors: new Array(10).fill("#000000") as CustomThemeColors,
|
|
};
|
|
// when
|
|
await UserDAL.addTheme(uid, { ...newTheme });
|
|
|
|
// then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.customThemes).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: "first",
|
|
colors: themeOne.colors,
|
|
}),
|
|
expect.objectContaining({
|
|
name: "newTheme",
|
|
colors: newTheme.colors,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("editTheme", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.editTheme("non existing uid", new ObjectId().toHexString(), {
|
|
name: "newName",
|
|
colors: [] as any,
|
|
}),
|
|
).rejects.toThrow("Custom theme not found\nStack: edit theme");
|
|
});
|
|
|
|
it("should fail if theme not found", async () => {
|
|
// given
|
|
const themeOne = {
|
|
_id: new ObjectId(),
|
|
name: "first",
|
|
colors: ["green", "white", "red"] as any,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
customThemes: [themeOne],
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.editTheme(uid, new ObjectId().toHexString(), {
|
|
name: "newName",
|
|
colors: [] as any,
|
|
}),
|
|
).rejects.toThrow("Custom theme not found\nStack: edit theme");
|
|
});
|
|
|
|
it("editTheme success", async () => {
|
|
// given
|
|
const themeOne = {
|
|
_id: new ObjectId(),
|
|
name: "first",
|
|
colors: ["green", "white", "red"] as any,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
customThemes: [themeOne],
|
|
});
|
|
// when
|
|
await UserDAL.editTheme(uid, themeOne._id.toHexString(), {
|
|
name: "newThemeName",
|
|
colors: ["red", "white", "blue"] as any,
|
|
});
|
|
|
|
// then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.customThemes ?? [][0]).toStrictEqual([
|
|
{ ...themeOne, name: "newThemeName", colors: ["red", "white", "blue"] },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("removeTheme", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeTheme("non existing uid", new ObjectId().toHexString()),
|
|
).rejects.toThrow("Custom theme not found\nStack: remove theme");
|
|
});
|
|
|
|
it("should return error if theme is unknown", async () => {
|
|
// given
|
|
const themeOne = {
|
|
_id: new ObjectId(),
|
|
name: "first",
|
|
colors: ["green", "white", "red"] as any,
|
|
};
|
|
const { uid } = await UserTestData.createUser({
|
|
customThemes: [themeOne],
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeTheme(uid, new ObjectId().toHexString()),
|
|
).rejects.toThrow("Custom theme not found\nStack: remove theme");
|
|
});
|
|
it("should remove theme", async () => {
|
|
// given
|
|
const themeOne = {
|
|
_id: new ObjectId(),
|
|
name: "first",
|
|
colors: [] as any,
|
|
};
|
|
const themeTwo = {
|
|
_id: new ObjectId(),
|
|
name: "second",
|
|
colors: [] as any,
|
|
};
|
|
|
|
const themeThree = {
|
|
_id: new ObjectId(),
|
|
name: "third",
|
|
colors: [] as any,
|
|
};
|
|
|
|
const { uid } = await UserTestData.createUser({
|
|
customThemes: [themeOne, themeTwo, themeThree],
|
|
});
|
|
|
|
// when, then
|
|
await UserDAL.removeTheme(uid, themeTwo._id.toHexString());
|
|
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.customThemes).toStrictEqual([themeOne, themeThree]);
|
|
});
|
|
});
|
|
|
|
describe("addFavoriteQuote", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addFavoriteQuote("non existing uid", "english", "1", 5),
|
|
).rejects.toThrow(
|
|
"Maximum number of favorite quotes reached\nStack: add favorite quote",
|
|
);
|
|
});
|
|
|
|
it("should return error if user has reached maximum", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
favoriteQuotes: {
|
|
english: ["1", "2"],
|
|
german: ["3", "4"],
|
|
polish: ["5"],
|
|
},
|
|
});
|
|
|
|
// when, then
|
|
await expect(
|
|
UserDAL.addFavoriteQuote(uid, "polish", "6", 5),
|
|
).rejects.toThrow(
|
|
"Maximum number of favorite quotes reached\nStack: add favorite quote",
|
|
);
|
|
});
|
|
|
|
it("addFavoriteQuote success", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
favoriteQuotes: {
|
|
english: ["1"],
|
|
german: ["2"],
|
|
polish: ["3"],
|
|
},
|
|
});
|
|
|
|
// when
|
|
await UserDAL.addFavoriteQuote(uid, "english", "4", 5);
|
|
|
|
// then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.favoriteQuotes).toStrictEqual({
|
|
english: ["1", "4"],
|
|
german: ["2"],
|
|
polish: ["3"],
|
|
});
|
|
});
|
|
|
|
it("should not add a quote twice", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
favoriteQuotes: {
|
|
english: ["1", "3", "4"],
|
|
german: ["2"],
|
|
},
|
|
});
|
|
// when
|
|
await UserDAL.addFavoriteQuote(uid, "english", "4", 5);
|
|
|
|
// then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
|
|
expect(read.favoriteQuotes).toStrictEqual({
|
|
english: ["1", "3", "4"],
|
|
german: ["2"],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("removeFavoriteQuote", () => {
|
|
it("should return error if uid not found", async () => {
|
|
// when, then
|
|
await expect(
|
|
UserDAL.removeFavoriteQuote("non existing uid", "english", "0"),
|
|
).rejects.toThrow("User not found\nStack: remove favorite quote");
|
|
});
|
|
|
|
it("should not fail if quote is not favorite", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
favoriteQuotes: {
|
|
english: ["1", "2"],
|
|
},
|
|
});
|
|
|
|
// when
|
|
await UserDAL.removeFavoriteQuote(uid, "english", "3");
|
|
await UserDAL.removeFavoriteQuote(uid, "german", "1");
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.favoriteQuotes).toStrictEqual({
|
|
english: ["1", "2"],
|
|
});
|
|
});
|
|
it("should remove", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
favoriteQuotes: {
|
|
english: ["1", "2", "3"],
|
|
},
|
|
});
|
|
|
|
// when
|
|
await UserDAL.removeFavoriteQuote(uid, "english", "2");
|
|
|
|
//%hen
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.favoriteQuotes).toStrictEqual({
|
|
english: ["1", "3"],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("clearStreakHourOffset", () => {
|
|
it("should clear streak hour offset", async () => {
|
|
// given
|
|
const { uid } = await UserTestData.createUser({
|
|
//@ts-expect-error
|
|
streak: {
|
|
hourOffset: 1,
|
|
},
|
|
});
|
|
|
|
// when
|
|
await UserDAL.clearStreakHourOffset(uid);
|
|
|
|
//then
|
|
const read = await UserDAL.getUser(uid, "read");
|
|
expect(read.streak?.hourOffset).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("getFriends", () => {
|
|
it("get list of friends", async () => {
|
|
//GIVEN
|
|
const me = await UserTestData.createUser({ name: "Me" });
|
|
const uid = me.uid;
|
|
|
|
const friendOne = await UserTestData.createUser({
|
|
name: "One",
|
|
personalBests: {
|
|
time: {
|
|
"15": [UserTestData.pb(100)],
|
|
"60": [UserTestData.pb(85), UserTestData.pb(90)],
|
|
},
|
|
} as any,
|
|
inventory: {
|
|
badges: [{ id: 42, selected: true }, { id: 23 }, { id: 5 }],
|
|
},
|
|
banned: true,
|
|
lbOptOut: true,
|
|
premium: { expirationTimestamp: -1 } as any,
|
|
});
|
|
const friendOneRequest = await createFriend({
|
|
initiatorUid: uid,
|
|
receiverUid: friendOne.uid,
|
|
status: "accepted",
|
|
lastModified: 100,
|
|
});
|
|
const friendTwo = await UserTestData.createUser({
|
|
name: "Two",
|
|
discordId: "discordId",
|
|
discordAvatar: "discordAvatar",
|
|
timeTyping: 600,
|
|
startedTests: 150,
|
|
completedTests: 125,
|
|
streak: {
|
|
length: 10,
|
|
maxLength: 50,
|
|
lastResultTimestamp: 0,
|
|
hourOffset: -1,
|
|
} as any,
|
|
xp: 42,
|
|
inventory: {
|
|
badges: [{ id: 23 }, { id: 5 }],
|
|
},
|
|
premium: {
|
|
expirationTimestamp: vi.getRealSystemTime() + 5000,
|
|
} as any,
|
|
});
|
|
const friendTwoRequest = await createFriend({
|
|
initiatorUid: uid,
|
|
receiverUid: friendTwo.uid,
|
|
status: "accepted",
|
|
lastModified: 200,
|
|
});
|
|
|
|
const friendThree = await UserTestData.createUser({ name: "Three" });
|
|
const friendThreeRequest = await createFriend({
|
|
receiverUid: uid,
|
|
initiatorUid: friendThree.uid,
|
|
status: "accepted",
|
|
lastModified: 300,
|
|
});
|
|
|
|
//non accepted
|
|
await createFriend({ receiverUid: uid, status: "pending" });
|
|
await createFriend({ initiatorUid: uid, status: "blocked" });
|
|
|
|
//WHEN
|
|
const friends = await UserDAL.getFriends(uid);
|
|
|
|
//THEN
|
|
expect(friends).toEqual([
|
|
{
|
|
uid: friendOne.uid,
|
|
name: "One",
|
|
lastModified: 100,
|
|
connectionId: friendOneRequest._id,
|
|
// oxlint-disable-next-line no-non-null-assertion
|
|
top15: friendOne.personalBests.time["15"]![0] as any,
|
|
// oxlint-disable-next-line no-non-null-assertion
|
|
top60: friendOne.personalBests.time["60"]![1] as any,
|
|
badgeId: 42,
|
|
banned: true,
|
|
lbOptOut: true,
|
|
isPremium: true,
|
|
},
|
|
{
|
|
uid: friendTwo.uid,
|
|
name: "Two",
|
|
lastModified: 200,
|
|
connectionId: friendTwoRequest._id,
|
|
discordId: friendTwo.discordId,
|
|
discordAvatar: friendTwo.discordAvatar,
|
|
timeTyping: friendTwo.timeTyping,
|
|
startedTests: friendTwo.startedTests,
|
|
completedTests: friendTwo.completedTests,
|
|
streak: {
|
|
length: friendTwo.streak?.length,
|
|
maxLength: friendTwo.streak?.maxLength,
|
|
},
|
|
xp: friendTwo.xp,
|
|
isPremium: true,
|
|
},
|
|
{
|
|
uid: friendThree.uid,
|
|
name: "Three",
|
|
lastModified: 300,
|
|
connectionId: friendThreeRequest._id,
|
|
},
|
|
{
|
|
uid: me.uid,
|
|
name: "Me",
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|