app.e2e-spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { INestApplication } from '@nestjs/common';
  2. import { Test } from '@nestjs/testing';
  3. import * as request from 'supertest';
  4. import { AppModule } from '../src/app.module';
  5. import { configureApp } from '../src/app.setup';
  6. import { GcsStorageDriver } from '../src/modules/storage/gcs-storage.driver';
  7. import { STORAGE_DRIVER } from '../src/modules/storage/storage.constants';
  8. describe('App (e2e)', () => {
  9. let app: INestApplication;
  10. const upload = jest.fn();
  11. beforeAll(async () => {
  12. upload.mockReset();
  13. upload.mockResolvedValue(undefined);
  14. const moduleRef = await Test.createTestingModule({
  15. imports: [AppModule],
  16. })
  17. .overrideProvider(STORAGE_DRIVER)
  18. .useValue({ upload })
  19. .overrideProvider(GcsStorageDriver)
  20. .useValue({ upload })
  21. .compile();
  22. app = moduleRef.createNestApplication();
  23. configureApp(app);
  24. await app.init();
  25. });
  26. afterAll(async () => {
  27. await app.close();
  28. });
  29. it('/api/v1/health (GET)', async () => {
  30. const response = await request(app.getHttpServer()).get('/api/v1/health').expect(200);
  31. expect(response.body).toEqual({
  32. ok: true,
  33. data: {
  34. status: 'ok',
  35. timestamp: expect.any(String),
  36. services: {
  37. database: expect.any(String),
  38. redis: expect.any(String),
  39. },
  40. },
  41. error: null,
  42. });
  43. });
  44. it('/api/v1/unknown (GET)', async () => {
  45. const response = await request(app.getHttpServer()).get('/api/v1/unknown').expect(404);
  46. expect(response.body).toEqual({
  47. ok: false,
  48. data: null,
  49. error: {
  50. code: 'HTTP_404',
  51. message: expect.any(String),
  52. },
  53. });
  54. });
  55. it('/api/v1/auth/stylist/login (POST) validates request body', async () => {
  56. const response = await request(app.getHttpServer())
  57. .post('/api/v1/auth/stylist/login')
  58. .send({ email: 'bad-email', password: '' })
  59. .expect(400);
  60. expect(response.body.ok).toBe(false);
  61. expect(response.body.data).toBeNull();
  62. expect(response.body.error.code).toBe('HTTP_400');
  63. });
  64. it('/api/v1/auth/stylist/login (POST) returns invalid credentials when no matching account exists', async () => {
  65. const response = await request(app.getHttpServer())
  66. .post('/api/v1/auth/stylist/login')
  67. .send({ email: 'missing-app-e2e@example.com', password: 'Passw0rd!' })
  68. .expect(401);
  69. expect(response.body).toEqual({
  70. ok: false,
  71. data: null,
  72. error: {
  73. code: 'AUTH_INVALID_CREDENTIALS',
  74. message: '邮箱或密码错误',
  75. },
  76. });
  77. });
  78. it('/api/v1/customers (GET) requires authentication', async () => {
  79. const response = await request(app.getHttpServer())
  80. .get('/api/v1/customers')
  81. .expect(401);
  82. expect(response.body).toEqual({
  83. ok: false,
  84. data: null,
  85. error: {
  86. code: 'AUTH_UNAUTHORIZED',
  87. message: '未登录或登录已失效',
  88. },
  89. });
  90. });
  91. it('/api/v1/templates (GET) returns an empty list envelope', async () => {
  92. const response = await request(app.getHttpServer())
  93. .get('/api/v1/templates')
  94. .expect(200);
  95. expect(response.body).toEqual({
  96. ok: true,
  97. data: {
  98. items: [],
  99. total: 0,
  100. },
  101. error: null,
  102. });
  103. });
  104. it('/api/v1/simulations (POST) validates create payload', async () => {
  105. await request(app.getHttpServer())
  106. .post('/api/v1/simulations')
  107. .send({})
  108. .expect(400);
  109. });
  110. it('/api/v1/storage/upload (POST) uploads a file and returns a public url', async () => {
  111. const response = await request(app.getHttpServer())
  112. .post('/api/v1/storage/upload')
  113. .field('type', 'template')
  114. .attach('file', Buffer.from('hello'), {
  115. filename: 'look.png',
  116. contentType: 'image/png',
  117. })
  118. .expect(201);
  119. expect(upload).toHaveBeenCalledWith(
  120. expect.objectContaining({
  121. objectKey: expect.stringMatching(/^stylist\/template\/.+\.png$/),
  122. contentType: 'image/png',
  123. }),
  124. );
  125. expect(response.body.ok).toBe(true);
  126. expect(response.body.data.objectKey).toMatch(/^stylist\/template\/.+\.png$/);
  127. expect(response.body.data.url).toBe(
  128. `https://storage.googleapis.com/aitori-public/${response.body.data.objectKey}`,
  129. );
  130. });
  131. it('/api/v1/storage/upload (POST) rejects requests without a file', async () => {
  132. const response = await request(app.getHttpServer())
  133. .post('/api/v1/storage/upload')
  134. .field('type', 'template')
  135. .expect(400);
  136. expect(response.body).toEqual({
  137. ok: false,
  138. data: null,
  139. error: {
  140. code: 'HTTP_400',
  141. message: '请选择要上传的文件',
  142. },
  143. });
  144. });
  145. it('/api/v1/storage/upload (POST) validates type', async () => {
  146. const response = await request(app.getHttpServer())
  147. .post('/api/v1/storage/upload')
  148. .field('type', '../template')
  149. .attach('file', Buffer.from('hello'), {
  150. filename: 'look.png',
  151. contentType: 'image/png',
  152. })
  153. .expect(400);
  154. expect(response.body).toEqual({
  155. ok: false,
  156. data: null,
  157. error: {
  158. code: 'HTTP_400',
  159. message: '上传类型不合法',
  160. },
  161. });
  162. });
  163. });