import { ReviewService, summarizeReviews } from '../review.service' import { PrismaService } from '../../prisma/prisma.service' import { Prisma } from '@prisma/client' import { CreateReviewDto } from '../dto/create-review.dto' import { validate } from 'class-validator' import 'reflect-metadata' import { Reflector } from '@nestjs/core' import { ExecutionContext } from '@nestjs/common' import { GUARDS_METADATA } from '@nestjs/common/constants' import { ReviewController, PublicReviewController } from '../review.controller' import { JwtAuthGuard } from '../../auth/jwt-auth.guard' import { RolesGuard } from '../../auth/roles.guard' describe('Class reviews', () => { const db = { booking: { findFirst: jest.fn() }, bookingReview: { create: jest.fn(), findMany: jest.fn(), count: jest.fn() } } let service: ReviewService beforeEach(() => { jest.resetAllMocks(); service = new ReviewService(db as unknown as PrismaService) }) const dto = { rating: 5, tags: ['氛围好'], comment: '很有收获' } it('does not expose another member booking or review', async () => { db.booking.findFirst.mockResolvedValue(null) await expect(service.get('other', 'booking')).rejects.toThrow('预约不存在') expect(db.booking.findFirst).toHaveBeenCalledWith(expect.objectContaining({ where: { id: 'booking', userId: 'other' } })) await expect(service.create('other', 'booking', dto)).rejects.toThrow('预约不存在') expect(db.bookingReview.create).not.toHaveBeenCalled() }) it.each(['CONFIRMED', 'NO_SHOW', 'CANCELLED', 'PENDING_CONFIRMATION'])('rejects %s booking', async status => { db.booking.findFirst.mockResolvedValue({ status }) await expect(service.create('member', 'booking', dto)).rejects.toThrow('完成课程后才能评价') }) it('allows older completed lessons, with database uniqueness protecting double submits', async () => { db.booking.findFirst.mockResolvedValue({ status: 'COMPLETED', completedAt: new Date('2020-01-01') }) db.bookingReview.create.mockResolvedValueOnce({ id: 'review' }).mockRejectedValueOnce(new Prisma.PrismaClientKnownRequestError('duplicate', { code: 'P2002', clientVersion: '5' })) await expect(service.create('member', 'booking', dto)).resolves.toEqual({ id: 'review' }) await expect(service.create('member', 'booking', dto)).rejects.toThrow('已经评价') }) it.each([{ rating: 0 }, { rating: 6 }, { rating: 2.5 }, { tags: ['伪造标签'] }, { tags: ['氛围好', '氛围好'] }, { tags: ['动作到位','氛围好','强度合适','讲解清晰'] }, { recommendation: 11 }, { recommendation: -1 }, { comment: '长'.repeat(201) }])('rejects invalid payload %j', async patch => { expect((await validate(Object.assign(new CreateReviewDto(), dto, patch))).length).toBeGreaterThan(0) }) it('accepts optional recommendation including zero', async () => { expect(await validate(Object.assign(new CreateReviewDto(), dto, { recommendation: 0 }))).toHaveLength(0) }) it('keeps star average separate from NPS and handles zero samples', () => { expect(summarizeReviews([])).toEqual({ count: 0, average: null, nps: null, npsCount: 0 }) expect(summarizeReviews([{ rating: 5, recommendation: 0 }, { rating: 1, recommendation: 9 }, { rating: 4, recommendation: 7 }, { rating: 4, recommendation: null }])).toEqual({ count: 4, average: 3.5, nps: 0, npsCount: 3 }) }) it('groups the six Chinese calendar months across year and UTC boundaries', async () => { db.bookingReview.findMany.mockResolvedValue([{ rating: 5, recommendation: 10, createdAt: new Date('2025-12-31T16:00:00Z') }, { rating: 1, recommendation: 0, createdAt: new Date('2025-12-31T15:59:59Z') }]) const result = await service.trend('2026-01') expect(result.map(r => r.month)).toEqual(['2025-08','2025-09','2025-10','2025-11','2025-12','2026-01']) expect(result[4].average).toBe(1); expect(result[5].average).toBe(5) expect(db.bookingReview.findMany.mock.calls[0][0].where.createdAt).toEqual({ gte: new Date('2025-07-31T16:00:00Z'), lt: new Date('2026-01-31T16:00:00Z') }) }) }) describe('Review authorization', () => { it('requires authentication for member and admin review endpoints', () => { expect(Reflect.getMetadata(GUARDS_METADATA, ReviewController)).toContain(JwtAuthGuard) }) it('keeps the public summary unauthenticated', () => { expect(Reflect.getMetadata(GUARDS_METADATA, PublicReviewController) || []).not.toContain(JwtAuthGuard) }) it.each(['list', 'trend'] as const)('restricts %s to admins', method => { const handler = ReviewController.prototype[method] expect(Reflect.getMetadata(GUARDS_METADATA, handler)).toContain(RolesGuard) const guard = new RolesGuard(new Reflector()) const context = (role: string) => ({ getHandler: () => handler, getClass: () => ReviewController, switchToHttp: () => ({ getRequest: () => ({ user: { role } }) }) }) as unknown as ExecutionContext expect(guard.canActivate(context('MEMBER'))).toBe(false) expect(guard.canActivate(context('ADMIN'))).toBe(true) }) })