redis pt 5: fix tests

This commit is contained in:
2026-03-31 00:43:22 +08:00
parent 4151984b5c
commit 302cf5cb0d
5 changed files with 194 additions and 16 deletions

View File

@@ -4,8 +4,11 @@ import {
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { decode, sign } from 'jsonwebtoken'; import { decode, sign } from 'jsonwebtoken';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import { PrismaService } from '../database/prisma.service'; import { PrismaService } from '../database/prisma.service';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { sha256 } from './auth.utils'; import { sha256 } from './auth.utils';
@@ -58,6 +61,27 @@ describe('AuthService', () => {
$transaction: jest.fn(), $transaction: jest.fn(),
}; };
const mockEventEmitter = {
emit: jest.fn(),
};
const mockCacheService = {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(true),
del: jest.fn().mockResolvedValue(true),
getNamespacedKey: jest
.fn()
.mockImplementation(
(namespace: string, key: string) => `friendolls:${namespace}:${key}`,
),
recordError: jest.fn(),
};
const mockCacheTagsService = {
rememberKeyForTag: jest.fn().mockResolvedValue(undefined),
invalidateTag: jest.fn().mockResolvedValue(undefined),
};
const socialProfile: SocialAuthProfile = { const socialProfile: SocialAuthProfile = {
provider: 'google', provider: 'google',
providerSubject: 'google-user-123', providerSubject: 'google-user-123',
@@ -94,6 +118,9 @@ describe('AuthService', () => {
AuthService, AuthService,
{ provide: PrismaService, useValue: mockPrismaService }, { provide: PrismaService, useValue: mockPrismaService },
{ provide: ConfigService, useValue: mockConfigService }, { provide: ConfigService, useValue: mockConfigService },
{ provide: EventEmitter2, useValue: mockEventEmitter },
{ provide: CacheService, useValue: mockCacheService },
{ provide: CacheTagsService, useValue: mockCacheTagsService },
], ],
}).compile(); }).compile();
@@ -135,6 +162,9 @@ describe('AuthService', () => {
const localService = new AuthService( const localService = new AuthService(
mockPrismaService as unknown as PrismaService, mockPrismaService as unknown as PrismaService,
mockConfigService as unknown as ConfigService, mockConfigService as unknown as ConfigService,
mockEventEmitter as unknown as EventEmitter2,
mockCacheService as unknown as CacheService,
mockCacheTagsService as unknown as CacheTagsService,
); );
expect(() => expect(() =>

View File

@@ -4,6 +4,9 @@ import { DollsService } from './dolls.service';
import { PrismaService } from '../database/prisma.service'; import { PrismaService } from '../database/prisma.service';
import { NotFoundException, ForbiddenException } from '@nestjs/common'; import { NotFoundException, ForbiddenException } from '@nestjs/common';
import { Doll } from '@prisma/client'; import { Doll } from '@prisma/client';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import { FriendsService } from '../friends/friends.service';
describe('DollsService', () => { describe('DollsService', () => {
let service: DollsService; let service: DollsService;
@@ -31,9 +34,6 @@ describe('DollsService', () => {
findFirst: jest.fn().mockResolvedValue(mockDoll), findFirst: jest.fn().mockResolvedValue(mockDoll),
update: jest.fn().mockResolvedValue(mockDoll), update: jest.fn().mockResolvedValue(mockDoll),
}, },
friendship: {
findMany: jest.fn().mockResolvedValue([]),
},
$transaction: jest.fn((callback) => { $transaction: jest.fn((callback) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return callback(mockPrismaService); return callback(mockPrismaService);
@@ -47,6 +47,25 @@ describe('DollsService', () => {
emit: jest.fn(), emit: jest.fn(),
}; };
const mockCacheService = {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(true),
getNamespacedKey: jest
.fn()
.mockImplementation(
(namespace: string, key: string) => `friendolls:${namespace}:${key}`,
),
recordError: jest.fn(),
};
const mockCacheTagsService = {
rememberKeyForTag: jest.fn().mockResolvedValue(undefined),
};
const mockFriendsService = {
areFriends: jest.fn().mockResolvedValue(false),
};
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
@@ -59,6 +78,18 @@ describe('DollsService', () => {
provide: EventEmitter2, provide: EventEmitter2,
useValue: mockEventEmitter, useValue: mockEventEmitter,
}, },
{
provide: CacheService,
useValue: mockCacheService,
},
{
provide: CacheTagsService,
useValue: mockCacheTagsService,
},
{
provide: FriendsService,
useValue: mockFriendsService,
},
], ],
}).compile(); }).compile();
@@ -112,10 +143,7 @@ describe('DollsService', () => {
const ownerId = 'friend-1'; const ownerId = 'friend-1';
const requestingUserId = 'user-1'; const requestingUserId = 'user-1';
// Mock friendship (mockFriendsService.areFriends as jest.Mock).mockResolvedValueOnce(true);
jest
.spyOn(prismaService.friendship, 'findMany')
.mockResolvedValueOnce([{ friendId: ownerId } as any]);
await service.listByOwner(ownerId, requestingUserId); await service.listByOwner(ownerId, requestingUserId);
@@ -134,10 +162,7 @@ describe('DollsService', () => {
const ownerId = 'stranger-1'; const ownerId = 'stranger-1';
const requestingUserId = 'user-1'; const requestingUserId = 'user-1';
// Mock empty friendship (default) (mockFriendsService.areFriends as jest.Mock).mockResolvedValueOnce(false);
jest
.spyOn(prismaService.friendship, 'findMany')
.mockResolvedValueOnce([]);
await expect( await expect(
service.listByOwner(ownerId, requestingUserId), service.listByOwner(ownerId, requestingUserId),
@@ -163,7 +188,10 @@ describe('DollsService', () => {
}); });
it('should throw NotFoundException if doll not accessible', async () => { it('should throw NotFoundException if doll not accessible', async () => {
jest.spyOn(prismaService.doll, 'findFirst').mockResolvedValueOnce(null); jest
.spyOn(prismaService.doll, 'findFirst')
.mockResolvedValueOnce({ ...mockDoll, userId: 'user-2' });
(mockFriendsService.areFriends as jest.Mock).mockResolvedValueOnce(false);
await expect(service.findOne('doll-1', 'user-1')).rejects.toThrow( await expect(service.findOne('doll-1', 'user-1')).rejects.toThrow(
NotFoundException, NotFoundException,
@@ -179,7 +207,7 @@ describe('DollsService', () => {
expect(prismaService.doll.update).toHaveBeenCalled(); expect(prismaService.doll.update).toHaveBeenCalled();
}); });
it('should throw ForbiddenException if not owner', async () => { it('should throw NotFoundException if not owner and not a friend', async () => {
jest jest
.spyOn(prismaService.doll, 'findFirst') .spyOn(prismaService.doll, 'findFirst')
.mockResolvedValueOnce({ ...mockDoll, userId: 'user-2' }); .mockResolvedValueOnce({ ...mockDoll, userId: 'user-2' });
@@ -187,7 +215,7 @@ describe('DollsService', () => {
const updateDto = { name: 'Updated Doll' }; const updateDto = { name: 'Updated Doll' };
await expect( await expect(
service.update('doll-1', 'user-1', updateDto), service.update('doll-1', 'user-1', updateDto),
).rejects.toThrow(ForbiddenException); ).rejects.toThrow(NotFoundException);
}); });
}); });
@@ -203,13 +231,13 @@ describe('DollsService', () => {
}); });
}); });
it('should throw ForbiddenException if not owner', async () => { it('should throw NotFoundException if not owner and not a friend', async () => {
jest jest
.spyOn(prismaService.doll, 'findFirst') .spyOn(prismaService.doll, 'findFirst')
.mockResolvedValueOnce({ ...mockDoll, userId: 'user-2' }); .mockResolvedValueOnce({ ...mockDoll, userId: 'user-2' });
await expect(service.remove('doll-1', 'user-1')).rejects.toThrow( await expect(service.remove('doll-1', 'user-1')).rejects.toThrow(
ForbiddenException, NotFoundException,
); );
}); });
}); });

View File

@@ -2,6 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { FriendsService } from './friends.service'; import { FriendsService } from './friends.service';
import { PrismaService } from '../database/prisma.service'; import { PrismaService } from '../database/prisma.service';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import { import {
NotFoundException, NotFoundException,
BadRequestException, BadRequestException,
@@ -17,6 +19,8 @@ enum FriendRequestStatus {
describe('FriendsService', () => { describe('FriendsService', () => {
let service: FriendsService; let service: FriendsService;
let eventEmitter: EventEmitter2; let eventEmitter: EventEmitter2;
let cacheService: CacheService;
let cacheTagsService: CacheTagsService;
const mockUser1 = { const mockUser1 = {
id: 'user-1', id: 'user-1',
@@ -90,6 +94,21 @@ describe('FriendsService', () => {
emit: jest.fn(), emit: jest.fn(),
}; };
const mockCacheService = {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(true),
getNamespacedKey: jest
.fn()
.mockImplementation(
(namespace: string, key: string) => `friendolls:${namespace}:${key}`,
),
recordError: jest.fn(),
};
const mockCacheTagsService = {
rememberKeyForTag: jest.fn().mockResolvedValue(undefined),
};
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
@@ -102,11 +121,21 @@ describe('FriendsService', () => {
provide: EventEmitter2, provide: EventEmitter2,
useValue: mockEventEmitter, useValue: mockEventEmitter,
}, },
{
provide: CacheService,
useValue: mockCacheService,
},
{
provide: CacheTagsService,
useValue: mockCacheTagsService,
},
], ],
}).compile(); }).compile();
service = module.get<FriendsService>(FriendsService); service = module.get<FriendsService>(FriendsService);
eventEmitter = module.get<EventEmitter2>(EventEmitter2); eventEmitter = module.get<EventEmitter2>(EventEmitter2);
cacheService = module.get<CacheService>(CacheService);
cacheTagsService = module.get<CacheTagsService>(CacheTagsService);
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@@ -420,6 +449,8 @@ describe('FriendsService', () => {
createdAt: 'desc', createdAt: 'desc',
}, },
}); });
expect(cacheService.set).toHaveBeenCalled();
expect(cacheTagsService.rememberKeyForTag).toHaveBeenCalled();
}); });
}); });
@@ -469,6 +500,12 @@ describe('FriendsService', () => {
const result = await service.areFriends('user-1', 'user-2'); const result = await service.areFriends('user-1', 'user-2');
expect(result).toBe(true); expect(result).toBe(true);
expect(cacheService.set).toHaveBeenCalledWith(
expect.any(String),
'1',
expect.any(Number),
);
expect(cacheTagsService.rememberKeyForTag).toHaveBeenCalled();
}); });
it('should return false when users are not friends', async () => { it('should return false when users are not friends', async () => {
@@ -477,6 +514,11 @@ describe('FriendsService', () => {
const result = await service.areFriends('user-1', 'user-2'); const result = await service.areFriends('user-1', 'user-2');
expect(result).toBe(false); expect(result).toBe(false);
expect(cacheService.set).toHaveBeenCalledWith(
expect.any(String),
'0',
expect.any(Number),
);
}); });
}); });
}); });

View File

@@ -5,9 +5,13 @@ import { NotFoundException, ForbiddenException } from '@nestjs/common';
import { User } from '@prisma/client'; import { User } from '@prisma/client';
import { UpdateUserDto } from './dto/update-user.dto'; import { UpdateUserDto } from './dto/update-user.dto';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
describe('UsersService', () => { describe('UsersService', () => {
let service: UsersService; let service: UsersService;
let cacheService: CacheService;
let cacheTagsService: CacheTagsService;
const mockUser: User & { passwordHash?: string | null } = { const mockUser: User & { passwordHash?: string | null } = {
id: '550e8400-e29b-41d4-a716-446655440000', id: '550e8400-e29b-41d4-a716-446655440000',
@@ -39,6 +43,21 @@ describe('UsersService', () => {
emit: jest.fn(), emit: jest.fn(),
}; };
const mockCacheService = {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(true),
getNamespacedKey: jest
.fn()
.mockImplementation(
(namespace: string, key: string) => `friendolls:${namespace}:${key}`,
),
recordError: jest.fn(),
};
const mockCacheTagsService = {
rememberKeyForTag: jest.fn().mockResolvedValue(undefined),
};
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
@@ -51,10 +70,20 @@ describe('UsersService', () => {
provide: EventEmitter2, provide: EventEmitter2,
useValue: mockEventEmitter, useValue: mockEventEmitter,
}, },
{
provide: CacheService,
useValue: mockCacheService,
},
{
provide: CacheTagsService,
useValue: mockCacheTagsService,
},
], ],
}).compile(); }).compile();
service = module.get<UsersService>(UsersService); service = module.get<UsersService>(UsersService);
cacheService = module.get<CacheService>(CacheService);
cacheTagsService = module.get<CacheTagsService>(CacheTagsService);
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@@ -227,6 +256,8 @@ describe('UsersService', () => {
username: 'asc', username: 'asc',
}, },
}); });
expect(cacheService.set).toHaveBeenCalled();
expect(cacheTagsService.rememberKeyForTag).toHaveBeenCalled();
}); });
it('should exclude specified user from results', async () => { it('should exclude specified user from results', async () => {

View File

@@ -6,6 +6,7 @@ import { JwtVerificationService } from '../../auth/services/jwt-verification.ser
import { PrismaService } from '../../database/prisma.service'; import { PrismaService } from '../../database/prisma.service';
import { UserSocketService } from './user-socket.service'; import { UserSocketService } from './user-socket.service';
import { WsNotificationService } from './ws-notification.service'; import { WsNotificationService } from './ws-notification.service';
import { ConfigService } from '@nestjs/config';
import { SendInteractionDto } from '../dto/send-interaction.dto'; import { SendInteractionDto } from '../dto/send-interaction.dto';
import { WsException } from '@nestjs/websockets'; import { WsException } from '@nestjs/websockets';
@@ -45,6 +46,7 @@ describe('StateGateway', () => {
let mockUserSocketService: Partial<UserSocketService>; let mockUserSocketService: Partial<UserSocketService>;
let mockRedisClient: { publish: jest.Mock }; let mockRedisClient: { publish: jest.Mock };
let mockRedisSubscriber: { subscribe: jest.Mock; on: jest.Mock }; let mockRedisSubscriber: { subscribe: jest.Mock; on: jest.Mock };
let mockConfigService: { get: jest.Mock };
let mockWsNotificationService: { let mockWsNotificationService: {
setIo: jest.Mock; setIo: jest.Mock;
emitToUser: jest.Mock; emitToUser: jest.Mock;
@@ -52,6 +54,8 @@ describe('StateGateway', () => {
emitToSocket: jest.Mock; emitToSocket: jest.Mock;
updateActiveDollCache: jest.Mock; updateActiveDollCache: jest.Mock;
publishActiveDollUpdate: jest.Mock; publishActiveDollUpdate: jest.Mock;
clearSenderNameCache: jest.Mock;
maybeTouchPresence: jest.Mock;
}; };
beforeEach(async () => { beforeEach(async () => {
@@ -92,9 +96,12 @@ describe('StateGateway', () => {
mockUserSocketService = { mockUserSocketService = {
setSocket: jest.fn().mockResolvedValue(undefined), setSocket: jest.fn().mockResolvedValue(undefined),
removeSocket: jest.fn().mockResolvedValue(undefined), removeSocket: jest.fn().mockResolvedValue(undefined),
removeSocketById: jest.fn().mockResolvedValue(undefined),
touchLastSeen: jest.fn().mockResolvedValue(undefined),
getSocket: jest.fn().mockResolvedValue(null), getSocket: jest.fn().mockResolvedValue(null),
isUserOnline: jest.fn().mockResolvedValue(false), isUserOnline: jest.fn().mockResolvedValue(false),
getFriendsSockets: jest.fn().mockResolvedValue([]), getFriendsSockets: jest.fn().mockResolvedValue([]),
cleanupStalePresence: jest.fn().mockResolvedValue(0),
}; };
mockRedisClient = { mockRedisClient = {
@@ -106,6 +113,10 @@ describe('StateGateway', () => {
on: jest.fn(), on: jest.fn(),
}; };
mockConfigService = {
get: jest.fn().mockReturnValue(undefined),
};
mockWsNotificationService = { mockWsNotificationService = {
setIo: jest.fn(), setIo: jest.fn(),
emitToUser: jest.fn(), emitToUser: jest.fn(),
@@ -113,6 +124,8 @@ describe('StateGateway', () => {
emitToSocket: jest.fn(), emitToSocket: jest.fn(),
updateActiveDollCache: jest.fn(), updateActiveDollCache: jest.fn(),
publishActiveDollUpdate: jest.fn(), publishActiveDollUpdate: jest.fn(),
clearSenderNameCache: jest.fn().mockResolvedValue(undefined),
maybeTouchPresence: jest.fn().mockResolvedValue(undefined),
}; };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@@ -125,6 +138,7 @@ describe('StateGateway', () => {
{ provide: PrismaService, useValue: mockPrismaService }, { provide: PrismaService, useValue: mockPrismaService },
{ provide: UserSocketService, useValue: mockUserSocketService }, { provide: UserSocketService, useValue: mockUserSocketService },
{ provide: WsNotificationService, useValue: mockWsNotificationService }, { provide: WsNotificationService, useValue: mockWsNotificationService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: 'REDIS_CLIENT', useValue: mockRedisClient }, { provide: 'REDIS_CLIENT', useValue: mockRedisClient },
{ provide: 'REDIS_SUBSCRIBER_CLIENT', useValue: mockRedisSubscriber }, { provide: 'REDIS_SUBSCRIBER_CLIENT', useValue: mockRedisSubscriber },
], ],
@@ -161,9 +175,32 @@ describe('StateGateway', () => {
expect(mockRedisSubscriber.subscribe).toHaveBeenCalledWith( expect(mockRedisSubscriber.subscribe).toHaveBeenCalledWith(
'active-doll-update', 'active-doll-update',
'friend-cache-update', 'friend-cache-update',
'user-profile-cache-invalidate',
expect.any(Function), expect.any(Function),
); );
}); });
it('should route user profile cache invalidation messages', async () => {
gateway.afterInit();
const onCalls = (mockRedisSubscriber.on as jest.Mock).mock.calls;
const messageHandler = onCalls.find(
(call) => call[0] === 'message',
)?.[1] as ((channel: string, message: string) => void) | undefined;
expect(messageHandler).toBeDefined();
messageHandler?.(
'user-profile-cache-invalidate',
JSON.stringify({ userId: 'user-1' }),
);
await Promise.resolve();
expect(
mockWsNotificationService.clearSenderNameCache,
).toHaveBeenCalledWith('user-1');
});
}); });
describe('handleConnection', () => { describe('handleConnection', () => {
@@ -260,6 +297,9 @@ describe('StateGateway', () => {
'user-id', 'user-id',
'client1', 'client1',
); );
expect(mockUserSocketService.touchLastSeen).toHaveBeenCalledWith(
'user-id',
);
// 2. Fetch State (DB) // 2. Fetch State (DB)
expect(mockPrismaService.user!.findUnique).toHaveBeenCalledWith({ expect(mockPrismaService.user!.findUnique).toHaveBeenCalledWith({
@@ -359,6 +399,13 @@ describe('StateGateway', () => {
expect(mockUserSocketService.getSocket).toHaveBeenCalledWith('user-id'); expect(mockUserSocketService.getSocket).toHaveBeenCalledWith('user-id');
expect(mockUserSocketService.removeSocket).toHaveBeenCalledWith( expect(mockUserSocketService.removeSocket).toHaveBeenCalledWith(
'user-id', 'user-id',
'client1',
);
expect(mockUserSocketService.touchLastSeen).toHaveBeenCalledWith(
'user-id',
);
expect(mockUserSocketService.removeSocketById).toHaveBeenCalledWith(
'client1',
); );
expect(mockWsNotificationService.emitToSocket).toHaveBeenCalledWith( expect(mockWsNotificationService.emitToSocket).toHaveBeenCalledWith(
'friend-socket-id', 'friend-socket-id',