Skip to content

Commit

Permalink
common: Update unit tests
Browse files Browse the repository at this point in the history
common: Improve common functions
  • Loading branch information
somnisomni committed Jan 24, 2025
1 parent 024bbfa commit 1df0c07
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 63 deletions.
121 changes: 121 additions & 0 deletions packages/Common/src/utils/__tests__/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { deleteKeys, emptyObject, toDateRangeString } from "../functions";

describe("Common Functions", () => {
describe("deleteKeys()", () => {
it("should delete keys from object in-place", () => {
const obj = { a: 1, b: 2, c: 3, d: 4 };
const keys = ["a", "c"] as const;

expect(obj).toBeInstanceOf(Object);
expect(Object.keys(obj)).toHaveLength(4);
expect(obj).toBeTruthy();
expect(obj).toHaveProperty("a");
expect(obj).toHaveProperty("b");

expect(keys).toBeInstanceOf(Array);
expect(keys).toHaveLength(2);
expect(keys).toBeTruthy();

deleteKeys(obj, keys);

expect(obj).toBeTruthy();
expect(Object.keys(obj)).toHaveLength(2);
expect(obj).not.toHaveProperty("a");
expect(obj).toHaveProperty("b");
expect(obj).not.toHaveProperty("c");
expect(obj).toHaveProperty("d");
});
});

describe("emptyObject()", () => {
it("should empty object in-place", () => {
const objStrKey: Record<string, unknown> = { a: 1, b: 2, c: 3, d: 4 };
const objNumKey: Record<number, unknown> = { 1: "a", 2: "b", 3: "c", 4: "d" };
const objSymbolKey: Record<symbol, unknown> = { [Symbol("a")]: 1, [Symbol("b")]: 2, [Symbol("c")]: 3, [Symbol("d")]: 4 };
const objMixedKey: Record<string | number | symbol, unknown> = { a: 1, 2: "b", [Symbol("c")]: 3 };

expect(objStrKey).toBeInstanceOf(Object);
expect(objNumKey).toBeInstanceOf(Object);
expect(objSymbolKey).toBeInstanceOf(Object);
expect(objMixedKey).toBeInstanceOf(Object);

emptyObject(objStrKey);
emptyObject(objNumKey);
emptyObject(objSymbolKey);
emptyObject(objMixedKey);

expect(objStrKey).toBeInstanceOf(Object);
expect(objNumKey).toBeInstanceOf(Object);
expect(objSymbolKey).toBeInstanceOf(Object);
expect(objMixedKey).toBeInstanceOf(Object);
expect(Object.keys(objStrKey)).toHaveLength(0);
expect(Object.keys(objNumKey)).toHaveLength(0);
expect(Object.keys(objSymbolKey)).toHaveLength(0);
expect(Object.keys(objMixedKey)).toHaveLength(0);
});

it("should return empty object if an empty object is provided", () => {
const objEmpty: Record<string, unknown> = { };

expect(objEmpty).toBeInstanceOf(Object);
expect(Object.keys(objEmpty)).toHaveLength(0);

emptyObject(objEmpty);

expect(objEmpty).toBeInstanceOf(Object);
expect(Object.keys(objEmpty)).toHaveLength(0);
});
});

describe("toDateRangeString()", () => {
const predefinedDate = new Date("2000-01-01");

it("should return date range string (sequential items)", () => {
const testData1 = [ "2024-01-01", "2024-01-02" ];
const testData2 = [ "2024-01-01", "2024-01-02", "2024-01-03" ];
const testData3 = [ "2024-01-01", "2025-01-01", "2026-01-01" ];
const testData4 = [ new Date("2024-01-01"), new Date("2024-01-02") ];
const testData5 = [ new Date("2024-01-01"), new Date("2024-01-02"), new Date("2024-01-03") ];
const testData6 = [ new Date("2024-01-01"), new Date("2025-01-01"), new Date("2026-01-01") ];

expect(toDateRangeString(testData1)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2024-01-02").toLocaleDateString()}`);
expect(toDateRangeString(testData2)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2024-01-03").toLocaleDateString()}`);
expect(toDateRangeString(testData3)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2026-01-01").toLocaleDateString()}`);
expect(toDateRangeString(testData4)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2024-01-02").toLocaleDateString()}`);
expect(toDateRangeString(testData5)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2024-01-03").toLocaleDateString()}`);
expect(toDateRangeString(testData6)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2026-01-01").toLocaleDateString()}`);
});

it("should return date range string (non-sequential items)", () => {
const testData1 = [ "2024-01-01", "2024-05-30", "2024-02-21" ];
const testData2 = [ predefinedDate, new Date("2025-01-01"), new Date("2020-04-05"), new Date("2022-12-12") ];

expect(toDateRangeString(testData1)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2024-05-30").toLocaleDateString()}`);
expect(toDateRangeString(testData2)).toBe(`${predefinedDate.toLocaleDateString()} ~ ${new Date("2025-01-01").toLocaleDateString()}`);
});

it("should return date range string (string/date mixed items)", () => {
const testData1 = [ "2024-01-01", new Date("2024-05-30"), "2024-02-21" ];
const testData2 = [ "2025-01-01", new Date("2020-04-05"), predefinedDate, "2022-12-12" ];

expect(toDateRangeString(testData1)).toBe(`${new Date("2024-01-01").toLocaleDateString()} ~ ${new Date("2024-05-30").toLocaleDateString()}`);
expect(toDateRangeString(testData2)).toBe(`${predefinedDate.toLocaleDateString()} ~ ${new Date("2025-01-01").toLocaleDateString()}`);
});

it("should return single locale date string if the array has only one date", () => {
const testData1 = [ "2024-01-01" ];
const testData2 = [ predefinedDate ];

expect(toDateRangeString(testData1)).toBe(new Date("2024-01-01").toLocaleDateString());
expect(toDateRangeString(testData2)).toBe(predefinedDate.toLocaleDateString());
});

it("should throw `TypeError` if invalid date is provided in the array", () => {
const testData1 = [ "2024-01-01", "Invalid", "What the?" ];
const testData2 = [ new Date(), new Date("HELLO WORLD"), new Date("2024-04-04") ];

expect(() => toDateRangeString(testData1)).toThrow(TypeError);
expect(() => toDateRangeString(testData2)).toThrow(TypeError);
});
});
});
2 changes: 1 addition & 1 deletion packages/Common/src/utils/__tests__/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const testBoothWithInternals: WithSequelizeInternals<IBoothAdmin> = {
updatedAt: new Date(),
};

describe("src/utils/internals.ts", () => {
describe("Common Internals", () => {
it("should delete sequelize internal keys", () => {
expect(testBoothWithInternals).toHaveProperty("createdAt");
expect(testBoothWithInternals).toHaveProperty("updatedAt");
Expand Down
37 changes: 30 additions & 7 deletions packages/Common/src/utils/functions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
export function emptyObject(target: Record<string, unknown>): void {
Object.keys(target).forEach((key) => delete target[key]);
/**
* Delete keyed members from an object in-place
* @param obj Target object
* @param keys Keys to delete
*/
export function deleteKeys<T>(obj: T, keys: readonly (keyof T)[]): void {
for(const key of keys) {
delete obj[key];
}
}

export function emptyNumberKeyObject(target: Record<number, unknown>): void {
Object.keys(target).forEach((key) => delete target[parseInt(key)]);
/**
* Delete all members from an object in-place, make the object empty
* @param obj Target object
*/
export function emptyObject<TKey extends string | number>(obj: Record<TKey, unknown>): void {
Object.keys(obj).forEach((key) => delete obj[key as TKey]);
}

/**
* Convert array of dates to date range string
* @param dates Array of `Date` instances or date strings can be parsed by `new Date()` constructor
* @returns Date range string with this format: `[minDateLocaleString] ~ [maxDateLocaleString]`. If the array has only one date, return the date's locale string
* @throws {TypeError} If some of elements in the array is not a valid date
*/
export function toDateRangeString(dates: Array<Date | string>): string {
if(dates.some((date) => isNaN(new Date(date).getTime()))) {
throw new TypeError("Some of elements in the array is not a valid date");
}

if(dates.length === 1) return new Date(dates[0]).toLocaleDateString();

const min = dates.reduce((prev, curr) => new Date(prev) < new Date(curr) ? prev : curr);
const max = dates.reduce((prev, curr) => new Date(prev) > new Date(curr) ? prev : curr);
const dateTimes = dates.map((date) => new Date(date).getTime());

const minTime = Math.min(...dateTimes);
const maxTime = Math.max(...dateTimes);

return `${new Date(min).toLocaleDateString()} ~ ${new Date(max).toLocaleDateString()}`;
return `${new Date(minTime).toLocaleDateString()} ~ ${new Date(maxTime).toLocaleDateString()}`;
}
2 changes: 1 addition & 1 deletion packages/Common/src/utils/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const DOMPURIFY_OPTIONS: Config = {
"source",
"template",
],
ADD_ATTR: ["target"],
ADD_ATTR: [ "target" ],
SANITIZE_DOM: true,
};

Expand Down
10 changes: 5 additions & 5 deletions projects/Admin/src/plugins/stores/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ const useAdminAPIStore = defineStore("admin-api", () => {
{
onFetch(response) {
if(!$adminStore.currentBooth.boothMembers) $adminStore.currentBooth.boothMembers = { };
C.emptyNumberKeyObject($adminStore.currentBooth.boothMembers);
C.emptyObject($adminStore.currentBooth.boothMembers);

for(const member of response) {
$adminStore.currentBooth.boothMembers[member.id] = member;
Expand Down Expand Up @@ -411,7 +411,7 @@ const useAdminAPIStore = defineStore("admin-api", () => {
{
onFetch(response) {
if(!$adminStore.currentBooth.goods) $adminStore.currentBooth.goods = { };
C.emptyNumberKeyObject($adminStore.currentBooth.goods);
C.emptyObject($adminStore.currentBooth.goods);

for(const goods of response) {
$adminStore.currentBooth.goods[goods.id] = new GoodsAdmin(goods);
Expand Down Expand Up @@ -514,7 +514,7 @@ const useAdminAPIStore = defineStore("admin-api", () => {
{
onFetch(response) {
if(!$adminStore.currentBooth.goodsCombinations) $adminStore.currentBooth.goodsCombinations = { };
C.emptyNumberKeyObject($adminStore.currentBooth.goodsCombinations);
C.emptyObject($adminStore.currentBooth.goodsCombinations);

for(const combination of response) {
$adminStore.currentBooth.goodsCombinations[combination.id] = new GoodsCombinationAdmin(combination);
Expand Down Expand Up @@ -613,7 +613,7 @@ const useAdminAPIStore = defineStore("admin-api", () => {
{
onFetch(response) {
if(!$adminStore.currentBooth.goodsCategories) $adminStore.currentBooth.goodsCategories = {};
C.emptyNumberKeyObject($adminStore.currentBooth.goodsCategories);
C.emptyObject($adminStore.currentBooth.goodsCategories);

for(const category of response) {
$adminStore.currentBooth.goodsCategories[category.id] = category;
Expand Down Expand Up @@ -697,7 +697,7 @@ const useAdminAPIStore = defineStore("admin-api", () => {
{
onFetch(response) {
if(!$adminStore.currentBooth.orders) $adminStore.currentBooth.orders = { };
C.emptyNumberKeyObject($adminStore.currentBooth.orders);
C.emptyObject($adminStore.currentBooth.orders);

for(const order of response) {
$adminStore.currentBooth.orders[order.id] = order;
Expand Down
4 changes: 3 additions & 1 deletion projects/Backend/src/db/__tests__/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Sequelize } from "sequelize-typescript";
import { Sequelize, SequelizeOptions } from "sequelize-typescript";
import { expectTypeOf } from "expect-type";
import generateConfig from "../config";

const testEnvVars = {
Expand Down Expand Up @@ -33,6 +34,7 @@ describe("DB Config", () => {
expect(config).toHaveProperty("username", testEnvVars.MYSQL_USER);
expect(config).toHaveProperty("password", testEnvVars.MYSQL_PASSWORD);
expect(config).toHaveProperty("database", testEnvVars.MYSQL_DATABASE);
expectTypeOf(config).toMatchTypeOf<SequelizeOptions>();
});

it("should be able to use as configuration while initializing Sequelize", () => {
Expand Down
18 changes: 10 additions & 8 deletions projects/Backend/src/lib/utils/__tests__/cache-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ describe("CacheMap", () => {
expect(initializedMap.has("값 가져오기")).toBe(true);

expect(await initializedMap.get("get")).toBe("get_fetched");
expect(await initializedMap.get("this map is already initialized")).toBe("this map is already initialized_fetched");
expect(await initializedMap.get("값 가져오기")).toBe("값 가져오기_fetched");
});

it("should be able to test value of cached value", async () => {
Expand All @@ -58,11 +60,11 @@ describe("CacheMap", () => {
it("should be able to test value even if it is not in cache yet", async () => {
const map = new TestCacheMap();

expect(map.has("test")).toBeFalsy();
expect(map.has("test")).toBe(false);
expect(await map.testValue("test", "test_fetched")).toBe(true);
expect(await map.testValue("test", "test_INVALIDVALUE")).toBe(false);

expect(map.has("invalid")).toBeFalsy();
expect(map.has("invalid")).toBe(false);
expect(await map.testValue("invalid", "INVALID_VALUE")).toBe(false);
});

Expand All @@ -72,16 +74,16 @@ describe("CacheMap", () => {
await map.get("test2");
await map.get("test3");

expect(map.has("test1")).toBeTruthy();
expect(map.has("test2")).toBeTruthy();
expect(map.has("test3")).toBeTruthy();
expect(map.has("test1")).toBe(true);
expect(map.has("test2")).toBe(true);
expect(map.has("test3")).toBe(true);
expect(map.count()).toBe(3);

map.invalidate("test1");

expect(map.has("test1")).toBeFalsy();
expect(map.has("test2")).toBeTruthy();
expect(map.has("test3")).toBeTruthy();
expect(map.has("test1")).toBe(false);
expect(map.has("test2")).toBe(true);
expect(map.has("test3")).toBe(true);
expect(map.count()).toBe(2);
});
});
29 changes: 0 additions & 29 deletions projects/Backend/src/lib/utils/__tests__/object.ts

This file was deleted.

1 change: 1 addition & 0 deletions projects/Backend/src/lib/utils/__tests__/security.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { generateRandomDigestFileName } from "@/lib/utils/security";

describe("Security Utilities", () => {
Expand Down
2 changes: 1 addition & 1 deletion projects/Backend/src/lib/utils/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { SEQUELIZE_INTERNAL_KEYS } from "@myboothmanager/common";
import { InternalServerErrorException, BadRequestException } from "@nestjs/common";
import { type Model, fn, col, where, type ModelDefined, BaseError, type FindOptions, type CreateOptions, type InstanceDestroyOptions, type ModelAttributes } from "sequelize";
import { type Model, fn, col, where, type ModelDefined, BaseError, type FindOptions, type CreateOptions, type InstanceDestroyOptions } from "sequelize";
import type { Fn, Where } from "sequelize/types/utils";
import { EntityNotFoundException } from "../exceptions";

Expand Down
10 changes: 0 additions & 10 deletions projects/Backend/src/lib/utils/object.ts

This file was deleted.

0 comments on commit 1df0c07

Please sign in to comment.