返回 AiToEarn
account.service.spec.ts
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / core / channels / accounts / account.service.spec.ts
1 import { AccountType, ResponseCode } from '@yikart/common'
2 import { AccountStatus, ClientType } from '@yikart/mongodb'
3 import { EventStream, EventTopic } from '@yikart/redis'
4 import { beforeEach, describe, expect, it, vi } from 'vitest'
5 import { AuthType, PlatformStatus } from '../platforms/platforms.interface'
6 import { AccountService } from './account.service'
7
8 vi.mock('@yikart/mongodb', () => ({
9 AssetType: {
10 AiImage: 'aiImage',
11 AiVideo: 'aiVideo',
12 AiCard: 'aiCard',
13 AiChatImage: 'aiChatImage',
14 AideoOutput: 'aideoOutput',
15 VideoEdit: 'videoEdit',
16 DramaRecap: 'dramaRecap',
17 StyleTransfer: 'styleTransfer',
18 ImageEdit: 'imageEdit',
19 Subtitle: 'subtitle',
20 UserMedia: 'userMedia',
21 UserFile: 'userFile',
22 PublishMedia: 'publishMedia',
23 Avatar: 'avatar',
24 AgentSession: 'agentSession',
25 VideoThumbnail: 'videoThumbnail',
26 GooglePlace: 'googlePlace',
27 Temp: 'temp',
28 },
29 AccountStatus: {
30 NORMAL: 1,
31 ABNORMAL: 0,
32 },
33 ClientType: {
34 WEB: 'web',
35 APP: 'app',
36 },
37 Transactional: () => () => undefined,
38 AccountGroupRepository: class AccountGroupRepository {},
39 AccountRepository: class AccountRepository {},
40 OAuth2CredentialRepository: class OAuth2CredentialRepository {},
41 }))
42
43 vi.mock('@yikart/redis', () => ({
44 EventStream: { Channels: 'channels' },
45 EventStreamService: class EventStreamService {},
46 EventTopic: { ChannelsAccountConnected: 'channels.account.connected' },
47 RedisService: class RedisService {},
48 }))
49
50 vi.mock('../relay/relay-client.service', () => ({
51 RelayClientService: class RelayClientService {},
52 }))
53
54 function createService(options: {
55 hasPlatform?: boolean
56 authType?: AuthType
57 status?: PlatformStatus
58 channelsAuthData?: Record<string, unknown>
59 relayClientService?: Record<string, unknown>
60 } = {}) {
61 const accountRepository = {
62 getByIdAndUserId: vi.fn(async () => ({
63 id: 'account_1',
64 userId: 'user_1',
65 type: AccountType.Twitter,
66 })),
67 updateById: vi.fn(async (id: string, data: Record<string, unknown>) => ({
68 id,
69 userId: 'user_1',
70 type: AccountType.Twitter,
71 ...data,
72 })),
73 getByIdentity: vi.fn(async () => null),
74 createByIdentity: vi.fn(async (_identity: unknown, data: Record<string, unknown>) => ({
75 id: 'account_1',
76 ...data,
77 })),
78 updateByIdentity: vi.fn(async (_identity: unknown, data: Record<string, unknown>) => ({
79 id: 'account_1',
80 ...data,
81 })),
82 deleteByIdAndUserId: vi.fn(async () => true),
83 deleteByUserIdAndIds: vi.fn(async () => true),
84 listByUserIdAndIds: vi.fn(async () => [{
85 id: 'account_1',
86 userId: 'user_1',
87 type: AccountType.Twitter,
88 }]),
89 }
90 const accountGroupRepository = {
91 getDefaultGroup: vi.fn(async () => ({ id: 'group_default' })),
92 getAccountGorupListByIds: vi.fn(async () => [{ id: 'group_custom' }]),
93 }
94 const platformRegistry = {
95 has: vi.fn(() => options.hasPlatform ?? true),
96 get: vi.fn(() => ({
97 status: options.status,
98 metadata: {
99 authType: options.authType ?? AuthType.Plugin,
100 },
101 })),
102 }
103 const wechatService = {
104 getChannelsAuthData: vi.fn(async () => options.channelsAuthData ?? {
105 uid: 'wechat_channels_uid',
106 nickname: 'WeChat Channels Account',
107 avatar: 'https://assets.example.test/avatar.jpg',
108 fansCount: 123,
109 }),
110 }
111 const credentialService = {
112 deleteCredential: vi.fn(async () => undefined),
113 }
114 const eventStream = {
115 emit: vi.fn(async () => 'event_1'),
116 }
117
118 return {
119 service: new AccountService(
120 accountRepository as never,
121 accountGroupRepository as never,
122 platformRegistry as never,
123 eventStream as never,
124 credentialService as never,
125 wechatService as never,
126 options.relayClientService as never,
127 ),
128 accountRepository,
129 accountGroupRepository,
130 platformRegistry,
131 wechatService,
132 credentialService,
133 eventStream,
134 }
135 }
136
137 describe('channel account service', () => {
138 beforeEach(() => {
139 vi.clearAllMocks()
140 })
141
142 it('allows creating WeChat Channels accounts when the platform is an available plugin platform', async () => {
143 const { service, accountRepository, eventStream } = createService({
144 status: PlatformStatus.Available,
145 authType: AuthType.Plugin,
146 })
147
148 const account = await service.addAccount('user_1', {
149 type: AccountType.WeChatChannels,
150 loginCookie: 'sessionid=abc',
151 })
152
153 expect(account).toMatchObject({
154 id: 'account_1',
155 userId: 'user_1',
156 type: AccountType.WeChatChannels,
157 uid: 'wechat_channels_uid',
158 nickname: 'WeChat Channels Account',
159 groupId: 'group_default',
160 loginCookie: 'sessionid=abc',
161 fansCount: 123,
162 })
163 expect(account).not.toHaveProperty('account')
164 expect(account).not.toHaveProperty('channelId')
165 expect(accountRepository.createByIdentity).toHaveBeenCalledWith(
166 { type: AccountType.WeChatChannels, uid: 'wechat_channels_uid' },
167 expect.objectContaining({
168 userId: 'user_1',
169 type: AccountType.WeChatChannels,
170 uid: 'wechat_channels_uid',
171 groupId: 'group_default',
172 loginCookie: 'sessionid=abc',
173 fansCount: 123,
174 }),
175 )
176 expect(accountRepository.createByIdentity.mock.calls[0][1]).not.toHaveProperty('account')
177 expect(accountRepository.createByIdentity.mock.calls[0][1]).not.toHaveProperty('channelId')
178 expect(eventStream.emit).toHaveBeenCalledWith(
179 EventStream.Channels,
180 EventTopic.ChannelsAccountConnected,
181 {
182 userId: 'user_1',
183 accountId: 'account_1',
184 platform: AccountType.WeChatChannels,
185 },
186 { source: 'account-service' },
187 )
188 })
189
190 it('emits account connected events for manually created plugin accounts', async () => {
191 const { service, eventStream } = createService({
192 status: PlatformStatus.Available,
193 authType: AuthType.Plugin,
194 })
195
196 await service.addAccount('user_1', {
197 type: AccountType.RedNote,
198 uid: 'rednote_uid',
199 nickname: 'RedNote Account',
200 })
201
202 expect(eventStream.emit).toHaveBeenCalledWith(
203 EventStream.Channels,
204 EventTopic.ChannelsAccountConnected,
205 {
206 userId: 'user_1',
207 accountId: 'account_1',
208 platform: AccountType.RedNote,
209 },
210 { source: 'account-service' },
211 )
212 })
213
214 it('restores abnormal plugin account status when the account is authorized again', async () => {
215 const { service, accountRepository } = createService({
216 status: PlatformStatus.Available,
217 authType: AuthType.Plugin,
218 })
219 accountRepository.getByIdentity.mockResolvedValueOnce({
220 id: 'account_1',
221 userId: 'user_1',
222 type: AccountType.WeChatChannels,
223 uid: 'wechat_channels_uid',
224 status: AccountStatus.ABNORMAL,
225 })
226 accountRepository.updateByIdentity.mockResolvedValueOnce({
227 id: 'account_1',
228 userId: 'user_1',
229 type: AccountType.WeChatChannels,
230 uid: 'wechat_channels_uid',
231 status: AccountStatus.NORMAL,
232 })
233
234 await service.addAccount('user_1', {
235 type: AccountType.WeChatChannels,
236 loginCookie: 'sessionid=abc',
237 })
238
239 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
240 expect(accountRepository.updateByIdentity).toHaveBeenCalledWith(
241 { type: AccountType.WeChatChannels, uid: 'wechat_channels_uid' },
242 expect.objectContaining({
243 status: AccountStatus.NORMAL,
244 loginCookie: 'sessionid=abc',
245 }),
246 )
247 })
248
249 it('defaults RedNote client type to web when creating manual plugin accounts', async () => {
250 const { service, accountRepository } = createService({
251 status: PlatformStatus.Available,
252 authType: AuthType.Plugin,
253 })
254
255 await service.addAccount('user_1', {
256 type: AccountType.RedNote,
257 uid: 'rednote_uid',
258 nickname: 'RedNote Account',
259 })
260
261 expect(accountRepository.createByIdentity).toHaveBeenCalledWith(
262 {
263 type: AccountType.RedNote,
264 uid: 'rednote_uid',
265 clientType: ClientType.WEB,
266 },
267 expect.objectContaining({
268 type: AccountType.RedNote,
269 uid: 'rednote_uid',
270 clientType: ClientType.WEB,
271 }),
272 )
273 })
274
275 it('keeps resolved default group when plugin input has undefined groupId', async () => {
276 const { service, accountRepository } = createService({
277 status: PlatformStatus.Available,
278 authType: AuthType.Plugin,
279 })
280
281 await service.addAccount('user_1', {
282 type: AccountType.RedNote,
283 uid: 'rednote_uid',
284 nickname: 'RedNote Account',
285 groupId: undefined,
286 })
287
288 expect(accountRepository.createByIdentity).toHaveBeenCalledWith(
289 expect.objectContaining({
290 type: AccountType.RedNote,
291 uid: 'rednote_uid',
292 }),
293 expect.objectContaining({
294 groupId: 'group_default',
295 userId: 'user_1',
296 status: AccountStatus.NORMAL,
297 }),
298 )
299 })
300
301 it('keeps explicit RedNote client type when creating manual plugin accounts', async () => {
302 const { service, accountRepository } = createService({
303 status: PlatformStatus.Available,
304 authType: AuthType.Plugin,
305 })
306
307 await service.addAccount('user_1', {
308 type: AccountType.RedNote,
309 uid: 'rednote_uid',
310 nickname: 'RedNote Account',
311 clientType: ClientType.APP,
312 })
313
314 expect(accountRepository.createByIdentity).toHaveBeenCalledWith(
315 {
316 type: AccountType.RedNote,
317 uid: 'rednote_uid',
318 clientType: ClientType.APP,
319 },
320 expect.objectContaining({
321 type: AccountType.RedNote,
322 uid: 'rednote_uid',
323 clientType: ClientType.APP,
324 }),
325 )
326 })
327
328 it('rejects plugin account creation when the platform uid belongs to another user', async () => {
329 const { service, accountRepository, eventStream } = createService({
330 status: PlatformStatus.Available,
331 authType: AuthType.Plugin,
332 })
333 accountRepository.getByIdentity.mockResolvedValueOnce({
334 id: 'account_2',
335 userId: 'user_2',
336 type: AccountType.WeChatChannels,
337 uid: 'wechat_channels_uid',
338 })
339
340 await expect(service.addAccount('user_1', {
341 type: AccountType.WeChatChannels,
342 loginCookie: 'sessionid=abc',
343 })).rejects.toMatchObject({
344 code: ResponseCode.ChannelAccountAlreadyConnectedToAnotherUser,
345 })
346 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
347 expect(accountRepository.updateByIdentity).not.toHaveBeenCalled()
348 expect(eventStream.emit).not.toHaveBeenCalled()
349 })
350
351 it('rejects WeChat Channels creation when request uid mismatches platform auth data', async () => {
352 const { service, accountRepository } = createService({
353 status: PlatformStatus.Available,
354 authType: AuthType.Plugin,
355 channelsAuthData: {
356 uid: 'actual_uid',
357 nickname: 'WeChat Channels Account',
358 },
359 })
360
361 await expect(service.addAccount('user_1', {
362 type: AccountType.WeChatChannels,
363 uid: 'other_uid',
364 loginCookie: 'sessionid=abc',
365 })).rejects.toMatchObject({
366 code: ResponseCode.ChannelPlatformResponseInvalid,
367 })
368 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
369 })
370
371 it('does not persist client supplied account or legacy channel id for WeChat Channels accounts', async () => {
372 const { service, accountRepository } = createService({
373 status: PlatformStatus.Available,
374 authType: AuthType.Plugin,
375 })
376
377 await service.addAccount('user_1', {
378 type: AccountType.WeChatChannels,
379 loginCookie: 'sessionid=abc',
380 account: 'client-account',
381 channelId: 'client-channel',
382 } as never)
383
384 expect(accountRepository.createByIdentity.mock.calls[0][1]).not.toHaveProperty('account')
385 expect(accountRepository.createByIdentity.mock.calls[0][1]).not.toHaveProperty('channelId')
386 })
387
388 it('requires loginCookie for WeChat Channels account creation', async () => {
389 const { service, accountRepository, wechatService } = createService({
390 status: PlatformStatus.Available,
391 authType: AuthType.Plugin,
392 })
393
394 await expect(service.addAccount('user_1', {
395 type: AccountType.WeChatChannels,
396 })).rejects.toMatchObject({
397 code: ResponseCode.ChannelAccountCreateRequiredFieldMissing,
398 })
399 expect(wechatService.getChannelsAuthData).not.toHaveBeenCalled()
400 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
401 })
402
403 it('keeps manual plugin account creation explicit for non-cookie platforms', async () => {
404 const { service, accountRepository } = createService({
405 status: PlatformStatus.Available,
406 authType: AuthType.Plugin,
407 })
408
409 await expect(service.addAccount('user_1', {
410 type: AccountType.RedNote,
411 loginCookie: 'cookie',
412 })).rejects.toMatchObject({
413 code: ResponseCode.ChannelAccountCreateRequiredFieldMissing,
414 })
415 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
416 })
417
418 it('rejects account creation for non-plugin platforms', async () => {
419 const { service, accountRepository } = createService({
420 status: PlatformStatus.Available,
421 authType: AuthType.OAuth2,
422 })
423
424 await expect(service.addAccount('user_1', {
425 type: AccountType.YouTube,
426 uid: 'youtube_uid',
427 nickname: 'YouTube Account',
428 })).rejects.toMatchObject({
429 code: ResponseCode.ChannelAccountCreateNotSupported,
430 })
431 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
432 })
433
434 it('rejects account creation for non-available plugin platforms', async () => {
435 const { service, accountRepository } = createService({
436 status: PlatformStatus.ComingSoon,
437 authType: AuthType.Plugin,
438 })
439
440 await expect(service.addAccount('user_1', {
441 type: AccountType.RedNote,
442 uid: 'rednote_uid',
443 nickname: 'RedNote Account',
444 })).rejects.toMatchObject({
445 code: ResponseCode.ChannelAccountCreateNotSupported,
446 })
447 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
448 })
449
450 it('rejects account creation for unregistered platforms', async () => {
451 const { service, accountRepository, platformRegistry, eventStream } = createService({
452 hasPlatform: false,
453 })
454
455 await expect(service.addAccount('user_1', {
456 type: AccountType.RedNote,
457 uid: 'rednote_uid',
458 nickname: 'RedNote Account',
459 })).rejects.toMatchObject({
460 code: ResponseCode.ChannelAccountCreateNotSupported,
461 })
462 expect(platformRegistry.get).not.toHaveBeenCalled()
463 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
464 expect(eventStream.emit).not.toHaveBeenCalled()
465 })
466
467 it('updates account info only after user ownership is verified', async () => {
468 const { service, accountRepository, accountGroupRepository } = createService()
469
470 await expect(service.updateAccountInfoById('user_1', 'account_1', {
471 nickname: 'Updated Name',
472 avatar: 'https://cdn.example.test/avatar.png',
473 groupId: 'group_custom',
474 })).resolves.toMatchObject({
475 id: 'account_1',
476 userId: 'user_1',
477 nickname: 'Updated Name',
478 })
479
480 expect(accountRepository.getByIdAndUserId).toHaveBeenCalledWith('account_1', 'user_1')
481 expect(accountGroupRepository.getAccountGorupListByIds).toHaveBeenCalledWith(['group_custom'], 'user_1')
482 expect(accountRepository.updateById).toHaveBeenCalledWith('account_1', {
483 nickname: 'Updated Name',
484 avatar: 'https://cdn.example.test/avatar.png',
485 groupId: 'group_custom',
486 })
487 })
488
489 it('does not write identity or credential fields through account info update', async () => {
490 const { service, accountRepository } = createService()
491
492 await service.updateAccountInfoById('user_1', 'account_1', {
493 nickname: 'Updated Name',
494 uid: 'evil-uid',
495 type: AccountType.YouTube,
496 access_token: 'access-token',
497 } as never)
498
499 expect(accountRepository.updateById).toHaveBeenCalledWith('account_1', {
500 nickname: 'Updated Name',
501 })
502 })
503
504 it('rejects moving an account into a group outside the user scope', async () => {
505 const { service, accountRepository, accountGroupRepository } = createService()
506 accountGroupRepository.getAccountGorupListByIds.mockResolvedValueOnce([])
507
508 await expect(service.updateAccountInfoById('user_1', 'account_1', {
509 groupId: 'group_other',
510 })).rejects.toMatchObject({
511 code: ResponseCode.AccountGroupNotFound,
512 })
513
514 expect(accountRepository.updateById).not.toHaveBeenCalled()
515 })
516
517 it('throws when updating an account outside the user scope', async () => {
518 const { service, accountRepository } = createService()
519 accountRepository.getByIdAndUserId.mockResolvedValueOnce(null)
520
521 await expect(service.updateAccountInfoById('user_1', 'account_2', {
522 nickname: 'Updated Name',
523 })).rejects.toMatchObject({
524 code: ResponseCode.AccountNotFound,
525 })
526
527 expect(accountRepository.updateById).not.toHaveBeenCalled()
528 })
529
530 it('deletes credential storage after deleting an owned account', async () => {
531 const { service, accountRepository, credentialService } = createService()
532
533 await expect(service.delete('user_1', 'account_1')).resolves.toBe(true)
534
535 expect(accountRepository.getByIdAndUserId).toHaveBeenCalledWith('account_1', 'user_1')
536 expect(accountRepository.deleteByIdAndUserId).toHaveBeenCalledWith('account_1', 'user_1')
537 expect(credentialService.deleteCredential).toHaveBeenCalledWith('account_1')
538 })
539
540 it('deletes credential storage by real account ids after bulk delete succeeds', async () => {
541 const { service, accountRepository, credentialService } = createService()
542
543 await expect(service.deleteMany('user_1', ['account_1', 'account_2'])).resolves.toBeUndefined()
544
545 expect(accountRepository.listByUserIdAndIds).toHaveBeenCalledWith('user_1', ['account_1', 'account_2'])
546 expect(accountRepository.deleteByUserIdAndIds).toHaveBeenCalledWith('user_1', ['account_1'])
547 expect(credentialService.deleteCredential).toHaveBeenCalledTimes(1)
548 expect(credentialService.deleteCredential).toHaveBeenCalledWith('account_1')
549 expect(credentialService.deleteCredential).not.toHaveBeenCalledWith('account_2')
550 })
551
552 it('does not delete credential storage when bulk delete does not remove accounts', async () => {
553 const { service, accountRepository, credentialService } = createService()
554 accountRepository.deleteByUserIdAndIds.mockResolvedValueOnce(false)
555
556 await expect(service.deleteMany('user_1', ['account_1', 'account_2'])).resolves.toBeUndefined()
557
558 expect(accountRepository.deleteByUserIdAndIds).toHaveBeenCalledWith('user_1', ['account_1'])
559 expect(credentialService.deleteCredential).not.toHaveBeenCalled()
560 })
561
562 it('returns without deleting accounts or credential storage when no requested account belongs to the user', async () => {
563 const { service, accountRepository, credentialService } = createService()
564 accountRepository.listByUserIdAndIds.mockResolvedValueOnce([])
565
566 await expect(service.deleteMany('user_1', ['account_2'])).resolves.toBeUndefined()
567
568 expect(accountRepository.listByUserIdAndIds).toHaveBeenCalledWith('user_1', ['account_2'])
569 expect(accountRepository.deleteByUserIdAndIds).not.toHaveBeenCalled()
570 expect(credentialService.deleteCredential).not.toHaveBeenCalled()
571 })
572
573 it('uses upstream account fields for relay accounts while keeping the local account id', async () => {
574 const relayClientService = {
575 enabled: true,
576 get: vi.fn(async () => ({
577 list: [{
578 id: 'relay_account_1',
579 userId: 'relay_user',
580 type: AccountType.Twitter,
581 uid: 'upstream_uid',
582 nickname: 'Upstream Name',
583 avatar: 'https://assets.example.test/upstream.png',
584 fansCount: 99,
585 status: 1,
586 }],
587 })),
588 }
589 const { service } = createService({ relayClientService })
590 const accountRepository = {
591 getUserAccounts: vi.fn(async () => [{
592 _id: 'local_account_1',
593 id: 'local_account_1',
594 userId: 'user_1',
595 type: AccountType.Twitter,
596 uid: 'local_uid',
597 nickname: 'Stale Name',
598 groupId: 'group_1',
599 relayAccountRef: 'relay_account_1',
600 status: 0,
601 }]),
602 }
603 Reflect.set(service, 'accountRepository', accountRepository)
604
605 const result = await service.list('user_1', {})
606
607 expect(relayClientService.get).toHaveBeenCalledWith('/v2/channels/accounts', {
608 ids: ['relay_account_1'],
609 })
610 expect(result.list).toEqual([expect.objectContaining({
611 id: 'local_account_1',
612 _id: 'local_account_1',
613 userId: 'user_1',
614 groupId: 'group_1',
615 relayAccountRef: 'relay_account_1',
616 uid: 'upstream_uid',
617 nickname: 'Upstream Name',
618 avatar: 'https://assets.example.test/upstream.png',
619 fansCount: 99,
620 status: 1,
621 })])
622 })
623
624 it('marks unavailable relay accounts while preserving local account fields', async () => {
625 const { service } = createService()
626 const accountRepository = {
627 getUserAccounts: vi.fn(async () => [{
628 _id: 'local_account_1',
629 id: 'local_account_1',
630 userId: 'user_1',
631 type: AccountType.Twitter,
632 uid: 'local_uid',
633 nickname: 'Stale Name',
634 avatar: 'https://assets.example.test/stale.png',
635 groupId: 'group_1',
636 relayAccountRef: 'relay_account_1',
637 status: 1,
638 }]),
639 }
640 Reflect.set(service, 'accountRepository', accountRepository)
641
642 const result = await service.list('user_1', {})
643
644 expect(result.list).toEqual([expect.objectContaining({
645 id: 'local_account_1',
646 userId: 'user_1',
647 type: AccountType.Twitter,
648 uid: 'local_uid',
649 nickname: 'relay_account_1',
650 avatar: 'https://assets.example.test/stale.png',
651 groupId: 'group_1',
652 relayAccountRef: 'relay_account_1',
653 status: 0,
654 })])
655 })
656
657 it('stores relay accounts as local ownership refs instead of upstream display data', async () => {
658 const { service, accountRepository, accountGroupRepository, eventStream } = createService()
659
660 await service.createRelayAccount('user_1', {
661 type: AccountType.Twitter,
662 uid: 'platform_uid',
663 nickname: 'Upstream Name',
664 avatar: 'https://assets.example.test/upstream.png',
665 relayAccountRef: 'relay_account_1',
666 groupId: 'group_custom',
667 })
668
669 expect(accountGroupRepository.getAccountGorupListByIds).toHaveBeenCalledWith(['group_custom'], 'user_1')
670 expect(accountRepository.createByIdentity).toHaveBeenCalledWith(
671 { type: AccountType.Twitter, uid: 'platform_uid' },
672 expect.objectContaining({
673 userId: 'user_1',
674 type: AccountType.Twitter,
675 uid: 'platform_uid',
676 nickname: 'relay_account_1',
677 groupId: 'group_custom',
678 relayAccountRef: 'relay_account_1',
679 }),
680 )
681 expect(accountRepository.createByIdentity).toHaveBeenCalledWith(
682 expect.anything(),
683 expect.not.objectContaining({
684 avatar: 'https://assets.example.test/upstream.png',
685 }),
686 )
687 expect(eventStream.emit).toHaveBeenCalledWith(
688 EventStream.Channels,
689 EventTopic.ChannelsAccountConnected,
690 {
691 userId: 'user_1',
692 accountId: 'account_1',
693 platform: AccountType.Twitter,
694 },
695 { source: 'account-service' },
696 )
697 })
698
699 it('rejects relay account creation when the platform account belongs to another user', async () => {
700 const { service, accountRepository, eventStream } = createService()
701 accountRepository.getByIdentity.mockResolvedValueOnce({
702 id: 'account_2',
703 userId: 'user_2',
704 type: AccountType.Twitter,
705 uid: 'platform_uid',
706 })
707
708 await expect(service.createRelayAccount('user_1', {
709 type: AccountType.Twitter,
710 uid: 'platform_uid',
711 nickname: 'Upstream Name',
712 relayAccountRef: 'relay_account_1',
713 })).rejects.toMatchObject({
714 code: ResponseCode.ChannelAccountAlreadyConnectedToAnotherUser,
715 })
716 expect(accountRepository.createByIdentity).not.toHaveBeenCalled()
717 expect(accountRepository.updateByIdentity).not.toHaveBeenCalled()
718 expect(eventStream.emit).not.toHaveBeenCalled()
719 })
720 })
721
721 lines TYPESCRIPT