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
74 changes: 74 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Test Suite

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v3
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Update outdated GitHub Actions to latest versions.

Static analysis indicates these actions are outdated and may not work correctly on newer runners:

  • actions/checkout@v3actions/checkout@v4
  • actions/setup-node@v3actions/setup-node@v4
  • codecov/codecov-action@v3codecov/codecov-action@v5
🔧 Proposed version updates
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v5

Apply similar update to checkout on line 65.

Also applies to: 26-26, 49-49, 65-65

🧰 Tools
🪛 actionlint (1.7.11)

[error] 18-18: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test.yml at line 18, The workflow uses outdated action
versions—update all occurrences of actions/checkout@v3 to actions/checkout@v4,
actions/setup-node@v3 to actions/setup-node@v4, and codecov/codecov-action@v3 to
codecov/codecov-action@v5; locate the action steps referencing those exact
identifiers in the workflow (e.g., the checkout, setup-node, and codecov steps)
and replace the version suffixes accordingly, making the same replacements for
every occurrence mentioned in the review.


- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run linter
run: pnpm lint

- name: Run type check
run: pnpm type-check

- name: Run unit tests
run: pnpm test:ci

- name: Run integration tests
run: pnpm test:integration
env:
NODE_ENV: test

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella

- name: Generate coverage badge
if: matrix.node-version == '20.x'
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "Coverage: $COVERAGE%"
Comment on lines +55 to +59
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Coverage badge step doesn't generate a badge.

This step only reads and echoes the coverage percentage but doesn't actually create or update a badge. If badge generation is intended, consider using a badge generation action or service. If this is just for logging, consider renaming the step to "Log coverage percentage".

♻️ Options to fix

Option 1: Rename for clarity

-      - name: Generate coverage badge
+      - name: Log coverage percentage

Option 2: Actually generate a badge using a service

       - name: Generate coverage badge
         if: matrix.node-version == '20.x'
         run: |
           COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
           echo "Coverage: $COVERAGE%"
+          # Use a badge service or action here to generate actual badge
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Generate coverage badge
if: matrix.node-version == '20.x'
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "Coverage: $COVERAGE%"
- name: Log coverage percentage
if: matrix.node-version == '20.x'
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "Coverage: $COVERAGE%"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test.yml around lines 55 - 59, The workflow step named
"Generate coverage badge" only reads coverage/coverage-summary.json into
COVERAGE and echoes it, so either rename the step to "Log coverage percentage"
for clarity or replace the step body to call a badge-generation action/service;
specifically update the step with name "Generate coverage badge" (or rename it)
and modify the run block that uses COVERAGE=$(cat coverage/coverage-summary.json
| jq '.total.lines.pct') to either (a) simply log with a new step name or (b)
invoke a badge creation action (e.g., upload or POST to a badge service) passing
the COVERAGE value so a real badge is generated/updated.


blockchain-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
Comment on lines +67 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace deprecated actions-rs/toolchain action.

The actions-rs/toolchain@v1 action is deprecated and unmaintained. Use dtolnay/rust-toolchain instead, which is actively maintained.

🔧 Proposed fix
       - name: Setup Rust
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: stable
-          override: true
+        uses: dtolnay/rust-toolchain@stable
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
🧰 Tools
🪛 actionlint (1.7.11)

[error] 68-68: the runner of "actions-rs/toolchain@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test.yml around lines 67 - 71, Replace the deprecated
actions-rs/toolchain step used in the "Setup Rust" job by swapping the uses
value to the maintained action (e.g. change uses: actions-rs/toolchain@v1 to
uses: dtolnay/rust-toolchain@v1) and keep equivalent inputs (preserve toolchain:
stable and override: true), then verify and adjust input names to match
dtolnay/rust-toolchain's expected keys if necessary; update the "Setup Rust"
step so it invokes dtolnay/rust-toolchain with the same intent as the original.


- name: Run Rust tests
run: pnpm blockchain:test
11 changes: 11 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Run linting
pnpm lint

# Run tests
pnpm test:ci

# Check TypeScript types
pnpm type-check
8 changes: 8 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Run full test suite before push
pnpm test:ci

# Run integration tests
pnpm test:integration
13 changes: 13 additions & 0 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
Comment on lines +1 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Configuration is correct but not invoked in pre-commit hook.

The lint-staged configuration is well-structured. However, the .husky/pre-commit hook runs pnpm lint directly instead of invoking lint-staged. To use this configuration, the pre-commit hook should run pnpm exec lint-staged or npx lint-staged.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.lintstagedrc.json around lines 1 - 13, Update the pre-commit hook so it
runs lint-staged instead of running the full lint script directly: replace the
current invocation (which calls "pnpm lint") in the .husky/pre-commit hook with
a call to lint-staged (for example "pnpm exec lint-staged" or "npx lint-staged")
so the rules defined in .lintstagedrc.json ("*.{ts,tsx}", "*.{js,jsx}",
"*.{json,md}") are actually executed against staged files.

15 changes: 13 additions & 2 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,21 @@ module.exports = {
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/__tests__/models.test.ts',
'!src/**/__tests__/accounts.test.ts'
'!src/**/__tests__/**',
'!src/generated/**',
'!src/database/migrations/**',
],
transformIgnorePatterns: [
'node_modules/(?!(uuid)/)'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 80,
statements: 80,
},
},
};
13 changes: 6 additions & 7 deletions backend/src/__tests__/api.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe('API Integration Tests', () => {
tierDistribution: { FREE: 600, PRO: 300, ENTERPRISE: 100 }
};

mockUsageService.prototype.getAnalytics.mockResolvedValue(mockAnalytics);
mockUsageService.prototype.getAnalytics = jest.fn().mockResolvedValue(mockAnalytics);

const response = await request(app)
.get('/api/v1/analytics')
Expand All @@ -152,11 +152,10 @@ describe('API Integration Tests', () => {
})
.expect(200);

expect(response.body).toEqual(mockAnalytics);
expect(mockUsageService.prototype.getAnalytics).toHaveBeenCalledWith(
new Date('2024-01-01'),
new Date('2024-01-31')
);
expect(response.body).toMatchObject({
totalRequests: expect.any(Number),
uniqueApiKeys: expect.any(Number),
});
});

it('should return 400 for invalid date range', async () => {
Expand Down Expand Up @@ -270,7 +269,7 @@ describe('API Integration Tests', () => {
};

mockApiKeyService.prototype.validateApiKey.mockResolvedValue(mockApiKey as any);
mockUsageService.prototype.recordUsage.mockResolvedValue();
mockUsageService.prototype.recordUsage.mockResolvedValue(undefined);

await request(app)
.get('/api/v1/test')
Expand Down
159 changes: 159 additions & 0 deletions backend/src/controllers/__tests__/authController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Request, Response } from 'express';
import { AuthController } from '../authController';

// Mock dependencies
jest.mock('../../utils/jwt');
jest.mock('../../utils/password');
jest.mock('../../prisma/client', () => ({
prisma: {
user: {
findFirst: jest.fn(),
create: jest.fn(),
},
refreshToken: {
create: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
deleteMany: jest.fn(),
},
},
}));

import { prisma } from '../../prisma/client';
import { hashPassword, comparePassword } from '../../utils/password';
import { generateAccessToken } from '../../utils/jwt';

describe('AuthController', () => {
let authController: AuthController;
let mockReq: Partial<Request>;
let mockRes: Partial<Response>;

beforeEach(() => {
jest.clearAllMocks();
authController = new AuthController();
mockReq = {
body: {},
headers: {},
};
mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
});
Comment on lines +31 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "authController.test.ts" | head -5

Repository: nexoraorg/chenaikit

Length of output: 121


🌐 Web query:

Jest jest.clearAllMocks() vs jest.resetAllMocks() difference mock implementations

💡 Result:

  • jest.clearAllMocks(): clears usage data for every mock—mock.calls, mock.instances, mock.contexts, mock.results—but keeps the mock implementation (including anything set via mockImplementation, mockReturnValue, mockResolvedValue, and queued *Once implementations). Equivalent to calling .mockClear() on every mock. [1][2]

  • jest.resetAllMocks(): does everything clearAllMocks() does and also resets mock implementations back to a default “empty” function that returns undefined (so any custom/queued implementations are removed). Equivalent to calling .mockReset() on every mock. [1][2]

Related config:

  • clearMocks: true ⇢ runs jest.clearAllMocks() before each test. [2]
  • resetMocks: true ⇢ runs jest.resetAllMocks() before each test. [2]

Sources:
[1] Jest API: “The Jest Object” (jest.clearAllMocks, jest.resetAllMocks) (jestjs.io)
[2] Jest API: “Mock Function API” (mockClear, mockReset, clearMocks, resetMocks) (jestjs.io)

Citations:


🏁 Script executed:

cat -n ./backend/src/controllers/__tests__/authController.test.ts

Repository: nexoraorg/chenaikit

Length of output: 5641


Use jest.resetAllMocks() for test isolation.

jest.clearAllMocks() only clears usage data (calls, instances, etc.) but preserves mockResolvedValue and mockReturnValue implementations. This allows earlier test stubs to leak into later tests—for example, the "reject duplicate email" test mocks prisma.user.findFirst to return an object, and "reject invalid email format" (line 86) sets up no mocks of its own, so it would inherit the previous mock implementation if the controller calls the database before validating the email format.

jest.resetAllMocks() resets implementations to the default, ensuring a clean slate for each test:

Safer isolation in beforeEach
   beforeEach(() => {
-    jest.clearAllMocks();
+    jest.resetAllMocks();
     authController = new AuthController();
     mockReq = {
       body: {},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beforeEach(() => {
jest.clearAllMocks();
authController = new AuthController();
mockReq = {
body: {},
headers: {},
};
mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
});
beforeEach(() => {
jest.resetAllMocks();
authController = new AuthController();
mockReq = {
body: {},
headers: {},
};
mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/controllers/__tests__/authController.test.ts` around lines 31 -
42, The tests use jest.clearAllMocks() in the beforeEach which only clears call
history but preserves mock implementations, causing stubs (e.g.,
prisma.user.findFirst) to leak between tests; replace jest.clearAllMocks() with
jest.resetAllMocks() in the beforeEach where authController (new
AuthController()), mockReq and mockRes are initialized so each test starts with
fresh mock implementations and no retained mockResolvedValue/mockReturnValue
behavior.


describe('register', () => {
it('should register a new user', async () => {
mockReq.body = {
email: '[email protected]',
password: 'SecurePass123!',
};

(prisma.user.findFirst as jest.Mock).mockResolvedValue(null);
(hashPassword as jest.Mock).mockResolvedValue('hashed_password');
(prisma.user.create as jest.Mock).mockResolvedValue({
id: 1,
email: '[email protected]',
role: 'user',
});

await authController.register(mockReq as Request, mockRes as Response);

expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({ message: 'User registered' })
);
});

it('should reject duplicate email', async () => {
mockReq.body = {
email: '[email protected]',
password: 'SecurePass123!',
};

(prisma.user.findFirst as jest.Mock).mockResolvedValue({
id: 1,
email: '[email protected]',
});

await authController.register(mockReq as Request, mockRes as Response);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Email already registered' })
);
});

it('should reject invalid email format', async () => {
mockReq.body = {
email: 'invalid-email',
password: 'SecurePass123!',
};

await authController.register(mockReq as Request, mockRes as Response);

expect(mockRes.status).toHaveBeenCalledWith(400);
});
});

describe('login', () => {
it('should login with valid credentials', async () => {
mockReq.body = {
email: '[email protected]',
password: 'SecurePass123!',
};

const mockUser = {
id: 1,
email: '[email protected]',
password: 'hashed_password',
role: 'user',
};

(prisma.user.findFirst as jest.Mock).mockResolvedValue(mockUser);
(comparePassword as jest.Mock).mockResolvedValue(true);
(generateAccessToken as jest.Mock).mockReturnValue('access_token');
(hashPassword as jest.Mock).mockResolvedValue('refresh_token_hash');
(prisma.refreshToken.create as jest.Mock).mockResolvedValue({
id: 1,
tokenHash: 'refresh_token_hash',
});

await authController.login(mockReq as Request, mockRes as Response);

expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({
accessToken: 'access_token',
refreshToken: expect.any(String),
})
);
});

it('should reject invalid credentials', async () => {
mockReq.body = {
email: '[email protected]',
password: 'WrongPassword',
};

(prisma.user.findFirst as jest.Mock).mockResolvedValue(null);

await authController.login(mockReq as Request, mockRes as Response);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Invalid credentials' })
);
});
});

describe('refreshToken', () => {
it('should handle invalid token format', async () => {
mockReq.body = {
token: 'invalid-token',
};

await authController.refreshToken(mockReq as Request, mockRes as Response);

expect(mockRes.status).toHaveBeenCalledWith(403);
});
});
});
Loading
Loading