@@ -9,7 +9,7 @@ describe('OpenSkyEmergencySquawkSource', () => {
99 vi . unstubAllGlobals ( ) ;
1010 } ) ;
1111
12- it ( 'should fetch token and emit emergency squawk events' , async ( ) => {
12+ it ( 'should emit emergency squawk events only after the code persists for 10 minutes ' , async ( ) => {
1313 vi . useFakeTimers ( ) ;
1414 const now = new Date ( '2026-02-05T00:00:00.000Z' ) ;
1515 vi . setSystemTime ( now ) ;
@@ -19,88 +19,45 @@ describe('OpenSkyEmergencySquawkSource', () => {
1919 vi . resetModules ( ) ;
2020 const { OpenSkyEmergencySquawkSource } = await import ( './opensky-emergency-squawk.source' ) ;
2121
22- const responseTime = Math . floor ( now . getTime ( ) / 1000 ) ;
23- const states = [
24- [
25- '769104' ,
26- 'SIA7436 ' ,
27- 'Singapore' ,
28- responseTime ,
29- responseTime ,
30- 126.9869 ,
31- 36.2345 ,
32- 6248.4 ,
33- false ,
34- 196.74 ,
35- 4.95 ,
36- - 7.15 ,
37- null ,
38- 6256.02 ,
39- '7700' ,
40- false ,
41- 0 ,
42- 2 ,
43- ] ,
44- [
45- '782160' ,
46- 'CSN652 ' ,
47- 'China' ,
48- responseTime ,
49- responseTime ,
50- 125.2541 ,
51- 37.3973 ,
52- 7802.88 ,
53- false ,
54- 192.46 ,
55- 268.62 ,
56- 0 ,
57- null ,
58- 7757.16 ,
59- '1200' ,
60- false ,
61- 0 ,
62- 2 ,
63- ] ,
64- ] ;
65-
66- const fetchMock = vi . fn ( ) . mockImplementation ( ( url : RequestInfo ) => {
67- const urlText = String ( url ) ;
68- if ( urlText . includes ( 'openid-connect/token' ) ) {
69- return Promise . resolve (
70- new Response ( JSON . stringify ( { access_token : 'token-1' , expires_in : 1800 } ) , { status : 200 } ) ,
71- ) ;
72- }
73- if ( urlText . includes ( '/api/states/all' ) ) {
74- return Promise . resolve ( new Response ( JSON . stringify ( { time : responseTime , states } ) , { status : 200 } ) ) ;
75- }
76- return Promise . resolve ( new Response ( '' , { status : 404 } ) ) ;
22+ const currentSquawk = '7700' ;
23+ const fetchMock = createOpenSkyFetchMock ( {
24+ resolveStates ( ) {
25+ return [ buildOpenSkyState ( currentSquawk ) ] ;
26+ } ,
7727 } ) ;
7828 vi . stubGlobal ( 'fetch' , fetchMock ) ;
7929
8030 const source = new OpenSkyEmergencySquawkSource ( ) ;
81- const result = await source . run ( null ) ;
8231
83- expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ;
84- const stateCalls = fetchMock . mock . calls . filter ( ( [ url ] ) => String ( url ) . includes ( '/api/states/all' ) ) ;
85- const authHeader = new Headers ( stateCalls [ 0 ] ?. [ 1 ] ?. headers as HeadersInit ) . get ( 'Authorization' ) ;
86- expect ( authHeader ) . toBe ( 'Bearer token-1' ) ;
32+ const firstRun = await source . run ( null ) ;
33+ expect ( firstRun . events ) . toHaveLength ( 0 ) ;
8734
88- expect ( result . events ) . toHaveLength ( 1 ) ;
89- expect ( result . events [ 0 ] . title ) . toBe ( '한국 상공 SIA7436편 7700(비상사태) 선언' ) ;
90- expect ( result . events [ 0 ] . body ) . toBe (
35+ vi . setSystemTime ( new Date ( now . getTime ( ) + 9 * 60 * 1000 ) ) ;
36+ const secondRun = await source . run ( firstRun . nextState ) ;
37+ expect ( secondRun . events ) . toHaveLength ( 0 ) ;
38+
39+ vi . setSystemTime ( new Date ( now . getTime ( ) + 10 * 60 * 1000 ) ) ;
40+ const thirdRun = await source . run ( secondRun . nextState ) ;
41+ expect ( thirdRun . events ) . toHaveLength ( 1 ) ;
42+ expect ( thirdRun . events [ 0 ] . title ) . toBe ( '한국 상공 SIA7436편 7700(비상사태) 선언' ) ;
43+ expect ( thirdRun . events [ 0 ] . body ) . toBe (
9144 [
9245 '국적: Singapore' ,
9346 '고도: 기압고도 6248m / 지오고도 6256m (공중)' ,
9447 '비행: 속도 197 m/s, 진로 5°, 상승률 -7.1 m/s' ,
9548 ] . join ( '\n' ) ,
9649 ) ;
97- expect ( result . events [ 0 ] . level ) . toBe ( EventLevels . Moderate ) ;
98- const payload = result . events [ 0 ] . payload as { squawk ?: string } ;
50+ expect ( thirdRun . events [ 0 ] . level ) . toBe ( EventLevels . Moderate ) ;
51+ const payload = thirdRun . events [ 0 ] . payload as { squawk ?: string } ;
9952 expect ( payload . squawk ) . toBe ( '7700' ) ;
100- expect ( result . nextState ) . not . toContain ( 'token-1' ) ;
53+ expect ( thirdRun . nextState ) . not . toContain ( 'token-1' ) ;
54+
55+ vi . setSystemTime ( new Date ( now . getTime ( ) + 15 * 60 * 1000 ) ) ;
56+ const fourthRun = await source . run ( thirdRun . nextState ) ;
57+ expect ( fourthRun . events ) . toHaveLength ( 0 ) ;
10158 } ) ;
10259
103- it ( 'should refresh token on unauthorized response ' , async ( ) => {
60+ it ( 'should reset pending state when squawk changes before confirmation ' , async ( ) => {
10461 vi . useFakeTimers ( ) ;
10562 const now = new Date ( '2026-02-05T00:00:00.000Z' ) ;
10663 vi . setSystemTime ( now ) ;
@@ -110,29 +67,44 @@ describe('OpenSkyEmergencySquawkSource', () => {
11067 vi . resetModules ( ) ;
11168 const { OpenSkyEmergencySquawkSource } = await import ( './opensky-emergency-squawk.source' ) ;
11269
113- const responseTime = Math . floor ( now . getTime ( ) / 1000 ) ;
114- const states = [
115- [
116- '769104' ,
117- 'SIA7436 ' ,
118- 'Singapore' ,
119- responseTime ,
120- responseTime ,
121- 126.9869 ,
122- 36.2345 ,
123- 6248.4 ,
124- false ,
125- 196.74 ,
126- 4.95 ,
127- - 7.15 ,
128- null ,
129- 6256.02 ,
130- '7500' ,
131- false ,
132- 0 ,
133- 2 ,
134- ] ,
135- ] ;
70+ let currentSquawk = '7700' ;
71+ const fetchMock = createOpenSkyFetchMock ( {
72+ resolveStates ( ) {
73+ return [ buildOpenSkyState ( currentSquawk ) ] ;
74+ } ,
75+ } ) ;
76+ vi . stubGlobal ( 'fetch' , fetchMock ) ;
77+
78+ const source = new OpenSkyEmergencySquawkSource ( ) ;
79+
80+ const firstRun = await source . run ( null ) ;
81+ expect ( firstRun . events ) . toHaveLength ( 0 ) ;
82+
83+ vi . setSystemTime ( new Date ( now . getTime ( ) + 4 * 60 * 1000 ) ) ;
84+ currentSquawk = '7600' ;
85+ const secondRun = await source . run ( firstRun . nextState ) ;
86+ expect ( secondRun . events ) . toHaveLength ( 0 ) ;
87+
88+ vi . setSystemTime ( new Date ( now . getTime ( ) + 13 * 60 * 1000 ) ) ;
89+ const thirdRun = await source . run ( secondRun . nextState ) ;
90+ expect ( thirdRun . events ) . toHaveLength ( 0 ) ;
91+
92+ vi . setSystemTime ( new Date ( now . getTime ( ) + 14 * 60 * 1000 ) ) ;
93+ const fourthRun = await source . run ( thirdRun . nextState ) ;
94+ expect ( fourthRun . events ) . toHaveLength ( 1 ) ;
95+ expect ( fourthRun . events [ 0 ] . title ) . toBe ( '한국 상공 SIA7436편 7600(통신장애) 선언' ) ;
96+ expect ( fourthRun . events [ 0 ] . level ) . toBe ( EventLevels . Minor ) ;
97+ } ) ;
98+
99+ it ( 'should refresh token on unauthorized response' , async ( ) => {
100+ vi . useFakeTimers ( ) ;
101+ const now = new Date ( '2026-02-05T00:00:00.000Z' ) ;
102+ vi . setSystemTime ( now ) ;
103+
104+ process . env . OPENSKY_CLIENT_ID = 'test-client' ;
105+ process . env . OPENSKY_CLIENT_SECRET = 'test-secret' ;
106+ vi . resetModules ( ) ;
107+ const { OpenSkyEmergencySquawkSource } = await import ( './opensky-emergency-squawk.source' ) ;
136108
137109 let tokenCalls = 0 ;
138110 let stateCalls = 0 ;
@@ -149,14 +121,29 @@ describe('OpenSkyEmergencySquawkSource', () => {
149121 if ( stateCalls === 1 ) {
150122 return Promise . resolve ( new Response ( '' , { status : 401 } ) ) ;
151123 }
152- return Promise . resolve ( new Response ( JSON . stringify ( { time : responseTime , states } ) , { status : 200 } ) ) ;
124+ const responseTime = Math . floor ( Date . now ( ) / 1000 ) ;
125+ return Promise . resolve (
126+ new Response ( JSON . stringify ( { time : responseTime , states : [ buildOpenSkyState ( '7500' , responseTime ) ] } ) , {
127+ status : 200 ,
128+ } ) ,
129+ ) ;
153130 }
154131 return Promise . resolve ( new Response ( '' , { status : 404 } ) ) ;
155132 } ) ;
156133 vi . stubGlobal ( 'fetch' , fetchMock ) ;
157134
158135 const source = new OpenSkyEmergencySquawkSource ( ) ;
159- const result = await source . run ( null ) ;
136+ const checkpointState = JSON . stringify ( {
137+ tracked : {
138+ '769104' : {
139+ squawk : '7500' ,
140+ firstObservedAt : new Date ( now . getTime ( ) - 10 * 60 * 1000 ) . toISOString ( ) ,
141+ emittedAt : null ,
142+ } ,
143+ } ,
144+ } ) ;
145+
146+ const result = await source . run ( checkpointState ) ;
160147
161148 expect ( tokenCalls ) . toBe ( 2 ) ;
162149 expect ( stateCalls ) . toBe ( 2 ) ;
@@ -177,3 +164,47 @@ describe('OpenSkyEmergencySquawkSource', () => {
177164 expect ( result . nextState ) . not . toContain ( 'token-2' ) ;
178165 } ) ;
179166} ) ;
167+
168+ function createOpenSkyFetchMock ( options : { resolveStates : ( ) => unknown [ ] [ ] } ) : ReturnType < typeof vi . fn > {
169+ let tokenCalls = 0 ;
170+
171+ return vi . fn ( ) . mockImplementation ( ( url : RequestInfo ) => {
172+ const urlText = String ( url ) ;
173+ if ( urlText . includes ( 'openid-connect/token' ) ) {
174+ tokenCalls += 1 ;
175+ return Promise . resolve (
176+ new Response ( JSON . stringify ( { access_token : `token-${ tokenCalls } ` , expires_in : 1800 } ) , { status : 200 } ) ,
177+ ) ;
178+ }
179+ if ( urlText . includes ( '/api/states/all' ) ) {
180+ const responseTime = Math . floor ( Date . now ( ) / 1000 ) ;
181+ return Promise . resolve (
182+ new Response ( JSON . stringify ( { time : responseTime , states : options . resolveStates ( ) } ) , { status : 200 } ) ,
183+ ) ;
184+ }
185+ return Promise . resolve ( new Response ( '' , { status : 404 } ) ) ;
186+ } ) ;
187+ }
188+
189+ function buildOpenSkyState ( squawk : string , responseTime = Math . floor ( Date . now ( ) / 1000 ) ) : unknown [ ] {
190+ return [
191+ '769104' ,
192+ 'SIA7436 ' ,
193+ 'Singapore' ,
194+ responseTime ,
195+ responseTime ,
196+ 126.9869 ,
197+ 36.2345 ,
198+ 6248.4 ,
199+ false ,
200+ 196.74 ,
201+ 4.95 ,
202+ - 7.15 ,
203+ null ,
204+ 6256.02 ,
205+ squawk ,
206+ false ,
207+ 0 ,
208+ 2 ,
209+ ] ;
210+ }
0 commit comments