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
19 changes: 16 additions & 3 deletions packages/cli/src/constructs/__tests__/grpc-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,36 @@ describe('GrpcMonitor', () => {
})

describe('validation', () => {
it('should error if degradedResponseTime is above 30000', async () => {
it('should error if degradedResponseTime is above 180000', async () => {
setupProject()
const check = new GrpcMonitor('test-check', {
name: 'Test Check',
request,
degradedResponseTime: 30001,
degradedResponseTime: 180001,
})
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 30000 or lower.'),
message: expect.stringContaining('The value of "degradedResponseTime" must be 180000 or lower.'),
}),
]))
})

it('should not error for a response time above 30000 (gRPC allows up to 180000)', async () => {
setupProject()
const check = new GrpcMonitor('test-check', {
name: 'Test Check',
request,
degradedResponseTime: 90000,
maxResponseTime: 120000,
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(false)
})

it('should not error within limits', async () => {
setupProject()
const check = new GrpcMonitor('test-check', {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest'

import { valueForSslAssertion } from '../ssl-assertion-codegen.js'
import { SslAssertion } from '../ssl-assertion.js'
import { GeneratedFile, Output } from '../../sourcegen/index.js'

function render (assertion: SslAssertion): string {
const output = new Output()
const file = new GeneratedFile('foo.ts')
const result = valueForSslAssertion(file, assertion)
result.render(output)
return output.finalize()
}

describe('SSL Assertion Codegen', () => {
it('generates the new SSL assertion sources', () => {
const cases: { input: SslAssertion, expected: string }[] = [
{
input: { source: 'HANDSHAKE_TIME_MS', property: '', comparison: 'LESS_THAN', target: '500', regex: null },
expected: 'SslAssertionBuilder.handshakeTimeMs().lessThan(500)\n',
},
{
input: { source: 'OCSP_STAPLED', property: '', comparison: 'EQUALS', target: 'true', regex: null },
expected: "SslAssertionBuilder.ocspStapled().equals('true')\n",
},
{
input: { source: 'SAN_CONTAINS', property: '', comparison: 'EQUALS', target: 'example.com', regex: null },
expected: "SslAssertionBuilder.sanContains().equals('example.com')\n",
},
]
for (const test of cases) {
expect(render(test.input)).toEqual(test.expected)
}
})

it('generates the new comparison operators', () => {
const cases: { input: SslAssertion, expected: string }[] = [
{
input: { source: 'KEY_SIZE_BITS', property: '', comparison: 'GREATER_THAN_OR_EQUAL', target: '2048', regex: null },
expected: 'SslAssertionBuilder.keySizeBits().greaterThanOrEqual(2048)\n',
},
{
input: { source: 'TLS_VERSION', property: '', comparison: 'GREATER_THAN_OR_EQUAL', target: 'TLS1.2', regex: null },
expected: "SslAssertionBuilder.tlsVersion().greaterThanOrEqual('TLS1.2')\n",
},
{
input: { source: 'CIPHER_SUITE', property: '', comparison: 'MATCHES', target: '^TLS_AES_.*', regex: null },
expected: "SslAssertionBuilder.cipherSuite().matches('^TLS_AES_.*')\n",
},
]
for (const test of cases) {
expect(render(test.input)).toEqual(test.expected)
}
})
})
87 changes: 87 additions & 0 deletions packages/cli/src/constructs/__tests__/ssl-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,56 @@ describe('SslMonitor', () => {
expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } })
})

describe('assertions', () => {
it('synthesizes the new SSL assertion sources with backend-compatible operators', () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
request: {
hostname: 'example.com',
sslConfig: {},
assertions: [
SslAssertionBuilder.keySizeBits().greaterThanOrEqual(2048),
SslAssertionBuilder.tlsVersion().greaterThanOrEqual('TLS1.2'),
SslAssertionBuilder.cipherSuite().matches('^TLS_AES_.*'),
SslAssertionBuilder.issuerCn().matches('(?i)let\'s encrypt.*'),
SslAssertionBuilder.handshakeTimeMs().lessThan(500),
SslAssertionBuilder.ocspStapled().equals(true),
SslAssertionBuilder.sanContains().equals('example.com'),
],
},
})

const payload = check.synthesize() as any
expect(payload.request.assertions).toEqual([
expect.objectContaining({ source: 'KEY_SIZE_BITS', comparison: 'GREATER_THAN_OR_EQUAL', target: '2048' }),
expect.objectContaining({ source: 'TLS_VERSION', comparison: 'GREATER_THAN_OR_EQUAL', target: 'TLS1.2' }),
expect.objectContaining({ source: 'CIPHER_SUITE', comparison: 'MATCHES', target: '^TLS_AES_.*' }),
expect.objectContaining({ source: 'ISSUER_CN', comparison: 'MATCHES' }),
expect.objectContaining({ source: 'HANDSHAKE_TIME_MS', comparison: 'LESS_THAN', target: '500' }),
expect.objectContaining({ source: 'OCSP_STAPLED', comparison: 'EQUALS', target: 'true' }),
expect.objectContaining({ source: 'SAN_CONTAINS', comparison: 'EQUALS', target: 'example.com' }),
])
})

it('accepts a "degrade" baseline severity', () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
request: {
hostname: 'example.com',
sslConfig: {
securityBaseline: {
minTLSVersion: { value: 'TLS1.2', severity: 'degrade' },
},
},
},
})
const payload = check.synthesize() as any
expect(payload.request.sslConfig.securityBaseline.minTLSVersion.severity).toBe('degrade')
})
})

describe('validation', () => {
it('should error when clientCertificateMode is explicit but no certificate id is set', async () => {
setupProject()
Expand Down Expand Up @@ -176,6 +226,43 @@ describe('SslMonitor', () => {
]))
})

it('should error when degradedResponseTime exceeds the default maxResponseTime and max is omitted', async () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
// 20000 is within the 30000 degraded bound but above the 10000 default
// maxResponseTime the backend applies when max is omitted.
degradedResponseTime: 20000,
request: {
hostname: 'example.com',
sslConfig: {},
},
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('must be less than or equal to the default "maxResponseTime" of 10000'),
}),
]))
})

it('should not error when degradedResponseTime is within the default maxResponseTime and max is omitted', async () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
degradedResponseTime: 5000,
request: {
hostname: 'example.com',
sslConfig: {},
},
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(false)
})

it('should not error for a valid config', async () => {
setupProject()
const check = new SslMonitor('test-check', { name: 'Test Check', request })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest'

import { valueForTracerouteAssertion } from '../traceroute-assertion-codegen.js'
import { TracerouteAssertion } from '../traceroute-assertion.js'
import { GeneratedFile, Output } from '../../sourcegen/index.js'

function render (assertion: TracerouteAssertion): string {
const output = new Output()
const file = new GeneratedFile('foo.ts')
const result = valueForTracerouteAssertion(file, assertion)
result.render(output)
return output.finalize()
}

describe('Traceroute Assertion Codegen', () => {
it('preserves the response-time property as a selector', () => {
const cases: { input: TracerouteAssertion, expected: string }[] = [
{
input: { source: 'RESPONSE_TIME', property: 'max', comparison: 'LESS_THAN', target: '1000', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime().max().lessThan(1000)\n',
},
{
input: { source: 'RESPONSE_TIME', property: 'min', comparison: 'GREATER_THAN', target: '10', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime().min().greaterThan(10)\n',
},
{
input: { source: 'RESPONSE_TIME', property: 'stdDev', comparison: 'LESS_THAN', target: '50', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime().stdDev().lessThan(50)\n',
},
{
input: { source: 'RESPONSE_TIME', property: 'avg', comparison: 'LESS_THAN', target: '1000', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime().avg().lessThan(1000)\n',
},
]
for (const test of cases) {
expect(render(test.input)).toEqual(test.expected)
}
})

it('emits a bare responseTime() when no property is set', () => {
const input: TracerouteAssertion =
{ source: 'RESPONSE_TIME', property: '', comparison: 'LESS_THAN', target: '1000', regex: null }
expect(render(input)).toEqual('TracerouteAssertionBuilder.responseTime().lessThan(1000)\n')
})

it('does not emit a property selector for HOP_COUNT / PACKET_LOSS', () => {
const hop: TracerouteAssertion =
{ source: 'HOP_COUNT', property: '', comparison: 'LESS_THAN', target: '20', regex: null }
expect(render(hop)).toEqual('TracerouteAssertionBuilder.hopCount().lessThan(20)\n')

const loss: TracerouteAssertion =
{ source: 'PACKET_LOSS', property: '', comparison: 'LESS_THAN', target: '10', regex: null }
expect(render(loss)).toEqual('TracerouteAssertionBuilder.packetLoss().lessThan(10)\n')
})
})
53 changes: 53 additions & 0 deletions packages/cli/src/constructs/__tests__/traceroute-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,60 @@ describe('TracerouteMonitor', () => {
expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } })
})

describe('responseTime() assertions', () => {
it('should default the property to "avg" so responseTime().lessThan() is usable', () => {
const assertion = TracerouteAssertionBuilder.responseTime().lessThan(1000)
expect(assertion).toMatchObject({
source: 'RESPONSE_TIME',
property: 'avg',
comparison: 'LESS_THAN',
target: '1000',
})
})

it('should support selecting a specific response-time property', () => {
expect(TracerouteAssertionBuilder.responseTime().max().lessThan(2000)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'max',
comparison: 'LESS_THAN',
target: '2000',
})
expect(TracerouteAssertionBuilder.responseTime().min().greaterThan(1)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'min',
comparison: 'GREATER_THAN',
})
expect(TracerouteAssertionBuilder.responseTime().stdDev().lessThan(50)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'stdDev',
})
expect(TracerouteAssertionBuilder.responseTime().avg().equals(100)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'avg',
comparison: 'EQUALS',
})
})
})

describe('validation', () => {
it('should error when degradedResponseTime exceeds maxResponseTime', async () => {
setupProject()
const check = new TracerouteMonitor('test-check', {
name: 'Test Check',
request,
degradedResponseTime: 20000,
maxResponseTime: 10000,
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('must be less than or equal to "maxResponseTime"'),
}),
]))
})

it('should error if maxResponseTime is above 30000', async () => {
setupProject()
const check = new TracerouteMonitor('test-check', {
Expand Down
12 changes: 8 additions & 4 deletions packages/cli/src/constructs/grpc-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface GrpcMonitorProps extends MonitorProps {
*
* @defaultValue 4000
* @minimum 0
* @maximum 30000
* @maximum 180000
* @example
* ```typescript
* degradedResponseTime: 3000 // Alert when the gRPC call takes longer than 3 seconds
Expand All @@ -30,7 +30,7 @@ export interface GrpcMonitorProps extends MonitorProps {
*
* @defaultValue 5000
* @minimum 0
* @maximum 30000
* @maximum 180000
* @example
* ```typescript
* maxResponseTime: 10000 // Fail if the gRPC call takes longer than 10 seconds
Expand Down Expand Up @@ -76,8 +76,12 @@ export class GrpcMonitor extends Monitor {
await super.validate(diagnostics)

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 30_000,
maxResponseTime: 30_000,
// gRPC allows thresholds up to 180s (calls can run to the 180s timeout),
// matching grpcResponseTimeLimitFields in response-time-limit-schema.ts.
degradedResponseTime: 180_000,
maxResponseTime: 180_000,
// Backend default applied when maxResponseTime is omitted.
defaultMaxResponseTime: 20_000,
})
}

Expand Down
Loading