1+ import { describe , it , expect , jest , beforeEach , afterEach , beforeAll } from '@jest/globals' ;
2+ import { ProposalFinishedTriggerHandler } from './proposal-finished-trigger.service' ;
3+ import { ISubscriptionClient , User , Notification } from '../../interfaces/subscription-client.interface' ;
4+ import { NotificationClientFactory } from '../notification/notification-factory.service' ;
5+ import { INotificationClient } from '../../interfaces/notification-client.interface' ;
6+ import { DispatcherMessage } from '../../interfaces/dispatcher-message.interface' ;
7+
8+ describe ( 'ProposalFinishedTriggerHandler' , ( ) => {
9+ let mockSubscriptionClient : jest . Mocked < ISubscriptionClient > ;
10+ let mockNotificationFactory : jest . Mocked < NotificationClientFactory > ;
11+ let mockNotificationClient : jest . Mocked < INotificationClient > ;
12+ let handler : ProposalFinishedTriggerHandler ;
13+ let mockUsers : User [ ] ;
14+ let mockNotifications : Notification [ ] ;
15+ let mockProposal : any ;
16+
17+ beforeAll ( ( ) => {
18+ mockUsers = [
19+ { id : '1' , channel : 'telegram' , channel_user_id : '123' , created_at : new Date ( ) } ,
20+ { id : '2' , channel : 'telegram' , channel_user_id : '456' , created_at : new Date ( ) }
21+ ] ;
22+
23+ mockNotifications = [
24+ { user_id : '1' , event_id : 'prop456-finished' , dao_id : 'dao123' } ,
25+ { user_id : '2' , event_id : 'prop456-finished' , dao_id : 'dao123' }
26+ ] ;
27+
28+ mockProposal = {
29+ id : 'prop456' ,
30+ daoId : 'dao123' ,
31+ description : 'Test Proposal\nDetailed description' ,
32+ endTimestamp : 1625086400
33+ } ;
34+ } ) ;
35+
36+ beforeEach ( ( ) => {
37+ mockSubscriptionClient = {
38+ getDaoSubscribers : jest . fn ( ) ,
39+ shouldSend : jest . fn ( ) ,
40+ markAsSent : jest . fn ( ) ,
41+ getWalletOwners : jest . fn ( )
42+ } as jest . Mocked < ISubscriptionClient > ;
43+
44+ mockNotificationClient = {
45+ sendNotification : jest . fn ( )
46+ } as jest . Mocked < INotificationClient > ;
47+
48+ mockNotificationFactory = {
49+ addClient : jest . fn ( ) ,
50+ getClient : jest . fn ( ) . mockReturnValue ( mockNotificationClient ) ,
51+ supportsChannel : jest . fn ( ) . mockReturnValue ( true )
52+ } as any ;
53+
54+ mockSubscriptionClient . getDaoSubscribers . mockResolvedValue ( mockUsers ) ;
55+ mockSubscriptionClient . shouldSend . mockResolvedValue ( mockNotifications ) ;
56+ mockSubscriptionClient . markAsSent . mockResolvedValue ( ) ;
57+ mockNotificationClient . sendNotification . mockResolvedValue ( ) ;
58+
59+ handler = new ProposalFinishedTriggerHandler ( mockSubscriptionClient , mockNotificationFactory ) ;
60+ } ) ;
61+
62+ afterEach ( ( ) => {
63+ jest . clearAllMocks ( ) ;
64+ } ) ;
65+
66+ describe ( 'handleMessage' , ( ) => {
67+ it ( 'should process single proposal finished message correctly' , async ( ) => {
68+ const mockMessage : DispatcherMessage < any > = {
69+ triggerId : 'proposal-finished' ,
70+ events : [ mockProposal ]
71+ } ;
72+
73+ await handler . handleMessage ( mockMessage ) ;
74+
75+ expect ( mockSubscriptionClient . getDaoSubscribers ) . toHaveBeenCalledWith ( 'dao123' , '1625086400' ) ;
76+ expect ( mockSubscriptionClient . shouldSend ) . toHaveBeenCalledWith ( mockUsers , 'prop456-finished' , 'dao123' ) ;
77+ expect ( mockNotificationClient . sendNotification ) . toHaveBeenCalledTimes ( 2 ) ;
78+ expect ( mockNotificationClient . sendNotification ) . toHaveBeenCalledWith ( expect . objectContaining ( {
79+ userId : expect . any ( String ) ,
80+ channel : expect . any ( String ) ,
81+ channelUserId : expect . any ( String ) ,
82+ message : 'The proposal "Test Proposal" has ended on dao dao123.'
83+ } ) ) ;
84+ } ) ;
85+
86+ it ( 'should process multiple proposals in a single message' , async ( ) => {
87+ const mockUsersForMultiple : User [ ] = [
88+ { id : '1' , channel : 'telegram' , channel_user_id : '123' , created_at : new Date ( ) }
89+ ] ;
90+ const mockNotificationsForMultiple : Notification [ ] = [
91+ { user_id : '1' , event_id : 'prop1-finished' , dao_id : 'dao123' } ,
92+ { user_id : '1' , event_id : 'prop2-finished' , dao_id : 'dao456' }
93+ ] ;
94+ const mockMessage : DispatcherMessage < any > = {
95+ triggerId : 'proposal-finished' ,
96+ events : [
97+ { id : 'prop1' , daoId : 'dao123' , description : 'First Proposal' , endTimestamp : 1625086401 } ,
98+ { id : 'prop2' , daoId : 'dao456' , description : 'Second Proposal' , endTimestamp : 1625086402 }
99+ ]
100+ } ;
101+
102+ mockSubscriptionClient . getDaoSubscribers . mockResolvedValue ( mockUsersForMultiple ) ;
103+ mockSubscriptionClient . shouldSend . mockResolvedValue ( mockNotificationsForMultiple ) ;
104+
105+ await handler . handleMessage ( mockMessage ) ;
106+
107+ expect ( mockSubscriptionClient . getDaoSubscribers ) . toHaveBeenCalledTimes ( 2 ) ;
108+ expect ( mockSubscriptionClient . getDaoSubscribers ) . toHaveBeenCalledWith ( 'dao123' , '1625086401' ) ;
109+ expect ( mockSubscriptionClient . getDaoSubscribers ) . toHaveBeenCalledWith ( 'dao456' , '1625086402' ) ;
110+ expect ( mockNotificationClient . sendNotification ) . toHaveBeenCalledTimes ( 2 ) ;
111+ } ) ;
112+
113+ it ( 'should handle empty proposals array' , async ( ) => {
114+ const mockMessage : DispatcherMessage < any > = {
115+ triggerId : 'proposal-finished' ,
116+ events : [ ]
117+ } ;
118+
119+ await handler . handleMessage ( mockMessage ) ;
120+
121+ expect ( mockSubscriptionClient . getDaoSubscribers ) . not . toHaveBeenCalled ( ) ;
122+ expect ( mockNotificationClient . sendNotification ) . not . toHaveBeenCalled ( ) ;
123+ } ) ;
124+
125+ it ( 'should skip proposals with no subscribers' , async ( ) => {
126+ const mockMessage : DispatcherMessage < any > = {
127+ triggerId : 'proposal-finished' ,
128+ events : [ mockProposal ]
129+ } ;
130+
131+ mockSubscriptionClient . getDaoSubscribers . mockResolvedValue ( [ ] ) ;
132+ mockSubscriptionClient . shouldSend . mockResolvedValue ( [ ] ) ;
133+
134+ await handler . handleMessage ( mockMessage ) ;
135+
136+ expect ( mockSubscriptionClient . getDaoSubscribers ) . toHaveBeenCalledWith ( 'dao123' , '1625086400' ) ;
137+ expect ( mockNotificationClient . sendNotification ) . not . toHaveBeenCalled ( ) ;
138+ } ) ;
139+
140+ it ( 'should extract title from multiline descriptions' , async ( ) => {
141+ const mockUsersForMultiline : User [ ] = [
142+ { id : '1' , channel : 'telegram' , channel_user_id : '123' , created_at : new Date ( ) }
143+ ] ;
144+ const mockNotificationsForMultiline : Notification [ ] = [
145+ { user_id : '1' , event_id : 'prop456-finished' , dao_id : 'dao123' }
146+ ] ;
147+ const proposalWithMultilineDesc = {
148+ id : 'prop456' ,
149+ daoId : 'dao123' ,
150+ description : 'Main Title\nDetailed description\nMore details' ,
151+ endTimestamp : 1625086400
152+ } ;
153+ const mockMessage : DispatcherMessage < any > = {
154+ triggerId : 'proposal-finished' ,
155+ events : [ proposalWithMultilineDesc ]
156+ } ;
157+
158+ mockSubscriptionClient . getDaoSubscribers . mockResolvedValue ( mockUsersForMultiline ) ;
159+ mockSubscriptionClient . shouldSend . mockResolvedValue ( mockNotificationsForMultiline ) ;
160+
161+ await handler . handleMessage ( mockMessage ) ;
162+
163+ expect ( mockNotificationClient . sendNotification ) . toHaveBeenCalledWith ( expect . objectContaining ( {
164+ message : 'The proposal "Main Title" has ended on dao dao123.'
165+ } ) ) ;
166+ } ) ;
167+
168+ it ( 'should handle markdown headers in descriptions' , async ( ) => {
169+ const mockUsersForMarkdown : User [ ] = [
170+ { id : '1' , channel : 'telegram' , channel_user_id : '123' , created_at : new Date ( ) }
171+ ] ;
172+ const mockNotificationsForMarkdown : Notification [ ] = [
173+ { user_id : '1' , event_id : 'prop456-finished' , dao_id : 'dao123' }
174+ ] ;
175+ const proposalWithMarkdownDesc = {
176+ id : 'prop456' ,
177+ daoId : 'dao123' ,
178+ description : '# Markdown Title\nDetailed description' ,
179+ endTimestamp : 1625086400
180+ } ;
181+ const mockMessage : DispatcherMessage < any > = {
182+ triggerId : 'proposal-finished' ,
183+ events : [ proposalWithMarkdownDesc ]
184+ } ;
185+
186+ mockSubscriptionClient . getDaoSubscribers . mockResolvedValue ( mockUsersForMarkdown ) ;
187+ mockSubscriptionClient . shouldSend . mockResolvedValue ( mockNotificationsForMarkdown ) ;
188+
189+ await handler . handleMessage ( mockMessage ) ;
190+
191+ expect ( mockNotificationClient . sendNotification ) . toHaveBeenCalledWith ( expect . objectContaining ( {
192+ message : 'The proposal "Markdown Title" has ended on dao dao123.'
193+ } ) ) ;
194+ } ) ;
195+
196+ it ( 'should handle empty descriptions' , async ( ) => {
197+ const mockUsersForEmpty : User [ ] = [
198+ { id : '1' , channel : 'telegram' , channel_user_id : '123' , created_at : new Date ( ) }
199+ ] ;
200+ const mockNotificationsForEmpty : Notification [ ] = [
201+ { user_id : '1' , event_id : 'prop456-finished' , dao_id : 'dao123' }
202+ ] ;
203+ const proposalWithEmptyDesc = {
204+ id : 'prop456' ,
205+ daoId : 'dao123' ,
206+ description : '' ,
207+ endTimestamp : 1625086400
208+ } ;
209+ const mockMessage : DispatcherMessage < any > = {
210+ triggerId : 'proposal-finished' ,
211+ events : [ proposalWithEmptyDesc ]
212+ } ;
213+
214+ mockSubscriptionClient . getDaoSubscribers . mockResolvedValue ( mockUsersForEmpty ) ;
215+ mockSubscriptionClient . shouldSend . mockResolvedValue ( mockNotificationsForEmpty ) ;
216+
217+ await handler . handleMessage ( mockMessage ) ;
218+
219+ expect ( mockNotificationClient . sendNotification ) . toHaveBeenCalledWith ( expect . objectContaining ( {
220+ message : 'A proposal has ended on dao dao123.'
221+ } ) ) ;
222+ } ) ;
223+
224+ it ( 'should return correct MessageProcessingResult' , async ( ) => {
225+ const mockMessage : DispatcherMessage < any > = {
226+ triggerId : 'proposal-finished' ,
227+ events : [ mockProposal ]
228+ } ;
229+
230+ const result = await handler . handleMessage ( mockMessage ) ;
231+
232+ expect ( result ) . toEqual ( {
233+ messageId : 'proposal-finished' ,
234+ timestamp : expect . any ( String )
235+ } ) ;
236+ expect ( new Date ( result . timestamp ) ) . toBeInstanceOf ( Date ) ;
237+ } ) ;
238+ } ) ;
239+ } ) ;
0 commit comments