11import * as path from 'path' ;
22import * as fs from 'fs-extra' ;
3- import { commands , env , ExtensionContext , ExtensionMode , QuickPickItem , Uri , window } from 'vscode' ;
43import { setupServer , SetupServerApi } from 'msw/node' ;
54
6- import { getMockGitHubApiServerScenariosPath , MockGitHubApiConfigListener } from '../config' ;
75import { DisposableObject } from '../pure/disposable-object' ;
86
97import { Recorder } from './recorder' ;
@@ -14,211 +12,128 @@ import { getDirectoryNamesInsidePath } from '../pure/files';
1412 * Enables mocking of the GitHub API server via HTTP interception, using msw.
1513 */
1614export class MockGitHubApiServer extends DisposableObject {
17- private isListening : boolean ;
18- private config : MockGitHubApiConfigListener ;
15+ private _isListening : boolean ;
1916
2017 private readonly server : SetupServerApi ;
2118 private readonly recorder : Recorder ;
2219
23- constructor (
24- private readonly ctx : ExtensionContext ,
25- ) {
20+ constructor ( ) {
2621 super ( ) ;
27- this . isListening = false ;
28- this . config = new MockGitHubApiConfigListener ( ) ;
22+ this . _isListening = false ;
2923
3024 this . server = setupServer ( ) ;
3125 this . recorder = this . push ( new Recorder ( this . server ) ) ;
32-
33- this . setupConfigListener ( ) ;
3426 }
3527
3628 public startServer ( ) : void {
37- if ( this . isListening ) {
29+ if ( this . _isListening ) {
3830 return ;
3931 }
4032
4133 this . server . listen ( ) ;
42- this . isListening = true ;
34+ this . _isListening = true ;
4335 }
4436
4537 public stopServer ( ) : void {
4638 this . server . close ( ) ;
47- this . isListening = false ;
39+ this . _isListening = false ;
4840 }
4941
50- public async loadScenario ( ) : Promise < void > {
51- const scenariosPath = await this . getScenariosPath ( ) ;
42+ public async loadScenario ( scenarioName : string , scenariosPath ?: string ) : Promise < void > {
5243 if ( ! scenariosPath ) {
53- return ;
54- }
55-
56- const scenarioNames = await getDirectoryNamesInsidePath ( scenariosPath ) ;
57- const scenarioQuickPickItems = scenarioNames . map ( s => ( { label : s } ) ) ;
58- const quickPickOptions = {
59- placeHolder : 'Select a scenario to load' ,
60- } ;
61- const selectedScenario = await window . showQuickPick < QuickPickItem > (
62- scenarioQuickPickItems ,
63- quickPickOptions ) ;
64- if ( ! selectedScenario ) {
65- return ;
44+ scenariosPath = await this . getDefaultScenariosPath ( ) ;
45+ if ( ! scenariosPath ) {
46+ return ;
47+ }
6648 }
6749
68- const scenarioName = selectedScenario . label ;
6950 const scenarioPath = path . join ( scenariosPath , scenarioName ) ;
7051
7152 const handlers = await createRequestHandlers ( scenarioPath ) ;
7253 this . server . resetHandlers ( ) ;
7354 this . server . use ( ...handlers ) ;
55+ }
56+
57+ public async saveScenario ( scenarioName : string , scenariosPath ?: string ) : Promise < string > {
58+ if ( ! scenariosPath ) {
59+ scenariosPath = await this . getDefaultScenariosPath ( ) ;
60+ if ( ! scenariosPath ) {
61+ throw new Error ( 'Could not find scenarios path' ) ;
62+ }
63+ }
64+
65+ const filePath = await this . recorder . save ( scenariosPath , scenarioName ) ;
7466
75- // Set a value in the context to track whether we have a scenario loaded.
76- // This allows us to use this to show/hide commands (see package.json)
77- await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.scenarioLoaded' , true ) ;
67+ await this . stopRecording ( ) ;
7868
79- await window . showInformationMessage ( `Loaded scenario ' ${ scenarioName } '` ) ;
69+ return filePath ;
8070 }
8171
8272 public async unloadScenario ( ) : Promise < void > {
83- if ( ! this . isScenarioLoaded ( ) ) {
84- await window . showInformationMessage ( 'No scenario currently loaded' ) ;
85- }
86- else {
87- await this . unloadAllScenarios ( ) ;
88- await window . showInformationMessage ( 'Unloaded scenario' ) ;
73+ if ( ! this . isScenarioLoaded ) {
74+ return ;
8975 }
76+
77+ await this . unloadAllScenarios ( ) ;
9078 }
9179
9280 public async startRecording ( ) : Promise < void > {
9381 if ( this . recorder . isRecording ) {
94- void window . showErrorMessage ( 'A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.' ) ;
9582 return ;
9683 }
9784
98- if ( this . isScenarioLoaded ( ) ) {
85+ if ( this . isScenarioLoaded ) {
9986 await this . unloadAllScenarios ( ) ;
100- void window . showInformationMessage ( 'A scenario was loaded so it has been unloaded' ) ;
10187 }
10288
10389 this . recorder . start ( ) ;
104- // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
105- await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , true ) ;
90+ }
10691
107- await window . showInformationMessage ( 'Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.' ) ;
92+ public async stopRecording ( ) : Promise < void > {
93+ await this . recorder . stop ( ) ;
94+ await this . recorder . clear ( ) ;
10895 }
10996
110- public async saveScenario ( ) : Promise < void > {
111- const scenariosPath = await this . getScenariosPath ( ) ;
97+ public async getScenarioNames ( scenariosPath ?: string ) : Promise < string [ ] > {
11298 if ( ! scenariosPath ) {
113- return ;
114- }
115-
116- // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
117- await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , false ) ;
118-
119- if ( ! this . recorder . isRecording ) {
120- void window . showErrorMessage ( 'No scenario is currently being recorded.' ) ;
121- return ;
122- }
123- if ( ! this . recorder . anyRequestsRecorded ) {
124- void window . showWarningMessage ( 'No requests were recorded. Cancelling scenario.' ) ;
125-
126- await this . stopRecording ( ) ;
127-
128- return ;
129- }
130-
131- const name = await window . showInputBox ( {
132- title : 'Save scenario' ,
133- prompt : 'Enter a name for the scenario.' ,
134- placeHolder : 'successful-run' ,
135- } ) ;
136- if ( ! name ) {
137- return ;
99+ scenariosPath = await this . getDefaultScenariosPath ( ) ;
100+ if ( ! scenariosPath ) {
101+ return [ ] ;
102+ }
138103 }
139104
140- const filePath = await this . recorder . save ( scenariosPath , name ) ;
141-
142- await this . stopRecording ( ) ;
143-
144- const action = await window . showInformationMessage ( `Scenario saved to ${ filePath } ` , 'Open directory' ) ;
145- if ( action === 'Open directory' ) {
146- await env . openExternal ( Uri . file ( filePath ) ) ;
147- }
105+ return await getDirectoryNamesInsidePath ( scenariosPath ) ;
148106 }
149107
150- public async cancelRecording ( ) : Promise < void > {
151- if ( ! this . recorder . isRecording ) {
152- void window . showErrorMessage ( 'No scenario is currently being recorded.' ) ;
153- return ;
154- }
155-
156- await this . stopRecording ( ) ;
157-
158- void window . showInformationMessage ( 'Recording cancelled.' ) ;
108+ public get isListening ( ) : boolean {
109+ return this . _isListening ;
159110 }
160111
161- private async stopRecording ( ) : Promise < void > {
162- // Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
163- await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , false ) ;
112+ public get isRecording ( ) : boolean {
113+ return this . recorder . isRecording ;
114+ }
164115
165- await this . recorder . stop ( ) ;
166- await this . recorder . clear ( ) ;
116+ public get anyRequestsRecorded ( ) : boolean {
117+ return this . recorder . anyRequestsRecorded ;
167118 }
168119
169- private async getScenariosPath ( ) : Promise < string | undefined > {
170- const scenariosPath = getMockGitHubApiServerScenariosPath ( ) ;
171- if ( scenariosPath ) {
172- return scenariosPath ;
173- }
120+ public get isScenarioLoaded ( ) : boolean {
121+ return this . server . listHandlers ( ) . length > 0 ;
122+ }
174123
175- if ( this . ctx . extensionMode === ExtensionMode . Development ) {
176- const developmentScenariosPath = Uri . joinPath ( this . ctx . extensionUri , 'src/mocks/scenarios' ) . fsPath . toString ( ) ;
177- if ( await fs . pathExists ( developmentScenariosPath ) ) {
178- return developmentScenariosPath ;
179- }
180- }
124+ public async getDefaultScenariosPath ( ) : Promise < string | undefined > {
125+ // This should be the directory where package.json is located
126+ const rootDirectory = path . resolve ( __dirname , '../..' ) ;
181127
182- const directories = await window . showOpenDialog ( {
183- canSelectFolders : true ,
184- canSelectFiles : false ,
185- canSelectMany : false ,
186- openLabel : 'Select scenarios directory' ,
187- title : 'Select scenarios directory' ,
188- } ) ;
189- if ( directories === undefined || directories . length === 0 ) {
190- void window . showErrorMessage ( 'No scenarios directory selected.' ) ;
191- return undefined ;
128+ const scenariosPath = path . resolve ( rootDirectory , 'src/mocks/scenarios' ) ;
129+ if ( await fs . pathExists ( scenariosPath ) ) {
130+ return scenariosPath ;
192131 }
193132
194- // Unfortunately, we cannot save the directory in the configuration because that requires
195- // the configuration to be registered. If we do that, it would be visible to all users; there
196- // is no "when" clause that would allow us to only show it to users who have enabled the feature flag.
197-
198- return directories [ 0 ] . fsPath ;
199- }
200-
201- private isScenarioLoaded ( ) : boolean {
202- return this . server . listHandlers ( ) . length > 0 ;
133+ return undefined ;
203134 }
204135
205136 private async unloadAllScenarios ( ) : Promise < void > {
206137 this . server . resetHandlers ( ) ;
207- await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.scenarioLoaded' , false ) ;
208- }
209-
210- private setupConfigListener ( ) : void {
211- // The config "changes" from the default at startup, so we need to call onConfigChange() to ensure the server is
212- // started if required.
213- this . onConfigChange ( ) ;
214- this . config . onDidChangeConfiguration ( ( ) => this . onConfigChange ( ) ) ;
215- }
216-
217- private onConfigChange ( ) : void {
218- if ( this . config . mockServerEnabled && ! this . isListening ) {
219- this . startServer ( ) ;
220- } else if ( ! this . config . mockServerEnabled && this . isListening ) {
221- this . stopServer ( ) ;
222- }
223138 }
224139}
0 commit comments