diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 6b8372f..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -registry=https://artifactory-vpc.smartling.net/artifactory/api/npm/nodejs diff --git a/api/base/index.ts b/api/base/index.ts index 247e0b1..521b601 100644 --- a/api/base/index.ts +++ b/api/base/index.ts @@ -162,7 +162,8 @@ export class SmartlingBaseApi { "dueDate", "actionTime", "publishDate", - "lastModified" + "lastModified", + "attemptDate" ]; if (dateProperties.includes(key) && value) { diff --git a/api/webhooks/dto/scrollable-response.ts b/api/webhooks/dto/scrollable-response.ts new file mode 100644 index 0000000..45a7eeb --- /dev/null +++ b/api/webhooks/dto/scrollable-response.ts @@ -0,0 +1,7 @@ +interface ScrollableResponse { + items: Array; + hasMore: boolean; + scrollId: string | null; +} + +export { ScrollableResponse }; diff --git a/api/webhooks/dto/subscription-attempted-event-dto.ts b/api/webhooks/dto/subscription-attempted-event-dto.ts new file mode 100644 index 0000000..a103998 --- /dev/null +++ b/api/webhooks/dto/subscription-attempted-event-dto.ts @@ -0,0 +1,10 @@ +import { SubscriptionEventAttemptStatus } from "./subscription-event-attempt-status"; + +interface SubscriptionAttemptedEventDto { + eventId: string; + eventType: string; + createdDate: Date; + lastAttemptStatus: SubscriptionEventAttemptStatus +} + +export { SubscriptionAttemptedEventDto }; diff --git a/api/webhooks/dto/subscription-dto.ts b/api/webhooks/dto/subscription-dto.ts new file mode 100644 index 0000000..d05a118 --- /dev/null +++ b/api/webhooks/dto/subscription-dto.ts @@ -0,0 +1,18 @@ +import { SubscriptionEvent } from "../params/subscription-event"; +import { SubscriptionRequestHeader } from "../params/subscription-request-header"; + +interface SubscriptionDto { + subscriptionUid: string; + subscriptionName: string; + subscriptionUrl: string; + accountUid: string; + enabled: boolean; + userUid: string; + description: string | null; + createdDate: string; + requestHeaders: SubscriptionRequestHeader[]; + events: SubscriptionEvent[]; + projectUids: string[]; +} + +export { SubscriptionDto }; diff --git a/api/webhooks/dto/subscription-event-attempt-dto.ts b/api/webhooks/dto/subscription-event-attempt-dto.ts new file mode 100644 index 0000000..68a2bd7 --- /dev/null +++ b/api/webhooks/dto/subscription-event-attempt-dto.ts @@ -0,0 +1,16 @@ +import { SubscriptionEventAttemptStatus } from "./subscription-event-attempt-status"; +import { SubscriptionEventTriggerType } from "./subscription-event-trigger-type"; + +interface SubscriptionEventAttemptDto { + attemptId: string; + eventId: string; + response: string | null; + responseDurationMs: number; + responseStatusCode: number; + attemptStatus: SubscriptionEventAttemptStatus; + attemptDate: Date; + url: string; + triggerType: SubscriptionEventTriggerType; +} + +export { SubscriptionEventAttemptDto }; diff --git a/api/webhooks/dto/subscription-event-attempt-status.ts b/api/webhooks/dto/subscription-event-attempt-status.ts new file mode 100644 index 0000000..62055f2 --- /dev/null +++ b/api/webhooks/dto/subscription-event-attempt-status.ts @@ -0,0 +1,8 @@ +enum SubscriptionEventAttemptStatus { + SUCCESS = "SUCCESS", + PENDING = "PENDING", + FAIL = "FAIL", + SENDING = "SENDING" +} + +export { SubscriptionEventAttemptStatus }; diff --git a/api/webhooks/dto/subscription-event-dto.ts b/api/webhooks/dto/subscription-event-dto.ts new file mode 100644 index 0000000..3f00d53 --- /dev/null +++ b/api/webhooks/dto/subscription-event-dto.ts @@ -0,0 +1,8 @@ +interface SubscriptionEventDto { + eventId: string; + eventType: string; + createdDate: Date; + payload: Record; +} + +export { SubscriptionEventDto }; diff --git a/api/webhooks/dto/subscription-event-trigger-type.ts b/api/webhooks/dto/subscription-event-trigger-type.ts new file mode 100644 index 0000000..8a558f2 --- /dev/null +++ b/api/webhooks/dto/subscription-event-trigger-type.ts @@ -0,0 +1,6 @@ +enum SubscriptionEventTriggerType { + SCHEDULED = "SCHEDULED", + MANUAL = "MANUAL" +} + +export { SubscriptionEventTriggerType }; diff --git a/api/webhooks/dto/subscription-secret-dto.ts b/api/webhooks/dto/subscription-secret-dto.ts new file mode 100644 index 0000000..d9ac3b5 --- /dev/null +++ b/api/webhooks/dto/subscription-secret-dto.ts @@ -0,0 +1,5 @@ +interface SubscriptionSecretDto { + payloadSecret: string; +} + +export { SubscriptionSecretDto }; diff --git a/api/webhooks/dto/subscription-statistics-dto.ts b/api/webhooks/dto/subscription-statistics-dto.ts new file mode 100644 index 0000000..a2fd6c6 --- /dev/null +++ b/api/webhooks/dto/subscription-statistics-dto.ts @@ -0,0 +1,8 @@ +interface SubscriptionStatisticsDto { + failEvents: number; + pendingEvents: number; + sendingEvents: number; + successEvents: number; +} + +export { SubscriptionStatisticsDto }; diff --git a/api/webhooks/dto/webhook-event-type.dto.ts b/api/webhooks/dto/webhook-event-type.dto.ts new file mode 100644 index 0000000..f5eca5a --- /dev/null +++ b/api/webhooks/dto/webhook-event-type.dto.ts @@ -0,0 +1,7 @@ +class WebhookEventTypeDto { + eventType: string; + description: string; + schema: Record; +} + +export { WebhookEventTypeDto }; diff --git a/api/webhooks/index.ts b/api/webhooks/index.ts new file mode 100644 index 0000000..aa92e01 --- /dev/null +++ b/api/webhooks/index.ts @@ -0,0 +1,17 @@ +export { SmartlingWebhooksApi } from "./smartling-webhooks-api"; +export { ScrollableResponse } from "./dto/scrollable-response"; +export { SubscriptionAttemptedEventDto } from "./dto/subscription-attempted-event-dto"; +export { SubscriptionEventDto } from "./dto/subscription-event-dto"; +export { SubscriptionEventAttemptDto } from "./dto/subscription-event-attempt-dto"; +export { SubscriptionEventAttemptStatus } from "./dto/subscription-event-attempt-status"; +export { SubscriptionEventTriggerType } from "./dto/subscription-event-trigger-type"; +export { SubscriptionStatisticsDto } from "./dto/subscription-statistics-dto"; +export { SubscriptionDto } from "./dto/subscription-dto"; +export { SubscriptionSecretDto } from "./dto/subscription-secret-dto"; +export { WebhookEventTypeDto } from "./dto/webhook-event-type.dto"; +export { SubscriptionEvent } from "./params/subscription-event"; +export { CreateSubscriptionParameters } from "./params/create-subscription-parameters"; +export { UpdateSubscriptionParameters } from "./params/update-subscription-parameters"; +export { SubscriptionRequestHeader } from "./params/subscription-request-header"; +export { UpdateSubscriptionSecretParameters } from "./params/update-subscription-secret-parameters"; +export { GetSubscriptionEventsParameters } from "./params/get-subscription-events-parameters"; diff --git a/api/webhooks/params/create-subscription-parameters.ts b/api/webhooks/params/create-subscription-parameters.ts new file mode 100644 index 0000000..df5a2f5 --- /dev/null +++ b/api/webhooks/params/create-subscription-parameters.ts @@ -0,0 +1,9 @@ +import { CreateUpdateSubscriptionParameters } from "./create-update-subscription-parameters"; + +export class CreateSubscriptionParameters extends CreateUpdateSubscriptionParameters { + setPayloadSecret(payloadSecret: string): this { + this.set("payloadSecret", payloadSecret); + + return this; + } +} diff --git a/api/webhooks/params/create-update-subscription-parameters.ts b/api/webhooks/params/create-update-subscription-parameters.ts new file mode 100644 index 0000000..d44e550 --- /dev/null +++ b/api/webhooks/params/create-update-subscription-parameters.ts @@ -0,0 +1,70 @@ +import { BaseParameters } from "../../parameters"; +import { SmartlingException } from "../../exception"; +import { SubscriptionEvent } from "./subscription-event"; +import { SubscriptionRequestHeader } from "./subscription-request-header"; + +const MAX_EVENTS_SIZE = 1000; +const MAX_SUBSCRIPTION_HEADERS_SIZE = 20; +const MAX_PROJECT_UIDS_SIZE = 10; + +export abstract class CreateUpdateSubscriptionParameters extends BaseParameters { + constructor( + subscriptionName: string, + subscriptionUrl: string, + events: SubscriptionEvent[] + ) { + super(); + + if (!subscriptionName) { + throw new SmartlingException("Subscription name is required."); + } + + if (!subscriptionUrl) { + throw new SmartlingException("Subscription url is required."); + } + + if (!events.length) { + throw new SmartlingException("At least one event is required."); + } + + if (events.length > MAX_EVENTS_SIZE) { + throw new SmartlingException(`The request contains too many events: ${events.length}. Maximum allowed events number is ${MAX_EVENTS_SIZE}.`); + } + + this.set("subscriptionName", subscriptionName); + this.set("subscriptionUrl", subscriptionUrl); + this.set("events", events); + } + + setDescription(description: string): this { + this.set("description", description); + + return this; + } + + setRequestHeaders(requestHeaders: SubscriptionRequestHeader[]): this { + if (!requestHeaders.length) { + throw new SmartlingException("At least one subscription header is required."); + } + + if (requestHeaders.length > MAX_SUBSCRIPTION_HEADERS_SIZE) { + throw new SmartlingException(`The request contains too many subscription headers: ${requestHeaders.length}. Maximum allowed subscription headers number is ${MAX_SUBSCRIPTION_HEADERS_SIZE}.`); + } + + this.set("requestHeaders", requestHeaders); + return this; + } + + setProjectUids(projectUids: string[]): this { + if (!projectUids.length) { + throw new SmartlingException("At least one project uid is required."); + } + + if (projectUids.length > MAX_PROJECT_UIDS_SIZE) { + throw new SmartlingException(`The request contains too many project uids: ${projectUids.length}. Maximum allowed project uids number is ${MAX_PROJECT_UIDS_SIZE}.`); + } + this.set("projectUids", projectUids); + + return this; + } +} diff --git a/api/webhooks/params/get-subscription-events-parameters.ts b/api/webhooks/params/get-subscription-events-parameters.ts new file mode 100644 index 0000000..ae21a2e --- /dev/null +++ b/api/webhooks/params/get-subscription-events-parameters.ts @@ -0,0 +1,38 @@ +import { BaseParameters } from "../../parameters"; +import { SubscriptionEventAttemptStatus } from "../dto/subscription-event-attempt-status"; + +export class GetSubscriptionEventsParameters extends BaseParameters { + setLimit(limit: number): this { + this.set("limit", limit); + return this; + } + + setScrollId(scrollId: string): this { + this.set("scrollId", scrollId); + return this; + } + + setAttemptStatus(attemptStatus: SubscriptionEventAttemptStatus): this { + this.set("attemptStatus", attemptStatus); + return this; + } + + setCreatedDateBefore(createdDateBefore: Date): this { + this.set("createdDateBefore", GetSubscriptionEventsParameters.prepareDateParameter(createdDateBefore)); + return this; + } + + setCreatedDateAfter(createdDateAfter: Date): this { + this.set("createdDateAfter", GetSubscriptionEventsParameters.prepareDateParameter(createdDateAfter)); + return this; + } + + setEventTypes(eventTypes: string[]): this { + this.set("eventTypes", eventTypes); + return this; + } + + private static prepareDateParameter(date: Date): string { + return `${date.toISOString().split(".")[0]}Z`; + } +} diff --git a/api/webhooks/params/subscription-event.ts b/api/webhooks/params/subscription-event.ts new file mode 100644 index 0000000..478f8dd --- /dev/null +++ b/api/webhooks/params/subscription-event.ts @@ -0,0 +1,6 @@ +interface SubscriptionEvent { + type: string; + schemaVersion: string; +} + +export { SubscriptionEvent }; diff --git a/api/webhooks/params/subscription-request-header.ts b/api/webhooks/params/subscription-request-header.ts new file mode 100644 index 0000000..13357ed --- /dev/null +++ b/api/webhooks/params/subscription-request-header.ts @@ -0,0 +1,6 @@ +interface SubscriptionRequestHeader { + headerName: string; + headerValue: string; +} + +export { SubscriptionRequestHeader }; diff --git a/api/webhooks/params/update-subscription-parameters.ts b/api/webhooks/params/update-subscription-parameters.ts new file mode 100644 index 0000000..e535a40 --- /dev/null +++ b/api/webhooks/params/update-subscription-parameters.ts @@ -0,0 +1,19 @@ +import { CreateUpdateSubscriptionParameters } from "./create-update-subscription-parameters"; +import { SubscriptionEvent } from "./subscription-event"; + +export class UpdateSubscriptionParameters extends CreateUpdateSubscriptionParameters { + constructor( + subscriptionName: string, + subscriptionUrl: string, + events: SubscriptionEvent[], + enabled: boolean + ) { + super( + subscriptionName, + subscriptionUrl, + events + ); + + this.set("enabled", enabled); + } +} diff --git a/api/webhooks/params/update-subscription-secret-parameters.ts b/api/webhooks/params/update-subscription-secret-parameters.ts new file mode 100644 index 0000000..8d9fa5a --- /dev/null +++ b/api/webhooks/params/update-subscription-secret-parameters.ts @@ -0,0 +1,14 @@ +import { BaseParameters } from "../../parameters"; +import { SmartlingException } from "../../exception"; + +export class UpdateSubscriptionSecretParameters extends BaseParameters { + constructor(payloadSecret: string) { + super(); + + if (!payloadSecret) { + throw new SmartlingException("Payload secret is required."); + } + + this.set("payloadSecret", payloadSecret); + } +} diff --git a/api/webhooks/smartling-webhooks-api.ts b/api/webhooks/smartling-webhooks-api.ts new file mode 100644 index 0000000..cb6b537 --- /dev/null +++ b/api/webhooks/smartling-webhooks-api.ts @@ -0,0 +1,190 @@ +import { SmartlingBaseApi } from "../base"; +import { SmartlingAuthApi } from "../auth"; +import { Logger } from "../logger"; +import { SmartlingListResponse } from "../http/smartling-list-response"; +import { SubscriptionDto } from "./dto/subscription-dto"; +import { SubscriptionSecretDto } from "./dto/subscription-secret-dto"; +import { SubscriptionStatisticsDto } from "./dto/subscription-statistics-dto"; +import { WebhookEventTypeDto } from "./dto/webhook-event-type.dto"; +import { CreateSubscriptionParameters } from "./params/create-subscription-parameters"; +import { UpdateSubscriptionParameters } from "./params/update-subscription-parameters"; +import { UpdateSubscriptionSecretParameters } from "./params/update-subscription-secret-parameters"; +import { ScrollableResponse } from "./dto/scrollable-response"; +import { SubscriptionAttemptedEventDto } from "./dto/subscription-attempted-event-dto"; +import { SubscriptionEventDto } from "./dto/subscription-event-dto"; +import { SubscriptionEventAttemptDto } from "./dto/subscription-event-attempt-dto"; +import { GetSubscriptionEventsParameters } from "./params/get-subscription-events-parameters"; + +export class SmartlingWebhooksApi extends SmartlingBaseApi { + constructor(smartlingApiBaseUrl: string, authApi: SmartlingAuthApi, logger: Logger) { + super(logger); + this.authApi = authApi; + this.entrypoint = `${smartlingApiBaseUrl}/webhooks-api/v2`; + } + + getSubscriptions( + accountUid: string + ): Promise> { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/subscriptions` + ); + } + + getSubscription( + accountUid: string, + subscriptionUid: string + ): Promise { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}` + ); + } + + createSubscription( + accountUid: string, + params: CreateSubscriptionParameters + ): Promise { + return this.makeRequest( + "post", + `${this.entrypoint}/accounts/${accountUid}/subscriptions`, + JSON.stringify(params.export()) + ); + } + + updateSubscription( + accountUid: string, + subscriptionUid: string, + params: UpdateSubscriptionParameters + ): Promise { + return this.makeRequest( + "put", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}`, + JSON.stringify(params.export()) + ); + } + + deleteSubscription( + accountUid: string, + subscriptionUid: string + ): Promise { + return this.makeRequest( + "delete", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}` + ); + } + + enableSubscription( + accountUid: string, + subscriptionUid: string + ): Promise { + return this.makeRequest( + "post", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/enable` + ); + } + + disableSubscription( + accountUid: string, + subscriptionUid: string + ): Promise { + return this.makeRequest( + "post", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/disable` + ); + } + + testSubscription( + accountUid: string, + subscriptionUid: string + ): Promise { + return this.makeRequest( + "post", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/test` + ); + } + + getSubscriptionSecret( + accountUid: string, + subscriptionUid: string + ): Promise { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/secret` + ); + } + + updateSubscriptionSecret( + accountUid: string, + subscriptionUid: string, + params: UpdateSubscriptionSecretParameters + ): Promise { + return this.makeRequest( + "put", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/secret`, + JSON.stringify(params.export()) + ); + } + + getSubscriptionStatistics( + accountUid: string, + subscriptionUid: string + ): Promise { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/statistics` + ); + } + + getSubscriptionEvents( + accountUid: string, + subscriptionUid: string, + params?: GetSubscriptionEventsParameters + ): Promise> { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/events`, + params ? params.export() : undefined + ); + } + + getSubscriptionEvent( + accountUid: string, + subscriptionUid: string, + eventId: string + ): Promise { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/events/${eventId}` + ); + } + + getSubscriptionEventAttempts( + accountUid: string, + subscriptionUid: string, + eventId: string + ): Promise> { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/events/${eventId}/attempts` + ); + } + + sendSubscriptionEvent( + accountUid: string, + subscriptionUid: string, + eventId: string + ): Promise { + return this.makeRequest( + "post", + `${this.entrypoint}/accounts/${accountUid}/subscriptions/${subscriptionUid}/events/${eventId}/send` + ); + } + + getAvailableEventTypes(accountUid: string): Promise { + return this.makeRequest( + "get", + `${this.entrypoint}/accounts/${accountUid}/available-event-types` + ); + } +} diff --git a/index.ts b/index.ts index 38ef8bd..17f953a 100644 --- a/index.ts +++ b/index.ts @@ -124,3 +124,4 @@ export * from "./api/mt/index"; export * from "./api/mt/params/smartling-mt-parameters"; export * from "./api/mt/params/source-text-item"; export * from "./api/mt/dto/translation-text-item-dto"; +export * from "./api/webhooks"; diff --git a/package-lock.json b/package-lock.json index 69b6028..f711e4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,23 @@ { "name": "smartling-api-sdk-nodejs", - "version": "2.21.1", + "version": "2.22.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "smartling-api-sdk-nodejs", - "version": "2.21.1", + "version": "2.22.0", "license": "ISC", "dependencies": { "@types/mocha": "^9.0.0", - "@types/node": "^16.4.7", + "@types/node": "^18.19.115", "cross-fetch": "^3.1.4", "default-user-agent": "^1.0.0", "form-data": "^4.0.4", "merge-deep": "^3.0.3", "querystring": "^0.2.1", "semver": "^5.7.2", - "string-to-file-stream": "^2.0.0", - "typescript": "^4.3.5" + "string-to-file-stream": "^2.0.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^4.29.0", @@ -39,7 +38,8 @@ "mocha-junit-reporter": "^2.0.2", "nyc": "^17.1.0", "sinon": "^11.1.2", - "ts-mocha": "^10.0.0" + "ts-mocha": "^10.0.0", + "typescript": "^4.9.5" }, "engines": { "node": ">=18" @@ -678,10 +678,12 @@ "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==" }, "node_modules/@types/node": { - "version": "16.18.126", - "resolved": "https://artifactory.smartling.net/artifactory/api/npm/nodejs/@types/node/-/node-16.18.126.tgz", - "integrity": "sha1-J4dfqikmwPR1s5qLseVGwBdvjUs=", - "license": "MIT" + "version": "18.19.120", + "resolved": "https://artifactory.smartling.net/artifactory/api/npm/nodejs/@types/node/-/node-18.19.120.tgz", + "integrity": "sha1-B7O9c4dZVtUoH6J+bXemZBX31FU=", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "4.33.0", @@ -5718,8 +5720,9 @@ }, "node_modules/typescript": { "version": "4.9.5", - "resolved": "https://artifactory.smartling.net/artifactory/api/npm/nodejs/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha1-CVl5+bzA0J2jJNWNA86Pg3TL5lo=", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5744,6 +5747,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://artifactory.smartling.net/artifactory/api/npm/nodejs/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/package.json b/package.json index a3ed5b9..22dd93a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smartling-api-sdk-nodejs", - "version": "2.21.1", + "version": "2.22.0", "description": "Package for Smartling API", "main": "built/index.js", "engines": { @@ -28,15 +28,14 @@ }, "homepage": "https://github.com/Smartling/api-sdk-nodejs#readme", "dependencies": { - "@types/node": "^16.4.7", "@types/mocha": "^9.0.0", + "@types/node": "^18.19.115", "cross-fetch": "^3.1.4", "default-user-agent": "^1.0.0", "form-data": "^4.0.4", "merge-deep": "^3.0.3", "querystring": "^0.2.1", "string-to-file-stream": "^2.0.0", - "typescript": "^4.3.5", "semver": "^5.7.2" }, "devDependencies": { @@ -57,6 +56,7 @@ "nyc": "^17.1.0", "sinon": "^11.1.2", "ts-mocha": "^10.0.0", + "typescript": "^4.9.5", "braces": "^3.0.3", "json5": "^1.0.2" }, diff --git a/test/webhooks.spec.ts b/test/webhooks.spec.ts new file mode 100644 index 0000000..2095586 --- /dev/null +++ b/test/webhooks.spec.ts @@ -0,0 +1,610 @@ +import sinon from "sinon"; +import assert from "assert"; +import { + SmartlingWebhooksApi, + CreateSubscriptionParameters, + UpdateSubscriptionParameters, + UpdateSubscriptionSecretParameters, + GetSubscriptionEventsParameters, + SubscriptionEventAttemptStatus, + SubscriptionEvent +} from "../api/webhooks"; +import { SmartlingAuthApi } from "../api/auth"; +import { SmartlingException } from "../api/exception"; +import { loggerMock, authMock, responseMock } from "./mock"; + +describe("SmartlingWebhooksApi class tests.", () => { + const accountUid = "testAccountUid"; + let webhooksApi: SmartlingWebhooksApi; + let webhooksApiFetchStub; + let webhooksApiUaStub; + let responseMockTextStub; + + beforeEach(() => { + webhooksApi = new SmartlingWebhooksApi("https://test.com", authMock as unknown as SmartlingAuthApi, loggerMock); + + webhooksApiFetchStub = sinon.stub(webhooksApi, "fetch"); + webhooksApiUaStub = sinon.stub(webhooksApi, "ua"); + responseMockTextStub = sinon.stub(responseMock, "text"); + + webhooksApiUaStub.returns("test_user_agent"); + webhooksApiFetchStub.returns(responseMock); + responseMockTextStub.returns("{\"response\": {}}"); + }); + + afterEach(() => { + webhooksApiFetchStub.restore(); + responseMockTextStub.restore(); + webhooksApiUaStub.restore(); + }); + + it("gets all subscriptions", async () => { + await webhooksApi.getSubscriptions(accountUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("gets a subscription", async () => { + const subscriptionUid = "subscriptionUid"; + await webhooksApi.getSubscription(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("gets available webhook event types", async () => { + await webhooksApi.getAvailableEventTypes(accountUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/available-event-types`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + describe("createSubscription", () => { + it("creates a subscription", async () => { + const event: SubscriptionEvent = { + type: "project.created", + schemaVersion: "1.0" + }; + + const params = new CreateSubscriptionParameters( + "TestSub", + "https://callback.url/hook", + [event] + ) + .setDescription("Webhook for new projects") + .setRequestHeaders([{ headerName: "X-Auth", headerValue: "secret" }]) + .setProjectUids(["project-1", "project-2"]) + .setPayloadSecret("mySecret"); + + await webhooksApi.createSubscription(accountUid, params); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "post", + body: JSON.stringify({ + subscriptionName: "TestSub", + subscriptionUrl: "https://callback.url/hook", + events: [{ + type: "project.created", + schemaVersion: "1.0" + }], + description: "Webhook for new projects", + requestHeaders: [{ headerName: "X-Auth", headerValue: "secret" }], + projectUids: ["project-1", "project-2"], + payloadSecret: "mySecret" + }) + } + ); + }); + + it("throws if subscription name is missing", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new CreateSubscriptionParameters("", "https://callback.url", [{ + type: "project.created", + schemaVersion: "1.0" + }]); + }, SmartlingException); + }); + + it("throws if subscription url is missing", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new CreateSubscriptionParameters("name", "", [{ + type: "project.created", + schemaVersion: "1.0" + }]); + }, SmartlingException); + }); + + it("throws if events are empty", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new CreateSubscriptionParameters("name", "https://callback.url", []); + }, SmartlingException); + }); + + it("throws if too many events", () => { + const events = new Array(1001).fill({ + type: "project.created", + schemaVersion: "1.0" + }); + + assert.throws(() => { + // eslint-disable-next-line no-new + new CreateSubscriptionParameters("name", "https://callback.url", events); + }, SmartlingException); + }); + + it("throws if too many headers", () => { + const headers = Array.from({ length: 21 }, (_, i) => ({ headerName: `h${i}`, headerValue: `v${i}` })); + const params = new CreateSubscriptionParameters("test", "https://callback", [{ + type: "project.created", + schemaVersion: "1.0" + }]); + + assert.throws(() => { + params.setRequestHeaders(headers); + }, SmartlingException); + }); + + it("throws if too many project uids", () => { + const uids = Array.from({ length: 11 }, (_, i) => `project-${i}`); + const params = new CreateSubscriptionParameters("test", "https://callback", [{ + type: "project.created", + schemaVersion: "1.0" + }]); + + assert.throws(() => { + params.setProjectUids(uids); + }, SmartlingException); + }); + }); + + describe("updateSubscription", () => { + const subscriptionUid = "subscription-uid"; + + it("updates a subscription", async () => { + const event: SubscriptionEvent = { + type: "project.created", + schemaVersion: "1.0" + }; + + const params = new UpdateSubscriptionParameters( + "UpdatedName", + "https://callback.url/updated", + [event], + true + ) + .setDescription("Updated description") + .setRequestHeaders([{ headerName: "X-Token", headerValue: "abc" }]) + .setProjectUids(["project-1", "project-2"]); + + await webhooksApi.updateSubscription(accountUid, subscriptionUid, params); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "put", + body: JSON.stringify({ + subscriptionName: "UpdatedName", + subscriptionUrl: "https://callback.url/updated", + events: [{ + type: "project.created", + schemaVersion: "1.0" + }], + enabled: true, + description: "Updated description", + requestHeaders: [{ headerName: "X-Token", headerValue: "abc" }], + projectUids: ["project-1", "project-2"] + }) + } + ); + }); + + it("throws if subscription name is missing", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new UpdateSubscriptionParameters("", "https://callback", [{ + type: "project.created", + schemaVersion: "1.0" + }], true); + }, SmartlingException); + }); + + it("throws if subscription url is missing", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new UpdateSubscriptionParameters("name", "", [{ + type: "project.created", + schemaVersion: "1.0" + }], true); + }, SmartlingException); + }); + + it("throws if events are empty", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new UpdateSubscriptionParameters("name", "https://callback", [], true); + }, SmartlingException); + }); + + it("throws if too many events", () => { + const events = new Array(1001).fill({ + type: "project.created", + schemaVersion: "1.0" + }); + + assert.throws(() => { + // eslint-disable-next-line no-new + new UpdateSubscriptionParameters("name", "https://callback", events, true); + }, SmartlingException); + }); + + it("throws if too many headers", () => { + const headers = Array.from({ length: 21 }, (_, i) => ({ headerName: `h${i}`, headerValue: `v${i}` })); + const params = new UpdateSubscriptionParameters("name", "https://callback", [{ + type: "project.created", + schemaVersion: "1.0" + }], true); + + assert.throws(() => { + params.setRequestHeaders(headers); + }, SmartlingException); + }); + + it("throws if too many project uids", () => { + const uids = Array.from({ length: 11 }, (_, i) => `project-${i}`); + const params = new UpdateSubscriptionParameters("name", "https://callback", [{ + type: "project.created", + schemaVersion: "1.0" + }], true); + + assert.throws(() => { + params.setProjectUids(uids); + }, SmartlingException); + }); + }); + + it("deletes a subscription", async () => { + const subscriptionUid = "sub-123"; + + await webhooksApi.deleteSubscription(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "delete" + } + ); + }); + + it("enables a subscription", async () => { + const subscriptionUid = "sub-456"; + + await webhooksApi.enableSubscription(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/enable`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "post" + } + ); + }); + + it("disables a subscription", async () => { + const subscriptionUid = "sub-789"; + + await webhooksApi.disableSubscription(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/disable`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "post" + } + ); + }); + + it("tests a subscription", async () => { + const subscriptionUid = "sub-999"; + + await webhooksApi.testSubscription(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/test`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "post" + } + ); + }); + + it("gets subscription secret", async () => { + const subscriptionUid = "sub-secret"; + + await webhooksApi.getSubscriptionSecret(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/secret`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("updates subscription secret", async () => { + const subscriptionUid = "sub-secret-update"; + + const params = new UpdateSubscriptionSecretParameters("newSecret"); + + await webhooksApi.updateSubscriptionSecret(accountUid, subscriptionUid, params); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/secret`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "put", + body: JSON.stringify({ + payloadSecret: "newSecret" + }) + } + ); + }); + + it("gets subscription statistics", async () => { + const subscriptionUid = "sub-stats"; + + await webhooksApi.getSubscriptionStatistics(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/statistics`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("gets available event types", async () => { + await webhooksApi.getAvailableEventTypes(accountUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/available-event-types`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + describe("getSubscriptionEvents", () => { + it("gets a subscription events when no parameters", async () => { + const subscriptionUid = "subscriptionUid"; + await webhooksApi.getSubscriptionEvents(accountUid, subscriptionUid); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/events`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("gets a subscription events when empty parameters", async () => { + const subscriptionUid = "subscriptionUid"; + await webhooksApi.getSubscriptionEvents( + accountUid, subscriptionUid, new GetSubscriptionEventsParameters()); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/events?`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("gets a subscription events when parameter specified", async () => { + const subscriptionUid = "subscriptionUid"; + const date = new Date(); + const dateString = encodeURIComponent(`${date.toISOString().split(".")[0]}Z`); + const params = new GetSubscriptionEventsParameters() + .setLimit(10) + .setScrollId("scrollId123") + .setAttemptStatus(SubscriptionEventAttemptStatus.SUCCESS) + .setCreatedDateBefore(date) + .setCreatedDateAfter(date) + .setEventTypes(["event1", "event2"]); + await webhooksApi.getSubscriptionEvents(accountUid, subscriptionUid, params); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/events?limit=10&scrollId=scrollId123&attemptStatus=SUCCESS&createdDateBefore=${dateString}&createdDateAfter=${dateString}&eventTypes=event1&eventTypes=event2`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + }); + + it("gets a subscription event", async () => { + const subscriptionUid = "subscriptionUid"; + const eventId = "eventId"; + await webhooksApi.getSubscriptionEvent(accountUid, subscriptionUid, eventId); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/events/${eventId}`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("gets a subscription event attempts", async () => { + const subscriptionUid = "subscriptionUid"; + const eventId = "eventId"; + await webhooksApi.getSubscriptionEventAttempts(accountUid, subscriptionUid, eventId); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/events/${eventId}/attempts`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "get" + } + ); + }); + + it("sends a subscription", async () => { + const subscriptionUid = "sub-999"; + const eventId = "event-123"; + + await webhooksApi.sendSubscriptionEvent(accountUid, subscriptionUid, eventId); + + sinon.assert.calledOnce(webhooksApiFetchStub); + sinon.assert.calledWithExactly( + webhooksApiFetchStub, + `https://test.com/webhooks-api/v2/accounts/${accountUid}/subscriptions/${subscriptionUid}/events/${eventId}/send`, + { + headers: { + Authorization: "test_token_type test_access_token", + "Content-Type": "application/json", + "User-Agent": "test_user_agent" + }, + method: "post" + } + ); + }); +});