1- import { MockGitHubApiConfigListener } from '../config' ;
1+ import * as fs from 'fs-extra' ;
2+ import { commands , env , ExtensionContext , ExtensionMode , Uri , window } from 'vscode' ;
3+ import { setupServer , SetupServerApi } from 'msw/node' ;
4+
5+ import { getMockGitHubApiServerScenariosPath , MockGitHubApiConfigListener } from '../config' ;
6+ import { DisposableObject } from '../pure/disposable-object' ;
7+
8+ import { Recorder } from './recorder' ;
29
310/**
411 * Enables mocking of the GitHub API server via HTTP interception, using msw.
512 */
6- export class MockGitHubApiServer {
13+ export class MockGitHubApiServer extends DisposableObject {
714 private isListening : boolean ;
815 private config : MockGitHubApiConfigListener ;
916
10- constructor ( ) {
17+ private readonly server : SetupServerApi ;
18+ private readonly recorder : Recorder ;
19+
20+ constructor (
21+ private readonly ctx : ExtensionContext ,
22+ ) {
23+ super ( ) ;
1124 this . isListening = false ;
1225 this . config = new MockGitHubApiConfigListener ( ) ;
26+
27+ this . server = setupServer ( ) ;
28+ this . recorder = this . push ( new Recorder ( this . server ) ) ;
29+
1330 this . setupConfigListener ( ) ;
1431 }
1532
1633 public startServer ( ) : void {
17- this . isListening = true ;
34+ if ( this . isListening ) {
35+ return ;
36+ }
1837
19- // TODO: Enable HTTP interception.
38+ this . server . listen ( ) ;
39+ this . isListening = true ;
2040 }
2141
2242 public stopServer ( ) : void {
43+ this . server . close ( ) ;
2344 this . isListening = false ;
24-
25- // TODO: Disable HTTP interception.
2645 }
2746
2847 public loadScenario ( ) : void {
@@ -33,17 +52,122 @@ export class MockGitHubApiServer {
3352 // TODO: Implement logic to list all available scenarios.
3453 }
3554
36- public recordScenario ( ) : void {
37- // TODO: Implement logic to record a new scenario to a directory.
55+ public async startRecording ( ) : Promise < void > {
56+ if ( this . recorder . isRecording ) {
57+ void window . showErrorMessage ( 'A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.' ) ;
58+ return ;
59+ }
60+
61+ this . recorder . start ( ) ;
62+ // 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)
63+ await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , true ) ;
64+
65+ await window . showInformationMessage ( 'Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.' ) ;
3866 }
3967
40- private setupConfigListener ( ) : void {
41- this . config . onDidChangeConfiguration ( ( ) => {
42- if ( this . config . mockServerEnabled && ! this . isListening ) {
43- this . startServer ( ) ;
44- } else if ( ! this . config . mockServerEnabled && this . isListening ) {
45- this . stopServer ( ) ;
68+ public async saveScenario ( ) : Promise < void > {
69+ const scenariosPath = await this . getScenariosPath ( ) ;
70+ if ( ! scenariosPath ) {
71+ return ;
72+ }
73+
74+ // 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)
75+ await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , false ) ;
76+
77+ if ( ! this . recorder . isRecording ) {
78+ void window . showErrorMessage ( 'No scenario is currently being recorded.' ) ;
79+ return ;
80+ }
81+ if ( ! this . recorder . anyRequestsRecorded ) {
82+ void window . showWarningMessage ( 'No requests were recorded. Cancelling scenario.' ) ;
83+
84+ await this . stopRecording ( ) ;
85+
86+ return ;
87+ }
88+
89+ const name = await window . showInputBox ( {
90+ title : 'Save scenario' ,
91+ prompt : 'Enter a name for the scenario.' ,
92+ placeHolder : 'successful-run' ,
93+ } ) ;
94+ if ( ! name ) {
95+ return ;
96+ }
97+
98+ const filePath = await this . recorder . save ( scenariosPath , name ) ;
99+
100+ await this . stopRecording ( ) ;
101+
102+ const action = await window . showInformationMessage ( `Scenario saved to ${ filePath } ` , 'Open directory' ) ;
103+ if ( action === 'Open directory' ) {
104+ await env . openExternal ( Uri . file ( filePath ) ) ;
105+ }
106+ }
107+
108+ public async cancelRecording ( ) : Promise < void > {
109+ if ( ! this . recorder . isRecording ) {
110+ void window . showErrorMessage ( 'No scenario is currently being recorded.' ) ;
111+ return ;
112+ }
113+
114+ await this . stopRecording ( ) ;
115+
116+ void window . showInformationMessage ( 'Recording cancelled.' ) ;
117+ }
118+
119+ private async stopRecording ( ) : Promise < void > {
120+ // 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)
121+ await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , false ) ;
122+
123+ await this . recorder . stop ( ) ;
124+ await this . recorder . clear ( ) ;
125+ }
126+
127+ private async getScenariosPath ( ) : Promise < string | undefined > {
128+ const scenariosPath = getMockGitHubApiServerScenariosPath ( ) ;
129+ if ( scenariosPath ) {
130+ return scenariosPath ;
131+ }
132+
133+ if ( this . ctx . extensionMode === ExtensionMode . Development ) {
134+ const developmentScenariosPath = Uri . joinPath ( this . ctx . extensionUri , 'src/mocks/scenarios' ) . fsPath . toString ( ) ;
135+ if ( await fs . pathExists ( developmentScenariosPath ) ) {
136+ return developmentScenariosPath ;
46137 }
138+ }
139+
140+ const directories = await window . showOpenDialog ( {
141+ canSelectFolders : true ,
142+ canSelectFiles : false ,
143+ canSelectMany : false ,
144+ openLabel : 'Select scenarios directory' ,
145+ title : 'Select scenarios directory' ,
47146 } ) ;
147+ if ( directories === undefined || directories . length === 0 ) {
148+ void window . showErrorMessage ( 'No scenarios directory selected.' ) ;
149+ return undefined ;
150+ }
151+
152+ // Unfortunately, we cannot save the directory in the configuration because that requires
153+ // the configuration to be registered. If we do that, it would be visible to all users; there
154+ // is no "when" clause that would allow us to only show it to users who have enabled the feature flag.
155+
156+ return directories [ 0 ] . fsPath ;
157+ }
158+
159+ private setupConfigListener ( ) : void {
160+ // The config "changes" from the default at startup, so we need to call onConfigChange() to ensure the server is
161+ // started if required.
162+ this . onConfigChange ( ) ;
163+ this . config . onDidChangeConfiguration ( ( ) => this . onConfigChange ( ) ) ;
164+ }
165+
166+ private onConfigChange ( ) : void {
167+ if ( this . config . mockServerEnabled && ! this . isListening ) {
168+ this . startServer ( ) ;
169+ } else if ( ! this . config . mockServerEnabled && this . isListening ) {
170+ this . stopServer ( ) ;
171+ }
48172 }
49173}
0 commit comments