返回 AiToEarn
platforms.service.spec.ts
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / core / channels / platforms / platforms.service.spec.ts
1 import type { Request, Response } from 'express'
2 import { AccountType, AppException, ResponseCode } from '@yikart/common'
3 import { beforeEach, describe, expect, it, vi } from 'vitest'
4 import { z } from 'zod'
5 import { ChannelPlatformException, PlatformErrorCategory, PlatformErrorCauseType } from './platforms.exception'
6 import { PublishOptionValueType } from './platforms.interface'
7 import { PlatformsService } from './platforms.service'
8
9 vi.mock('@yikart/mongodb', () => ({
10 AccountRepository: class AccountRepository {},
11 }))
12
13 vi.mock('../auth/auth.service', () => ({
14 AuthService: class AuthService {},
15 }))
16
17 function createResponse(): Response {
18 const response = {
19 headersSent: false,
20 status: vi.fn().mockReturnThis(),
21 send: vi.fn(() => {
22 response.headersSent = true
23 return response
24 }),
25 }
26 return response as unknown as Response
27 }
28
29 function createRequest(): Request {
30 return {
31 method: 'POST',
32 originalUrl: '/v2/channels/platforms/tiktok/webhooks',
33 } as Request
34 }
35
36 function createService() {
37 const handler = {
38 handle: vi.fn(async (_request: Request, response: Response) => {
39 response.status(200).send('ok')
40 }),
41 }
42 const registry = {
43 getWebhook: vi.fn(() => handler),
44 }
45
46 return {
47 service: new PlatformsService(registry as never, {} as never, {} as never),
48 handler,
49 registry,
50 }
51 }
52
53 function createOptionService() {
54 const provider = {
55 listSources: vi.fn(() => [{
56 field: 'boardId',
57 label: 'Board',
58 valueType: PublishOptionValueType.List,
59 requiresAccount: true,
60 createSchema: z.object({
61 name: z.string().min(1).describe('名称'),
62 }),
63 }]),
64 getValues: vi.fn(),
65 createValue: vi.fn(),
66 }
67 provider.createValue.mockImplementation(function (this: unknown) {
68 expect(this).toBe(provider)
69 return Promise.resolve({
70 field: 'boardId',
71 valueType: PublishOptionValueType.List,
72 item: {
73 value: 'board-id',
74 label: 'Launch Board',
75 },
76 })
77 })
78 const registry = {
79 getPublishOptions: vi.fn(() => provider),
80 }
81 const authService = {
82 getValidCredential: vi.fn(async () => ({
83 accessToken: 'access-token',
84 refreshToken: 'refresh-token',
85 })),
86 markAccountOfflineForCredentialFailure: vi.fn(async () => true),
87 }
88 const accountRepository = {
89 getByIdAndUserId: vi.fn(async () => ({
90 id: 'account-id',
91 type: AccountType.Pinterest,
92 uid: 'pinterest-user-id',
93 account: 'pinterest_user',
94 })),
95 }
96
97 return {
98 service: new PlatformsService(registry as never, authService as never, accountRepository as never),
99 provider,
100 registry,
101 authService,
102 accountRepository,
103 }
104 }
105
106 describe('platform webhook service', () => {
107 beforeEach(() => {
108 vi.clearAllMocks()
109 })
110
111 it('dispatches raw request and response to platform handler', async () => {
112 const { service, handler } = createService()
113 const request = createRequest()
114 const response = createResponse()
115
116 await service.dispatchWebhook(AccountType.TikTok, request, response)
117
118 expect(handler.handle).toHaveBeenCalledWith(request, response, { platform: AccountType.TikTok })
119 })
120
121 it('throws multilingual business error when platform webhook is not registered', async () => {
122 const { service, registry } = createService()
123 registry.getWebhook.mockReturnValue(undefined)
124
125 await expect(service.dispatchWebhook(AccountType.Twitter, createRequest(), createResponse()))
126 .rejects
127 .toMatchObject<AppException>({ code: ResponseCode.ChannelWebhookNotSupported })
128 })
129 })
130
131 describe('platform publish option service', () => {
132 it('exposes publish option creation schemas in source metadata', () => {
133 const { service } = createOptionService()
134
135 const result = service.listSources(AccountType.Pinterest)
136
137 expect(result[0]).toEqual(expect.objectContaining({
138 field: 'boardId',
139 createSchema: expect.objectContaining({
140 type: 'object',
141 properties: expect.objectContaining({
142 name: expect.objectContaining({
143 type: 'string',
144 }),
145 }),
146 }),
147 }))
148 })
149
150 it('creates account publish option values through the registered provider', async () => {
151 const { service, provider, registry, authService, accountRepository } = createOptionService()
152
153 const result = await service.createAccountValue('user-id', 'account-id', 'boardId', {
154 name: 'Launch Board',
155 })
156
157 expect(accountRepository.getByIdAndUserId).toHaveBeenCalledWith('account-id', 'user-id')
158 expect(registry.getPublishOptions).toHaveBeenCalledWith(AccountType.Pinterest)
159 expect(authService.getValidCredential).toHaveBeenCalledWith('account-id', 'user-id')
160 expect(provider.createValue).toHaveBeenCalledWith({
161 userId: 'user-id',
162 accountId: 'account-id',
163 field: 'boardId',
164 data: { name: 'Launch Board' },
165 credential: {
166 accessToken: 'access-token',
167 refreshToken: 'refresh-token',
168 platformUid: 'pinterest-user-id',
169 account: 'pinterest_user',
170 },
171 })
172 expect(result).toEqual({
173 field: 'boardId',
174 valueType: PublishOptionValueType.List,
175 item: {
176 value: 'board-id',
177 label: 'Launch Board',
178 },
179 })
180 })
181
182 it('rejects unsupported publish option creation before loading credentials', async () => {
183 const { service, provider, authService } = createOptionService()
184 delete (provider as { createValue?: unknown }).createValue
185
186 await expect(service.createAccountValue('user-id', 'account-id', 'boardId', {
187 name: 'Launch Board',
188 })).rejects.toMatchObject<AppException>({
189 code: ResponseCode.ChannelPlatformOperationNotSupported,
190 })
191
192 expect(authService.getValidCredential).not.toHaveBeenCalled()
193 })
194
195 it('marks the account offline when publish option provider returns an auth failure', async () => {
196 const { service, provider, authService } = createOptionService()
197 const platformError = new ChannelPlatformException({
198 code: ResponseCode.ChannelAccessTokenFailed,
199 platform: AccountType.Pinterest,
200 category: PlatformErrorCategory.Auth,
201 retryable: false,
202 cause: {
203 type: PlatformErrorCauseType.Http,
204 httpStatus: 401,
205 },
206 })
207 provider.getValues.mockRejectedValue(platformError)
208
209 await expect(service.getAccountValues('user-id', 'account-id', 'boardId'))
210 .rejects
211 .toBe(platformError)
212
213 expect(authService.markAccountOfflineForCredentialFailure).toHaveBeenCalledWith(
214 'account-id',
215 platformError,
216 'platform_auth_failed',
217 )
218 })
219 })
220
220 lines TYPESCRIPT