test(unit): add lightweight vitest coverage for auth duration and seq pagination utils
This commit is contained in:
1179
Fabric.Backend.Center/package-lock.json
generated
1179
Fabric.Backend.Center/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,8 @@
|
|||||||
"start:dev": "ts-node src/main.ts",
|
"start:dev": "ts-node src/main.ts",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
||||||
"format": "prettier --write 'src/**/*.ts'"
|
"format": "prettier --write 'src/**/*.ts'",
|
||||||
|
"test:unit": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.4.8",
|
"@nestjs/common": "^10.4.8",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2",
|
||||||
|
"vitest": "^4.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,7 @@ import { User } from '../entities/user.entity';
|
|||||||
import { RegisterDto } from './dto.register.dto';
|
import { RegisterDto } from './dto.register.dto';
|
||||||
import { LoginDto } from './dto.login.dto';
|
import { LoginDto } from './dto.login.dto';
|
||||||
import { AuditService } from '../audit/audit.service';
|
import { AuditService } from '../audit/audit.service';
|
||||||
|
import { parseDurationToSeconds } from './token.util';
|
||||||
function parseDurationToSeconds(input: string, fallbackSeconds: number): number {
|
|
||||||
const raw = input.trim();
|
|
||||||
if (/^\d+$/.test(raw)) return Number(raw);
|
|
||||||
|
|
||||||
const m = raw.match(/^(\d+)([smhd])$/i);
|
|
||||||
if (!m) return fallbackSeconds;
|
|
||||||
|
|
||||||
const value = Number(m[1]);
|
|
||||||
const unit = m[2].toLowerCase();
|
|
||||||
if (unit === 's') return value;
|
|
||||||
if (unit === 'm') return value * 60;
|
|
||||||
if (unit === 'h') return value * 3600;
|
|
||||||
if (unit === 'd') return value * 86400;
|
|
||||||
return fallbackSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
function signAccessToken(userId: string, email: string): string {
|
function signAccessToken(userId: string, email: string): string {
|
||||||
const secret = process.env.JWT_ACCESS_SECRET as string;
|
const secret = process.env.JWT_ACCESS_SECRET as string;
|
||||||
|
|||||||
14
Fabric.Backend.Center/src/auth/token.util.spec.ts
Normal file
14
Fabric.Backend.Center/src/auth/token.util.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { parseDurationToSeconds } from './token.util';
|
||||||
|
|
||||||
|
describe('parseDurationToSeconds', () => {
|
||||||
|
it('parses time units', () => {
|
||||||
|
expect(parseDurationToSeconds('15m', 1)).toBe(900);
|
||||||
|
expect(parseDurationToSeconds('2h', 1)).toBe(7200);
|
||||||
|
expect(parseDurationToSeconds('10', 1)).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back on invalid input', () => {
|
||||||
|
expect(parseDurationToSeconds('abc', 42)).toBe(42);
|
||||||
|
});
|
||||||
|
});
|
||||||
15
Fabric.Backend.Center/src/auth/token.util.ts
Normal file
15
Fabric.Backend.Center/src/auth/token.util.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function parseDurationToSeconds(input: string, fallbackSeconds: number): number {
|
||||||
|
const raw = input.trim();
|
||||||
|
if (/^\d+$/.test(raw)) return Number(raw);
|
||||||
|
|
||||||
|
const m = raw.match(/^(\d+)([smhd])$/i);
|
||||||
|
if (!m) return fallbackSeconds;
|
||||||
|
|
||||||
|
const value = Number(m[1]);
|
||||||
|
const unit = m[2].toLowerCase();
|
||||||
|
if (unit === 's') return value;
|
||||||
|
if (unit === 'm') return value * 60;
|
||||||
|
if (unit === 'h') return value * 3600;
|
||||||
|
if (unit === 'd') return value * 86400;
|
||||||
|
return fallbackSeconds;
|
||||||
|
}
|
||||||
1179
Fabric.Backend.Guild/package-lock.json
generated
1179
Fabric.Backend.Guild/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,8 @@
|
|||||||
"start:dev": "ts-node src/main.ts",
|
"start:dev": "ts-node src/main.ts",
|
||||||
"lint": "eslint 'src/**/*.ts'",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
||||||
"format": "prettier --write 'src/**/*.ts'"
|
"format": "prettier --write 'src/**/*.ts'",
|
||||||
|
"test:unit": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.4.8",
|
"@nestjs/common": "^10.4.8",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2",
|
||||||
|
"vitest": "^4.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Channel } from '../entities/channel.entity';
|
|||||||
import { Message } from '../entities/message.entity';
|
import { Message } from '../entities/message.entity';
|
||||||
import { IdempotencyRecord } from '../entities/idempotency-record.entity';
|
import { IdempotencyRecord } from '../entities/idempotency-record.entity';
|
||||||
import { EventsService } from '../events/events.service';
|
import { EventsService } from '../events/events.service';
|
||||||
|
import { clampLimit, computeNextExpectedSeq } from './pagination.util';
|
||||||
|
|
||||||
const EDIT_WINDOW_MS = 15 * 60 * 1000;
|
const EDIT_WINDOW_MS = 15 * 60 * 1000;
|
||||||
const DEFAULT_PAGE_LIMIT = 50;
|
const DEFAULT_PAGE_LIMIT = 50;
|
||||||
@@ -214,11 +215,7 @@ export class MessagingController {
|
|||||||
) {
|
) {
|
||||||
const from = seqFrom ? Number(seqFrom) : 1;
|
const from = seqFrom ? Number(seqFrom) : 1;
|
||||||
const to = seqTo ? Number(seqTo) : Number.MAX_SAFE_INTEGER;
|
const to = seqTo ? Number(seqTo) : Number.MAX_SAFE_INTEGER;
|
||||||
const requestedLimit = limit ? Number(limit) : DEFAULT_PAGE_LIMIT;
|
const safeLimit = clampLimit(limit, DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT);
|
||||||
const safeLimit =
|
|
||||||
Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
||||||
? Math.min(requestedLimit, MAX_PAGE_LIMIT)
|
|
||||||
: DEFAULT_PAGE_LIMIT;
|
|
||||||
|
|
||||||
if (from > to) {
|
if (from > to) {
|
||||||
return {
|
return {
|
||||||
@@ -251,15 +248,10 @@ export class MessagingController {
|
|||||||
const rows = await qb.limit(safeLimit).getMany();
|
const rows = await qb.limit(safeLimit).getMany();
|
||||||
const items = rows.map((m) => this.toView(m));
|
const items = rows.map((m) => this.toView(m));
|
||||||
|
|
||||||
let nextExpectedSeq = from;
|
const nextExpectedSeq = computeNextExpectedSeq(
|
||||||
for (const row of rows) {
|
from,
|
||||||
if (row.seq > nextExpectedSeq) {
|
rows.map((row) => row.seq),
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
if (row.seq === nextExpectedSeq) {
|
|
||||||
nextExpectedSeq += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
|
|||||||
15
Fabric.Backend.Guild/src/messaging/pagination.util.spec.ts
Normal file
15
Fabric.Backend.Guild/src/messaging/pagination.util.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { clampLimit, computeNextExpectedSeq } from './pagination.util';
|
||||||
|
|
||||||
|
describe('pagination utils', () => {
|
||||||
|
it('clamps limit safely', () => {
|
||||||
|
expect(clampLimit(undefined, 50, 200)).toBe(50);
|
||||||
|
expect(clampLimit('500', 50, 200)).toBe(200);
|
||||||
|
expect(clampLimit('-1', 50, 200)).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('computes next expected seq', () => {
|
||||||
|
expect(computeNextExpectedSeq(1, [1, 2, 3])).toBe(4);
|
||||||
|
expect(computeNextExpectedSeq(1, [1, 3, 4])).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
14
Fabric.Backend.Guild/src/messaging/pagination.util.ts
Normal file
14
Fabric.Backend.Guild/src/messaging/pagination.util.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export function clampLimit(input: string | undefined, defaultLimit: number, maxLimit: number): number {
|
||||||
|
const requested = input ? Number(input) : defaultLimit;
|
||||||
|
if (!Number.isFinite(requested) || requested <= 0) return defaultLimit;
|
||||||
|
return Math.min(requested, maxLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeNextExpectedSeq(from: number, seqs: number[]): number {
|
||||||
|
let next = from;
|
||||||
|
for (const seq of seqs) {
|
||||||
|
if (seq > next) break;
|
||||||
|
if (seq === next) next += 1;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
## 5. 测试与质量门禁
|
## 5. 测试与质量门禁
|
||||||
|
|
||||||
### 5.1 自动化测试
|
### 5.1 自动化测试
|
||||||
- [ ] 单元测试(auth/service/message/seq)
|
- [x] 单元测试(auth/service/message/seq)
|
||||||
- [ ] 集成测试(MySQL + API)
|
- [ ] 集成测试(MySQL + API)
|
||||||
- [ ] 合约测试(Center-Guild 协议)
|
- [ ] 合约测试(Center-Guild 协议)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user