11import { MockGitHubApiConfigListener } from '../config' ;
22
3+ import { setupServer , SetupServerApi } from 'msw/node' ;
4+ import { Recorder } from './recorder' ;
5+ import { commands , env , Uri , window } from 'vscode' ;
6+ import { DisposableObject } from '../pure/disposable-object' ;
7+ import { getMockGitHubApiServerScenariosPath } from '../config' ;
8+
39/**
410 * Enables mocking of the GitHub API server via HTTP interception, using msw.
511 */
6- export class MockGitHubApiServer {
12+ export class MockGitHubApiServer extends DisposableObject {
713 private isListening : boolean ;
814 private config : MockGitHubApiConfigListener ;
915
16+ private readonly server : SetupServerApi ;
17+ private readonly recorder : Recorder ;
18+
1019 constructor ( ) {
20+ super ( ) ;
1121 this . isListening = false ;
1222 this . config = new MockGitHubApiConfigListener ( ) ;
23+
24+ this . server = setupServer ( ) ;
25+ this . recorder = this . push ( new Recorder ( this . server ) ) ;
26+
1327 this . setupConfigListener ( ) ;
1428 }
1529
1630 public startServer ( ) : void {
17- this . isListening = true ;
31+ if ( this . isListening ) {
32+ return ;
33+ }
1834
19- // TODO: Enable HTTP interception.
35+ this . server . listen ( ) ;
36+ this . isListening = true ;
2037 }
2138
2239 public stopServer ( ) : void {
40+ this . server . close ( ) ;
2341 this . isListening = false ;
24-
25- // TODO: Disable HTTP interception.
2642 }
2743
2844 public loadScenario ( ) : void {
@@ -33,8 +49,98 @@ export class MockGitHubApiServer {
3349 // TODO: Implement logic to list all available scenarios.
3450 }
3551
36- public recordScenario ( ) : void {
37- // TODO: Implement logic to record a new scenario to a directory.
52+ public async recordScenario ( ) : Promise < void > {
53+ if ( this . recorder . isRecording ) {
54+ void window . showErrorMessage ( 'A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.' ) ;
55+ return ;
56+ }
57+
58+ this . recorder . start ( ) ;
59+ await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , true ) ;
60+
61+ await window . showInformationMessage ( 'Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.' ) ;
62+ }
63+
64+ public async saveScenario ( ) : Promise < void > {
65+ const scenariosDirectory = await this . getScenariosDirectory ( ) ;
66+ if ( ! scenariosDirectory ) {
67+ return ;
68+ }
69+
70+ await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , false ) ;
71+
72+ if ( ! this . recorder . isRecording ) {
73+ void window . showErrorMessage ( 'No scenario is currently being recorded.' ) ;
74+ return ;
75+ }
76+ if ( this . recorder . scenarioRequestCount === 0 ) {
77+ void window . showWarningMessage ( 'No requests were recorded. Cancelling scenario.' ) ;
78+
79+ await this . stopRecording ( ) ;
80+
81+ return ;
82+ }
83+
84+ const name = await window . showInputBox ( {
85+ title : 'Save scenario' ,
86+ prompt : 'Enter a name for the scenario.' ,
87+ placeHolder : 'successful-run' ,
88+ } ) ;
89+ if ( ! name ) {
90+ return ;
91+ }
92+
93+ const filepath = await this . recorder . save ( scenariosDirectory , name ) ;
94+
95+ await this . stopRecording ( ) ;
96+
97+ const action = await window . showInformationMessage ( `Scenario saved to ${ filepath } ` , 'Open directory' ) ;
98+ if ( action === 'Open directory' ) {
99+ await env . openExternal ( Uri . file ( filepath ) ) ;
100+ }
101+ }
102+
103+ public async cancelRecording ( ) : Promise < void > {
104+ if ( ! this . recorder . isRecording ) {
105+ void window . showErrorMessage ( 'No scenario is currently being recorded.' ) ;
106+ return ;
107+ }
108+
109+ await this . stopRecording ( ) ;
110+
111+ void window . showInformationMessage ( 'Recording cancelled.' ) ;
112+ }
113+
114+ private async stopRecording ( ) : Promise < void > {
115+ await commands . executeCommand ( 'setContext' , 'codeQL.mockGitHubApiServer.recording' , false ) ;
116+
117+ await this . recorder . stop ( ) ;
118+ await this . recorder . clear ( ) ;
119+ }
120+
121+ private async getScenariosDirectory ( ) : Promise < string | undefined > {
122+ const scenariosDirectory = getMockGitHubApiServerScenariosPath ( ) ;
123+ if ( scenariosDirectory ) {
124+ return scenariosDirectory ;
125+ }
126+
127+ const directories = await window . showOpenDialog ( {
128+ canSelectFolders : true ,
129+ canSelectFiles : false ,
130+ canSelectMany : false ,
131+ openLabel : 'Select scenarios directory' ,
132+ title : 'Select scenarios directory' ,
133+ } ) ;
134+ if ( directories === undefined || directories . length === 0 ) {
135+ void window . showErrorMessage ( 'No scenarios directory selected.' ) ;
136+ return undefined ;
137+ }
138+
139+ // Unfortunately, we cannot save the directory in the configuration because that requires
140+ // the configuration to be registered. If we do that, it would be visible to all users; there
141+ // is no "when" clause that would allow us to only show it to users who have enabled the feature flag.
142+
143+ return directories [ 0 ] . fsPath ;
38144 }
39145
40146 private setupConfigListener ( ) : void {
0 commit comments