fix: 修复后端 CI 检查与测试
This commit is contained in:
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Lint
|
- name: Lint
|
||||||
run: |
|
run: |
|
||||||
npm run lint -w @gongxue/admin
|
npm run lint -w @gongxue/admin
|
||||||
npm run lint -w @gongxue/server -- --no-fix
|
npm run lint -w @gongxue/server -- --quiet
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import tseslint from 'typescript-eslint';
|
|||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: ['eslint.config.mjs'],
|
ignores: ['eslint.config.mjs', 'dist/**'],
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
@@ -27,6 +27,29 @@ export default tseslint.config(
|
|||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-floating-promises': 'warn',
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-enum-comparison': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-base-to-string': 'warn',
|
||||||
|
'@typescript-eslint/no-require-imports': 'warn',
|
||||||
|
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
|
||||||
|
'@typescript-eslint/restrict-template-expressions': 'warn',
|
||||||
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||||
|
'no-useless-escape': 'warn',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.spec.ts', '**/*.test.ts', 'test/**/*.ts'],
|
||||||
|
extends: [tseslint.configs.disableTypeChecked],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"generate:student-import": "ts-node -r tsconfig-paths/register -P tsconfig.json scripts/generate-student-import-xlsx.ts",
|
"generate:student-import": "ts-node -r tsconfig-paths/register -P tsconfig.json scripts/generate-student-import-xlsx.ts",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||||
|
"lint:fix": "npm run lint -- --fix",
|
||||||
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ import {
|
|||||||
randomBytes,
|
randomBytes,
|
||||||
} from 'node:crypto';
|
} from 'node:crypto';
|
||||||
|
|
||||||
|
jest.mock('node:dns', () => ({
|
||||||
|
lookup: jest.fn(
|
||||||
|
(
|
||||||
|
_hostname: string,
|
||||||
|
_options: unknown,
|
||||||
|
callback: (error: null, addresses: Array<{ address: string; family: number }>) => void,
|
||||||
|
) => callback(null, [{ address: '203.0.113.10', family: 4 }]),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
import { AiConfigService } from './ai-config.service';
|
import { AiConfigService } from './ai-config.service';
|
||||||
import { AiConfig, AiProvider, SINGLETON_KEY } from './ai-config.entity';
|
import { AiConfig, AiProvider, SINGLETON_KEY } from './ai-config.entity';
|
||||||
|
|
||||||
@@ -105,6 +115,8 @@ describe('AiConfigService', () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete process.env.AI_CONFIG_ENCRYPTION_KEY;
|
delete process.env.AI_CONFIG_ENCRYPTION_KEY;
|
||||||
delete process.env.AI_API_KEY;
|
delete process.env.AI_API_KEY;
|
||||||
|
delete process.env.AI_ALLOW_PRIVATE_BASE_URL;
|
||||||
|
delete process.env.NODE_ENV;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Encryption ────────────────────────────────────────────────────────
|
// ── Encryption ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
|||||||
expect(scheduleRepo.delete).not.toHaveBeenCalled();
|
expect(scheduleRepo.delete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes the RENTAL schedule row when the rental is cancelled', async () => {
|
it('deactivates the RENTAL schedule row when the rental is cancelled', async () => {
|
||||||
const rental = {
|
const rental = {
|
||||||
id: 1,
|
id: 1,
|
||||||
classroomId: 1,
|
classroomId: 1,
|
||||||
@@ -379,7 +379,10 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
|||||||
await service.cancel(1);
|
await service.cancel(1);
|
||||||
|
|
||||||
expect(rentalRepo.update).toHaveBeenCalledWith(1, { status: 'cancelled' });
|
expect(rentalRepo.update).toHaveBeenCalledWith(1, { status: 'cancelled' });
|
||||||
expect(scheduleRepo.delete).toHaveBeenCalledWith({ rentalId: 1, scheduleType: 'RENTAL' });
|
expect(scheduleRepo.update).toHaveBeenCalledWith(
|
||||||
|
{ rentalId: 1, scheduleType: 'RENTAL' },
|
||||||
|
{ status: 'inactive' },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -419,14 +422,14 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('remove()', () => {
|
describe('remove()', () => {
|
||||||
it('deletes the RENTAL schedule row and the rental', async () => {
|
it('archives the rental and deactivates its RENTAL schedule row', async () => {
|
||||||
const rental = {
|
const rental = {
|
||||||
id: 1,
|
id: 1,
|
||||||
classroomId: 1,
|
classroomId: 1,
|
||||||
lesseeOrganizationId: 2,
|
lesseeOrganizationId: 2,
|
||||||
startDate: '2026-03-01',
|
startDate: '2026-03-01',
|
||||||
endDate: '2026-03-31',
|
endDate: '2026-03-31',
|
||||||
status: 'cancelled',
|
status: 'active',
|
||||||
lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
|
lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
|
||||||
} as ClassroomRental;
|
} as ClassroomRental;
|
||||||
|
|
||||||
@@ -434,8 +437,12 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
|||||||
|
|
||||||
await service.remove(1);
|
await service.remove(1);
|
||||||
|
|
||||||
expect(scheduleRepo.delete).toHaveBeenCalledWith({ rentalId: 1, scheduleType: 'RENTAL' });
|
expect(rentalRepo.update).toHaveBeenCalledWith(1, { status: 'cancelled' });
|
||||||
expect(rentalRepo.delete).toHaveBeenCalledWith(1);
|
expect(scheduleRepo.update).toHaveBeenCalledWith(
|
||||||
|
{ rentalId: 1, scheduleType: 'RENTAL' },
|
||||||
|
{ status: 'inactive' },
|
||||||
|
);
|
||||||
|
expect(rentalRepo.delete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ describe('DatabaseMigrationsService — course attendance schema', () => {
|
|||||||
],
|
],
|
||||||
getTable: { name: 'attendance_records', columns: [{ name: 'id' }] },
|
getTable: { name: 'attendance_records', columns: [{ name: 'id' }] },
|
||||||
});
|
});
|
||||||
await bootstrapCourseAttendance(runner);
|
const service = await bootstrapCourseAttendance(runner);
|
||||||
|
|
||||||
await service.ensureCourseAttendanceSchema();
|
await service.ensureCourseAttendanceSchema();
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ describe('DatabaseMigrationsService — course attendance schema', () => {
|
|||||||
? { name, columns: [{ name: 'id' }] }
|
? { name, columns: [{ name: 'id' }] }
|
||||||
: { name, columns: [{ name: 'id' }, { name: 'schedule_id' }, { name: 'attendance_session_id' }] },
|
: { name, columns: [{ name: 'id' }, { name: 'schedule_id' }, { name: 'attendance_session_id' }] },
|
||||||
);
|
);
|
||||||
await bootstrapCourseAttendance(runner);
|
const service = await bootstrapCourseAttendance(runner);
|
||||||
|
|
||||||
await service.ensureCourseAttendanceSchema();
|
await service.ensureCourseAttendanceSchema();
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ describe('DatabaseMigrationsService — course attendance schema', () => {
|
|||||||
getTables: [{ name: 'attendance_records', columns: [{ name: 'id' }] }],
|
getTables: [{ name: 'attendance_records', columns: [{ name: 'id' }] }],
|
||||||
getTable: { name: 'attendance_records', columns: [{ name: 'id' }] },
|
getTable: { name: 'attendance_records', columns: [{ name: 'id' }] },
|
||||||
});
|
});
|
||||||
await bootstrapCourseAttendance(runner);
|
const service = await bootstrapCourseAttendance(runner);
|
||||||
await service.ensureCourseAttendanceSchema();
|
await service.ensureCourseAttendanceSchema();
|
||||||
|
|
||||||
const createSql: string = (runner.query as jest.Mock).mock.calls
|
const createSql: string = (runner.query as jest.Mock).mock.calls
|
||||||
@@ -497,5 +497,5 @@ async function bootstrapCourseAttendance(runner: MockRunner) {
|
|||||||
{ provide: getDataSourceToken(), useValue: dataSource },
|
{ provide: getDataSourceToken(), useValue: dataSource },
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
service = module.get(DatabaseMigrationsService);
|
return module.get<MigrationsPrivate & DatabaseMigrationsService>(DatabaseMigrationsService);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user