Skip to content

Commit 5c1ea14

Browse files
sorccuclaude
andauthored
fix(cli): emit ocspStapled in SSL assertion codegen [SIM-313] (#1396)
* fix(cli): emit ocspStapled in SSL assertion codegen [SIM-313] PR #1392 added 'OCSP_STAPLED' to SslAssertionSource and the matching SslAssertionBuilder.ocspStapled() builder, but never added the codegen case. valueForSslAssertion fell through to its `default:` clause, so `checkly import` threw `Unsupported SSL assertion source OCSP_STAPLED` and aborted whenever it rendered an SSL monitor carrying an OCSP-stapling assertion. Add the case, mapping to ocspStapled with the existing generalNoArgs shape — the same no-property/no-regex shape as the neighbouring CHAIN_TRUSTED, HOSTNAME_VERIFIED and CERT_NOT_EXPIRED cases. Also extend the SSL codegen round-trip test to cover all 12 assertion sources with their exact emitted call chains; it previously exercised only CERT_EXPIRES_IN_DAYS and CHAIN_TRUSTED, which is why the gap went unnoticed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cli): make assertion-source codegen switches exhaustive [SIM-313] Every assertion-source `switch (assertion.source)` ended in `default: throw`, which leaves the switch non-exhaustive at compile time: adding a member to an assertion source union produces no type error, so tsc stays green and the missing codegen case surfaces only as a runtime throw during `checkly import`. That is exactly how the OCSP_STAPLED gap reached main. Add a shared unsupportedAssertionSource(source: never, kind?: string) guard and use it as the default clause in all eight switches (api, dns, grpc, icmp, ssl, tcp, traceroute, url). Since Assertion<Source>.source is typed as the literal union, TypeScript narrows it to never in an exhaustive default, so an unhandled member now fails to compile. The runtime throw is retained because wire data may carry a source this version of the CLI does not know about. `kind` is optional because the API check's message carries no kind word; all eight messages remain byte-identical, now locked by assertion-codegen.spec.ts. The comparison switches cannot be guarded the same way: Assertion.comparison is typed `string` rather than the Comparison union, which would need wire-data retyping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent aa21de8 commit 5c1ea14

11 files changed

Lines changed: 125 additions & 16 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, it, expect } from 'vitest'
2+
3+
import { GeneratedFile } from '../../sourcegen/index.js'
4+
import { valueForAssertion } from '../api-assertion-codegen.js'
5+
import { valueForDnsAssertion } from '../dns-assertion-codegen.js'
6+
import { valueForGrpcAssertion } from '../grpc-assertion-codegen.js'
7+
import { valueForIcmpAssertion } from '../icmp-assertion-codegen.js'
8+
import { valueForSslAssertion } from '../ssl-assertion-codegen.js'
9+
import { valueForTcpAssertion } from '../tcp-monitor-codegen.js'
10+
import { valueForTracerouteAssertion } from '../traceroute-assertion-codegen.js'
11+
import { valueForUrlAssertion } from '../url-assertion-codegen.js'
12+
13+
const genfile = new GeneratedFile('foo.ts')
14+
15+
// A source no assertion union contains. The compile-time exhaustiveness guard is
16+
// what normally prevents this, so reaching it requires bypassing the type — wire
17+
// data from a newer backend can still carry such a source at runtime.
18+
const unknownSource = {
19+
source: 'NOT_A_REAL_SOURCE',
20+
property: '',
21+
comparison: 'EQUALS',
22+
target: 'x',
23+
regex: null,
24+
} as unknown as never
25+
26+
describe('unsupportedAssertionSource', () => {
27+
// Locks the exact user-visible text of every assertion-source default clause.
28+
// The shared helper builds these messages from an optional `kind`, so a
29+
// regression there (an empty-string kind, a changed template) would otherwise
30+
// pass silently — note the API check's message deliberately carries no kind word.
31+
it.each([
32+
['API', () => valueForAssertion(genfile, unknownSource), 'Unsupported assertion source NOT_A_REAL_SOURCE'],
33+
['DNS', () => valueForDnsAssertion(genfile, unknownSource), 'Unsupported DNS assertion source NOT_A_REAL_SOURCE'],
34+
['gRPC', () => valueForGrpcAssertion(genfile, unknownSource), 'Unsupported gRPC assertion source NOT_A_REAL_SOURCE'],
35+
['ICMP', () => valueForIcmpAssertion(genfile, unknownSource), 'Unsupported ICMP assertion source NOT_A_REAL_SOURCE'],
36+
['SSL', () => valueForSslAssertion(genfile, unknownSource), 'Unsupported SSL assertion source NOT_A_REAL_SOURCE'],
37+
['TCP', () => valueForTcpAssertion(genfile, unknownSource), 'Unsupported TCP assertion source NOT_A_REAL_SOURCE'],
38+
[
39+
'traceroute',
40+
() => valueForTracerouteAssertion(genfile, unknownSource),
41+
'Unsupported traceroute assertion source NOT_A_REAL_SOURCE',
42+
],
43+
['URL', () => valueForUrlAssertion(genfile, unknownSource), 'Unsupported URL assertion source NOT_A_REAL_SOURCE'],
44+
])('%s codegen throws with its own message for an unknown source', (_name, valueFor, message) => {
45+
let thrown: unknown
46+
try {
47+
valueFor()
48+
} catch (err) {
49+
thrown = err
50+
}
51+
expect(thrown).toBeInstanceOf(Error)
52+
expect((thrown as Error).message).toBe(message)
53+
})
54+
})

packages/cli/src/constructs/__tests__/uptime-monitor-codegen.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,46 @@ describe('SslMonitorCodegen', () => {
181181
expect(source).toContain('certExpiresInDays()')
182182
expect(source).toContain('chainTrusted()')
183183
})
184+
185+
it('emits every SslAssertionBuilder source with its target', async () => {
186+
const source = await renderResource(env, p => new SslMonitorCodegen(p), resource({
187+
request: {
188+
sslConfig: { hostname: 'example.com' },
189+
assertions: [
190+
{ source: 'CERT_EXPIRES_IN_DAYS', property: '', comparison: 'GREATER_THAN', target: '20', regex: null },
191+
{ source: 'KEY_SIZE_BITS', property: '', comparison: 'EQUALS', target: '2048', regex: null },
192+
{ source: 'CERT_NOT_EXPIRED', property: '', comparison: 'EQUALS', target: 'true', regex: null },
193+
{ source: 'HOSTNAME_VERIFIED', property: '', comparison: 'EQUALS', target: 'true', regex: null },
194+
{ source: 'CHAIN_TRUSTED', property: '', comparison: 'EQUALS', target: 'true', regex: null },
195+
{ source: 'OCSP_STAPLED', property: '', comparison: 'EQUALS', target: 'true', regex: null },
196+
{ source: 'TLS_VERSION', property: '', comparison: 'EQUALS', target: 'TLS1.3', regex: null },
197+
{
198+
source: 'CIPHER_SUITE',
199+
property: '',
200+
comparison: 'EQUALS',
201+
target: 'TLS_AES_256_GCM_SHA384',
202+
regex: null,
203+
},
204+
{ source: 'SIGNATURE_ALGORITHM', property: '', comparison: 'EQUALS', target: 'SHA256-RSA', regex: null },
205+
{ source: 'ISSUER_CN', property: '', comparison: 'EQUALS', target: 'Example CA', regex: null },
206+
{ source: 'CERT_FINGERPRINT_SHA256', property: '', comparison: 'EQUALS', target: 'abc123', regex: null },
207+
{ source: 'ISSUER_FINGERPRINT_SHA256', property: '', comparison: 'EQUALS', target: 'def456', regex: null },
208+
],
209+
},
210+
}))
211+
expect(source).toContain('certExpiresInDays().greaterThan(20)')
212+
expect(source).toContain('keySizeBits().equals(2048)')
213+
expect(source).toContain('certNotExpired().equals(\'true\')')
214+
expect(source).toContain('hostnameVerified().equals(\'true\')')
215+
expect(source).toContain('chainTrusted().equals(\'true\')')
216+
expect(source).toContain('ocspStapled().equals(\'true\')')
217+
expect(source).toContain('tlsVersion().equals(\'TLS1.3\')')
218+
expect(source).toContain('cipherSuite().equals(\'TLS_AES_256_GCM_SHA384\')')
219+
expect(source).toContain('signatureAlgorithm().equals(\'SHA256-RSA\')')
220+
expect(source).toContain('issuerCn().equals(\'Example CA\')')
221+
expect(source).toContain('certFingerprintSha256().equals(\'abc123\')')
222+
expect(source).toContain('issuerFingerprintSha256().equals(\'def456\')')
223+
})
184224
})
185225

186226
describe('TracerouteMonitorCodegen', () => {

packages/cli/src/constructs/api-assertion-codegen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GeneratedFile, Value } from '../sourcegen/index.js'
22
import { Assertion } from './api-assertion.js'
3-
import { valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
3+
import { unsupportedAssertionSource, valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
44

55
export function valueForAssertion (genfile: GeneratedFile, assertion: Assertion): Value {
66
genfile.namedImport('AssertionBuilder', 'checkly/constructs')
@@ -17,6 +17,6 @@ export function valueForAssertion (genfile: GeneratedFile, assertion: Assertion)
1717
case 'RESPONSE_TIME':
1818
return valueForNumericAssertion('AssertionBuilder', 'responseTime', assertion)
1919
default:
20-
throw new Error(`Unsupported assertion source ${assertion.source}`)
20+
return unsupportedAssertionSource(assertion.source)
2121
}
2222
}

packages/cli/src/constructs/dns-assertion-codegen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GeneratedFile, Value } from '../sourcegen/index.js'
2-
import { valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
2+
import { unsupportedAssertionSource, valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
33
import { DnsAssertion } from './dns-assertion.js'
44

55
export function valueForDnsAssertion (genfile: GeneratedFile, assertion: DnsAssertion): Value {
@@ -24,6 +24,6 @@ export function valueForDnsAssertion (genfile: GeneratedFile, assertion: DnsAsse
2424
hasRegex: false,
2525
})
2626
default:
27-
throw new Error(`Unsupported DNS assertion source ${assertion.source}`)
27+
return unsupportedAssertionSource(assertion.source, 'DNS')
2828
}
2929
}

packages/cli/src/constructs/grpc-assertion-codegen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GeneratedFile, Value } from '../sourcegen/index.js'
2-
import { valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
2+
import { unsupportedAssertionSource, valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
33
import { GrpcAssertion } from './grpc-assertion.js'
44

55
export function valueForGrpcAssertion (genfile: GeneratedFile, assertion: GrpcAssertion): Value {
@@ -31,6 +31,6 @@ export function valueForGrpcAssertion (genfile: GeneratedFile, assertion: GrpcAs
3131
hasRegex: false,
3232
})
3333
default:
34-
throw new Error(`Unsupported gRPC assertion source ${assertion.source}`)
34+
return unsupportedAssertionSource(assertion.source, 'gRPC')
3535
}
3636
}

packages/cli/src/constructs/icmp-assertion-codegen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GeneratedFile, Value } from '../sourcegen/index.js'
2-
import { valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
2+
import { unsupportedAssertionSource, valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
33
import { IcmpAssertion } from './icmp-assertion.js'
44

55
export function valueForIcmpAssertion (genfile: GeneratedFile, assertion: IcmpAssertion): Value {
@@ -16,6 +16,6 @@ export function valueForIcmpAssertion (genfile: GeneratedFile, assertion: IcmpAs
1616
hasRegex: false,
1717
})
1818
default:
19-
throw new Error(`Unsupported ICMP assertion source ${assertion.source}`)
19+
return unsupportedAssertionSource(assertion.source, 'ICMP')
2020
}
2121
}

packages/cli/src/constructs/internal/assertion-codegen.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { expr, ident, Value } from '../../sourcegen/index.js'
22
import { Assertion } from './assertion.js'
33

4+
/**
5+
* Rejects an assertion source that has no codegen case.
6+
*
7+
* The `never` parameter makes an unhandled source a compile-time error: adding a
8+
* member to a monitor's assertion source union without adding the matching codegen
9+
* case fails to typecheck. The runtime throw remains because wire data may carry a
10+
* source this version of the CLI does not know about.
11+
*/
12+
export function unsupportedAssertionSource (source: never, kind?: string): never {
13+
const prefix = kind === undefined ? 'Unsupported' : `Unsupported ${kind}`
14+
throw new Error(`${prefix} assertion source ${String(source)}`)
15+
}
16+
417
export interface ValueForNumericAssertionOptions {
518
hasProperty?: boolean
619
}

packages/cli/src/constructs/ssl-assertion-codegen.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GeneratedFile, Value } from '../sourcegen/index.js'
2-
import { valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
2+
import { unsupportedAssertionSource, valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
33
import { SslAssertion } from './ssl-assertion.js'
44

55
const generalNoArgs = { hasProperty: false, hasRegex: false }
@@ -18,6 +18,8 @@ export function valueForSslAssertion (genfile: GeneratedFile, assertion: SslAsse
1818
return valueForGeneralAssertion('SslAssertionBuilder', 'hostnameVerified', assertion, generalNoArgs)
1919
case 'CHAIN_TRUSTED':
2020
return valueForGeneralAssertion('SslAssertionBuilder', 'chainTrusted', assertion, generalNoArgs)
21+
case 'OCSP_STAPLED':
22+
return valueForGeneralAssertion('SslAssertionBuilder', 'ocspStapled', assertion, generalNoArgs)
2123
case 'TLS_VERSION':
2224
return valueForGeneralAssertion('SslAssertionBuilder', 'tlsVersion', assertion, generalNoArgs)
2325
case 'CIPHER_SUITE':
@@ -31,6 +33,6 @@ export function valueForSslAssertion (genfile: GeneratedFile, assertion: SslAsse
3133
case 'SIGNATURE_ALGORITHM':
3234
return valueForGeneralAssertion('SslAssertionBuilder', 'signatureAlgorithm', assertion, generalNoArgs)
3335
default:
34-
throw new Error(`Unsupported SSL assertion source ${assertion.source}`)
36+
return unsupportedAssertionSource(assertion.source, 'SSL')
3537
}
3638
}

packages/cli/src/constructs/tcp-monitor-codegen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expr, GeneratedFile, ident, Value } from '../sourcegen/index.js'
2-
import { valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
2+
import { unsupportedAssertionSource, valueForGeneralAssertion, valueForNumericAssertion } from './internal/assertion-codegen.js'
33
import { Codegen, Context } from './internal/codegen/index.js'
44
import { buildMonitorProps, MonitorResource } from './monitor-codegen.js'
55
import { TcpAssertion, TcpRequest } from './tcp-monitor.js'
@@ -20,7 +20,7 @@ export function valueForTcpAssertion (genfile: GeneratedFile, assertion: TcpAsse
2020
case 'RESPONSE_TIME':
2121
return valueForNumericAssertion('TcpAssertionBuilder', 'responseTime', assertion)
2222
default:
23-
throw new Error(`Unsupported TCP assertion source ${assertion.source}`)
23+
return unsupportedAssertionSource(assertion.source, 'TCP')
2424
}
2525
}
2626

packages/cli/src/constructs/traceroute-assertion-codegen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GeneratedFile, Value } from '../sourcegen/index.js'
2-
import { valueForNumericAssertion } from './internal/assertion-codegen.js'
2+
import { unsupportedAssertionSource, valueForNumericAssertion } from './internal/assertion-codegen.js'
33
import { TracerouteAssertion } from './traceroute-assertion.js'
44

55
export function valueForTracerouteAssertion (genfile: GeneratedFile, assertion: TracerouteAssertion): Value {
@@ -13,6 +13,6 @@ export function valueForTracerouteAssertion (genfile: GeneratedFile, assertion:
1313
case 'PACKET_LOSS':
1414
return valueForNumericAssertion('TracerouteAssertionBuilder', 'packetLoss', assertion)
1515
default:
16-
throw new Error(`Unsupported traceroute assertion source ${assertion.source}`)
16+
return unsupportedAssertionSource(assertion.source, 'traceroute')
1717
}
1818
}

0 commit comments

Comments
 (0)