1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+ using Azure . Sdk . Tools . Cli . Helpers ;
5+ using Azure . Sdk . Tools . Cli . Services ;
6+ using Azure . Sdk . Tools . Cli . Services . Languages ;
7+ using Microsoft . Extensions . Logging . Abstractions ;
8+ using Moq ;
9+
10+ namespace Azure . Sdk . Tools . Cli . Tests . Services . Languages
11+ {
12+ [ TestFixture ]
13+ public class TypeScriptTypecheckerTests
14+ {
15+ private Mock < IDockerService > mockDockerService ;
16+ private TypeScriptTypechecker typechecker ;
17+
18+ [ SetUp ]
19+ public void SetUp ( )
20+ {
21+ mockDockerService = new Mock < IDockerService > ( ) ;
22+ typechecker = new TypeScriptTypechecker ( mockDockerService . Object , NullLogger < TypeScriptTypechecker > . Instance ) ;
23+ }
24+
25+ [ Test ]
26+ public void Constructor_WithNullDockerService_ThrowsArgumentNullException ( )
27+ {
28+ Assert . Throws < ArgumentNullException > ( ( ) =>
29+ new TypeScriptTypechecker ( null ! , NullLogger < TypeScriptTypechecker > . Instance ) ) ;
30+ }
31+
32+ [ Test ]
33+ public void Constructor_WithNullLogger_ThrowsArgumentNullException ( )
34+ {
35+ Assert . Throws < ArgumentNullException > ( ( ) =>
36+ new TypeScriptTypechecker ( mockDockerService . Object , null ! ) ) ;
37+ }
38+
39+ [ Test ]
40+ public void ParseImportedPackages_WithBasicImports_ReturnsExpectedPackages ( )
41+ {
42+ var code = @"
43+ import { Client } from '@azure/storage-blob';
44+ import * as fs from 'fs';
45+ import './relative-file';
46+ import { DefaultAzureCredential } from '@azure/identity';
47+ const path = require('path');
48+ " ;
49+
50+ var excludedPackages = new HashSet < string > ( ) ;
51+ var packages = TypeScriptTypechecker . ParseImportedPackages ( code , excludedPackages ) ;
52+
53+ Assert . That ( packages , Contains . Item ( "@azure/storage-blob" ) ) ;
54+ Assert . That ( packages , Contains . Item ( "@azure/identity" ) ) ;
55+ Assert . That ( packages , Contains . Item ( "fs" ) ) ;
56+ Assert . That ( packages , Contains . Item ( "path" ) ) ;
57+ Assert . That ( packages , Contains . Item ( "typescript" ) ) ;
58+ Assert . That ( packages , Contains . Item ( "@types/node" ) ) ;
59+
60+ // Should not contain relative imports
61+ Assert . That ( packages , Does . Not . Contain ( "./relative-file" ) ) ;
62+ }
63+
64+ [ Test ]
65+ public void ParseImportedPackages_WithScopedPackages_ExtractsCorrectPackageRoot ( )
66+ {
67+ var code = @"
68+ import { Client } from '@azure/storage-blob/types';
69+ import { helper } from '@azure/core-auth/helpers/utils';
70+ import { normalPkg } from 'normal-package/sub/path';
71+ " ;
72+
73+ var excludedPackages = new HashSet < string > ( ) ;
74+ var packages = TypeScriptTypechecker . ParseImportedPackages ( code , excludedPackages ) ;
75+
76+ Assert . That ( packages , Contains . Item ( "@azure/storage-blob" ) ) ;
77+ Assert . That ( packages , Contains . Item ( "@azure/core-auth" ) ) ;
78+ Assert . That ( packages , Contains . Item ( "normal-package" ) ) ;
79+ }
80+
81+ [ Test ]
82+ public void ParseImportedPackages_WithExcludedPackages_FiltersCorrectly ( )
83+ {
84+ var code = @"
85+ import { Client } from '@azure/storage-blob';
86+ import { DefaultAzureCredential } from '@azure/identity';
87+ " ;
88+
89+ var excludedPackages = new HashSet < string > { "@azure/identity" } ;
90+ var packages = TypeScriptTypechecker . ParseImportedPackages ( code , excludedPackages ) ;
91+
92+ Assert . That ( packages , Contains . Item ( "@azure/storage-blob" ) ) ;
93+ Assert . That ( packages , Does . Not . Contain ( "@azure/identity" ) ) ;
94+ }
95+
96+ [ Test ]
97+ public void ParseImportedPackages_WithDynamicImports_ExtractsPackages ( )
98+ {
99+ var code = @"
100+ const module = await import('@azure/storage-blob');
101+ const fs = await import('fs/promises');
102+ " ;
103+
104+ var excludedPackages = new HashSet < string > ( ) ;
105+ var packages = TypeScriptTypechecker . ParseImportedPackages ( code , excludedPackages ) ;
106+
107+ Assert . That ( packages , Contains . Item ( "@azure/storage-blob" ) ) ;
108+ Assert . That ( packages , Contains . Item ( "fs" ) ) ;
109+ }
110+
111+ [ Test ]
112+ public async Task TypecheckAsync_WithValidCode_ReturnsSuccess ( )
113+ {
114+ // Arrange
115+ var code = "const message: string = 'Hello, World!';" ;
116+ var parameters = new TypeCheckParameters ( code , null , null ) ;
117+
118+ SetupSuccessfulDockerInteractions ( ) ;
119+
120+ // Act
121+ var result = await typechecker . TypecheckAsync ( parameters , CancellationToken . None ) ;
122+
123+ // Assert
124+ Assert . That ( result . Succeeded , Is . True ) ;
125+ VerifyDockerInteractionSequence ( ) ;
126+ }
127+
128+ [ Test ]
129+ public async Task TypecheckAsync_WithFailedTypeCheck_ReturnsFailure ( )
130+ {
131+ // Arrange
132+ var code = "const message: string = 123; // Type error" ;
133+ var parameters = new TypeCheckParameters ( code , null , null ) ;
134+
135+ SetupFailedTypecheckInteraction ( ) ;
136+
137+ // Act
138+ var result = await typechecker . TypecheckAsync ( parameters , CancellationToken . None ) ;
139+
140+ // Assert
141+ Assert . That ( result . Succeeded , Is . False ) ;
142+ Assert . That ( result . Output , Contains . Substring ( "tsc output:" ) ) ;
143+ }
144+
145+ [ Test ]
146+ public async Task TypecheckAsync_WithClientDist_InstallsClientPackage ( )
147+ {
148+ // Arrange
149+ var code = "const message: string = 'Hello, World!';" ;
150+ var clientDist = "/path/to/client.tgz" ;
151+ var parameters = new TypeCheckParameters ( code , clientDist , null ) ;
152+
153+ SetupSuccessfulDockerInteractions ( ) ;
154+ SetupClientDistInstallation ( clientDist ) ;
155+
156+ // Create a mock temporary file to simulate the client dist file existing
157+ var tempFile = Path . GetTempFileName ( ) ;
158+ try
159+ {
160+ await File . WriteAllTextAsync ( tempFile , "mock client dist content" ) ;
161+
162+ // Update the test to use the actual temp file path
163+ var parametersWithRealFile = new TypeCheckParameters ( code , tempFile , null ) ;
164+
165+ // Act
166+ var result = await typechecker . TypecheckAsync ( parametersWithRealFile , CancellationToken . None ) ;
167+
168+ // Assert
169+ Assert . That ( result . Succeeded , Is . True ) ;
170+ VerifyClientDistInstallation ( tempFile ) ;
171+ }
172+ finally
173+ {
174+ if ( File . Exists ( tempFile ) )
175+ {
176+ File . Delete ( tempFile ) ;
177+ }
178+ }
179+ }
180+
181+ private void SetupSuccessfulDockerInteractions ( )
182+ {
183+ // Container creation and startup
184+ var createResult = new ProcessResult { ExitCode = 0 } ;
185+ createResult . AppendStdout ( "container-id" ) ;
186+ mockDockerService . Setup ( x => x . CreateContainerAsync (
187+ "node:alpine" , It . IsAny < string > ( ) , null , null , It . IsAny < CancellationToken > ( ) ) )
188+ . ReturnsAsync ( createResult ) ;
189+
190+ mockDockerService . Setup ( x => x . StartContainerAsync (
191+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
192+ . ReturnsAsync ( new ProcessResult { ExitCode = 0 } ) ;
193+
194+ mockDockerService . Setup ( x => x . IsContainerRunningAsync (
195+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
196+ . ReturnsAsync ( false ) ; // Force container creation
197+
198+ // File operations
199+ mockDockerService . Setup ( x => x . RunCommandInContainerAsync (
200+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . Contains ( "mkdir" ) ) ,
201+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
202+ . ReturnsAsync ( new ProcessResult { ExitCode = 0 } ) ;
203+
204+ mockDockerService . Setup ( x => x . CopyToContainerAsync (
205+ It . IsAny < string > ( ) , It . IsAny < string > ( ) , It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
206+ . ReturnsAsync ( new ProcessResult { ExitCode = 0 } ) ;
207+
208+ // npm install
209+ var npmInstallResult = new ProcessResult { ExitCode = 0 } ;
210+ npmInstallResult . AppendStdout ( "npm install completed" ) ;
211+ mockDockerService . Setup ( x => x . RunCommandInContainerAsync (
212+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . SequenceEqual ( new [ ] { "npm" , "install" } ) ) ,
213+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
214+ . ReturnsAsync ( npmInstallResult ) ;
215+
216+ // TypeScript compilation
217+ var tscResult = new ProcessResult { ExitCode = 0 } ;
218+ tscResult . AppendStdout ( "Compilation successful" ) ;
219+ mockDockerService . Setup ( x => x . RunCommandInContainerAsync (
220+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . SequenceEqual ( new [ ] { "npm" , "run" , "typecheck" } ) ) ,
221+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
222+ . ReturnsAsync ( tscResult ) ;
223+
224+ // Cleanup
225+ mockDockerService . Setup ( x => x . RunCommandInContainerAsync (
226+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . Contains ( "rm" ) ) ,
227+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
228+ . ReturnsAsync ( new ProcessResult { ExitCode = 0 } ) ;
229+ }
230+
231+ private void SetupFailedTypecheckInteraction ( )
232+ {
233+ SetupSuccessfulDockerInteractions ( ) ;
234+
235+ // Override TypeScript compilation to fail
236+ var failedTscResult = new ProcessResult { ExitCode = 1 } ;
237+ failedTscResult . AppendStderr ( "Type error: string is not assignable to number" ) ;
238+ mockDockerService . Setup ( x => x . RunCommandInContainerAsync (
239+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . SequenceEqual ( new [ ] { "npm" , "run" , "typecheck" } ) ) ,
240+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
241+ . ReturnsAsync ( failedTscResult ) ;
242+ }
243+
244+ private void SetupClientDistInstallation ( string clientDist )
245+ {
246+ // Client dist installation
247+ var installResult = new ProcessResult { ExitCode = 0 } ;
248+ installResult . AppendStdout ( "Client package installed" ) ;
249+ mockDockerService . Setup ( x => x . RunCommandInContainerAsync (
250+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args =>
251+ args . Length >= 4 && args [ 0 ] == "npm" && args [ 1 ] == "install" && args [ 2 ] == "--no-save" ) ,
252+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) )
253+ . ReturnsAsync ( installResult ) ;
254+ }
255+
256+ private void VerifyDockerInteractionSequence ( )
257+ {
258+ // Verify container creation
259+ mockDockerService . Verify ( x => x . CreateContainerAsync (
260+ "node:alpine" , It . IsAny < string > ( ) , null , null , It . IsAny < CancellationToken > ( ) ) ,
261+ Times . Once ) ;
262+
263+ // Verify npm install
264+ mockDockerService . Verify ( x => x . RunCommandInContainerAsync (
265+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . SequenceEqual ( new [ ] { "npm" , "install" } ) ) ,
266+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) ,
267+ Times . Once ) ;
268+
269+ // Verify TypeScript compilation
270+ mockDockerService . Verify ( x => x . RunCommandInContainerAsync (
271+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . SequenceEqual ( new [ ] { "npm" , "run" , "typecheck" } ) ) ,
272+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) ,
273+ Times . Once ) ;
274+
275+ // Verify cleanup
276+ mockDockerService . Verify ( x => x . RunCommandInContainerAsync (
277+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args => args . Contains ( "rm" ) ) ,
278+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) ,
279+ Times . Once ) ;
280+ }
281+
282+ private void VerifyClientDistInstallation ( string clientDist )
283+ {
284+ var fileName = Path . GetFileName ( clientDist ) ;
285+
286+ // Verify client dist copy
287+ mockDockerService . Verify ( x => x . CopyToContainerAsync (
288+ It . IsAny < string > ( ) , clientDist , It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) ,
289+ Times . Once ) ;
290+
291+ // Verify client dist installation
292+ mockDockerService . Verify ( x => x . RunCommandInContainerAsync (
293+ It . IsAny < string > ( ) , It . Is < string [ ] > ( args =>
294+ args . Length >= 4 && args [ 0 ] == "npm" && args [ 1 ] == "install" && args [ 2 ] == "--no-save" && args [ 3 ] == fileName ) ,
295+ It . IsAny < string > ( ) , It . IsAny < CancellationToken > ( ) ) ,
296+ Times . Once ) ;
297+ }
298+ }
299+ }
0 commit comments