Skip to content

Commit 454da0d

Browse files
aborovskyorubin
andauthored
feat(entrypoints): add host update job status polling and bulk host update commands (#657)
Closes #656 --------- Co-authored-by: Or Rubin <or.rubin@hotmail.com>
1 parent 354ec16 commit 454da0d

12 files changed

+928
-2
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import 'reflect-metadata';
2+
import { Logger, logger, ErrorMessageFactory } from '../Utils';
3+
import { EntryPointsUpdateHost } from './EntryPointsUpdateHost';
4+
import { EntryPoints } from '../EntryPoint';
5+
import {
6+
mock,
7+
reset,
8+
spy,
9+
instance,
10+
when,
11+
anything,
12+
verify,
13+
objectContaining
14+
} from 'ts-mockito';
15+
import { container } from 'tsyringe';
16+
import { Arguments } from 'yargs';
17+
18+
describe('EntryPointsUpdateHost', () => {
19+
let loggerSpy!: Logger;
20+
21+
beforeEach(() => {
22+
loggerSpy = spy(logger);
23+
});
24+
25+
afterEach(() => reset(loggerSpy));
26+
27+
describe('handler', () => {
28+
let entryPointsUpdateHost: EntryPointsUpdateHost;
29+
const mockedEntryPoints = mock<EntryPoints>();
30+
31+
beforeEach(() => {
32+
container.registerInstance(EntryPoints, instance(mockedEntryPoints));
33+
entryPointsUpdateHost = new EntryPointsUpdateHost();
34+
});
35+
36+
afterEach(() => {
37+
container.clearInstances();
38+
reset(mockedEntryPoints);
39+
});
40+
41+
it('should correctly pass update host options from args', async () => {
42+
const args = {
43+
project: 'project-id',
44+
oldHostname: 'old.example.com',
45+
newHostname: 'new.example.com',
46+
entrypointIds: ['ep1', 'ep2'],
47+
token: 'api-token',
48+
_: [],
49+
$0: ''
50+
} as Arguments;
51+
52+
const expectedTaskId = 'task-123';
53+
54+
when(
55+
mockedEntryPoints.updateHost(
56+
objectContaining({
57+
projectId: 'project-id',
58+
oldHostname: 'old.example.com',
59+
newHostname: 'new.example.com',
60+
entryPointIds: ['ep1', 'ep2']
61+
})
62+
)
63+
).thenResolve({ taskId: expectedTaskId });
64+
65+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
66+
67+
await entryPointsUpdateHost.handler(args);
68+
69+
expect(consoleLogSpy).toHaveBeenCalledWith(expectedTaskId);
70+
verify(loggerSpy.error(anything())).never();
71+
expect(process.exitCode).toBe(0);
72+
73+
consoleLogSpy.mockRestore();
74+
});
75+
76+
it('should handle errors correctly', async () => {
77+
const args = {
78+
project: 'project-id',
79+
oldHostname: 'old.example.com',
80+
newHostname: 'new.example.com',
81+
token: 'api-token',
82+
_: [],
83+
$0: ''
84+
} as Arguments;
85+
86+
const error = new Error('Update host failed');
87+
88+
when(mockedEntryPoints.updateHost(anything())).thenReject(error);
89+
90+
await entryPointsUpdateHost.handler(args);
91+
92+
verify(
93+
loggerSpy.error(
94+
ErrorMessageFactory.genericCommandError({
95+
error,
96+
command: 'entrypoints:update-host'
97+
})
98+
)
99+
).once();
100+
101+
expect(process.exitCode).toBe(1);
102+
});
103+
});
104+
});

src/Commands/EntryPointsUpdateHost.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ErrorMessageFactory, logger } from '../Utils';
2+
import {
3+
EntryPoints,
4+
RestProjectsOptions,
5+
UpdateHostOptions
6+
} from '../EntryPoint';
7+
import { container } from 'tsyringe';
8+
import { Arguments, Argv, CommandModule } from 'yargs';
9+
10+
export class EntryPointsUpdateHost implements CommandModule {
11+
public readonly command = 'entrypoints:update-host [options]';
12+
public readonly describe = 'Bulk update target entry points host.';
13+
14+
public builder(argv: Argv): Argv {
15+
return argv
16+
.option('token', {
17+
alias: 't',
18+
describe: 'Bright API-key',
19+
string: true,
20+
requiresArg: true,
21+
demandOption: true
22+
})
23+
.option('project', {
24+
alias: 'p',
25+
describe: 'ID of the project',
26+
string: true,
27+
requiresArg: true,
28+
demandOption: true
29+
})
30+
.option('old-hostname', {
31+
alias: 'o',
32+
describe: 'Old hostname of entrypoints.',
33+
string: true,
34+
requiresArg: true,
35+
demandOption: true
36+
})
37+
.option('new-hostname', {
38+
alias: 'n',
39+
describe: 'New hostname of entrypoints.',
40+
string: true,
41+
requiresArg: true,
42+
demandOption: true
43+
})
44+
.option('entrypoint-ids', {
45+
alias: 'e',
46+
describe: 'IDs of entrypoints to update.',
47+
string: true,
48+
requiresArg: true,
49+
array: true
50+
})
51+
.middleware((args: Arguments) =>
52+
container.register<RestProjectsOptions>(RestProjectsOptions, {
53+
useValue: {
54+
insecure: args.insecure as boolean,
55+
baseURL: args.api as string,
56+
apiKey: args.token as string,
57+
proxyURL: (args.proxyBright ?? args.proxy) as string,
58+
timeout: args.timeout as number
59+
}
60+
})
61+
);
62+
}
63+
64+
public async handler(args: Arguments): Promise<void> {
65+
try {
66+
const entryPointsManager: EntryPoints = container.resolve(EntryPoints);
67+
68+
const projectId = args.project as string;
69+
70+
const { taskId } = await entryPointsManager.updateHost({
71+
projectId,
72+
entryPointIds: args.entrypointIds as undefined | string[],
73+
newHostname: args.newHostname as string,
74+
oldHostname: args.oldHostname as string
75+
} as UpdateHostOptions);
76+
77+
// eslint-disable-next-line no-console
78+
console.log(taskId);
79+
80+
process.exitCode = 0;
81+
} catch (error) {
82+
logger.error(
83+
ErrorMessageFactory.genericCommandError({
84+
error,
85+
command: 'entrypoints:update-host'
86+
})
87+
);
88+
process.exitCode = 1;
89+
}
90+
}
91+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import 'reflect-metadata';
2+
import { Logger, logger, ErrorMessageFactory } from '../Utils';
3+
import { PollingHostUpdateJobStatus } from './PollingHostUpdateJobStatus';
4+
import { HostUpdateJobStatusPollingFactory } from '../EntryPoint/HostUpdateJobStatusPollingFactory';
5+
import { Polling } from '../Utils/Polling';
6+
import {
7+
mock,
8+
reset,
9+
spy,
10+
instance,
11+
when,
12+
anything,
13+
verify,
14+
objectContaining
15+
} from 'ts-mockito';
16+
import { container } from 'tsyringe';
17+
import { Arguments } from 'yargs';
18+
19+
describe('PollingHostUpdateJobStatus', () => {
20+
let processSpy!: NodeJS.Process;
21+
let loggerSpy!: Logger;
22+
23+
beforeEach(() => {
24+
processSpy = spy(process);
25+
loggerSpy = spy(logger);
26+
});
27+
28+
afterEach(() => reset<NodeJS.Process | Logger>(processSpy, loggerSpy));
29+
30+
describe('handler', () => {
31+
let pollingHostUpdateJobStatus: PollingHostUpdateJobStatus;
32+
const mockedPollingFactory = mock<HostUpdateJobStatusPollingFactory>();
33+
34+
const mockedPollingStart = jest.fn();
35+
const mockedPollingStop = jest.fn();
36+
37+
const mockedPolling: Polling = {
38+
start: mockedPollingStart,
39+
stop: mockedPollingStop
40+
};
41+
42+
beforeEach(() => {
43+
container.registerInstance(
44+
HostUpdateJobStatusPollingFactory,
45+
instance(mockedPollingFactory)
46+
);
47+
pollingHostUpdateJobStatus = new PollingHostUpdateJobStatus();
48+
});
49+
50+
afterEach(() => {
51+
container.clearInstances();
52+
reset(mockedPollingFactory);
53+
jest.clearAllMocks();
54+
});
55+
56+
it('should correctly configure polling from args', async () => {
57+
const args = {
58+
project: 'project-id',
59+
jobId: 'job-123',
60+
token: 'api-token',
61+
interval: 1000,
62+
timeout: 30000,
63+
_: ['job-123'],
64+
$0: ''
65+
} as Arguments;
66+
67+
when(processSpy.exit(anything())).thenReturn(undefined);
68+
when(
69+
mockedPollingFactory.create(
70+
objectContaining({
71+
projectId: 'project-id',
72+
jobId: 'job-123',
73+
interval: 1000,
74+
timeout: 30000
75+
})
76+
)
77+
).thenReturn(mockedPolling as Polling);
78+
79+
await pollingHostUpdateJobStatus.handler(args);
80+
81+
// Verify polling.start was called
82+
expect(mockedPolling.start).toHaveBeenCalled();
83+
84+
// Verify process.exit was called with 0 (success)
85+
verify(processSpy.exit(0)).once();
86+
87+
// Verify logger.error was never called
88+
verify(loggerSpy.error(anything())).never();
89+
});
90+
91+
it('should use default interval if not specified', async () => {
92+
const args = {
93+
project: 'project-id',
94+
jobId: 'job-123',
95+
token: 'api-token',
96+
timeout: 30000,
97+
_: ['job-123'],
98+
$0: ''
99+
} as Arguments;
100+
101+
when(processSpy.exit(anything())).thenReturn(undefined);
102+
when(
103+
mockedPollingFactory.create(
104+
objectContaining({
105+
projectId: 'project-id',
106+
jobId: 'job-123',
107+
timeout: 30000
108+
})
109+
)
110+
).thenReturn(mockedPolling);
111+
112+
await pollingHostUpdateJobStatus.handler(args);
113+
114+
expect(mockedPolling.start).toHaveBeenCalled();
115+
verify(processSpy.exit(0)).once();
116+
});
117+
118+
it('should handle errors correctly', async () => {
119+
const args = {
120+
project: 'project-id',
121+
jobId: 'job-123',
122+
token: 'api-token',
123+
_: ['job-123'],
124+
$0: ''
125+
} as Arguments;
126+
127+
const error = new Error('Polling failed');
128+
129+
when(processSpy.exit(anything())).thenReturn(undefined);
130+
when(mockedPollingFactory.create(anything())).thenReturn(mockedPolling);
131+
132+
mockedPollingStart.mockRejectedValue(error);
133+
134+
await pollingHostUpdateJobStatus.handler(args);
135+
136+
// Verify logger.error was called with appropriate error message
137+
verify(
138+
loggerSpy.error(
139+
ErrorMessageFactory.genericCommandError({
140+
error,
141+
command: 'entrypoints:update-host-polling'
142+
})
143+
)
144+
).once();
145+
146+
// Verify process.exit was called with 1 (error)
147+
verify(processSpy.exit(1)).once();
148+
});
149+
});
150+
});

0 commit comments

Comments
 (0)