11import { createPrivateUserMessageChannel } from 'api/create-private-user-message-channel'
22import { AuthedUser } from 'api/helpers/endpoint'
33import * as privateMessageModules from 'api/helpers/private-messages'
4+ import { sendDiscordMessage } from 'common/discord/core'
45import { sqlMatch } from 'common/test-utils'
56import * as utilArrayModules from 'common/util/array'
67import * as admin from 'firebase-admin'
78import * as supabaseInit from 'shared/supabase/init'
9+ import { updateUser } from 'shared/supabase/users'
810import * as sharedUtils from 'shared/utils'
911
1012jest . mock ( 'shared/supabase/init' )
1113jest . mock ( 'common/util/array' )
14+ jest . mock ( 'common/discord/core' )
1215jest . mock ( 'api/helpers/private-messages' )
16+ jest . mock ( 'shared/supabase/users' )
1317jest . mock ( 'shared/utils' )
1418jest . mock ( 'firebase-admin' , ( ) => ( {
1519 auth : jest . fn ( ) ,
@@ -110,7 +114,10 @@ describe('createPrivateUserMessageChannel', () => {
110114 ; ( sharedUtils . getUser as jest . Mock ) . mockResolvedValue ( mockCreator )
111115 ; ( utilArrayModules . filterDefined as jest . Mock ) . mockReturnValue ( mockPrivateUsers )
112116 ; ( mockPg . oneOrNone as jest . Mock ) . mockResolvedValue ( false )
113- ; ( mockPg . one as jest . Mock ) . mockResolvedValue ( mockChannel )
117+ // First pg.one is the recent-channel count (under limit), second is the channel insert.
118+ ; ( mockPg . one as jest . Mock )
119+ . mockResolvedValueOnce ( { count : '0' } )
120+ . mockResolvedValueOnce ( mockChannel )
114121
115122 const results = await createPrivateUserMessageChannel ( mockBody , mockAuth , mockReq )
116123
@@ -121,7 +128,9 @@ describe('createPrivateUserMessageChannel', () => {
121128 expect ( sharedUtils . getPrivateUser ) . toBeCalledTimes ( 2 )
122129 expect ( sharedUtils . getPrivateUser ) . toBeCalledWith ( mockUserIds [ 0 ] )
123130 expect ( sharedUtils . getPrivateUser ) . toBeCalledWith ( mockUserIds [ 1 ] )
124- expect ( mockPg . one ) . toBeCalledTimes ( 1 )
131+ expect ( updateUser ) . not . toHaveBeenCalled ( )
132+ expect ( sendDiscordMessage ) . not . toHaveBeenCalled ( )
133+ expect ( mockPg . one ) . toBeCalledTimes ( 2 )
125134 expect ( mockPg . one ) . toBeCalledWith (
126135 sqlMatch (
127136 'insert into private_user_message_channels default\n values\n returning id' ,
@@ -143,6 +152,79 @@ describe('createPrivateUserMessageChannel', () => {
143152 } )
144153 } )
145154
155+ describe ( 'when the daily new-conversation limit is exceeded' , ( ) => {
156+ const mockBody = { userIds : [ '123' ] }
157+ const mockAuth = { uid : '321' } as AuthedUser
158+ const mockReq = { } as any
159+ const mockPrivateUsers = [
160+ { id : '123' , blockedUserIds : [ ] , blockedByUserIds : [ ] } ,
161+ { id : '321' , blockedUserIds : [ ] , blockedByUserIds : [ ] } ,
162+ ]
163+ const mockCreator = {
164+ id : '321' ,
165+ name : 'Spammy McSpam' ,
166+ username : 'spammy' ,
167+ isBannedFromPosting : false ,
168+ }
169+
170+ beforeEach ( ( ) => {
171+ ; ( sharedUtils . getUser as jest . Mock ) . mockResolvedValue ( mockCreator )
172+ ; ( utilArrayModules . filterDefined as jest . Mock ) . mockReturnValue ( mockPrivateUsers )
173+ // getProfile + no-existing-channel lookups both return falsy.
174+ ; ( mockPg . oneOrNone as jest . Mock ) . mockResolvedValue ( null )
175+ } )
176+
177+ it ( 'bans the user and notifies admins on the 6th conversation within 24h' , async ( ) => {
178+ // Already created 5 channels in the last 24h → this new one is over the limit.
179+ ; ( mockPg . one as jest . Mock ) . mockResolvedValueOnce ( { count : '5' } )
180+ ; ( sendDiscordMessage as jest . Mock ) . mockResolvedValue ( null )
181+
182+ await expect (
183+ createPrivateUserMessageChannel ( mockBody , mockAuth , mockReq ) ,
184+ ) . rejects . toThrowError ( 'You are banned' )
185+
186+ expect ( updateUser ) . toHaveBeenCalledWith ( mockAuth . uid , { isBannedFromPosting : true } )
187+ expect ( sendDiscordMessage ) . toHaveBeenCalledTimes ( 1 )
188+ expect ( sendDiscordMessage ) . toHaveBeenCalledWith (
189+ expect . stringContaining ( 'Auto-ban' ) ,
190+ 'reports' ,
191+ )
192+ // The count query runs, but the channel is never created once the user is banned.
193+ expect ( mockPg . one ) . toHaveBeenCalledTimes ( 1 )
194+ expect ( privateMessageModules . addUsersToPrivateMessageChannel ) . not . toHaveBeenCalled ( )
195+ } )
196+
197+ it ( 'still bans when the Discord notification fails' , async ( ) => {
198+ ; ( mockPg . one as jest . Mock ) . mockResolvedValueOnce ( { count : '5' } )
199+ ; ( sendDiscordMessage as jest . Mock ) . mockRejectedValue ( new Error ( 'Discord down' ) )
200+ const errorSpy = jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } )
201+
202+ await expect (
203+ createPrivateUserMessageChannel ( mockBody , mockAuth , mockReq ) ,
204+ ) . rejects . toThrowError ( 'You are banned' )
205+
206+ expect ( updateUser ) . toHaveBeenCalledWith ( mockAuth . uid , { isBannedFromPosting : true } )
207+ expect ( errorSpy ) . toHaveBeenCalledWith (
208+ expect . stringContaining ( 'Failed to send auto-ban discord report' ) ,
209+ expect . any ( Error ) ,
210+ )
211+ } )
212+
213+ it ( 'does not ban when still under the limit' , async ( ) => {
214+ // 4 channels so far → this 5th one is allowed; second pg.one is the channel insert.
215+ ; ( mockPg . one as jest . Mock )
216+ . mockResolvedValueOnce ( { count : '4' } )
217+ . mockResolvedValueOnce ( { id : '333' } )
218+
219+ const results = await createPrivateUserMessageChannel ( mockBody , mockAuth , mockReq )
220+
221+ expect ( results . status ) . toBe ( 'success' )
222+ expect ( updateUser ) . not . toHaveBeenCalled ( )
223+ expect ( sendDiscordMessage ) . not . toHaveBeenCalled ( )
224+ expect ( privateMessageModules . addUsersToPrivateMessageChannel ) . toHaveBeenCalledTimes ( 1 )
225+ } )
226+ } )
227+
146228 describe ( 'when an error occurs' , ( ) => {
147229 it ( 'should throw if user email is not verified' , async ( ) => {
148230 const mockBody = {
0 commit comments