diff --git a/packages/cli/e2e/__tests__/deploy.spec.ts b/packages/cli/e2e/__tests__/deploy.spec.ts index 3e10777a..1686e951 100644 --- a/packages/cli/e2e/__tests__/deploy.spec.ts +++ b/packages/cli/e2e/__tests__/deploy.spec.ts @@ -240,6 +240,7 @@ Update and Unchanged: ApiCheck: api-check-high-freq HeartbeatCheck: heartbeat-check-1 BrowserCheck: homepage-browser-check + TcpCheck: tcp-check CheckGroup: my-group-1 Dashboard: dashboard-1 MaintenanceWindow: maintenance-window-1 @@ -252,6 +253,7 @@ Update and Unchanged: HeartbeatCheck: heartbeat-check-1 BrowserCheck: homepage-browser-check BrowserCheck: snapshot-test.test.ts + TcpCheck: tcp-check CheckGroup: my-group-1 Dashboard: dashboard-1 MaintenanceWindow: maintenance-window-1 diff --git a/packages/cli/e2e/__tests__/fixtures/deploy-project/tcp.check.ts b/packages/cli/e2e/__tests__/fixtures/deploy-project/tcp.check.ts new file mode 100644 index 00000000..8f45ea56 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/deploy-project/tcp.check.ts @@ -0,0 +1,13 @@ +/* eslint-disable no-new */ +import { TcpCheck } from 'checkly/constructs' + +new TcpCheck('tcp-check', { + name: 'TCP Check', + activated: false, + request: { + hostname: 'api.checklyhq.com', + port: 443, + }, + degradedResponseTime: 5000, + maxResponseTime: 20000, +}) diff --git a/packages/cli/src/constructs/__tests__/tcp-check.spec.ts b/packages/cli/src/constructs/__tests__/tcp-check.spec.ts new file mode 100644 index 00000000..3e6b3978 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/tcp-check.spec.ts @@ -0,0 +1,103 @@ +import { TcpCheck, CheckGroup, TcpRequest } from '../index' +import { Project, Session } from '../project' + +const runtimes = { + '2022.10': { name: '2022.10', default: false, stage: 'CURRENT', description: 'Main updates are Playwright 1.28.0, Node.js 16.x and Typescript support. We are also dropping support for Puppeteer', dependencies: { '@playwright/test': '1.28.0', '@opentelemetry/api': '1.0.4', '@opentelemetry/sdk-trace-base': '1.0.1', '@faker-js/faker': '5.5.3', aws4: '1.11.0', axios: '0.27.2', btoa: '1.2.1', chai: '4.3.7', 'chai-string': '1.5.0', 'crypto-js': '4.1.1', expect: '29.3.1', 'form-data': '4.0.0', jsonwebtoken: '8.5.1', lodash: '4.17.21', mocha: '10.1.0', moment: '2.29.2', node: '16.x', otpauth: '9.0.2', playwright: '1.28.0', typescript: '4.8.4', uuid: '9.0.0' } }, +} + +const request: TcpRequest = { + hostname: 'acme.com', + port: 443, +} + +describe('TcpCheck', () => { + it('should not synthesize runtime if not specified even if default runtime is set', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + const apiCheck = new TcpCheck('test-check', { + name: 'Test Check', + request, + }) + const payload = apiCheck.synthesize() + expect(payload.runtimeId).toBeUndefined() + delete Session.defaultRuntimeId + }) + + it('should synthesize runtime if specified', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + const apiCheck = new TcpCheck('test-check', { + name: 'Test Check', + runtimeId: '2022.02', + request, + }) + const payload = apiCheck.synthesize() + expect(payload.runtimeId).toEqual('2022.02') + delete Session.defaultRuntimeId + }) + + it('should apply default check settings', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.checkDefaults = { tags: ['default tags'] } + const apiCheck = new TcpCheck('test-check', { + name: 'Test Check', + request, + }) + delete Session.checkDefaults + expect(apiCheck).toMatchObject({ tags: ['default tags'] }) + }) + + it('should overwrite default check settings with check-specific config', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.checkDefaults = { tags: ['default tags'] } + const apiCheck = new TcpCheck('test-check', { + name: 'Test Check', + tags: ['test check'], + request, + }) + delete Session.checkDefaults + expect(apiCheck).toMatchObject({ tags: ['test check'] }) + }) + + it('should support setting groups with `groupId`', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + const group = new CheckGroup('main-group', { name: 'Main Group', locations: [] }) + const check = new TcpCheck('main-check', { + name: 'Main Check', + request, + groupId: group.ref(), + }) + expect(check.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } }) + }) + + it('should support setting groups with `group`', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + const group = new CheckGroup('main-group', { name: 'Main Group', locations: [] }) + const check = new TcpCheck('main-check', { + name: 'Main Check', + request, + group, + }) + expect(check.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } }) + }) +}) diff --git a/packages/cli/src/constructs/api-check.ts b/packages/cli/src/constructs/api-check.ts index 24b10363..2c356c35 100644 --- a/packages/cli/src/constructs/api-check.ts +++ b/packages/cli/src/constructs/api-check.ts @@ -6,172 +6,41 @@ import { QueryParam } from './query-param' import { pathToPosix } from '../services/util' import { printDeprecationWarning } from '../reporters/util' import { Content, Entrypoint } from './construct' +import { Assertion as CoreAssertion, NumericAssertionBuilder, GeneralAssertionBuilder } from './internal/assertion' -// eslint-disable-next-line no-restricted-syntax -enum AssertionSource { - STATUS_CODE = 'STATUS_CODE', - JSON_BODY = 'JSON_BODY', - HEADERS = 'HEADERS', - TEXT_BODY = 'TEXT_BODY', - RESPONSE_TIME = 'RESPONSE_TIME', -} - -// eslint-disable-next-line no-restricted-syntax -enum AssertionComparison { - EQUALS = 'EQUALS', - NOT_EQUALS = 'NOT_EQUALS', - HAS_KEY = 'HAS_KEY', - NOT_HAS_KEY = 'NOT_HAS_KEY', - HAS_VALUE = 'HAS_VALUE', - NOT_HAS_VALUE = 'NOT_HAS_VALUE', - IS_EMPTY = 'IS_EMPTY', - NOT_EMPTY = 'NOT_EMPTY', - GREATER_THAN = 'GREATER_THAN', - LESS_THAN = 'LESS_THAN', - CONTAINS = 'CONTAINS', - NOT_CONTAINS = 'NOT_CONTAINS', - IS_NULL = 'IS_NULL', - NOT_NULL = 'NOT_NULL', -} +type AssertionSource = + | 'STATUS_CODE' + | 'JSON_BODY' + | 'HEADERS' + | 'TEXT_BODY' + | 'RESPONSE_TIME' -export interface Assertion { - source: string, - property: string, - comparison: string, - target: string, - regex: string|null, -} +export type Assertion = CoreAssertion export class AssertionBuilder { static statusCode () { - return new NumericAssertionBuilder(AssertionSource.STATUS_CODE) + return new NumericAssertionBuilder('STATUS_CODE') } static jsonBody (property?: string) { - return new GeneralAssertionBuilder(AssertionSource.JSON_BODY, property) + return new GeneralAssertionBuilder('JSON_BODY', property) } static headers (property?: string, regex?: string) { - return new GeneralAssertionBuilder(AssertionSource.HEADERS, property, regex) + return new GeneralAssertionBuilder('HEADERS', property, regex) } static textBody (property?: string) { - return new GeneralAssertionBuilder(AssertionSource.TEXT_BODY, property) + return new GeneralAssertionBuilder('TEXT_BODY', property) } /** @deprecated Use responseTime() instead */ static responseTme () { - return new NumericAssertionBuilder(AssertionSource.RESPONSE_TIME) + return new NumericAssertionBuilder('RESPONSE_TIME') } static responseTime () { - return new NumericAssertionBuilder(AssertionSource.RESPONSE_TIME) - } -} - -class NumericAssertionBuilder { - source: AssertionSource - constructor (source: AssertionSource) { - this.source = source - } - - equals (target: number): Assertion { - return this._toAssertion(AssertionComparison.EQUALS, target) - } - - notEquals (target: number): Assertion { - return this._toAssertion(AssertionComparison.NOT_EQUALS, target) - } - - lessThan (target: number): Assertion { - return this._toAssertion(AssertionComparison.LESS_THAN, target) - } - - greaterThan (target: number): Assertion { - return this._toAssertion(AssertionComparison.GREATER_THAN, target) - } - - /** @private */ - private _toAssertion (comparison: AssertionComparison, target: number): Assertion { - return { source: this.source, comparison, property: '', target: target.toString(), regex: null } - } -} - -class GeneralAssertionBuilder { - source: AssertionSource - property?: string - regex?: string - constructor (source: AssertionSource, property?: string, regex?: string) { - this.source = source - this.property = property - this.regex = regex - } - - equals (target: string|number|boolean): Assertion { - return this._toAssertion(AssertionComparison.EQUALS, target) - } - - notEquals (target: string|number|boolean): Assertion { - return this._toAssertion(AssertionComparison.NOT_EQUALS, target) - } - - hasKey (target: string): Assertion { - return this._toAssertion(AssertionComparison.HAS_KEY, target) - } - - notHasKey (target: string): Assertion { - return this._toAssertion(AssertionComparison.NOT_HAS_KEY, target) - } - - hasValue (target: string|number|boolean): Assertion { - return this._toAssertion(AssertionComparison.HAS_VALUE, target) - } - - notHasValue (target: string|number|boolean): Assertion { - return this._toAssertion(AssertionComparison.NOT_HAS_VALUE, target) - } - - isEmpty () { - return this._toAssertion(AssertionComparison.IS_EMPTY) - } - - notEmpty () { - return this._toAssertion(AssertionComparison.NOT_EMPTY) - } - - lessThan (target: string|number|boolean): Assertion { - return this._toAssertion(AssertionComparison.LESS_THAN, target) - } - - greaterThan (target: string|number|boolean): Assertion { - return this._toAssertion(AssertionComparison.GREATER_THAN, target) - } - - contains (target: string): Assertion { - return this._toAssertion(AssertionComparison.CONTAINS, target) - } - - notContains (target: string): Assertion { - return this._toAssertion(AssertionComparison.NOT_CONTAINS, target) - } - - isNull () { - return this._toAssertion(AssertionComparison.IS_NULL) - } - - isNotNull () { - return this._toAssertion(AssertionComparison.NOT_NULL) - } - - /** @private */ - private _toAssertion (comparison: AssertionComparison, target?: string|number|boolean): Assertion { - return { - source: this.source, - comparison, - property: this.property ?? '', - target: target?.toString() ?? '', - regex: this.regex ?? null, - } + return new NumericAssertionBuilder('RESPONSE_TIME') } } diff --git a/packages/cli/src/constructs/index.ts b/packages/cli/src/constructs/index.ts index f1c1a3b5..dd91eb10 100644 --- a/packages/cli/src/constructs/index.ts +++ b/packages/cli/src/constructs/index.ts @@ -25,3 +25,4 @@ export * from './phone-call-alert-channel' export * from './retry-strategy' export * from './multi-step-check' export * from './alert-escalation-policy' +export * from './tcp-check' diff --git a/packages/cli/src/constructs/internal/assertion.ts b/packages/cli/src/constructs/internal/assertion.ts new file mode 100644 index 00000000..07f97d50 --- /dev/null +++ b/packages/cli/src/constructs/internal/assertion.ts @@ -0,0 +1,137 @@ +type Comparison = + | 'EQUALS' + | 'NOT_EQUALS' + | 'HAS_KEY' + | 'NOT_HAS_KEY' + | 'HAS_VALUE' + | 'NOT_HAS_VALUE' + | 'IS_EMPTY' + | 'NOT_EMPTY' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'CONTAINS' + | 'NOT_CONTAINS' + | 'IS_NULL' + | 'NOT_NULL' + +export interface Assertion { + source: Source + property: string + comparison: string + target: string + regex: string|null +} + +export class NumericAssertionBuilder { + source: Source + + constructor (source: Source) { + this.source = source + } + + equals (target: number): Assertion { + return this._toAssertion('EQUALS', target) + } + + notEquals (target: number): Assertion { + return this._toAssertion('NOT_EQUALS', target) + } + + lessThan (target: number): Assertion { + return this._toAssertion('LESS_THAN', target) + } + + greaterThan (target: number): Assertion { + return this._toAssertion('GREATER_THAN', target) + } + + /** @private */ + private _toAssertion (comparison: Comparison, target: number): Assertion { + return { + source: this.source, + comparison, + property: '', + target: target.toString(), + regex: null, + } + } +} + +export class GeneralAssertionBuilder { + source: Source + property?: string + regex?: string + + constructor (source: Source, property?: string, regex?: string) { + this.source = source + this.property = property + this.regex = regex + } + + equals (target: string|number|boolean): Assertion { + return this._toAssertion('EQUALS', target) + } + + notEquals (target: string|number|boolean): Assertion { + return this._toAssertion('NOT_EQUALS', target) + } + + hasKey (target: string): Assertion { + return this._toAssertion('HAS_KEY', target) + } + + notHasKey (target: string): Assertion { + return this._toAssertion('NOT_HAS_KEY', target) + } + + hasValue (target: string|number|boolean): Assertion { + return this._toAssertion('HAS_VALUE', target) + } + + notHasValue (target: string|number|boolean): Assertion { + return this._toAssertion('NOT_HAS_VALUE', target) + } + + isEmpty () { + return this._toAssertion('IS_EMPTY') + } + + notEmpty () { + return this._toAssertion('NOT_EMPTY') + } + + lessThan (target: string|number|boolean): Assertion { + return this._toAssertion('LESS_THAN', target) + } + + greaterThan (target: string|number|boolean): Assertion { + return this._toAssertion('GREATER_THAN', target) + } + + contains (target: string): Assertion { + return this._toAssertion('CONTAINS', target) + } + + notContains (target: string): Assertion { + return this._toAssertion('NOT_CONTAINS', target) + } + + isNull () { + return this._toAssertion('IS_NULL') + } + + isNotNull () { + return this._toAssertion('NOT_NULL') + } + + /** @private */ + private _toAssertion (comparison: Comparison, target?: string|number|boolean): Assertion { + return { + source: this.source, + comparison, + property: this.property ?? '', + target: target?.toString() ?? '', + regex: this.regex ?? null, + } + } +} diff --git a/packages/cli/src/constructs/tcp-check.ts b/packages/cli/src/constructs/tcp-check.ts new file mode 100644 index 00000000..f8f47143 --- /dev/null +++ b/packages/cli/src/constructs/tcp-check.ts @@ -0,0 +1,105 @@ +import { Check, CheckProps } from './check' +import { IPFamily } from './api-check' +import { Session } from './project' +import { Assertion as CoreAssertion, NumericAssertionBuilder, GeneralAssertionBuilder } from './internal/assertion' + +type TcpAssertionSource = 'RESPONSE_DATA' | 'RESPONSE_TIME' + +export type TcpAssertion = CoreAssertion + +export class TcpAssertionBuilder { + static responseData (property?: string) { + return new GeneralAssertionBuilder('RESPONSE_DATA', property) + } + + static responseTime () { + return new NumericAssertionBuilder('RESPONSE_TIME') + } +} + +export interface TcpRequest { + /** + * The hostname the connection should be made to. + * + * Do not include a scheme or a port in the hostname. + */ + hostname: string + /** + * The port the connection should be made to. + */ + port: number + /** + * Check the main Checkly documentation on TCP assertions for specific values + * that you can use in the "property" field. + */ + assertions?: Array + /** + * The IP family to use for the connection. + * + * @default "IPv4" + */ + ipFamily?: IPFamily + /** + * The data to send to the target host. + */ + data?: string +} + +export interface TcpCheckProps extends CheckProps { + /** + * Determines the request that the check is going to run. + */ + request: TcpRequest + /** + * The response time in milliseconds where a check should be considered degraded. + */ + degradedResponseTime?: number + /** + * The response time in milliseconds where a check should be considered failing. + */ + maxResponseTime?: number +} + +/** + * Creates an TCP Check + * + * @remarks + * + * This class make use of the TCP Checks endpoints. + */ +export class TcpCheck extends Check { + request: TcpRequest + degradedResponseTime?: number + maxResponseTime?: number + + /** + * Constructs the TCP Check instance + * + * @param logicalId unique project-scoped resource name identification + * @param props check configuration properties + * + * {@link https://checklyhq.com/docs/cli/constructs/#tcpcheck Read more in the docs} + */ + + constructor (logicalId: string, props: TcpCheckProps) { + super(logicalId, props) + + this.request = props.request + this.degradedResponseTime = props.degradedResponseTime + this.maxResponseTime = props.maxResponseTime + + Session.registerConstruct(this) + this.addSubscriptions() + this.addPrivateLocationCheckAssignments() + } + + synthesize () { + return { + ...super.synthesize(), + checkType: 'TCP', + request: this.request, + degradedResponseTime: this.degradedResponseTime, + maxResponseTime: this.maxResponseTime, + } + } +}