Skip to content
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
26 changes: 26 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Testing
on: [push]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '23.x'
- run: npm install -g yarn@2
- run: yarn install
- name: Build packages
run: |
yarn workspace @courselit/icons build
yarn workspace @courselit/common-models build
yarn workspace @courselit/utils build
yarn workspace @courselit/text-editor build
yarn workspace @courselit/state-management build
yarn workspace @courselit/components-library build
yarn workspace @courselit/common-widgets build
- name: Running tests
run: |
yarn test
working-directory: ./
3 changes: 2 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"

yarn dlx lint-staged
yarn dlx lint-staged
yarn test
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions apps/web/__mocks__/nanoid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const nanoid = jest.fn();
20 changes: 20 additions & 0 deletions apps/web/__mocks__/next-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class AuthError extends Error {
type: string;
constructor(type: string) {
super(type);
this.type = type;
}
}

const NextAuth = () => ({
auth: jest.fn(),
signIn: jest.fn(),
signOut: jest.fn(),
handlers: {
GET: jest.fn(),
POST: jest.fn(),
},
AuthError: AuthError,
});

export default NextAuth;
3 changes: 3 additions & 0 deletions apps/web/__mocks__/slugify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const slugify = jest.fn();

export default slugify;
118 changes: 118 additions & 0 deletions apps/web/app/api/payment/initiate/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* @jest-environment node
*/

import { NextRequest } from "next/server";
import { POST } from "../route";
import Domain from "@models/Domain";
import { auth } from "@/auth";
import mongoose from "mongoose";
import User from "@models/User";
import { Constants } from "@courselit/common-models";
import Course from "@models/Course";

jest.mock("@models/Domain");
jest.mock("@models/User");
jest.mock("@models/Course");
jest.mock("@/auth");

describe("Payment Initiate Route", () => {
let mockRequest: NextRequest;

beforeEach(() => {
jest.clearAllMocks();

(Domain.findOne as jest.Mock).mockResolvedValue({
_id: new mongoose.Types.ObjectId("666666666666666666666666"),
settings: {},
});

(User.findOne as jest.Mock).mockResolvedValue({
userId: "tester",
name: "Tester",
active: true,
domain: new mongoose.Types.ObjectId("666666666666666666666666"),
});
mockRequest = {
json: jest.fn().mockResolvedValue({
id: "course-123",
type: Constants.MembershipEntityType.COURSE,
planId: "planA",
}),
headers: {
get: jest.fn().mockResolvedValue("test.com"),
},
} as unknown as NextRequest;

(auth as jest.Mock).mockResolvedValue({
user: {
email: "test@test.com",
},
});

// (getUser as jest.Mock).mockResolvedValue({
// userId: 'tester',
// name: 'Tester',
// active: true,
// domain: new mongoose.Types.ObjectId("666666666666666666666666"),
// })

// (getUser as jest.Mock).mockResolvedValue({
// userId: 'tester',
// name: 'Tester',
// active: true,
// domain: new mongoose.Types.ObjectId("666666666666666666666666"),
// });
});

it("returns 401 if user is not authenticated", async () => {
(auth as jest.Mock).mockResolvedValue(null);

const response = await POST(mockRequest);
expect(response.status).toBe(401);
});

it("returns 400 if id is missing", async () => {
mockRequest.json = jest.fn().mockResolvedValue({
type: Constants.MembershipEntityType.COURSE,
planId: "planA",
});

const response = await POST(mockRequest);
expect(response.status).toBe(400);
});

it("returns 400 if type is missing", async () => {
mockRequest.json = jest.fn().mockResolvedValue({
id: "course-123",
planId: "planA",
});

const response = await POST(mockRequest);
expect(response.status).toBe(400);
});

it("returns 400 if planId is missing", async () => {
mockRequest.json = jest.fn().mockResolvedValue({
id: "course-123",
type: Constants.MembershipEntityType.COURSE,
});

const response = await POST(mockRequest);
expect(response.status).toBe(400);
});

it("returns 404 if payment plan does not belong to the entity", async () => {
mockRequest.json = jest.fn().mockResolvedValue({
id: "course-123",
type: Constants.MembershipEntityType.COURSE,
planId: "planA",
});
(Course.findOne as jest.Mock).mockResolvedValue({
title: "Test Course",
paymentPlans: ["planC", "planB"],
});
const response = await POST(mockRequest);
expect(response.status).toBe(404);
});
});
10 changes: 9 additions & 1 deletion apps/web/app/api/payment/initiate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ export async function POST(req: NextRequest) {
if (!entity) {
return Response.json(
{ message: responses.item_not_found },
{ status: 400 },
{ status: 404 },
);
}

if (!(entity.paymentPlans as unknown as string[]).includes(planId)) {
return Response.json(
{ message: "Invalid payment plan" },
{ status: 404 },
);
}

Expand Down Expand Up @@ -258,5 +265,6 @@ async function getPaymentPlan(
domain: domainId,
planId,
archived: false,
internal: false,
});
}
1 change: 1 addition & 0 deletions apps/web/app/api/payment/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ async function getPaymentPlan(
return PaymentPlanModel.findOne<PaymentPlan>({
domain: domainId,
planId: paymentPlanId,
internal: false,
});
}

Expand Down
114 changes: 114 additions & 0 deletions apps/web/components/public/__tests__/pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { PaginationControls } from "../pagination";

// Mock lucide-react icons
jest.mock("lucide-react", () => ({
ChevronLeft: () => <span>ChevronLeft</span>,
ChevronRight: () => <span>ChevronRight</span>,
MoreHorizontal: () => <span>MoreHorizontal</span>,
}));

describe("PaginationControls", () => {
const mockOnPageChange = jest.fn();

beforeEach(() => {
mockOnPageChange.mockClear();
});

it("renders current page and total pages correctly", () => {
render(
<PaginationControls
currentPage={2}
totalPages={5}
onPageChange={mockOnPageChange}
/>,
);

expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("of 5")).toBeInTheDocument();
});

it("disables previous button on first page", () => {
render(
<PaginationControls
currentPage={1}
totalPages={5}
onPageChange={mockOnPageChange}
/>,
);

const previousButton = screen.getByText("Previous").closest("a");
expect(previousButton).toHaveClass("pointer-events-none", "opacity-50");
});

it("disables next button on last page", () => {
render(
<PaginationControls
currentPage={5}
totalPages={5}
onPageChange={mockOnPageChange}
/>,
);

const nextButton = screen.getByText("Next").closest("a");
expect(nextButton).toHaveClass("pointer-events-none", "opacity-50");
});

it("calls onPageChange with previous page when previous button is clicked", () => {
render(
<PaginationControls
currentPage={3}
totalPages={5}
onPageChange={mockOnPageChange}
/>,
);

const previousButton = screen.getByText("Previous").closest("a");
fireEvent.click(previousButton!);
expect(mockOnPageChange).toHaveBeenCalledWith(2);
});

it("calls onPageChange with next page when next button is clicked", () => {
render(
<PaginationControls
currentPage={3}
totalPages={5}
onPageChange={mockOnPageChange}
/>,
);

const nextButton = screen.getByText("Next").closest("a");
fireEvent.click(nextButton!);
expect(mockOnPageChange).toHaveBeenCalledWith(4);
});

it("disables both buttons when totalPages is 0", () => {
render(
<PaginationControls
currentPage={1}
totalPages={0}
onPageChange={mockOnPageChange}
/>,
);

const previousButton = screen.getByText("Previous").closest("a");
const nextButton = screen.getByText("Next").closest("a");

expect(previousButton).toHaveClass("pointer-events-none", "opacity-50");
expect(nextButton).toHaveClass("pointer-events-none", "opacity-50");
});

it("does not call onPageChange when clicking disabled buttons", () => {
render(
<PaginationControls
currentPage={1}
totalPages={5}
onPageChange={mockOnPageChange}
/>,
);

const previousButton = screen.getByText("Previous").closest("a");
fireEvent.click(previousButton!);
expect(mockOnPageChange).not.toHaveBeenCalled();
});
});
35 changes: 35 additions & 0 deletions apps/web/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Config } from "jest";
import nextJest from "next/jest";

const createJestConfig = nextJest({
dir: "./apps/web",
});

const config: Config = {
coverageProvider: "v8",
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/setupTests.ts"],
// collectCoverage: true,
// collectCoverageFrom: [
// '**/*.{js,jsx,ts,tsx}',
// '!**/*.d.ts',
// '!**/node_modules/**',
// '!**/.next/**',
// '!**/coverage/**',
// '!**/jest.config.ts',
// '!**/setupTests.ts',
// ],
globalSetup: "<rootDir>/jest.setup.ts",
globalTeardown: "<rootDir>/jest.teardown.ts",
moduleNameMapper: {
"next-auth": "<rootDir>/__mocks__/next-auth.ts",
"@courselit/utils": "<rootDir>/../../packages/utils/src",
"@courselit/common-logic": "<rootDir>/../../packages/common-logic/src",
nanoid: "<rootDir>/__mocks__/nanoid.ts",
slugify: "<rootDir>/__mocks__/slugify.ts",
"@models/(.*)": "<rootDir>/models/$1",
"@/auth": "<rootDir>/auth.ts",
},
};

export default createJestConfig(config);
12 changes: 12 additions & 0 deletions apps/web/jest.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
jest.mock("next/router", () => ({
useRouter() {
return {
route: "/",
pathname: "",
query: {},
asPath: "",
push: jest.fn(),
replace: jest.fn(),
};
},
}));
Loading
Loading