@@ -67,6 +67,7 @@ describe('MeetingService.createMeeting', () => {
6767 saved . push ( m ) ;
6868 } ,
6969 findByCode : async ( ) => null ,
70+ listOpenCodes : async ( ) => [ ] ,
7071 } ,
7172 chatRepository : noopChatRepository ( ) ,
7273 codeGenerator : { next : ( ) => code } ,
@@ -120,17 +121,30 @@ describe('MeetingService.createMeeting', () => {
120121 expect ( result . source ) . toBe ( 'notion-issue' ) ;
121122 } ) ;
122123
123- it ( '생성 후 meeting.created 도메인 이벤트를 발행한다' , async ( ) => {
124+ it ( '생성 후 meeting.created와 meeting.opened를 발행한다' , async ( ) => {
124125 const { service, events } = makeService ( ) ;
125126 await service . createMeeting ( { source : 'web' , externalReference : externalReference ( ) } ) ;
126127 expect ( events ) . toEqual ( [
127128 {
128129 name : MEETING_EVENTS . CREATED ,
129130 payload : { code : code . value , source : 'web' , startedAt : fakeNow } ,
130131 } ,
132+ { name : MEETING_EVENTS . OPENED , payload : { code : code . value } } ,
131133 ] ) ;
132134 } ) ;
133135
136+ it ( '예약 생성(scheduled)은 방을 열지 않아 meeting.opened를 발행하지 않는다' , async ( ) => {
137+ const { service, events } = makeService ( ) ;
138+ const meeting = await service . createMeeting ( {
139+ source : 'notion-issue' ,
140+ externalReference : externalReference ( { issueId : 'NTN-1' } ) ,
141+ scheduled : true ,
142+ } ) ;
143+ expect ( meeting . status ) . toBe ( 'scheduled' ) ;
144+ expect ( meeting . isOpen ) . toBe ( false ) ;
145+ expect ( events . map ( ( e ) => e . name ) ) . toEqual ( [ MEETING_EVENTS . CREATED ] ) ;
146+ } ) ;
147+
134148 it ( 'HostTokenGenerator가 발급한 hostToken을 Meeting에 부여한다' , async ( ) => {
135149 const { service } = makeService ( ) ;
136150 const result = await service . createMeeting ( {
@@ -163,6 +177,7 @@ describe('MeetingService.joinMeeting', () => {
163177 const service = new MeetingService ( {
164178 repository : {
165179 findByCode : async ( c ) => ( meeting && c === meeting . code . value ? meeting : null ) ,
180+ listOpenCodes : async ( ) => [ ] ,
166181 save : async ( m ) => {
167182 saved . push ( m ) ;
168183 } ,
@@ -227,6 +242,42 @@ describe('MeetingService.joinMeeting', () => {
227242 } ,
228243 ] ) ;
229244 } ) ;
245+
246+ it ( '빈 방에 처음 들어온 참가자는 hostToken을 받는다' , async ( ) => {
247+ const meeting = makeMeeting ( t0 ) ;
248+ const { service } = makeService ( meeting ) ;
249+ const result = await service . joinMeeting ( {
250+ code : 'abc12xyz' ,
251+ participantId : 's1' ,
252+ nickname : 'alice' ,
253+ } ) ;
254+ expect ( result . hostToken ) . toBe ( 'host-token-1' ) ;
255+ } ) ;
256+
257+ it ( '이미 참가자가 있는 방에 들어오면 hostToken을 받지 못한다' , async ( ) => {
258+ const meeting = makeMeeting ( t0 ) ;
259+ meeting . addParticipant ( 's1' , 'alice' , t0 ) ;
260+ const { service } = makeService ( meeting ) ;
261+ const result = await service . joinMeeting ( {
262+ code : 'abc12xyz' ,
263+ participantId : 's2' ,
264+ nickname : 'bob' ,
265+ } ) ;
266+ expect ( result . hostToken ) . toBeNull ( ) ;
267+ } ) ;
268+
269+ it ( '모두 나간 방에 다시 들어오면 host를 다시 가져간다' , async ( ) => {
270+ const meeting = makeMeeting ( t0 ) ;
271+ meeting . addParticipant ( 's1' , 'alice' , t0 ) ;
272+ meeting . removeParticipant ( 's1' , t0 ) ;
273+ const { service } = makeService ( meeting ) ;
274+ const result = await service . joinMeeting ( {
275+ code : 'abc12xyz' ,
276+ participantId : 's2' ,
277+ nickname : 'bob' ,
278+ } ) ;
279+ expect ( result . hostToken ) . toBe ( 'host-token-1' ) ;
280+ } ) ;
230281} ) ;
231282
232283describe ( 'MeetingService.leaveMeeting' , ( ) => {
@@ -246,6 +297,7 @@ describe('MeetingService.leaveMeeting', () => {
246297 const service = new MeetingService ( {
247298 repository : {
248299 findByCode : async ( c ) => ( meeting && c === meeting . code . value ? meeting : null ) ,
300+ listOpenCodes : async ( ) => [ ] ,
249301 save : async ( m ) => {
250302 saved . push ( m ) ;
251303 } ,
@@ -318,6 +370,7 @@ describe('MeetingService.postChat', () => {
318370 const service = new MeetingService ( {
319371 repository : {
320372 findByCode : async ( c ) => ( meeting && c === meeting . code . value ? meeting : null ) ,
373+ listOpenCodes : async ( ) => [ ] ,
321374 save : async ( m ) => {
322375 saved . push ( m ) ;
323376 } ,
@@ -401,6 +454,7 @@ describe('MeetingService.closeMeeting', () => {
401454 const service = new MeetingService ( {
402455 repository : {
403456 findByCode : async ( c ) => ( meeting && c === meeting . code . value ? meeting : null ) ,
457+ listOpenCodes : async ( ) => [ ] ,
404458 save : async ( m ) => {
405459 saved . push ( m ) ;
406460 } ,
@@ -456,6 +510,7 @@ describe('MeetingService.closeMeeting', () => {
456510 const service = new MeetingService ( {
457511 repository : {
458512 findByCode : async ( c ) => ( c === 'abc12xyz' ? meeting : null ) ,
513+ listOpenCodes : async ( ) => [ ] ,
459514 save : async ( m ) => {
460515 saved . push ( m ) ;
461516 } ,
@@ -519,6 +574,7 @@ describe('MeetingService.detectIdleAndClose', () => {
519574 const service = new MeetingService ( {
520575 repository : {
521576 findByCode : async ( c ) => ( meeting && c === meeting . code . value ? meeting : null ) ,
577+ listOpenCodes : async ( ) => [ ] ,
522578 save : async ( m ) => {
523579 saved . push ( m ) ;
524580 } ,
@@ -609,3 +665,73 @@ describe('MeetingService.detectIdleAndClose', () => {
609665 expect ( events ) . toHaveLength ( 0 ) ;
610666 } ) ;
611667} ) ;
668+
669+ describe ( 'MeetingService.sweepIdleMeetings' , ( ) => {
670+ const t0 = new Date ( '2026-01-01T00:00:00Z' ) ;
671+ const tJoin = new Date ( '2026-01-01T00:01:00Z' ) ;
672+ const tLeave = new Date ( '2026-01-01T00:02:00Z' ) ;
673+ const tIdleElapsed = new Date ( '2026-01-01T00:03:30Z' ) ;
674+
675+ /** idle=true면 참가자가 모두 나간 상태(= 만료 대상). */
676+ const meetingOf = ( codeStr : string , idle : boolean ) => {
677+ const m = Meeting . create ( {
678+ code : MeetingCode . from ( codeStr ) ,
679+ source : 'web' ,
680+ externalReference : externalReference ( ) ,
681+ idleTimeout : IdleTimeout . default ( ) ,
682+ startedAt : t0 ,
683+ hostToken : 'host-token-1' ,
684+ title : null ,
685+ } ) ;
686+ m . addParticipant ( 's1' , 'alice' , tJoin ) ;
687+ if ( idle ) m . removeParticipant ( 's1' , tLeave ) ;
688+ return m ;
689+ } ;
690+
691+ const makeService = ( meetings : Meeting [ ] , brokenCode ?: string ) => {
692+ const byCode = new Map ( meetings . map ( ( m ) => [ m . code . value , m ] ) ) ;
693+ const { publisher, events } = makeEventPublisher ( ) ;
694+ const service = new MeetingService ( {
695+ repository : {
696+ findByCode : async ( c ) => {
697+ if ( c === brokenCode ) throw new Error ( 'redis down' ) ;
698+ return byCode . get ( c ) ?? null ;
699+ } ,
700+ listOpenCodes : async ( ) => [ ...byCode . keys ( ) , ...( brokenCode ? [ brokenCode ] : [ ] ) ] ,
701+ save : async ( ) => { } ,
702+ } ,
703+ chatRepository : noopChatRepository ( ) ,
704+ codeGenerator : { next : ( ) => code } ,
705+ hostTokenGenerator : { next : ( ) => 'host-token-generated' } ,
706+ clock : { now : ( ) => tIdleElapsed } ,
707+ eventPublisher : publisher ,
708+ logger : noopLogger ( ) ,
709+ } ) ;
710+ return { service, events } ;
711+ } ;
712+
713+ it ( '열린 회의를 훑어 idle인 회의만 닫고 조회·종료 건수를 돌려준다' , async ( ) => {
714+ const idle = meetingOf ( 'aaa11aaa' , true ) ;
715+ const busy = meetingOf ( 'bbb22bbb' , false ) ;
716+ const { service } = makeService ( [ idle , busy ] ) ;
717+
718+ await expect ( service . sweepIdleMeetings ( ) ) . resolves . toEqual ( { scanned : 2 , closed : 1 } ) ;
719+ expect ( idle . isOpen ) . toBe ( false ) ;
720+ expect ( busy . isOpen ) . toBe ( true ) ;
721+ } ) ;
722+
723+ it ( '한 회의 처리가 실패해도 나머지 회의를 계속 훑는다' , async ( ) => {
724+ const idle = meetingOf ( 'aaa11aaa' , true ) ;
725+ const { service } = makeService ( [ idle ] , 'ccc33ccc' ) ;
726+
727+ await expect ( service . sweepIdleMeetings ( ) ) . resolves . toEqual ( { scanned : 2 , closed : 1 } ) ;
728+ expect ( idle . isOpen ) . toBe ( false ) ;
729+ } ) ;
730+
731+ it ( '열린 회의가 없으면 아무것도 닫지 않는다' , async ( ) => {
732+ const { service, events } = makeService ( [ ] ) ;
733+
734+ await expect ( service . sweepIdleMeetings ( ) ) . resolves . toEqual ( { scanned : 0 , closed : 0 } ) ;
735+ expect ( events ) . toHaveLength ( 0 ) ;
736+ } ) ;
737+ } ) ;
0 commit comments