Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests for API and updater modules #98

Merged
merged 2 commits into from
Mar 8, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
174 changes: 174 additions & 0 deletions apps/api/src/lib/__tests__/getCarsByFuelType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getCarsByFuelType } from "../getCarsByFuelType";
import db from "@api/config/db";
import { getLatestMonth } from "../getLatestMonth";
import { getTrailingTwelveMonths } from "@sgcarstrends/utils";
import { and, asc, between, desc, eq, ilike, or } from "drizzle-orm";
import { cars } from "@sgcarstrends/schema";

// Mock the cars schema
vi.mock("@sgcarstrends/schema", () => ({
cars: {
month: "month",
make: "make",
fuel_type: "fuel_type",
}
}));

// Mock drizzle-orm
vi.mock("drizzle-orm", () => ({
and: vi.fn(),
asc: vi.fn(),
between: vi.fn(),
desc: vi.fn(),
eq: vi.fn(),
ilike: vi.fn(),
or: vi.fn(),
}));

vi.mock("@api/config/db", () => ({
default: {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({
orderBy: vi.fn(),
})),
})),
})),
},
}));

vi.mock("../getLatestMonth", () => ({
getLatestMonth: vi.fn(),
}));

vi.mock("@sgcarstrends/utils", () => ({
getTrailingTwelveMonths: vi.fn(),
}));

describe("getCarsByFuelType", () => {
const mockLatestMonth = "2023-12";
const mockTrailingMonth = "2023-01";

beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getLatestMonth).mockResolvedValue(mockLatestMonth);
vi.mocked(getTrailingTwelveMonths).mockReturnValue(mockTrailingMonth);

// Setup db chain mocks
const mockWhere = vi.fn().mockReturnValue({
orderBy: vi.fn(),
});

const mockFrom = vi.fn().mockReturnValue({
where: mockWhere,
});

vi.mocked(db.select).mockReturnValue({
from: mockFrom,
});
});

afterEach(() => {
vi.clearAllMocks();
});

it("should fetch cars by fuel type for specified month", async () => {
const mockFuelType = "Electric";
const mockMonth = "2023-10";
const mockResults = [
{ month: mockMonth, make: "Tesla", number: "100", fuel_type: "Electric" },
{ month: mockMonth, make: "Nissan", number: "50", fuel_type: "Electric" },
];

// Setup the orderBy to return our mock results
vi.mocked(db.select().from().where().orderBy).mockResolvedValue(mockResults);

const result = await getCarsByFuelType(mockFuelType, mockMonth);

expect(getLatestMonth).toHaveBeenCalled();
expect(db.select).toHaveBeenCalled();
expect(db.select().from).toHaveBeenCalled();
expect(db.select().from().where).toHaveBeenCalled();
expect(db.select().from().where().orderBy).toHaveBeenCalled();

expect(result).toHaveLength(2);
expect(result[0].number).toBe(100);
expect(result[1].number).toBe(50);
});

it("should fetch cars by fuel type for trailing 12 months when month not specified", async () => {
const mockFuelType = "Electric";
const mockResults = [
{ month: "2023-12", make: "Tesla", number: "100", fuel_type: "Electric" },
{ month: "2023-11", make: "Tesla", number: "90", fuel_type: "Electric" },
];

// Setup the orderBy to return our mock results
vi.mocked(db.select().from().where().orderBy).mockResolvedValue(mockResults);

const result = await getCarsByFuelType(mockFuelType);

expect(getLatestMonth).toHaveBeenCalled();
expect(getTrailingTwelveMonths).toHaveBeenCalledWith(mockLatestMonth);
expect(db.select).toHaveBeenCalled();
expect(db.select().from).toHaveBeenCalled();
expect(db.select().from().where).toHaveBeenCalled();
expect(db.select().from().where().orderBy).toHaveBeenCalled();

expect(result).toHaveLength(2);
});

it("should aggregate results by month and make", async () => {
const mockFuelType = "Electric";
const mockResults = [
{ month: "2023-12", make: "Tesla", number: "50", fuel_type: "Electric" },
{ month: "2023-12", make: "Tesla", number: "50", fuel_type: "Electric" },
{ month: "2023-12", make: "Nissan", number: "30", fuel_type: "Electric" },
];

// Setup the orderBy to return our mock results
vi.mocked(db.select().from().where().orderBy).mockResolvedValue(mockResults);

const result = await getCarsByFuelType(mockFuelType);

expect(result).toHaveLength(2);

const teslaResult = result.find((car) => car.make === "Tesla");
expect(teslaResult).toBeDefined();
expect(teslaResult?.number).toBe(100);

const nissanResult = result.find((car) => car.make === "Nissan");
expect(nissanResult).toBeDefined();
expect(nissanResult?.number).toBe(30);
});

it("should handle hybrid fuel types correctly", async () => {
const mockFuelType = "Hybrid";
const mockResults = [];

// Setup the orderBy to return our mock results
vi.mocked(db.select().from().where().orderBy).mockResolvedValue(mockResults);

await getCarsByFuelType(mockFuelType);

// We can't easily test the SQL filter, but we can verify the function was called
expect(or).toHaveBeenCalled();
});

it("should handle errors properly", async () => {
const mockFuelType = "Electric";
const mockError = new Error("Database error");

// Setup the orderBy to throw our mock error
vi.mocked(db.select().from().where().orderBy).mockRejectedValue(mockError);

const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

await expect(getCarsByFuelType(mockFuelType)).rejects.toThrow(mockError);

expect(consoleSpy).toHaveBeenCalledWith(mockError);

consoleSpy.mockRestore();
});
});
66 changes: 66 additions & 0 deletions apps/api/src/lib/__tests__/getLatestMonth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getLatestMonth } from "../getLatestMonth";
import db from "@api/config/db";

vi.mock("@api/config/db", () => ({
default: {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
},
}));

describe("getLatestMonth", () => {
const mockTable = { month: "month" } as any;

beforeEach(() => {
vi.resetAllMocks();
});

afterEach(() => {
vi.clearAllMocks();
});

it("should return the latest month from the table", async () => {
const mockMonth = "2023-12";

vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockResolvedValue([{ month: mockMonth }]),
} as any);

const result = await getLatestMonth(mockTable);

expect(db.select).toHaveBeenCalled();
expect(result).toBe(mockMonth);
});

it("should return null when no data is found", async () => {
vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockResolvedValue([{ month: null }]),
} as any);

const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

const result = await getLatestMonth(mockTable);

expect(consoleSpy).toHaveBeenCalled();
expect(result).toBeNull();

consoleSpy.mockRestore();
});

it("should handle errors properly", async () => {
const mockError = new Error("Database error");

vi.mocked(db.select).mockReturnValue({
from: vi.fn().mockRejectedValue(mockError),
} as any);

const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

await expect(getLatestMonth(mockTable)).rejects.toThrow(mockError);

expect(consoleSpy).toHaveBeenCalledWith(mockError);

consoleSpy.mockRestore();
});
});
114 changes: 114 additions & 0 deletions apps/api/src/lib/__tests__/getUniqueMonths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getUniqueMonths } from "../getUniqueMonths";
import db from "@api/config/db";
import redis from "@api/config/redis";
import { getTableName } from "drizzle-orm";

vi.mock("@api/config/db", () => ({
default: {
selectDistinct: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
},
}));

vi.mock("@api/config/redis", () => ({
default: {
smembers: vi.fn(),
sadd: vi.fn().mockResolvedValue(true),
expire: vi.fn().mockResolvedValue(true),
},
}));

vi.mock("drizzle-orm", () => ({
desc: vi.fn(),
getTableName: vi.fn(),
}));

describe("getUniqueMonths", () => {
const mockTable = { month: "month" } as any;
const mockTableName = "test_table";

beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getTableName).mockReturnValue(mockTableName);
});

afterEach(() => {
vi.clearAllMocks();
});

it("should return cached months if available", async () => {
const cachedMonths = ["2023-12", "2023-11", "2023-10"];
vi.mocked(redis.smembers).mockResolvedValue(cachedMonths);

const result = await getUniqueMonths(mockTable);

expect(redis.smembers).toHaveBeenCalledWith(`${mockTableName}:months`);
expect(db.selectDistinct).not.toHaveBeenCalled();
expect(result).toEqual(cachedMonths);
});

it("should fetch months from database when cache is empty", async () => {
const dbMonths = [
{ month: "2023-12" },
{ month: "2023-11" },
{ month: "2023-10" },
];
const expectedMonths = ["2023-12", "2023-11", "2023-10"];

vi.mocked(redis.smembers).mockResolvedValue([]);
vi.mocked(db.selectDistinct).mockReturnValue({
from: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockResolvedValue(dbMonths),
} as any);

const result = await getUniqueMonths(mockTable);

expect(redis.smembers).toHaveBeenCalledWith(`${mockTableName}:months`);
expect(db.selectDistinct).toHaveBeenCalled();
expect(redis.sadd).toHaveBeenCalledWith(`${mockTableName}:months`, expectedMonths);
expect(redis.expire).toHaveBeenCalled();
expect(result).toEqual(expectedMonths);
});

it("should handle custom key parameter", async () => {
const customKey = "custom_month";
const customTable = { [customKey]: customKey } as any;

vi.mocked(redis.smembers).mockResolvedValue([]);
vi.mocked(db.selectDistinct).mockReturnValue({
from: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockResolvedValue([]),
} as any);

await getUniqueMonths(customTable, customKey);

expect(db.selectDistinct).toHaveBeenCalled();
});

it("should handle errors properly", async () => {
const mockError = new Error("Redis error");

vi.mocked(redis.smembers).mockRejectedValue(mockError);

const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

await expect(getUniqueMonths(mockTable)).rejects.toThrow(mockError);

expect(consoleSpy).toHaveBeenCalledWith(mockError);

consoleSpy.mockRestore();
});

it("should sort months in descending order", async () => {
const unsortedMonths = ["2023-10", "2023-12", "2023-11"];
const expectedSortedMonths = ["2023-12", "2023-11", "2023-10"];

vi.mocked(redis.smembers).mockResolvedValue(unsortedMonths);

const result = await getUniqueMonths(mockTable);

expect(result).toEqual(expectedSortedMonths);
});
});
Loading