Skip to content

Commit

Permalink
feat: add discovery-polling command (#634)
Browse files Browse the repository at this point in the history
Co-authored-by: Or Rubin <or.rubin@hotmail.com>
  • Loading branch information
SevenWhite and orubin authored Dec 17, 2024
1 parent 936cd28 commit e58e2d1
Show file tree
Hide file tree
Showing 17 changed files with 525 additions and 9 deletions.
83 changes: 83 additions & 0 deletions src/Commands/PollingDiscoveryStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { RestDiscoveryOptions } from 'src/Discovery';
import { DiscoveryPollingFactory } from 'src/Discovery/DiscoveryPollingFactory';
import { ErrorMessageFactory, logger } from 'src/Utils';
import { container } from 'tsyringe';
import { Arguments, Argv, CommandModule } from 'yargs';

export class PollingDiscoveryStatus implements CommandModule {
public readonly command = 'discovery:polling [options] <discoveryId>';
public readonly describe =
'Allows to configure a polling of discovery status.';

public builder(argv: Argv): Argv {
return argv
.option('token', {
alias: 't',
describe: 'Bright API-key',
requiresArg: true,
demandOption: true
})
.option('project', {
alias: 'p',
describe: 'ID of the project',
string: true,
requiresArg: true,
demandOption: true
})
.option('interval', {
requiresArg: true,
describe:
'The sampling interval between status checks. ' +
'Eg: 60, "2min", "10h", "7d". A numeric value is interpreted as a milliseconds count.',
default: 5000
})
.option('timeout', {
requiresArg: true,
describe:
'Period of time between the end of a timeout period or completion of a discovery status request, and the next request for status. ' +
'Eg: 60, "2min", "10h", "7d". A numeric value is interpreted as a milliseconds count.'
})
.positional('discoveryId', {
describe: 'ID of an existing discovery.',
demandOption: true,
type: 'string'
})
.middleware((args: Arguments) =>
container.register<RestDiscoveryOptions>(RestDiscoveryOptions, {
useValue: {
insecure: args.insecure as boolean,
baseURL: args.api as string,
apiKey: args.token as string,
proxyURL: (args.proxyBright ?? args.proxy) as string,
timeout: args.timeout as number
}
})
);
}

public async handler(args: Arguments): Promise<void> {
try {
const pollingFactory = container.resolve<DiscoveryPollingFactory>(
DiscoveryPollingFactory
);
const polling = pollingFactory.create({
discoveryId: args.discoveryId as string,
projectId: args.project as string,
timeout: args.timeout as number,
interval: args.interval as number
});

await polling.start();

process.exit(0);
} catch (error) {
logger.error(
ErrorMessageFactory.genericCommandError({
error,
command: 'discovery:polling'
})
);
process.exit(1);
}
}
}
20 changes: 20 additions & 0 deletions src/Discovery/DefaultDiscoveryPollingFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
DiscoveryPollingConfig,
DiscoveryPollingFactory
} from './DiscoveryPollingFactory';
import { Polling } from '../Utils/Polling';
import { DiscoveryPolling } from './DiscoveryPolling';
import { Discoveries } from './Discoveries';
import { inject, injectable } from 'tsyringe';

@injectable()
export class DefaultDiscoveryPollingFactory implements DiscoveryPollingFactory {
constructor(
@inject(Discoveries)
private readonly discoveries: Discoveries
) {}

public create(options: DiscoveryPollingConfig): Polling {
return new DiscoveryPolling(options, this.discoveries);
}
}
8 changes: 8 additions & 0 deletions src/Discovery/Discoveries.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DiscoveryView } from './DiscoveryView';

export interface DiscoveryConfig {
name: string;
authObjectId?: string;
Expand Down Expand Up @@ -34,6 +36,12 @@ export interface Discoveries {
stop(projectId: string, discoveryId: string): Promise<void>;

delete(projectId: string, discoveryId: string): Promise<void>;

get(
projectId: string,
discoveryId: string,
options?: { signal?: AbortSignal }
): Promise<DiscoveryView>;
}

export const Discoveries: unique symbol = Symbol('Discoveries');
Expand Down
169 changes: 169 additions & 0 deletions src/Discovery/DiscoveryPolling.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import 'reflect-metadata';
import { Logger, logger } from '../Utils';
import { DiscoveryPollingConfig } from './DiscoveryPollingFactory';
import { DiscoveryView, DiscoveryStatus } from './DiscoveryView';
import { Discoveries } from './Discoveries';
import { DiscoveryPolling } from './DiscoveryPolling';
import { instance, mock, reset, spy, verify, when } from 'ts-mockito';
import { setTimeout } from 'node:timers/promises';

describe('DiscoveryPolling', () => {
const discoveryId = 'hAXZjjahZqpvgK3yNEdp6t';
const projectId = 'hADZjiahZqpvgK3yNEdp8b';

const firstResponse: DiscoveryView = {
id: discoveryId,
name: 'some name',
status: DiscoveryStatus.RUNNING
};

const discoveryManagerMock = mock<Discoveries>();

let loggerSpy!: Logger;

beforeEach(() => {
loggerSpy = spy(logger);
});

afterEach(() => {
reset<Discoveries | Logger>(discoveryManagerMock, loggerSpy);
});

describe('constructor', () => {
it('should warn if timeout is not specified', () => {
// arrange
const options: DiscoveryPollingConfig = {
projectId,
discoveryId
};

// act
new DiscoveryPolling(options, instance(discoveryManagerMock));

// assert
verify(
loggerSpy.warn(
`Warning: It looks like you've been running polling without "timeout" option.`
)
).once();
verify(
loggerSpy.warn(
`The recommended way to install polling with a minimal timeout: 10-20min.`
)
).once();
});

it('should warn if interval is less than 10s', () => {
// arrange
const options = {
discoveryId,
projectId,
interval: 5000
};

// act
new DiscoveryPolling(options, instance(discoveryManagerMock));

// assert
verify(
loggerSpy.warn(
`Warning: The minimal value for polling interval is 10 seconds.`
)
).once();
});
});

describe('start', () => {
const options: DiscoveryPollingConfig = {
discoveryId,
projectId,
interval: 1
};
const spiedOptions = spy(options);

let sut!: DiscoveryPolling;

beforeEach(() => {
sut = new DiscoveryPolling(options, instance(discoveryManagerMock));
});

afterEach(() => reset(spiedOptions));

it.each([
DiscoveryStatus.DONE,
DiscoveryStatus.DISRUPTED,
DiscoveryStatus.FAILED,
DiscoveryStatus.STOPPED
])(
'should start polling and stop on discovery status changed to "%s"',
async (status) => {
// arrange
when(discoveryManagerMock.get(projectId, discoveryId))
.thenResolve(firstResponse)
.thenResolve({ ...firstResponse, status });

// act
await sut.start();

// assert
verify(discoveryManagerMock.get(projectId, discoveryId)).twice();
verify(
loggerSpy.log(
`The discovery has been finished with status: ${status}.`
)
).once();
}
);

it('should start polling and stop on timeout', async () => {
// arrange
const timeout = 1500;
const interval = 1000;
when(spiedOptions.timeout).thenReturn(timeout);
when(spiedOptions.interval).thenReturn(interval);

when(discoveryManagerMock.get(projectId, discoveryId)).thenResolve(
firstResponse
);

// act
jest.useFakeTimers();
const promise = sut.start();
await setTimeout(10);
jest.runAllTimers();
await promise;
jest.useRealTimers();

// assert
verify(discoveryManagerMock.get(projectId, discoveryId)).once();
verify(loggerSpy.log('Polling has been stopped by timeout.')).once();
});
});

describe('stop', () => {
it('should stop polling', async () => {
// arrange
const sut = new DiscoveryPolling(
{
projectId,
discoveryId,
interval: 1000
},
instance(discoveryManagerMock)
);

when(discoveryManagerMock.get(projectId, discoveryId)).thenResolve(
firstResponse
);

// act
const start = sut.start();
await setTimeout(10);
await sut.stop();
await start;

// assert
verify(discoveryManagerMock.get(projectId, discoveryId)).once();
});
});
});
Loading

0 comments on commit e58e2d1

Please sign in to comment.