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,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
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 { AuthService } from './auth.service';
import { sha256 } from './auth.utils';
@@ -58,6 +61,27 @@ describe('AuthService', () => {
$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 = {
provider: 'google',
providerSubject: 'google-user-123',
@@ -94,6 +118,9 @@ describe('AuthService', () => {
AuthService,
{ provide: PrismaService, useValue: mockPrismaService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: EventEmitter2, useValue: mockEventEmitter },
{ provide: CacheService, useValue: mockCacheService },
{ provide: CacheTagsService, useValue: mockCacheTagsService },
],
}).compile();
@@ -135,6 +162,9 @@ describe('AuthService', () => {
const localService = new AuthService(
mockPrismaService as unknown as PrismaService,
mockConfigService as unknown as ConfigService,
mockEventEmitter as unknown as EventEmitter2,
mockCacheService as unknown as CacheService,
mockCacheTagsService as unknown as CacheTagsService,
);
expect(() =>

View File

@@ -4,6 +4,9 @@ import { DollsService } from './dolls.service';
import { PrismaService } from '../database/prisma.service';
import { NotFoundException, ForbiddenException } from '@nestjs/common';
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', () => {
let service: DollsService;
@@ -31,9 +34,6 @@ describe('DollsService', () => {
findFirst: jest.fn().mockResolvedValue(mockDoll),
update: jest.fn().mockResolvedValue(mockDoll),
},
friendship: {
findMany: jest.fn().mockResolvedValue([]),
},
$transaction: jest.fn((callback) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return callback(mockPrismaService);
@@ -47,6 +47,25 @@ describe('DollsService', () => {
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 () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -59,6 +78,18 @@ describe('DollsService', () => {
provide: EventEmitter2,
useValue: mockEventEmitter,
},
{
provide: CacheService,
useValue: mockCacheService,
},
{
provide: CacheTagsService,
useValue: mockCacheTagsService,
},
{
provide: FriendsService,
useValue: mockFriendsService,
},
],
}).compile();
@@ -112,10 +143,7 @@ describe('DollsService', () => {
const ownerId = 'friend-1';
const requestingUserId = 'user-1';
// Mock friendship
jest
.spyOn(prismaService.friendship, 'findMany')
.mockResolvedValueOnce([{ friendId: ownerId } as any]);
(mockFriendsService.areFriends as jest.Mock).mockResolvedValueOnce(true);
await service.listByOwner(ownerId, requestingUserId);
@@ -134,10 +162,7 @@ describe('DollsService', () => {
const ownerId = 'stranger-1';
const requestingUserId = 'user-1';
// Mock empty friendship (default)
jest
.spyOn(prismaService.friendship, 'findMany')
.mockResolvedValueOnce([]);
(mockFriendsService.areFriends as jest.Mock).mockResolvedValueOnce(false);
await expect(
service.listByOwner(ownerId, requestingUserId),
@@ -163,7 +188,10 @@ describe('DollsService', () => {
});
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(
NotFoundException,
@@ -179,7 +207,7 @@ describe('DollsService', () => {
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
.spyOn(prismaService.doll, 'findFirst')
.mockResolvedValueOnce({ ...mockDoll, userId: 'user-2' });
@@ -187,7 +215,7 @@ describe('DollsService', () => {
const updateDto = { name: 'Updated Doll' };
await expect(
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
.spyOn(prismaService.doll, 'findFirst')
.mockResolvedValueOnce({ ...mockDoll, userId: 'user-2' });
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 { FriendsService } from './friends.service';
import { PrismaService } from '../database/prisma.service';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import {
NotFoundException,
BadRequestException,
@@ -17,6 +19,8 @@ enum FriendRequestStatus {
describe('FriendsService', () => {
let service: FriendsService;
let eventEmitter: EventEmitter2;
let cacheService: CacheService;
let cacheTagsService: CacheTagsService;
const mockUser1 = {
id: 'user-1',
@@ -90,6 +94,21 @@ describe('FriendsService', () => {
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 () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -102,11 +121,21 @@ describe('FriendsService', () => {
provide: EventEmitter2,
useValue: mockEventEmitter,
},
{
provide: CacheService,
useValue: mockCacheService,
},
{
provide: CacheTagsService,
useValue: mockCacheTagsService,
},
],
}).compile();
service = module.get<FriendsService>(FriendsService);
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
cacheService = module.get<CacheService>(CacheService);
cacheTagsService = module.get<CacheTagsService>(CacheTagsService);
jest.clearAllMocks();
});
@@ -420,6 +449,8 @@ describe('FriendsService', () => {
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');
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 () => {
@@ -477,6 +514,11 @@ describe('FriendsService', () => {
const result = await service.areFriends('user-1', 'user-2');
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 { UpdateUserDto } from './dto/update-user.dto';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
describe('UsersService', () => {
let service: UsersService;
let cacheService: CacheService;
let cacheTagsService: CacheTagsService;
const mockUser: User & { passwordHash?: string | null } = {
id: '550e8400-e29b-41d4-a716-446655440000',
@@ -39,6 +43,21 @@ describe('UsersService', () => {
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 () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -51,10 +70,20 @@ describe('UsersService', () => {
provide: EventEmitter2,
useValue: mockEventEmitter,
},
{
provide: CacheService,
useValue: mockCacheService,
},
{
provide: CacheTagsService,
useValue: mockCacheTagsService,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
cacheService = module.get<CacheService>(CacheService);
cacheTagsService = module.get<CacheTagsService>(CacheTagsService);
jest.clearAllMocks();
});
@@ -227,6 +256,8 @@ describe('UsersService', () => {
username: 'asc',
},
});
expect(cacheService.set).toHaveBeenCalled();
expect(cacheTagsService.rememberKeyForTag).toHaveBeenCalled();
});
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 { UserSocketService } from './user-socket.service';
import { WsNotificationService } from './ws-notification.service';
import { ConfigService } from '@nestjs/config';
import { SendInteractionDto } from '../dto/send-interaction.dto';
import { WsException } from '@nestjs/websockets';
@@ -45,6 +46,7 @@ describe('StateGateway', () => {
let mockUserSocketService: Partial<UserSocketService>;
let mockRedisClient: { publish: jest.Mock };
let mockRedisSubscriber: { subscribe: jest.Mock; on: jest.Mock };
let mockConfigService: { get: jest.Mock };
let mockWsNotificationService: {
setIo: jest.Mock;
emitToUser: jest.Mock;
@@ -52,6 +54,8 @@ describe('StateGateway', () => {
emitToSocket: jest.Mock;
updateActiveDollCache: jest.Mock;
publishActiveDollUpdate: jest.Mock;
clearSenderNameCache: jest.Mock;
maybeTouchPresence: jest.Mock;
};
beforeEach(async () => {
@@ -92,9 +96,12 @@ describe('StateGateway', () => {
mockUserSocketService = {
setSocket: jest.fn().mockResolvedValue(undefined),
removeSocket: jest.fn().mockResolvedValue(undefined),
removeSocketById: jest.fn().mockResolvedValue(undefined),
touchLastSeen: jest.fn().mockResolvedValue(undefined),
getSocket: jest.fn().mockResolvedValue(null),
isUserOnline: jest.fn().mockResolvedValue(false),
getFriendsSockets: jest.fn().mockResolvedValue([]),
cleanupStalePresence: jest.fn().mockResolvedValue(0),
};
mockRedisClient = {
@@ -106,6 +113,10 @@ describe('StateGateway', () => {
on: jest.fn(),
};
mockConfigService = {
get: jest.fn().mockReturnValue(undefined),
};
mockWsNotificationService = {
setIo: jest.fn(),
emitToUser: jest.fn(),
@@ -113,6 +124,8 @@ describe('StateGateway', () => {
emitToSocket: jest.fn(),
updateActiveDollCache: jest.fn(),
publishActiveDollUpdate: jest.fn(),
clearSenderNameCache: jest.fn().mockResolvedValue(undefined),
maybeTouchPresence: jest.fn().mockResolvedValue(undefined),
};
const module: TestingModule = await Test.createTestingModule({
@@ -125,6 +138,7 @@ describe('StateGateway', () => {
{ provide: PrismaService, useValue: mockPrismaService },
{ provide: UserSocketService, useValue: mockUserSocketService },
{ provide: WsNotificationService, useValue: mockWsNotificationService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: 'REDIS_CLIENT', useValue: mockRedisClient },
{ provide: 'REDIS_SUBSCRIBER_CLIENT', useValue: mockRedisSubscriber },
],
@@ -161,9 +175,32 @@ describe('StateGateway', () => {
expect(mockRedisSubscriber.subscribe).toHaveBeenCalledWith(
'active-doll-update',
'friend-cache-update',
'user-profile-cache-invalidate',
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', () => {
@@ -260,6 +297,9 @@ describe('StateGateway', () => {
'user-id',
'client1',
);
expect(mockUserSocketService.touchLastSeen).toHaveBeenCalledWith(
'user-id',
);
// 2. Fetch State (DB)
expect(mockPrismaService.user!.findUnique).toHaveBeenCalledWith({
@@ -359,6 +399,13 @@ describe('StateGateway', () => {
expect(mockUserSocketService.getSocket).toHaveBeenCalledWith('user-id');
expect(mockUserSocketService.removeSocket).toHaveBeenCalledWith(
'user-id',
'client1',
);
expect(mockUserSocketService.touchLastSeen).toHaveBeenCalledWith(
'user-id',
);
expect(mockUserSocketService.removeSocketById).toHaveBeenCalledWith(
'client1',
);
expect(mockWsNotificationService.emitToSocket).toHaveBeenCalledWith(
'friend-socket-id',