stylist.service.spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { ConflictException, NotFoundException } from '@nestjs/common';
  2. import { Test } from '@nestjs/testing';
  3. import { PrismaService } from '../../prisma/prisma.service';
  4. import { StylistService } from './stylist.service';
  5. describe('StylistService', () => {
  6. let service: StylistService;
  7. const prisma = {
  8. hairstylistUser: {
  9. findFirst: jest.fn(),
  10. update: jest.fn(),
  11. },
  12. hairTemplate: {
  13. count: jest.fn(),
  14. findMany: jest.fn(),
  15. },
  16. };
  17. const stylist = {
  18. id: BigInt(7),
  19. username: 'Yasuaki',
  20. email: 'yasuaki@example.com',
  21. simulationInviteCode: 'ABCD1234',
  22. displayName: 'YASUAKI OGAWA',
  23. slug: 'yasuaki-ogawa',
  24. photoUrl: 'https://cdn.example.com/photo.jpg',
  25. lead: '短介绍',
  26. bio: '详细介绍',
  27. bookingUrl: 'https://example.com/booking',
  28. experienceYears: 22,
  29. rating: '5.0',
  30. reviewCount: 124,
  31. specialties: '鲍勃',
  32. address: '东京',
  33. hours: '预约制',
  34. password: 'hashed',
  35. createTime: new Date('2026-09-17T00:00:00.000Z'),
  36. updateTime: new Date('2026-09-17T00:00:00.000Z'),
  37. isDeleted: 0,
  38. };
  39. beforeEach(async () => {
  40. jest.resetAllMocks();
  41. const moduleRef = await Test.createTestingModule({
  42. providers: [StylistService, { provide: PrismaService, useValue: prisma }],
  43. }).compile();
  44. service = moduleRef.get(StylistService);
  45. });
  46. it('returns the authenticated stylist profile with published template count', async () => {
  47. prisma.hairstylistUser.findFirst.mockResolvedValue(stylist);
  48. prisma.hairTemplate.count.mockResolvedValue(3);
  49. prisma.hairTemplate.findMany.mockResolvedValue([
  50. {
  51. id: BigInt(11),
  52. templateName: '柔和长发',
  53. templateNameI18n: { zh: '柔和长发', ja: '柔らかいロング', en: 'Soft long hair' },
  54. coverUrl: 'https://cdn.example.com/cover.jpg',
  55. category: 'LONG',
  56. description: '自然长发',
  57. },
  58. ]);
  59. await expect(service.getMyProfile(BigInt(7))).resolves.toMatchObject({
  60. id: '7',
  61. displayName: 'YASUAKI OGAWA',
  62. slug: 'yasuaki-ogawa',
  63. inviteCode: 'ABCD1234',
  64. publishedTemplateCount: 3,
  65. experienceYears: 22,
  66. });
  67. });
  68. it('rejects reserved public slugs', async () => {
  69. prisma.hairstylistUser.findFirst.mockResolvedValue(stylist);
  70. await expect(
  71. service.updateMyProfile(BigInt(7), {
  72. displayName: 'YASUAKI OGAWA',
  73. slug: 'manage',
  74. }),
  75. ).rejects.toThrow('该公开主页路径不可用');
  76. });
  77. it('rejects duplicate public slugs', async () => {
  78. prisma.hairstylistUser.findFirst
  79. .mockResolvedValueOnce(stylist)
  80. .mockResolvedValueOnce({ id: BigInt(8) });
  81. await expect(
  82. service.updateMyProfile(BigInt(7), {
  83. displayName: 'YASUAKI OGAWA',
  84. slug: 'taken-slug',
  85. }),
  86. ).rejects.toBeInstanceOf(ConflictException);
  87. });
  88. it('returns a public profile with published templates only', async () => {
  89. prisma.hairstylistUser.findFirst.mockResolvedValue(stylist);
  90. prisma.hairTemplate.count.mockResolvedValue(1);
  91. prisma.hairTemplate.findMany.mockResolvedValue([
  92. {
  93. id: BigInt(11),
  94. templateName: '柔和长发',
  95. templateNameI18n: { zh: '柔和长发', ja: '柔らかいロング', en: 'Soft long hair' },
  96. coverUrl: 'https://cdn.example.com/cover.jpg',
  97. category: 'LONG',
  98. description: '自然长发',
  99. },
  100. ]);
  101. const result = await service.getPublicProfile('yasuaki-ogawa');
  102. expect(result).not.toHaveProperty('email');
  103. expect(result).not.toHaveProperty('username');
  104. expect(result.templates).toEqual([
  105. expect.objectContaining({
  106. id: '11',
  107. templateName: '柔和长发',
  108. coverUrl: 'https://cdn.example.com/cover.jpg',
  109. }),
  110. ]);
  111. expect(prisma.hairTemplate.findMany).toHaveBeenCalledWith(
  112. expect.objectContaining({
  113. where: expect.objectContaining({
  114. status: 'PUBLIC',
  115. isSystemTemplate: 'NO',
  116. templateUrl: { not: '' },
  117. }),
  118. }),
  119. );
  120. });
  121. it('persists a public slug and invite code when they are missing', async () => {
  122. const pending = { ...stylist, slug: null, simulationInviteCode: null };
  123. prisma.hairstylistUser.findFirst
  124. .mockResolvedValueOnce(pending)
  125. .mockResolvedValueOnce(null)
  126. .mockResolvedValueOnce(null);
  127. prisma.hairstylistUser.update.mockResolvedValue({
  128. ...pending,
  129. slug: 'yasuaki',
  130. simulationInviteCode: 'ABCD2345',
  131. });
  132. prisma.hairTemplate.count.mockResolvedValue(0);
  133. prisma.hairTemplate.findMany.mockResolvedValue([]);
  134. await expect(service.getMyProfile(BigInt(7))).resolves.toMatchObject({
  135. slug: 'yasuaki',
  136. inviteCode: 'ABCD2345',
  137. });
  138. expect(prisma.hairstylistUser.update).toHaveBeenCalledWith({
  139. where: { id: stylist.id },
  140. data: {
  141. slug: 'yasuaki',
  142. simulationInviteCode: expect.stringMatching(/^[A-HJ-NP-Z2-9]{8}$/),
  143. },
  144. });
  145. });
  146. it('throws when the public slug does not exist', async () => {
  147. prisma.hairstylistUser.findFirst.mockResolvedValue(null);
  148. await expect(service.getPublicProfile('missing')).rejects.toBeInstanceOf(NotFoundException);
  149. });
  150. });