Skip to content

Commit 5584367

Browse files
committed
feat: Added test for connect + improvements and fixes
1 parent a2280ee commit 5584367

File tree

7 files changed

+192
-45
lines changed

7 files changed

+192
-45
lines changed

src/connect/login-helper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export class LoginHelper {
6565
const credentialsPath = path.join(os.homedir(), '.codify', 'credentials.json');
6666
await fs.rm(credentialsPath);
6767
} catch {}
68+
69+
this.instance.isLoggedIn = false;
70+
this.instance.credentials = undefined;
6871
}
6972

7073
private static async read(): Promise<Credentials | undefined> {

src/orchestrators/connect.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import { SocketServer } from '../connect/socket-server.js';
1414
import { ProcessName, ctx } from '../events/context.js';
1515
import { Reporter } from '../ui/reporters/reporter.js';
1616
import { LoginOrchestrator } from './login.js';
17+
import { registerKillListeners } from '../utils/register-kill-listeners.js';
1718

1819
export class ConnectOrchestrator {
1920
static rootCommand: string;
2021
static nodeBinary: string;
2122

22-
static async run(rootCommand: string, reporter: Reporter, openBrowser = true, onOpen?: (connectionCode: string) => void) {
23+
static async run(rootCommand: string, reporter: Reporter, openBrowser = true, onOpen?: (connectionCode: string, server: Server) => void) {
2324
const login = LoginHelper.get()?.isLoggedIn;
2425
if (!login) {
2526
ctx.log('User is not logged in. Attempting to log in...')
@@ -36,7 +37,7 @@ export class ConnectOrchestrator {
3637
app.use(json())
3738
app.use(router);
3839

39-
const server = await ConnectOrchestrator.listen(app, reporter, () => {
40+
const server = await ConnectOrchestrator.listen(app, reporter, (server) => {
4041
if (openBrowser) {
4142
open(`${config.dashboardUrl}/connection/success?code=${connectionSecret}`)
4243
console.log(`Open browser window to store code.
@@ -45,13 +46,13 @@ If unsuccessful manually enter the code:
4546
${connectionSecret}`)
4647
}
4748

48-
onOpen?.(connectionSecret);
49+
onOpen?.(connectionSecret, server);
4950
});
5051

5152
SocketServer.init(server, connectionSecret);
5253
}
5354

54-
private static listen(app: express.Application, reporter: Reporter, onOpen: () => void): Promise<Server> {
55+
private static listen(app: express.Application, reporter: Reporter, onOpen: (server: Server) => void): Promise<Server> {
5556
return new Promise((resolve) => {
5657
const server = app.listen(config.connectServerPort, async (error) => {
5758
if (error) {
@@ -77,9 +78,11 @@ ${connectionSecret}`)
7778
}
7879
} else {
7980
resolve(server);
80-
onOpen();
81+
onOpen(server);
8182
}
8283
});
84+
85+
registerKillListeners(() => server.close());
8386
});
8487
}
8588

src/orchestrators/login.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DashboardApiClient } from '../api/dashboard/index.js';
77
import { config } from '../config.js';
88
import { LoginHelper } from '../connect/login-helper.js';
99
import { ajv } from '../utils/ajv.js';
10+
import { registerKillListeners } from '../utils/register-kill-listeners.js';
1011

1112
const schema = {
1213
type: 'object',
@@ -76,6 +77,8 @@ Manually open it here: ${config.dashboardUrl}/auth/cli`
7677
open(`${config.dashboardUrl}/auth/cli`);
7778
})
7879

80+
registerKillListeners(() => server.close());
81+
7982
await new Promise<void>((resolve, reject) => {
8083
app.post('/', async (req, res) => {
8184
try {

src/plugins/plugin-manager.ts

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ResourceConfig } from '../entities/resource-config.js';
1111
import { ResourceInfo } from '../entities/resource-info.js';
1212
import { SubProcessName, ctx } from '../events/context.js';
1313
import { groupBy } from '../utils/index.js';
14+
import { registerKillListeners } from '../utils/register-kill-listeners.js';
1415
import { Plugin } from './plugin.js';
1516
import { PluginResolver } from './resolver.js';
1617

@@ -35,7 +36,11 @@ export class PluginManager {
3536
this.plugins.set(plugin.name, plugin)
3637
}
3738

38-
this.registerKillListeners(plugins)
39+
registerKillListeners(() => {
40+
for (const plugin of plugins) {
41+
plugin.kill()
42+
}
43+
});
3944
return this.initializePlugins(plugins, secureMode, verbosityLevel);
4045
}
4146

@@ -193,43 +198,4 @@ export class PluginManager {
193198

194199
return resourceMap;
195200
}
196-
197-
/** Clean up any stranglers and child processes if the CLI is killed */
198-
private registerKillListeners(plugins: Plugin[]) {
199-
const kill = (code: number | string) => {
200-
plugins.forEach((p) => {
201-
p.kill()
202-
})
203-
204-
let exitCode = 0;
205-
switch (code) {
206-
case 'SIGTERM': {
207-
exitCode = 143;
208-
break;
209-
}
210-
211-
case 'SIGHUP': {
212-
exitCode = 129;
213-
break;
214-
}
215-
216-
case 'SIGINT': {
217-
exitCode = 130;
218-
break;
219-
}
220-
}
221-
222-
const parsedCode = typeof code === 'string' ? Number.parseInt(code, 10) : code;
223-
if (Number.isInteger(parsedCode)) {
224-
exitCode = parsedCode;
225-
}
226-
227-
process.exit(exitCode);
228-
}
229-
230-
process.on('exit', kill)
231-
process.on('SIGINT', kill)
232-
process.on('SIGTERM', kill)
233-
process.on('SIGHUP', kill)
234-
}
235201
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export function registerKillListeners(kill: (code: number | string) => void) {
2+
const killHandler = (code: number | string) => {
3+
kill(code);
4+
5+
let exitCode = 0;
6+
switch (code) {
7+
case 'SIGTERM': {
8+
exitCode = 143;
9+
break;
10+
}
11+
12+
case 'SIGHUP': {
13+
exitCode = 129;
14+
break;
15+
}
16+
17+
case 'SIGINT': {
18+
exitCode = 130;
19+
break;
20+
}
21+
}
22+
23+
const parsedCode = typeof code === 'string' ? Number.parseInt(code, 10) : code;
24+
if (Number.isInteger(parsedCode)) {
25+
exitCode = parsedCode;
26+
}
27+
28+
process.exit(exitCode);
29+
}
30+
31+
process.on('exit', killHandler)
32+
process.on('SIGINT', killHandler)
33+
process.on('SIGTERM', killHandler)
34+
process.on('SIGHUP', killHandler)
35+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { MockOs } from '../mocks/system.js';
3+
import { MockReporter } from '../mocks/reporter.js';
4+
import { ConnectOrchestrator } from '../../../src/orchestrators/connect';
5+
import * as net from 'node:net';
6+
import { config } from '../../../src/config.js';
7+
import { fakeLogin, fakeLogout } from '../mocks/mock-login';
8+
import * as open from 'open'
9+
import { LoginOrchestrator } from '../../../src/orchestrators/login';
10+
import { Server } from 'node:http';
11+
import { vol } from 'memfs';
12+
13+
vi.mock(import('../../../src/orchestrators/login'), async () => {
14+
return {
15+
LoginOrchestrator: {
16+
run: async () => {}
17+
},
18+
}
19+
})
20+
21+
vi.mock('node:fs', async () => {
22+
const { fs } = await import('memfs');
23+
return fs
24+
})
25+
26+
vi.mock('node:fs/promises', async () => {
27+
const { fs } = await import('memfs');
28+
return fs.promises;
29+
})
30+
31+
vi.mock(import('open'), async () => {
32+
return {
33+
default: vi.fn()
34+
}
35+
})
36+
37+
// The apply orchestrator directly calls plan so this will test both
38+
describe('Connect orchestrator tests', () => {
39+
beforeEach(() => {
40+
vol.reset();
41+
})
42+
43+
it('It will start a local server on config.connectServerPort', async () => {
44+
const reporter = new MockReporter();
45+
await fakeLogin();
46+
47+
const openSpy = vi.spyOn(open, 'default');
48+
49+
await new Promise<void>((done) => {
50+
ConnectOrchestrator.run('codify', reporter, false, async (connectionCode: string , server: Server) => {
51+
expect(connectionCode).to.be.a('string');
52+
53+
const portInUse = await checkPortStatus(config.connectServerPort);
54+
expect(portInUse).to.be.true;
55+
56+
server.close();
57+
done();
58+
})
59+
})
60+
});
61+
62+
it('It will ask for a login if the user is not logged in', async () => {
63+
const reporter = new MockReporter({});
64+
await fakeLogout();
65+
66+
const loginRunSpy = vi.spyOn(LoginOrchestrator, 'run');
67+
const openSpy = vi.spyOn(open, 'default');
68+
69+
await new Promise<void>((done) => {
70+
ConnectOrchestrator.run('codify', reporter, false, async (connectionCode: string, server: Server) => {
71+
expect(connectionCode).to.be.a('string');
72+
73+
const portInUse = await checkPortStatus(config.connectServerPort);
74+
expect(portInUse).to.be.true;
75+
expect(loginRunSpy).toHaveBeenCalledOnce();
76+
77+
done();
78+
server.close();
79+
})
80+
81+
});
82+
});
83+
84+
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
85+
86+
afterEach(() => {
87+
vi.resetAllMocks();
88+
MockOs.reset();
89+
})
90+
91+
function checkPortStatus(port: number, host = '127.0.0.1') {
92+
return new Promise((resolve, reject) => {
93+
const socket = new net.Socket();
94+
95+
socket.once('connect', () => {
96+
// If 'connect' event fires, the port is open and listening
97+
socket.destroy();
98+
resolve(true); // Port is in use
99+
});
100+
101+
socket.once('error', (err) => {
102+
// Any error typically means the port is not listening
103+
// EADDRNOTAVAIL, ECONNREFUSED, etc.
104+
reject(err); // Port is likely free or unreachable
105+
});
106+
107+
socket.once('timeout', () => {
108+
socket.destroy();
109+
reject(new Error('Connection attempt timed out'));
110+
});
111+
112+
socket.connect(port, host);
113+
});
114+
}
115+
116+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { vi } from 'vitest';
2+
3+
import { LoginHelper } from '../../../src/connect/login-helper.js';
4+
5+
/**
6+
* Must mock node:fs/promises before calling this function
7+
*/
8+
export async function fakeLogin(): Promise<string> {
9+
await LoginHelper.load();
10+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30'
11+
await LoginHelper.save(token);
12+
return token;
13+
}
14+
15+
/**
16+
* Must mock node:fs/promises before calling this function
17+
*/
18+
export async function fakeLogout() {
19+
await LoginHelper.load();
20+
await LoginHelper.logout();
21+
}

0 commit comments

Comments
 (0)