Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions packages/cli/src/constructs/__tests__/dns-monitor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { describe, it, expect } from 'vitest'

import { DnsMonitor, CheckGroup, DnsRequest, Diagnostics } from '../index'
import { Project, Session } from '../project'

const request: DnsRequest = {
recordType: 'A',
query: 'acme.com',
}

describe('DnsMonitor', () => {
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 check = new DnsMonitor('test-check', {
name: 'Test Check',
request,
})
Session.checkDefaults = undefined
expect(check).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 check = new DnsMonitor('test-check', {
name: 'Test Check',
tags: ['test check'],
request,
})
Session.checkDefaults = undefined
expect(check).toMatchObject({ tags: ['test check'] })
})

it('should support setting groups with `groupId`', async () => {
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 DnsMonitor('main-check', {
name: 'Main Check',
request,
groupId: group.ref(),
})
const bundle = await check.bundle()
expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } })
})

it('should support setting groups with `group`', async () => {
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 DnsMonitor('main-check', {
name: 'Main Check',
request,
group,
})
const bundle = await check.bundle()
expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } })
})

describe('validation', () => {
it('should error if doubleCheck is set', async () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})

const check = new DnsMonitor('test-check', {
name: 'Test Check',
request,
// @ts-expect-error Not available in the type.
doubleCheck: true,
})

const diags = new Diagnostics()
await check.validate(diags)

expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('Property "doubleCheck" is not supported.'),
}),
]))
})

it('should error if degradedResponseTime is above 500', async () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})

const check = new DnsMonitor('test-check', {
name: 'Test Check',
request,
degradedResponseTime: 501,
})

const diags = new Diagnostics()
await check.validate(diags)

expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('The value of "degradedResponseTime" must be 500 or lower.'),
}),
]))
})

it('should error if maxResponseTime is above 1000', async () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})

const check = new DnsMonitor('test-check', {
name: 'Test Check',
request,
maxResponseTime: 1001,
})

const diags = new Diagnostics()
await check.validate(diags)

expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('The value of "maxResponseTime" must be 1000 or lower.'),
}),
]))
})

it('should error if nameServer is set but port is not', async () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})

const check = new DnsMonitor('test-check', {
name: 'Test Check',
request: {
...request,
nameServer: '1.1.1.1',
},
})

const diags = new Diagnostics()
await check.validate(diags)

expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('A value for "port" is required when "nameServer" is set.'),
}),
]))
})

it('should error if port is set but nameServer is not', async () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})

const check = new DnsMonitor('test-check', {
name: 'Test Check',
request: {
...request,
port: 53,
},
})

const diags = new Diagnostics()
await check.validate(diags)

expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('A value for "nameServer" is required when "port" is set.'),
}),
]))
})

it('should not error if neither nameServer nor port are set', async () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})

const check = new DnsMonitor('test-check', {
name: 'Test Check',
request: {
...request,
},
})

const diags = new Diagnostics()
await check.validate(diags)

expect(diags.isFatal()).toEqual(false)
})

it('should not error if both nameServer and port are set', async () => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})

const check = new DnsMonitor('test-check', {
name: 'Test Check',
request: {
...request,
port: 53,
nameServer: '1.1.1.1',
},
})

const diags = new Diagnostics()
await check.validate(diags)

expect(diags.isFatal()).toEqual(false)
})
})
})
8 changes: 8 additions & 0 deletions packages/cli/src/constructs/check-codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TcpMonitorCodegen, TcpMonitorResource } from './tcp-monitor-codegen'
import { UrlMonitorCodegen, UrlMonitorResource } from './url-monitor-codegen'
import { valueForPrivateLocationFromId } from './private-location-codegen'
import { valueForAlertChannelFromId } from './alert-channel-codegen'
import { DnsMonitorCodegen, DnsMonitorResource } from './dns-monitor-codegen'

export interface CheckResource {
id: string
Expand Down Expand Up @@ -214,6 +215,7 @@ export class CheckCodegen extends Codegen<CheckResource> {
multiStepCheckCodegen: MultiStepCheckCodegen
tcpMonitorCodegen: TcpMonitorCodegen
urlMonitorCodegen: UrlMonitorCodegen
dnsMonitorCodegen: DnsMonitorCodegen

constructor (program: Program) {
super(program)
Expand All @@ -224,6 +226,7 @@ export class CheckCodegen extends Codegen<CheckResource> {
this.multiStepCheckCodegen = new MultiStepCheckCodegen(program)
this.tcpMonitorCodegen = new TcpMonitorCodegen(program)
this.urlMonitorCodegen = new UrlMonitorCodegen(program)
this.dnsMonitorCodegen = new DnsMonitorCodegen(program)
}

describe (resource: CheckResource): string {
Expand All @@ -242,6 +245,8 @@ export class CheckCodegen extends Codegen<CheckResource> {
return this.heartbeatMonitorCodegen.describe(resource as HeartbeatMonitorResource)
case 'URL':
return this.urlMonitorCodegen.describe(resource as UrlMonitorResource)
case 'DNS':
return this.dnsMonitorCodegen.describe(resource as DnsMonitorResource)
default:
throw new Error(`Unable to describe unsupported check type '${checkType}'.`)
}
Expand Down Expand Up @@ -269,6 +274,9 @@ export class CheckCodegen extends Codegen<CheckResource> {
case 'URL':
this.urlMonitorCodegen.gencode(logicalId, resource as UrlMonitorResource, context)
return
case 'DNS':
this.dnsMonitorCodegen.gencode(logicalId, resource as DnsMonitorResource, context)
return
default:
throw new Error(`Unable to generate code for unsupported check type '${checkType}'.`)
}
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/constructs/construct-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ export class InvalidPropertyValueDiagnostic extends ErrorDiagnostic {
}
}

export class RequiredPropertyDiagnostic extends ErrorDiagnostic {
property: string

constructor (property: string, error: Error) {
super({
title: `Missing required property`,
message:
`Property "${property}" is required and must be set.`
+ `\n\n`
+ `Reason: ${error.message}`,
error,
})

this.property = property
}
}

export class ConflictingPropertyDiagnostic extends ErrorDiagnostic {
property1: string
property2: string
Expand Down
29 changes: 29 additions & 0 deletions packages/cli/src/constructs/dns-assertion-codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { GeneratedFile, Value } from '../sourcegen'
import { valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen'
import { DnsAssertion } from './dns-assertion'

export function valueForDnsAssertion (genfile: GeneratedFile, assertion: DnsAssertion): Value {
genfile.namedImport('DnsAssertionBuilder', 'checkly/constructs')

switch (assertion.source) {
case 'RESPONSE_CODE':
return valueForGeneralAssertion('DnsAssertionBuilder', 'responseCode', assertion, {
hasProperty: false,
hasRegex: false,
})
case 'RESPONSE_TIME':
return valueForNumericAssertion('DnsAssertionBuilder', 'responseTime', assertion)
case 'TEXT_ANSWER':
return valueForGeneralAssertion('DnsAssertionBuilder', 'textAnswer', assertion, {
hasProperty: false,
hasRegex: true,
})
case 'JSON_ANSWER':
return valueForGeneralAssertion('DnsAssertionBuilder', 'jsonAnswer', assertion, {
hasProperty: true,
hasRegex: false,
})
default:
throw new Error(`Unsupported DNS assertion source ${assertion.source}`)
}
}
59 changes: 59 additions & 0 deletions packages/cli/src/constructs/dns-assertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Assertion as CoreAssertion, NumericAssertionBuilder, GeneralAssertionBuilder } from './internal/assertion'

type DnsAssertionSource =
| 'RESPONSE_CODE'
| 'RESPONSE_TIME'
| 'TEXT_ANSWER'
| 'JSON_ANSWER'

export type DnsAssertion = CoreAssertion<DnsAssertionSource>

/**
* Builder class for creating DNS monitor assertions.
* Provides methods to create assertions for DNS query responses.
*
* @example
* ```typescript
* // Response time assertions
* DnsAssertionBuilder.responseTime().lessThan(1000)
* DnsAssertionBuilder.responseTime().greaterThan(100)
*
* // Response code assertions
* DnsAssertionBuilder.responseCode().equals('NOERROR')
* DnsAssertionBuilder.responseCode().equals('NXDOMAIN')
* ```
*/
export class DnsAssertionBuilder {
/**
* Creates an assertion builder for DNS response codes.
* @returns A general assertion builder for the response code.
*/
static responseCode () {
return new GeneralAssertionBuilder<DnsAssertionSource>('RESPONSE_CODE')
}

/**
* Creates an assertion builder for DNS response time.
* @returns A numeric assertion builder for response time in milliseconds.
*/
static responseTime () {
return new NumericAssertionBuilder<DnsAssertionSource>('RESPONSE_TIME')
}

/**
* Creates an assertion builder for the answer in common plain text format.
* @returns A general assertion builder for the answer.
*/
static textAnswer (regex?: string) {
return new GeneralAssertionBuilder<DnsAssertionSource>('TEXT_ANSWER', undefined, regex)
}

/**
* Creates an assertion builder for the JSON formatted answer.
* @param property Optional JSON path to specific property (e.g., '$.Answer[0].data')
* @returns A general assertion builder for the JSON formatted answer.
*/
static jsonAnswer (property?: string) {
return new GeneralAssertionBuilder<DnsAssertionSource>('JSON_ANSWER', property)
}
}
Loading