66 * module semantics per file (no phantom name collisions) and a module target that
77 * permits top-level await.
88 *
9- * The download is not exercised — it needs the network. `--offline` covers the rest.
9+ * The download is exercised against a stubbed `fetch` rather than the network: what
10+ * matters is that a response which is not the declaration file never reaches the disk.
11+ * `--offline` covers the rest.
1012 */
1113
1214import { describe , it } from 'node:test' ;
@@ -22,6 +24,19 @@ async function readTsconfig(root: string, scriptRoot: string): Promise<Record<st
2224 return JSON . parse ( raw ) as Record < string , never > ;
2325}
2426
27+ /** Run `fn` with `fetch` replaced by one that answers every call with `res`. */
28+ async function withStubbedFetch ( res : Response , fn : ( ) => Promise < void > ) : Promise < void > {
29+ const real = globalThis . fetch ;
30+ globalThis . fetch = ( ) => Promise . resolve ( res . clone ( ) ) ;
31+ try {
32+ await fn ( ) ;
33+ } finally {
34+ globalThis . fetch = real ;
35+ }
36+ }
37+
38+ const DTS_PATH = [ '.iobroker' , 'types' , 'javascript.d.ts' ] ;
39+
2540describe ( 'types' , ( ) => {
2641 it ( 'writes a config that gives each script its own scope' , async ( ) => {
2742 const project = await makeTempProject ( ) ;
@@ -96,6 +111,67 @@ describe('types', () => {
96111 }
97112 } ) ;
98113
114+ it ( 'writes the declaration file when the download is the real thing' , async ( ) => {
115+ const project = await makeTempProject ( ) ;
116+ try {
117+ const dts = 'declare global {\n function log(msg: string): void;\n}\n' ;
118+ const { log } = makeCapturingLogger ( ) ;
119+ await withStubbedFetch ( new Response ( dts , { status : 200 } ) , async ( ) => {
120+ await setupTypes ( project . root , 'scripts' , { } , log ) ;
121+ } ) ;
122+
123+ assert . equal ( await fs . readFile ( path . join ( project . root , ...DTS_PATH ) , 'utf8' ) , dts ) ;
124+ } finally {
125+ await project . cleanup ( ) ;
126+ }
127+ } ) ;
128+
129+ it ( 'leaves a working copy alone when a portal answers 200 with a login page' , async ( ) => {
130+ const project = await makeTempProject ( ) ;
131+ try {
132+ const good = 'declare global {\n function log(msg: string): void;\n}\n' ;
133+ const dtsPath = path . join ( project . root , ...DTS_PATH ) ;
134+ await fs . mkdir ( path . dirname ( dtsPath ) , { recursive : true } ) ;
135+ await fs . writeFile ( dtsPath , good , 'utf8' ) ;
136+
137+ const portal = '<!doctype html><html><body>Sign in to continue</body></html>' ;
138+ const { log, captured } = makeCapturingLogger ( ) ;
139+ await withStubbedFetch ( new Response ( portal , { status : 200 } ) , async ( ) => {
140+ await setupTypes ( project . root , 'scripts' , { } , log ) ;
141+ } ) ;
142+
143+ assert . equal ( await fs . readFile ( dtsPath , 'utf8' ) , good , 'the HTML must not overwrite it' ) ;
144+ assert . ok (
145+ captured . warn . some ( ( l ) => / n o t a T y p e S c r i p t d e c l a r a t i o n f i l e / i. test ( l ) ) ,
146+ `expected a warning about the response, got ${ JSON . stringify ( captured . warn ) } ` ,
147+ ) ;
148+ } finally {
149+ await project . cleanup ( ) ;
150+ }
151+ } ) ;
152+
153+ it ( 'refuses a body far too large to be the declaration file' , async ( ) => {
154+ const project = await makeTempProject ( ) ;
155+ try {
156+ const huge = `declare global {}\n${ 'x' . repeat ( 3 * 1024 * 1024 ) } ` ;
157+ const { log, captured } = makeCapturingLogger ( ) ;
158+ await withStubbedFetch ( new Response ( huge , { status : 200 } ) , async ( ) => {
159+ await setupTypes ( project . root , 'scripts' , { } , log ) ;
160+ } ) ;
161+
162+ await assert . rejects (
163+ ( ) => fs . access ( path . join ( project . root , ...DTS_PATH ) ) ,
164+ 'nothing should have been written' ,
165+ ) ;
166+ assert . ok (
167+ captured . warn . some ( ( l ) => l . includes ( 'KB' ) ) ,
168+ `expected a warning naming the size, got ${ JSON . stringify ( captured . warn ) } ` ,
169+ ) ;
170+ } finally {
171+ await project . cleanup ( ) ;
172+ }
173+ } ) ;
174+
99175 it ( 'says what is missing rather than failing when offline and nothing is cached' , async ( ) => {
100176 const project = await makeTempProject ( ) ;
101177 try {
0 commit comments