aws-iot-device-sdk-v2
Version:
NodeJS API for the AWS IoT service
391 lines (300 loc) • 12.2 kB
text/typescript
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
import {iot, mqtt as mqtt311, mqtt5, mqtt_request_response} from "aws-crt";
import {v4 as uuid} from "uuid";
import {once} from "events";
import {IotJobsClientv2} from "./iotjobsclientv2";
import {
AddThingToThingGroupCommand,
CreateJobCommand,
CreateThingCommand,
CreateThingGroupCommand,
DeleteJobCommand,
DeleteThingCommand,
DeleteThingGroupCommand,
IoTClient
} from "@aws-sdk/client-iot";
import * as model from "./model";
jest.setTimeout(30000);
function hasTestEnvironment() : boolean {
if (process.env.AWS_TEST_MQTT5_IOT_CORE_HOST === undefined) {
return false;
}
if (process.env.AWS_TEST_MQTT5_IOT_CERTIFICATE_PATH === undefined) {
return false;
}
if (process.env.AWS_TEST_MQTT5_IOT_KEY_PATH === undefined) {
return false;
}
if (process.env.AWS_TEST_MQTT5_IOT_CORE_REGION === undefined) {
return false;
}
return true;
}
const conditional_test = (condition : boolean) => condition ? it : it.skip;
function build_protocol_client_mqtt5() : mqtt5.Mqtt5Client {
let builder = iot.AwsIotMqtt5ClientConfigBuilder.newDirectMqttBuilderWithMtlsFromPath(
// @ts-ignore
process.env.AWS_TEST_MQTT5_IOT_CORE_HOST,
process.env.AWS_TEST_MQTT5_IOT_CERTIFICATE_PATH,
process.env.AWS_TEST_MQTT5_IOT_KEY_PATH
);
builder.withConnectProperties({
clientId : `test-${uuid()}`,
keepAliveIntervalSeconds: 1200,
});
return new mqtt5.Mqtt5Client(builder.build());
}
function build_protocol_client_mqtt311() : mqtt311.MqttClientConnection {
// @ts-ignore
let builder = iot.AwsIotMqttConnectionConfigBuilder.new_mtls_builder_from_path(process.env.AWS_TEST_MQTT5_IOT_CERTIFICATE_PATH, process.env.AWS_TEST_MQTT5_IOT_KEY_PATH);
// @ts-ignore
builder.with_endpoint(process.env.AWS_TEST_MQTT5_IOT_CORE_HOST);
builder.with_client_id(`test-${uuid()}`);
let client = new mqtt311.MqttClient();
return client.new_connection(builder.build());
}
enum ProtocolVersion {
Mqtt311,
Mqtt5
}
interface TestingOptions {
version: ProtocolVersion,
timeoutSeconds?: number,
}
class JobsTestingContext {
mqtt311Client?: mqtt311.MqttClientConnection;
mqtt5Client?: mqtt5.Mqtt5Client;
client: IotJobsClientv2;
private protocolStarted : boolean = false;
async startProtocolClient() {
if (!this.protocolStarted) {
this.protocolStarted = true;
if (this.mqtt5Client) {
let connected = once(this.mqtt5Client, mqtt5.Mqtt5Client.CONNECTION_SUCCESS);
this.mqtt5Client.start();
await connected;
}
if (this.mqtt311Client) {
await this.mqtt311Client.connect();
}
}
}
async stopProtocolClient() {
if (this.protocolStarted) {
this.protocolStarted = false;
if (this.mqtt5Client) {
let stopped = once(this.mqtt5Client, mqtt5.Mqtt5Client.STOPPED);
this.mqtt5Client.stop();
await stopped;
this.mqtt5Client.close();
}
if (this.mqtt311Client) {
await this.mqtt311Client.disconnect();
}
}
}
constructor(options: TestingOptions) {
if (options.version == ProtocolVersion.Mqtt5) {
this.mqtt5Client = build_protocol_client_mqtt5();
let rrOptions : mqtt_request_response.RequestResponseClientOptions = {
maxRequestResponseSubscriptions : 6,
maxStreamingSubscriptions : 2,
operationTimeoutInSeconds : options.timeoutSeconds ?? 60,
}
this.client = IotJobsClientv2.newFromMqtt5(this.mqtt5Client, rrOptions);
} else {
this.mqtt311Client = build_protocol_client_mqtt311();
let rrOptions : mqtt_request_response.RequestResponseClientOptions = {
maxRequestResponseSubscriptions : 6,
maxStreamingSubscriptions : 2,
operationTimeoutInSeconds : options.timeoutSeconds ?? 60,
}
this.client = IotJobsClientv2.newFromMqtt311(this.mqtt311Client, rrOptions);
}
}
async open() {
await this.startProtocolClient();
}
async close() {
this.client.close();
await this.stopProtocolClient();
}
}
async function doCreateDestroyTest(version: ProtocolVersion) {
let context = new JobsTestingContext({
version: version
});
await context.open();
await context.close();
}
conditional_test(hasTestEnvironment())('jobsv2 - create destroy mqtt5', async () => {
await doCreateDestroyTest(ProtocolVersion.Mqtt5);
});
conditional_test(hasTestEnvironment())('jobsv2 - create destroy mqtt311', async () => {
await doCreateDestroyTest(ProtocolVersion.Mqtt311);
});
interface TestResources {
thingGroupName?: string,
thingGroupArn?: string,
thingName?: string,
jobId1?: string,
}
//@ts-ignore
let jobResources : TestResources = {};
async function createJob(client : IoTClient, index: number) : Promise<string> {
let jobId = 'jobid-' + uuid();
let jobDocument = {
test: `do-something${index}`
};
const createJobCommand = new CreateJobCommand({
jobId: jobId,
targets: [ jobResources.thingGroupArn ?? "" ],
document: JSON.stringify(jobDocument),
targetSelection: "CONTINUOUS"
});
await client.send(createJobCommand);
return jobId;
}
async function deleteJob(client: IoTClient, jobId: string | undefined) : Promise<void> {
if (jobId) {
const command = new DeleteJobCommand({
jobId: jobId,
force: true
});
await client.send(command);
}
}
beforeEach(async () => {
jobResources = {}
const client = new IoTClient({
region: process.env.AWS_TEST_MQTT5_IOT_CORE_REGION,
});
let thingGroupName = 'tgn-' + uuid();
const createThingGroupCommand = new CreateThingGroupCommand({
thingGroupName: thingGroupName
});
const createThingGroupResponse = await client.send(createThingGroupCommand);
jobResources.thingGroupName = thingGroupName;
jobResources.thingGroupArn = createThingGroupResponse.thingGroupArn;
let thingName = 't-' + uuid();
const createThingCommand = new CreateThingCommand({
thingName: thingName
});
await client.send(createThingCommand);
jobResources.thingName = thingName;
await new Promise(r => setTimeout(r, 1000));
jobResources.jobId1 = await createJob(client, 1);
await new Promise(r => setTimeout(r, 1000));
});
afterEach(async () => {
const client = new IoTClient({
region: process.env.AWS_TEST_MQTT5_IOT_CORE_REGION,
});
await new Promise(r => setTimeout(r, 1000));
await deleteJob(client, jobResources.jobId1);
await new Promise(r => setTimeout(r, 1000));
if (jobResources.thingName) {
const command = new DeleteThingCommand({
thingName: jobResources.thingName
});
await client.send(command);
await new Promise(r => setTimeout(r, 1000));
}
if (jobResources.thingGroupName) {
const command = new DeleteThingGroupCommand({
thingGroupName: jobResources.thingGroupName
});
await client.send(command);
}
jobResources = {}
});
async function verifyNoJobExecutions(context: JobsTestingContext) {
let response = await context.client.getPendingJobExecutions({
thingName: jobResources.thingName ?? ""
});
// @ts-ignore
expect(response.inProgressJobs.length).toEqual(0);
// @ts-ignore
expect(response.queuedJobs.length).toEqual(0);
}
async function attachThingToThingGroup(client: IoTClient) {
const addThingToThingGroupCommand = new AddThingToThingGroupCommand({
thingName: jobResources.thingName,
thingGroupName: jobResources.thingGroupName
});
await client.send(addThingToThingGroupCommand);
}
async function doProcessingTest(version: ProtocolVersion) {
const client = new IoTClient({});
let context = new JobsTestingContext({
version: version
});
await context.open();
// set up streaming operations for our test's thing
let jobExecutionChangedStream = context.client.createJobExecutionsChangedStream({
thingName: jobResources.thingName ?? ""
});
jobExecutionChangedStream.open();
let nextJobExecutionChangedStream = context.client.createNextJobExecutionChangedStream({
thingName: jobResources.thingName ?? ""
});
nextJobExecutionChangedStream.open();
let initialExecutionChangedWaiter = once(jobExecutionChangedStream, 'incomingPublish');
let initialNextJobExecutionChangedWaiter = once(nextJobExecutionChangedStream, 'incomingPublish');
// thing is brand new, nothing should be pending
await verifyNoJobExecutions(context);
// as soon as we attach the thing to the thing group which has a continuous job associated with it, a
// job execution should become queued for our thing
await attachThingToThingGroup(client);
let initialJobExecutionChanged : model.JobExecutionsChangedEvent = (await initialExecutionChangedWaiter)[0].message;
// @ts-ignore
expect(initialJobExecutionChanged.jobs['QUEUED'].length).toEqual(1);
// @ts-ignore
expect(initialJobExecutionChanged.jobs['QUEUED'][0].jobId).toEqual(jobResources.jobId1);
let initialNextJobExecutionChanged : model.NextJobExecutionChangedEvent = (await initialNextJobExecutionChangedWaiter)[0].message;
expect(initialNextJobExecutionChanged.execution?.jobId).toEqual(jobResources.jobId1);
expect(initialNextJobExecutionChanged.execution?.status).toEqual(model.JobStatus.QUEUED);
let finalExecutionChangedWaiter = once(jobExecutionChangedStream, 'incomingPublish');
let finalNextJobExecutionChangedWaiter = once(nextJobExecutionChangedStream, 'incomingPublish');
// tell the service we'll run the next job
let startNextResponse = await context.client.startNextPendingJobExecution({
thingName: jobResources.thingName ?? ""
});
expect(startNextResponse.execution?.jobId).toEqual(jobResources.jobId1);
// pretend to do the job
await new Promise(r => setTimeout(r, 1000));
// job execution should be in progress
let describeResponse = await context.client.describeJobExecution({
thingName: jobResources.thingName ?? "",
jobId: jobResources.jobId1 ?? "",
});
expect(describeResponse.execution?.jobId).toEqual(jobResources.jobId1);
expect(describeResponse.execution?.status).toEqual(model.JobStatus.IN_PROGRESS);
// tell the service we completed the job successfully
await context.client.updateJobExecution({
thingName: jobResources.thingName ?? "",
jobId: jobResources.jobId1 ?? "",
status: model.JobStatus.SUCCEEDED
});
await new Promise(r => setTimeout(r, 3000));
let finalJobExecutionChanged : model.JobExecutionsChangedEvent = (await finalExecutionChangedWaiter)[0].message;
expect(finalJobExecutionChanged.jobs).toEqual({});
let finalNextJobExecutionChanged : model.NextJobExecutionChangedEvent = (await finalNextJobExecutionChangedWaiter)[0].message;
expect(finalNextJobExecutionChanged.timestamp).toBeDefined();
let getPendingResponse = await context.client.getPendingJobExecutions({
thingName: jobResources.thingName ?? ""
});
expect(getPendingResponse.queuedJobs?.length).toEqual(0);
expect(getPendingResponse.inProgressJobs?.length).toEqual(0);
context.client.close();
await context.close();
}
conditional_test(hasTestEnvironment())('jobsv2 processing mqtt5', async () => {
await doProcessingTest(ProtocolVersion.Mqtt5);
});
conditional_test(hasTestEnvironment())('jobsv2 processing mqtt311', async () => {
await doProcessingTest(ProtocolVersion.Mqtt311);
});