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
4 changes: 0 additions & 4 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,3 @@ jobs:
- name: Run linting
working-directory: ./langsuit
run: npm run lint

- name: Build check
working-directory: ./langsuit
run: npm run build
28 changes: 23 additions & 5 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
schedule:
- cron: "0 0 * * 0" # Run weekly

permissions:
contents: write
pull-requests: write

jobs:
security:
runs-on: ubuntu-latest
Expand All @@ -22,10 +26,24 @@ jobs:
working-directory: ./langsuit
run: npm ci

- name: Run security audit
- name: Run security audit and fix
working-directory: ./langsuit
run: npm audit
run: |
if ! npm audit; then
echo "Vulnerabilities found. Attempting to fix..."
npm audit fix
# If there are still issues that require manual review
if ! npm audit; then
echo "Some vulnerabilities require manual review. Check the logs."
fi
fi

- name: Check for outdated packages
working-directory: ./langsuit
run: npm outdated
- name: Create PR if fixes were applied
if: failure()
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.PAT_TOKEN }}
commit-message: "fix: npm audit automatic fixes"
title: "fix: Security vulnerabilities fixes"
body: "Automated fixes from npm audit"
branch: "fix/security-updates"
4 changes: 4 additions & 0 deletions langsuit/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
2 changes: 2 additions & 0 deletions langsuit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

# testing
/coverage
test-results.json
test-coverage.xml

# next.js
/.next/
Expand Down
141 changes: 141 additions & 0 deletions langsuit/__tests__/adminOrchestration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import db from "@/db/drizzle";
import dashboardOrchestration, {
DashboardOrchestration,
} from "@/utils/dashboard/adminOrchestration";

// Mock the database
jest.mock("@/db/drizzle", () => ({
execute: jest.fn(),
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
}));

describe("Dashboard Orchestration", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("Singleton Pattern", () => {
it("should return the same instance", () => {
const instance1 = dashboardOrchestration;
const instance2 = DashboardOrchestration.getInstance();
expect(instance1).toBe(instance2);
});
});

describe("Section Registration", () => {
it("should have overview section registered", () => {
const section = dashboardOrchestration.getSection("overview");
expect(section).toBeDefined();
});

it("should have courses section registered", () => {
const section = dashboardOrchestration.getSection("courses");
expect(section).toBeDefined();
});

it("should have users section registered", () => {
const section = dashboardOrchestration.getSection("users");
expect(section).toBeDefined();
});

it("should have sales section registered", () => {
const section = dashboardOrchestration.getSection("sales");
expect(section).toBeDefined();
});

it("should return undefined for unregistered section", () => {
const section = dashboardOrchestration.getSection("nonexistent");
expect(section).toBeUndefined();
});
});

describe("Overview Section", () => {
const overviewSection = dashboardOrchestration.getSection("overview");

it("should have correct chart types", () => {
const charts = overviewSection?.getCharts();
expect(charts?.get("pie")).toBeDefined();
expect(charts?.get("bar")).toBeDefined();
expect(charts?.get("line")).toBeUndefined();
});
});

describe("Users Section", () => {
const usersSection = dashboardOrchestration.getSection("users");

it("should have correct chart types", () => {
const charts = usersSection?.getCharts();
expect(charts?.get("pie")).toBeDefined();
expect(charts?.get("line")).toBeDefined();
expect(charts?.get("table")).toBeDefined();
expect(charts?.get("bar")).toBeUndefined();
});

it("should fetch user statistics", async () => {
const mockUserStats = {
rows: [{ total_users: "100" }],
};
(db.execute as jest.Mock).mockResolvedValue(mockUserStats);

jest.spyOn(Promise, "all").mockResolvedValueOnce([100, 5, 75, 0.25]);

const data = await usersSection?.getStatCardData();

expect(data).toEqual({
totalUsers: 100,
todayNewUsers: 5,
ActiveUsers: 75,
curnRate: 0.25,
});
});
});

describe("Sales Section Charts", () => {
const salesSection = dashboardOrchestration.getSection("sales");

it("should fetch pie chart data correctly", async () => {
const mockData = [
{ category: "Category1", totalSales: "1000" },
{ category: "Category2", totalSales: "2000" },
];

(db.select as jest.Mock).mockReturnThis();
(db.from as jest.Mock).mockReturnThis();
(db.innerJoin as jest.Mock).mockReturnThis();
(db.groupBy as jest.Mock).mockResolvedValue(mockData);

const charts = salesSection?.getCharts();
const pieChart = charts?.get("pie");
const data = await pieChart?.fetchData();

expect(data).toEqual([
{ name: "Category1", value: 1000 },
{ name: "Category2", value: 2000 },
]);
});

it("should fetch bar chart data with correct structure", async () => {
const mockData = [
{ dayOfWeek: 1, category: "Category1", totalSales: "1000" },
];

(db.select as jest.Mock).mockReturnThis();
(db.innerJoin as jest.Mock).mockReturnThis();
(db.groupBy as jest.Mock).mockResolvedValue(mockData);

const charts = salesSection?.getCharts();
const barChart = charts?.get("bar");
const data = await barChart?.fetchData();

expect(data.length).toBe(7); // Should have data for all days of week
expect(data[0]).toHaveProperty("name");
expect(data[0]).toHaveProperty("sales");
});
});
});
170 changes: 170 additions & 0 deletions langsuit/__tests__/morganLogger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { morgan } from "@/utils/morganLogger";

// Mock Request class
class MockRequest {
public url: string;
public method: string;
public headers: Headers;

constructor(url: string, init?: RequestInit) {
this.url = url;
this.method = init?.method || "GET";
this.headers = new Headers(init?.headers);
}
}

describe("Morgan Logger", () => {
let mockRequest: MockRequest;
let mockConsoleLog: jest.SpyInstance;
let mockStream: { write: jest.Mock };

beforeEach(() => {
mockConsoleLog = jest.spyOn(console, "log").mockImplementation(() => {});
mockStream = { write: jest.fn() };
mockRequest = new MockRequest("http://localhost:3000/test", {
method: "GET",
headers: {
"user-agent": "test-agent",
referer: "http://localhost:3000",
},
});
});

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

// Helper function to strip ANSI color codes
const stripAnsi = (str: string) => str.replace(/\x1B\[\d+m/g, "");

// Helper function to clean log line (strip colors and line endings)
const cleanLogLine = (str: string) => stripAnsi(str.replace(/[\r\n]+$/, ""));

describe("Format: tiny", () => {
it("should format logs in tiny format", async () => {
const logger = morgan({ format: "tiny" });
await logger(mockRequest as unknown as Request);

const logLine = cleanLogLine(mockConsoleLog.mock.calls[0][0]);
expect(logLine).toBe("GET /test 200");
});
});

describe("Format: dev", () => {
it("should format logs in dev format with colors", async () => {
const logger = morgan({ format: "dev" });
await logger(mockRequest as unknown as Request);

const calls = mockConsoleLog.mock.calls;
expect(calls.length).toBe(1);
const logLine = cleanLogLine(calls[0][0]);
expect(logLine).toBe("GET /test 200");
});

it("should use different colors for different HTTP methods", async () => {
const methods = ["GET", "POST", "DELETE", "PUT"];
const logger = morgan({ format: "dev" });

for (const method of methods) {
mockRequest = new MockRequest("http://localhost:3000/test", {
method: method,
headers: {
"user-agent": "test-agent",
referer: "http://localhost:3000",
},
});
await logger(mockRequest as unknown as Request);
const logLine = cleanLogLine(
mockConsoleLog.mock.calls[mockConsoleLog.mock.calls.length - 1][0]
);
expect(logLine).toBe(`${method} /test 200`);
}

expect(mockConsoleLog).toHaveBeenCalledTimes(4);
});

it("should use different colors for different status codes", async () => {
const logger = morgan({ format: "dev" });
const statusCodes = [200, 300, 400, 500];

for (const status of statusCodes) {
mockRequest = new MockRequest("http://localhost:3000/test", {
method: "GET",
headers: {
"user-agent": "test-agent",
referer: "http://localhost:3000",
},
});
await logger(mockRequest as unknown as Request);
const logLine = cleanLogLine(
mockConsoleLog.mock.calls[mockConsoleLog.mock.calls.length - 1][0]
);
expect(logLine).toBe("GET /test 200");
}

expect(mockConsoleLog).toHaveBeenCalledTimes(4);
});
});

describe("Format: common", () => {
it("should format logs in common format", async () => {
const logger = morgan({ format: "common" });
await logger(mockRequest as unknown as Request);

const logLine = cleanLogLine(mockConsoleLog.mock.calls[0][0]);
expect(logLine).toMatch(/GET \/test 200 - test-agent/);
});
});

describe("Format: combined", () => {
it("should format logs in combined format", async () => {
const logger = morgan({ format: "combined" });
await logger(mockRequest as unknown as Request);

const logLine = cleanLogLine(mockConsoleLog.mock.calls[0][0]);
expect(logLine).toMatch(
/GET \/test 200 - test-agent http:\/\/localhost:3000/
);
});
});

describe("Format: short", () => {
it("should format logs in short format", async () => {
const logger = morgan({ format: "short" });
await logger(mockRequest as unknown as Request);

const logLine = cleanLogLine(mockConsoleLog.mock.calls[0][0]);
expect(logLine).toMatch(/GET \/test HTTP\/1\.1 200/);
});
});

describe("Custom stream", () => {
it("should use custom stream when provided", async () => {
const logger = morgan({ format: "tiny", stream: mockStream });
await logger(mockRequest as unknown as Request);

expect(mockStream.write).toHaveBeenCalled();
expect(mockConsoleLog).not.toHaveBeenCalled();
});
});

describe("Default options", () => {
it("should use dev format by default", async () => {
const logger = morgan();
await logger(mockRequest as unknown as Request);

const logLine = cleanLogLine(mockConsoleLog.mock.calls[0][0]);
expect(logLine).toBe("GET /test 200");
});
});

describe("Invalid format", () => {
it("should fall back to dev format for invalid format string", async () => {
const logger = morgan({ format: "invalid-format" as any });
await logger(mockRequest as unknown as Request);

const logLine = cleanLogLine(mockConsoleLog.mock.calls[0][0]);
expect(logLine).toBe("GET /test 200");
});
});
});
Loading