Best Kotest code snippet using io.kotest.matchers.throwable.matchers.Throwable.shouldHaveCauseOfType
AttestationValidatorTest.kt
Source:AttestationValidatorTest.kt
1package ch.veehait.devicecheck.appattest.attestation2import ch.veehait.devicecheck.appattest.AppleAppAttest3import ch.veehait.devicecheck.appattest.CertUtils4import ch.veehait.devicecheck.appattest.TestExtensions.encode5import ch.veehait.devicecheck.appattest.TestExtensions.fixedUtcClock6import ch.veehait.devicecheck.appattest.TestUtils.cborObjectMapper7import ch.veehait.devicecheck.appattest.common.App8import ch.veehait.devicecheck.appattest.common.AppleAppAttestEnvironment9import ch.veehait.devicecheck.appattest.common.AuthenticatorData10import ch.veehait.devicecheck.appattest.common.AuthenticatorDataFlag11import ch.veehait.devicecheck.appattest.receipt.Receipt12import ch.veehait.devicecheck.appattest.receipt.ReceiptException13import ch.veehait.devicecheck.appattest.receipt.ReceiptValidator14import ch.veehait.devicecheck.appattest.util.Extensions.createAppleKeyId15import ch.veehait.devicecheck.appattest.util.Extensions.sha25616import ch.veehait.devicecheck.appattest.util.Extensions.toBase6417import com.fasterxml.jackson.module.kotlin.readValue18import io.kotest.assertions.throwables.shouldNotThrowAny19import io.kotest.assertions.throwables.shouldThrow20import io.kotest.core.spec.style.FreeSpec21import io.kotest.matchers.nulls.shouldBeNull22import io.kotest.matchers.shouldBe23import io.kotest.matchers.throwable.shouldHaveCauseOfType24import io.kotest.matchers.throwable.shouldHaveMessage25import nl.jqno.equalsverifier.EqualsVerifier26import org.bouncycastle.asn1.ASN1ObjectIdentifier27import org.bouncycastle.asn1.DEROctetString28import org.bouncycastle.asn1.DLSequence29import org.bouncycastle.asn1.DLTaggedObject30import org.bouncycastle.jce.provider.BouncyCastleProvider31import java.security.Security32import java.security.cert.TrustAnchor33import java.security.interfaces.ECPublicKey34import java.time.Clock35import java.time.Duration36import java.time.Instant37import java.util.UUID38@Suppress("LargeClass")39class AttestationValidatorTest : FreeSpec() {40 private fun AttestationSample.defaultValidator(): AttestationValidator {41 val appleAppAttest = this.defaultAppleAppAttest()42 return appleAppAttest.createAttestationValidator(43 clock = timestamp.fixedUtcClock(),44 )45 }46 init {47 Security.addProvider(BouncyCastleProvider())48 "equals/hashCode" - {49 "AttestationObject.AttestationStatement" {50 EqualsVerifier.forClass(AttestationObject.AttestationStatement::class.java).verify()51 }52 "AttestationObject: equals/hashCode" {53 EqualsVerifier.forClass(AttestationObject::class.java).verify()54 }55 }56 "Accepts valid attestation samples" - {57 AttestationSample.all.forEach { sample ->58 "${sample.id}" {59 val attestationValidator = sample.defaultValidator()60 val response = shouldNotThrowAny {61 attestationValidator.validate(62 attestationObject = sample.attestation,63 keyIdBase64 = sample.keyId.toBase64(),64 serverChallenge = sample.clientData65 )66 }67 response.certificate.publicKey shouldBe sample.publicKey68 response.iOSVersion shouldBe sample.iOSVersion69 }70 }71 }72 "Accepts valid fake attestation samples" - {73 AttestationSample.all.forEach { sample ->74 "${sample.id}" {75 val attestationValidatorOriginal = sample.defaultValidator()76 val attestationResponse = attestationValidatorOriginal.validate(77 attestationObject = sample.attestation,78 keyIdBase64 = sample.keyId.toBase64(),79 serverChallenge = sample.clientData80 )81 val attestationObject: AttestationObject = cborObjectMapper.readValue(sample.attestation)82 val authData: AuthenticatorData = AuthenticatorData.parse(83 attestationObject.authData,84 cborObjectMapper.readerForMapOf(Any::class.java)85 )86 val credCertKeyPair = CertUtils.generateP256KeyPair()87 val authDataFake = authData.copy(88 attestedCredentialData = authData.attestedCredentialData?.copy(89 credentialId = credCertKeyPair.public.createAppleKeyId()90 )91 ).encode()92 val nonceFake = authDataFake.plus(sample.clientData.sha256()).sha256()93 val attCertChain = CertUtils.createCustomAttestationCertificate(94 x5c = attestationObject.attStmt.x5c,95 credCertKeyPair = credCertKeyPair,96 mutatorCredCert = { builder ->97 val fakeNonceEncoded = DLSequence(98 DLTaggedObject(true, 1, DEROctetString(nonceFake))99 ).encoded100 builder.replaceExtension(101 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.NONCE_OID),102 false,103 fakeNonceEncoded104 )105 }106 )107 val resignedReceiptResponse = CertUtils.resignReceipt(108 receipt = attestationResponse.receipt,109 payloadMutator = {110 it.copy(111 attestationCertificate = Receipt.ReceiptAttribute.X509Certificate(112 it.attestationCertificate.sequence.copy(113 value = attCertChain.credCert.encoded114 )115 )116 )117 },118 )119 val attestationObjectFake = attestationObject.copy(120 attStmt = attestationObject.attStmt.copy(121 x5c = listOf(attCertChain.credCert.encoded, attCertChain.intermediateCa.encoded),122 receipt = resignedReceiptResponse.receipt.p7,123 ),124 authData = authDataFake,125 )126 val appleAppAttest = sample.defaultAppleAppAttest()127 val attestationValidator = appleAppAttest.createAttestationValidator(128 clock = sample.timestamp.fixedUtcClock(),129 receiptValidator = appleAppAttest.createReceiptValidator(130 clock = sample.timestamp.fixedUtcClock(),131 trustAnchor = resignedReceiptResponse.trustAnchor,132 ),133 trustAnchor = TrustAnchor(attCertChain.rootCa, null)134 )135 attestationValidator.validate(136 attestationObject = cborObjectMapper.writeValueAsBytes(attestationObjectFake),137 keyIdBase64 = attCertChain.credCert.createAppleKeyId().toBase64(),138 serverChallenge = sample.clientData,139 )140 }141 }142 }143 "Accepts valid fake attestation samples with missing iOS version extension" - {144 AttestationSample.all.forEach { sample ->145 "${sample.id}" {146 val attestationValidatorOriginal = sample.defaultValidator()147 val attestationResponse = attestationValidatorOriginal.validate(148 attestationObject = sample.attestation,149 keyIdBase64 = sample.keyId.toBase64(),150 serverChallenge = sample.clientData151 )152 val attestationObject: AttestationObject = cborObjectMapper.readValue(sample.attestation)153 val authData: AuthenticatorData = AuthenticatorData.parse(154 attestationObject.authData,155 cborObjectMapper.readerForMapOf(Any::class.java)156 )157 val credCertKeyPair = CertUtils.generateP256KeyPair()158 val authDataFake = authData.copy(159 attestedCredentialData = authData.attestedCredentialData?.copy(160 credentialId = credCertKeyPair.public.createAppleKeyId()161 )162 ).encode()163 val nonceFake = authDataFake.plus(sample.clientData.sha256()).sha256()164 val attCertChain = CertUtils.createCustomAttestationCertificate(165 x5c = attestationObject.attStmt.x5c,166 credCertKeyPair = credCertKeyPair,167 mutatorCredCert = { builder ->168 val fakeNonceEncoded = DLSequence(169 DLTaggedObject(true, 1, DEROctetString(nonceFake))170 ).encoded171 builder.replaceExtension(172 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.NONCE_OID),173 false,174 fakeNonceEncoded175 )176 // Validation should still succeed even if the iOS version cannot be parsed177 builder.removeExtension(178 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.OS_VERSION_OID),179 )180 }181 )182 val resignedReceiptResponse = CertUtils.resignReceipt(183 receipt = attestationResponse.receipt,184 payloadMutator = {185 it.copy(186 attestationCertificate = Receipt.ReceiptAttribute.X509Certificate(187 it.attestationCertificate.sequence.copy(188 value = attCertChain.credCert.encoded189 )190 )191 )192 },193 )194 val attestationObjectFake = attestationObject.copy(195 attStmt = attestationObject.attStmt.copy(196 x5c = listOf(attCertChain.credCert.encoded, attCertChain.intermediateCa.encoded),197 receipt = resignedReceiptResponse.receipt.p7,198 ),199 authData = authDataFake,200 )201 val appleAppAttest = sample.defaultAppleAppAttest()202 val attestationValidator = appleAppAttest.createAttestationValidator(203 clock = sample.timestamp.fixedUtcClock(),204 receiptValidator = appleAppAttest.createReceiptValidator(205 clock = sample.timestamp.fixedUtcClock(),206 trustAnchor = resignedReceiptResponse.trustAnchor,207 ),208 trustAnchor = TrustAnchor(attCertChain.rootCa, null)209 )210 val result = attestationValidator.validate(211 attestationObject = cborObjectMapper.writeValueAsBytes(attestationObjectFake),212 keyIdBase64 = attCertChain.credCert.createAppleKeyId().toBase64(),213 serverChallenge = sample.clientData,214 )215 result.iOSVersion.shouldBeNull()216 }217 }218 }219 "Throw InvalidReceipt for invalid receipt" - {220 AttestationSample.all.forEach { sample ->221 "${sample.id}" {222 val appleAppAttest = sample.defaultAppleAppAttest()223 val attestationValidator = appleAppAttest.createAttestationValidator(224 clock = sample.timestamp.fixedUtcClock(),225 receiptValidator = object : ReceiptValidator {226 override val app: App = appleAppAttest.app227 override val trustAnchor: TrustAnchor =228 ReceiptValidator.APPLE_PUBLIC_ROOT_CA_G3_BUILTIN_TRUST_ANCHOR229 override val maxAge: Duration = ReceiptValidator.APPLE_RECOMMENDED_MAX_AGE230 override val clock: Clock = sample.timestamp.fixedUtcClock()231 override suspend fun validateReceiptAsync(232 receiptP7: ByteArray,233 publicKey: ECPublicKey,234 notAfter: Instant235 ): Receipt {236 throw ReceiptException.InvalidPayload("Always rejected")237 }238 override fun validateReceipt(239 receiptP7: ByteArray,240 publicKey: ECPublicKey,241 notAfter: Instant242 ): Receipt {243 throw ReceiptException.InvalidPayload("Always rejected")244 }245 }246 )247 shouldThrow<AttestationException.InvalidReceipt> {248 attestationValidator.validateAsync(249 attestationObject = sample.attestation,250 keyIdBase64 = sample.keyId.toBase64(),251 serverChallenge = sample.clientData,252 )253 }254 }255 }256 }257 "Throws InvalidFormatException for wrong attestation format" - {258 AttestationSample.all.forEach { sample ->259 "${sample.id}" {260 val attestationValidator = sample.defaultValidator()261 shouldThrow<AttestationException.InvalidFormatException> {262 with(sample) {263 val attestationStatement =264 cborObjectMapper.readValue(attestation, AttestationObject::class.java)265 val attestationStatementWrong = attestationStatement.copy(fmt = "wurzelpfropf")266 val attestationWrongFormat = cborObjectMapper.writeValueAsBytes(attestationStatementWrong)267 attestationValidator.validate(268 attestationObject = attestationWrongFormat,269 keyIdBase64 = sample.keyId.toBase64(),270 serverChallenge = sample.clientData,271 )272 }273 }274 }275 }276 }277 "Throws InvalidAuthenticatorData for wrong appId" - {278 AttestationSample.all.forEach { sample ->279 "${sample.id}" {280 val attestationValidator = AppleAppAttest(281 app = App("WURZELPFRO", "PF"),282 appleAppAttestEnvironment = sample.environment,283 ).createAttestationValidator(284 clock = sample.timestamp.fixedUtcClock(),285 )286 val exception = shouldThrow<AttestationException.InvalidAuthenticatorData> {287 attestationValidator.validate(288 attestationObject = sample.attestation,289 keyIdBase64 = sample.keyId.toBase64(),290 serverChallenge = sample.clientData,291 )292 }293 exception.message.shouldBe("App ID does not match RP ID hash")294 }295 }296 }297 "Throws InvalidPublicKey for wrong keyId" - {298 AttestationSample.all.forEach { sample ->299 "${sample.id}" {300 val attestationValidator = sample.defaultValidator()301 shouldThrow<AttestationException.InvalidPublicKey> {302 val wrongKeyId = "fporfplezruw".toByteArray().sha256().toBase64()303 attestationValidator.validate(304 attestationObject = sample.attestation,305 keyIdBase64 = wrongKeyId,306 serverChallenge = sample.clientData,307 )308 }309 }310 }311 }312 "Throws InvalidNonce for wrong challenge" - {313 AttestationSample.all.forEach { sample ->314 "${sample.id}" {315 val attestationValidator = sample.defaultValidator()316 shouldThrow<AttestationException.InvalidNonce> {317 val wrongChallenge = "fporfplezruw".toByteArray()318 attestationValidator.validate(319 attestationObject = sample.attestation,320 keyIdBase64 = sample.keyId.toBase64(),321 serverChallenge = wrongChallenge,322 )323 }324 }325 }326 }327 "Throws InvalidNonce for malformatted challenge" - {328 AttestationSample.all.forEach { sample ->329 "${sample.id}" {330 val attestationValidatorOriginal = sample.defaultValidator()331 val attestationResponse = attestationValidatorOriginal.validate(332 attestationObject = sample.attestation,333 keyIdBase64 = sample.keyId.toBase64(),334 serverChallenge = sample.clientData335 )336 val attestationObject: AttestationObject = cborObjectMapper.readValue(sample.attestation)337 val authData: AuthenticatorData = AuthenticatorData.parse(338 attestationObject.authData,339 cborObjectMapper.readerForMapOf(Any::class.java)340 )341 val credCertKeyPair = CertUtils.generateP256KeyPair()342 val authDataFake = authData.copy(343 attestedCredentialData = authData.attestedCredentialData?.copy(344 credentialId = credCertKeyPair.public.createAppleKeyId(),345 )346 ).encode()347 val attCertChain = CertUtils.createCustomAttestationCertificate(348 x5c = attestationObject.attStmt.x5c,349 credCertKeyPair = credCertKeyPair,350 mutatorCredCert = { builder ->351 builder.removeExtension(352 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.NONCE_OID),353 )354 }355 )356 val resignedReceiptResponse = CertUtils.resignReceipt(357 receipt = attestationResponse.receipt,358 payloadMutator = {359 it.copy(360 attestationCertificate = Receipt.ReceiptAttribute.X509Certificate(361 it.attestationCertificate.sequence.copy(362 value = attCertChain.credCert.encoded363 )364 )365 )366 },367 )368 val attestationObjectFake = attestationObject.copy(369 attStmt = attestationObject.attStmt.copy(370 x5c = listOf(attCertChain.credCert.encoded, attCertChain.intermediateCa.encoded),371 receipt = resignedReceiptResponse.receipt.p7,372 ),373 authData = authDataFake,374 )375 val appleAppAttest = sample.defaultAppleAppAttest()376 val attestationValidator = appleAppAttest.createAttestationValidator(377 clock = sample.timestamp.fixedUtcClock(),378 receiptValidator = appleAppAttest.createReceiptValidator(379 clock = sample.timestamp.fixedUtcClock(),380 trustAnchor = resignedReceiptResponse.trustAnchor,381 ),382 trustAnchor = TrustAnchor(attCertChain.rootCa, null)383 )384 val exception = shouldThrow<AttestationException.InvalidNonce> {385 attestationValidator.validate(386 attestationObject = cborObjectMapper.writeValueAsBytes(attestationObjectFake),387 keyIdBase64 = attCertChain.credCert.createAppleKeyId().toBase64(),388 serverChallenge = sample.clientData,389 )390 }391 exception.cause!!.shouldHaveCauseOfType<NullPointerException>()392 }393 }394 }395 "Throws InvalidAuthenticatorData for missing attested credentials" - {396 AttestationSample.all.forEach { sample ->397 "${sample.id}" {398 val attestationValidatorOriginal = sample.defaultValidator()399 val attestationResponse = attestationValidatorOriginal.validate(400 attestationObject = sample.attestation,401 keyIdBase64 = sample.keyId.toBase64(),402 serverChallenge = sample.clientData403 )404 val attestationObject: AttestationObject = cborObjectMapper.readValue(sample.attestation)405 val authData: AuthenticatorData = AuthenticatorData.parse(406 attestationObject.authData,407 cborObjectMapper.readerForMapOf(Any::class.java)408 )409 val credCertKeyPair = CertUtils.generateP256KeyPair()410 // Omit attestedCredentialData for this test411 val authDataFake = authData.copy(412 attestedCredentialData = null,413 flags = authData.flags.filterNot { it == AuthenticatorDataFlag.AT }414 ).encode()415 val nonceFake = authDataFake.plus(sample.clientData.sha256()).sha256()416 val attCertChain = CertUtils.createCustomAttestationCertificate(417 x5c = attestationObject.attStmt.x5c,418 credCertKeyPair = credCertKeyPair,419 mutatorCredCert = { builder ->420 val fakeNonceEncoded = DLSequence(421 DLTaggedObject(true, 1, DEROctetString(nonceFake))422 ).encoded423 builder.replaceExtension(424 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.NONCE_OID),425 false,426 fakeNonceEncoded427 )428 }429 )430 val resignedReceiptResponse = CertUtils.resignReceipt(431 receipt = attestationResponse.receipt,432 payloadMutator = {433 it.copy(434 attestationCertificate = Receipt.ReceiptAttribute.X509Certificate(435 it.attestationCertificate.sequence.copy(436 value = attCertChain.credCert.encoded437 )438 )439 )440 },441 )442 val attestationObjectFake = attestationObject.copy(443 attStmt = attestationObject.attStmt.copy(444 x5c = listOf(attCertChain.credCert.encoded, attCertChain.intermediateCa.encoded),445 receipt = resignedReceiptResponse.receipt.p7,446 ),447 authData = authDataFake,448 )449 val appleAppAttest = sample.defaultAppleAppAttest()450 val attestationValidator = appleAppAttest.createAttestationValidator(451 clock = sample.timestamp.fixedUtcClock(),452 receiptValidator = appleAppAttest.createReceiptValidator(453 clock = sample.timestamp.fixedUtcClock(),454 trustAnchor = resignedReceiptResponse.trustAnchor,455 ),456 trustAnchor = TrustAnchor(attCertChain.rootCa, null)457 )458 val exception = shouldThrow<AttestationException.InvalidAuthenticatorData> {459 attestationValidator.validate(460 attestationObject = cborObjectMapper.writeValueAsBytes(attestationObjectFake),461 keyIdBase64 = attCertChain.credCert.createAppleKeyId().toBase64(),462 serverChallenge = sample.clientData,463 )464 }465 exception.shouldHaveMessage("Does not contain attested credentials")466 }467 }468 }469 "Throws InvalidAuthenticatorData for non-zero counter" - {470 AttestationSample.all.forEach { sample ->471 "${sample.id}" {472 val attestationValidatorOriginal = sample.defaultValidator()473 val attestationResponse = attestationValidatorOriginal.validate(474 attestationObject = sample.attestation,475 keyIdBase64 = sample.keyId.toBase64(),476 serverChallenge = sample.clientData477 )478 val attestationObject: AttestationObject = cborObjectMapper.readValue(sample.attestation)479 val authData: AuthenticatorData = AuthenticatorData.parse(480 attestationObject.authData,481 cborObjectMapper.readerForMapOf(Any::class.java)482 )483 val credCertKeyPair = CertUtils.generateP256KeyPair()484 val authDataFake = authData.copy(485 attestedCredentialData = authData.attestedCredentialData?.copy(486 credentialId = credCertKeyPair.public.createAppleKeyId(),487 ),488 signCount = 1337L,489 ).encode()490 val nonceFake = authDataFake.plus(sample.clientData.sha256()).sha256()491 val attCertChain = CertUtils.createCustomAttestationCertificate(492 x5c = attestationObject.attStmt.x5c,493 credCertKeyPair = credCertKeyPair,494 mutatorCredCert = { builder ->495 val fakeNonceEncoded = DLSequence(496 DLTaggedObject(true, 1, DEROctetString(nonceFake))497 ).encoded498 builder.replaceExtension(499 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.NONCE_OID),500 false,501 fakeNonceEncoded502 )503 }504 )505 val resignedReceiptResponse = CertUtils.resignReceipt(506 receipt = attestationResponse.receipt,507 payloadMutator = {508 it.copy(509 attestationCertificate = Receipt.ReceiptAttribute.X509Certificate(510 it.attestationCertificate.sequence.copy(511 value = attCertChain.credCert.encoded512 )513 )514 )515 },516 )517 val attestationObjectFake = attestationObject.copy(518 attStmt = attestationObject.attStmt.copy(519 x5c = listOf(attCertChain.credCert.encoded, attCertChain.intermediateCa.encoded),520 receipt = resignedReceiptResponse.receipt.p7,521 ),522 authData = authDataFake,523 )524 val appleAppAttest = sample.defaultAppleAppAttest()525 val attestationValidator = appleAppAttest.createAttestationValidator(526 clock = sample.timestamp.fixedUtcClock(),527 receiptValidator = appleAppAttest.createReceiptValidator(528 clock = sample.timestamp.fixedUtcClock(),529 trustAnchor = resignedReceiptResponse.trustAnchor,530 ),531 trustAnchor = TrustAnchor(attCertChain.rootCa, null)532 )533 val exception = shouldThrow<AttestationException.InvalidAuthenticatorData> {534 attestationValidator.validate(535 attestationObject = cborObjectMapper.writeValueAsBytes(attestationObjectFake),536 keyIdBase64 = attCertChain.credCert.createAppleKeyId().toBase64(),537 serverChallenge = sample.clientData,538 )539 }540 exception.shouldHaveMessage("Counter is not zero")541 }542 }543 }544 "Throws InvalidAuthenticatorData for invalid AAGUID" - {545 AttestationSample.all.forEach { sample ->546 "${sample.id}" {547 val attestationValidatorOriginal = sample.defaultValidator()548 val attestationResponse = attestationValidatorOriginal.validate(549 attestationObject = sample.attestation,550 keyIdBase64 = sample.keyId.toBase64(),551 serverChallenge = sample.clientData552 )553 val attestationObject: AttestationObject = cborObjectMapper.readValue(sample.attestation)554 val authData: AuthenticatorData = AuthenticatorData.parse(555 attestationObject.authData,556 cborObjectMapper.readerForMapOf(Any::class.java)557 )558 val credCertKeyPair = CertUtils.generateP256KeyPair()559 val authDataFake = authData.copy(560 attestedCredentialData = authData.attestedCredentialData?.copy(561 credentialId = credCertKeyPair.public.createAppleKeyId(),562 aaguid = UUID.randomUUID(),563 ),564 ).encode()565 val nonceFake = authDataFake.plus(sample.clientData.sha256()).sha256()566 val attCertChain = CertUtils.createCustomAttestationCertificate(567 x5c = attestationObject.attStmt.x5c,568 credCertKeyPair = credCertKeyPair,569 mutatorCredCert = { builder ->570 val fakeNonceEncoded = DLSequence(571 DLTaggedObject(true, 1, DEROctetString(nonceFake))572 ).encoded573 builder.replaceExtension(574 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.NONCE_OID),575 false,576 fakeNonceEncoded577 )578 }579 )580 val resignedReceiptResponse = CertUtils.resignReceipt(581 receipt = attestationResponse.receipt,582 payloadMutator = {583 it.copy(584 attestationCertificate = Receipt.ReceiptAttribute.X509Certificate(585 it.attestationCertificate.sequence.copy(586 value = attCertChain.credCert.encoded587 )588 )589 )590 },591 )592 val attestationObjectFake = attestationObject.copy(593 attStmt = attestationObject.attStmt.copy(594 x5c = listOf(attCertChain.credCert.encoded, attCertChain.intermediateCa.encoded),595 receipt = resignedReceiptResponse.receipt.p7,596 ),597 authData = authDataFake,598 )599 val appleAppAttest = sample.defaultAppleAppAttest()600 val attestationValidator = appleAppAttest.createAttestationValidator(601 clock = sample.timestamp.fixedUtcClock(),602 receiptValidator = appleAppAttest.createReceiptValidator(603 clock = sample.timestamp.fixedUtcClock(),604 trustAnchor = resignedReceiptResponse.trustAnchor,605 ),606 trustAnchor = TrustAnchor(attCertChain.rootCa, null)607 )608 val exception = shouldThrow<AttestationException.InvalidAuthenticatorData> {609 attestationValidator.validate(610 attestationObject = cborObjectMapper.writeValueAsBytes(attestationObjectFake),611 keyIdBase64 = attCertChain.credCert.createAppleKeyId().toBase64(),612 serverChallenge = sample.clientData,613 )614 }615 exception.shouldHaveMessage(616 "AAGUID does match neither ${AppleAppAttestEnvironment.DEVELOPMENT} " +617 "nor ${AppleAppAttestEnvironment.PRODUCTION}"618 )619 }620 }621 }622 "Throws InvalidAuthenticatorData for wrong credentials ID" - {623 AttestationSample.all.forEach { sample ->624 "${sample.id}" {625 val attestationValidatorOriginal = sample.defaultValidator()626 val attestationResponse = attestationValidatorOriginal.validate(627 attestationObject = sample.attestation,628 keyIdBase64 = sample.keyId.toBase64(),629 serverChallenge = sample.clientData630 )631 val attestationObject: AttestationObject = cborObjectMapper.readValue(sample.attestation)632 val authData: AuthenticatorData = AuthenticatorData.parse(633 attestationObject.authData,634 cborObjectMapper.readerForMapOf(Any::class.java)635 )636 val credCertKeyPair = CertUtils.generateP256KeyPair()637 val authDataFake = authData.copy(638 attestedCredentialData = authData.attestedCredentialData?.copy(639 credentialId = CertUtils.generateP256KeyPair().public.createAppleKeyId(),640 ),641 ).encode()642 val nonceFake = authDataFake.plus(sample.clientData.sha256()).sha256()643 val attCertChain = CertUtils.createCustomAttestationCertificate(644 x5c = attestationObject.attStmt.x5c,645 credCertKeyPair = credCertKeyPair,646 mutatorCredCert = { builder ->647 val fakeNonceEncoded = DLSequence(648 DLTaggedObject(true, 1, DEROctetString(nonceFake))649 ).encoded650 builder.replaceExtension(651 ASN1ObjectIdentifier(AttestationValidator.AppleCertificateExtensions.NONCE_OID),652 false,653 fakeNonceEncoded654 )655 }656 )657 val resignedReceiptResponse = CertUtils.resignReceipt(658 receipt = attestationResponse.receipt,659 payloadMutator = {660 it.copy(661 attestationCertificate = Receipt.ReceiptAttribute.X509Certificate(662 it.attestationCertificate.sequence.copy(663 value = attCertChain.credCert.encoded664 )665 )666 )667 },668 )669 val attestationObjectFake = attestationObject.copy(670 attStmt = attestationObject.attStmt.copy(671 x5c = listOf(attCertChain.credCert.encoded, attCertChain.intermediateCa.encoded),672 receipt = resignedReceiptResponse.receipt.p7,673 ),674 authData = authDataFake,675 )676 val appleAppAttest = sample.defaultAppleAppAttest()677 val attestationValidator = appleAppAttest.createAttestationValidator(678 clock = sample.timestamp.fixedUtcClock(),679 receiptValidator = appleAppAttest.createReceiptValidator(680 clock = sample.timestamp.fixedUtcClock(),681 trustAnchor = resignedReceiptResponse.trustAnchor,682 ),683 trustAnchor = TrustAnchor(attCertChain.rootCa, null)684 )685 val exception = shouldThrow<AttestationException.InvalidAuthenticatorData> {686 attestationValidator.validate(687 attestationObject = cborObjectMapper.writeValueAsBytes(attestationObjectFake),688 keyIdBase64 = attCertChain.credCert.createAppleKeyId().toBase64(),689 serverChallenge = sample.clientData,690 )691 }692 exception.shouldHaveMessage("Credentials ID is not equal to Key ID")693 }694 }695 }696 "Throws InvalidCertificateChain for wrong trust anchor" - {697 AttestationSample.all.forEach { sample ->698 "${sample.id}" {699 val appleAppAttest = sample.defaultAppleAppAttest()700 val attestationValidator = appleAppAttest.createAttestationValidator(701 clock = sample.timestamp.fixedUtcClock(),702 receiptValidator = appleAppAttest.createReceiptValidator(703 clock = sample.timestamp.fixedUtcClock(),704 ),705 trustAnchor = ReceiptValidator.APPLE_PUBLIC_ROOT_CA_G3_BUILTIN_TRUST_ANCHOR,706 )707 shouldThrow<AttestationException.InvalidCertificateChain> {708 attestationValidator.validate(sample.attestation, sample.keyId.toBase64(), sample.clientData)709 }710 }711 }712 }713 "Rejects expired attestation" - {714 AttestationSample.all715 .filter { it.timestamp.plus(Duration.ofDays(90)) < Instant.now() }716 .forEach { sample ->717 "${sample.id}" {718 val appleAppAttest = sample.defaultAppleAppAttest()719 val attestationValidator = appleAppAttest.createAttestationValidator()720 shouldThrow<AttestationException.InvalidCertificateChain> {721 attestationValidator.validate(722 attestationObject = sample.attestation,723 keyIdBase64 = sample.keyId.toBase64(),724 serverChallenge = sample.clientData725 )726 }727 }728 }729 }730 }731}...
HttpClientsTests.kt
Source:HttpClientsTests.kt
1package ru.fix.armeria.facade2import com.fasterxml.jackson.annotation.JsonIgnore3import com.fasterxml.jackson.databind.ObjectMapper4import com.linecorp.armeria.client.ResponseTimeoutException5import com.linecorp.armeria.client.UnprocessedRequestException6import com.linecorp.armeria.common.HttpResponse7import com.linecorp.armeria.common.HttpStatus8import com.linecorp.armeria.common.MediaType9import io.kotest.assertions.json.shouldMatchJson10import io.kotest.assertions.throwables.shouldThrowAny11import io.kotest.assertions.timing.eventually12import io.kotest.inspectors.forAll13import io.kotest.matchers.collections.shouldBeOneOf14import io.kotest.matchers.maps.shouldContainAll15import io.kotest.matchers.nulls.shouldNotBeNull16import io.kotest.matchers.should17import io.kotest.matchers.shouldBe18import io.kotest.matchers.throwable.shouldHaveCauseOfType19import io.kotest.matchers.types.shouldBeInstanceOf20import io.kotest.matchers.types.shouldBeTypeOf21import org.apache.logging.log4j.kotlin.Logging22import org.junit.jupiter.api.Test23import retrofit2.converter.jackson.JacksonConverterFactory24import retrofit2.create25import retrofit2.http.Body26import retrofit2.http.POST27import ru.fix.aggregating.profiler.AggregatingProfiler28import ru.fix.armeria.commons.testing.ArmeriaMockServer29import ru.fix.armeria.commons.testing.delayedOn30import ru.fix.armeria.commons.testing.j31import ru.fix.armeria.dynamic.request.endpoint.SocketAddress32import ru.fix.armeria.facade.ProfilerTestUtils.profiledCallReportWithName33import ru.fix.dynamic.property.api.AtomicProperty34import ru.fix.dynamic.property.api.DynamicProperty35import ru.fix.stdlib.ratelimiter.ConfigurableRateLimiter36import ru.fix.stdlib.ratelimiter.RateLimitedDispatcher37import ru.fix.stdlib.socket.SocketChecker38import java.io.IOException39import java.net.ConnectException40import kotlin.time.ExperimentalTime41import kotlin.time.milliseconds42import kotlin.time.seconds43@ExperimentalTime44internal class HttpClientsTest {45 @Test46 suspend fun `client retrying on 503 and unprocessed error with next features - profiled, dynamically configured, rate limited`() {47 val mockServer = ArmeriaMockServer("test-retrying-armeria-mock-server", defaultServicePath = PATH).start()48 val mockServerAddress = SocketAddress("localhost", mockServer.httpPort())49 val nonExistingServerPort = SocketChecker.getAvailableRandomPort()50 val nonExistingServerAddress = SocketAddress("localhost", nonExistingServerPort)51 val addressListProperty = AtomicProperty(listOf(mockServerAddress))52 val profiler = AggregatingProfiler()53 val reporter = profiler.createReporter()54 val wholeRequestTimeoutProperty = AtomicProperty(1.seconds)55 try {56 val clientName = "test-retrying-client"57 val rateLimitedDispatcher = RateLimitedDispatcher(58 clientName,59 ConfigurableRateLimiter("$clientName-rateLimiter", 10),60 profiler,61 DynamicProperty.of(20),62 DynamicProperty.of(1.seconds.toLongMilliseconds())63 )64 HttpClients.builder()65 .setClientName(clientName)66 //dynamic endpoints67 .setDynamicEndpoints(addressListProperty)68 .setIoThreadsCount(2)69 //rate limiting70 .enableRateLimit(rateLimitedDispatcher)71 //connections profiling72 .enableConnectionsProfiling(profiler)73 //retrying74 .withRetriesOn503AndRetriableError(4)75 //profiling requests76 .enableEachAttemptProfiling(profiler)77 .enableWholeRequestProfiling(profiler)78 //dynamic response timeouts79 .withCustomResponseTimeouts()80 .setResponseTimeouts(81 eachAttemptTimeout = 500.milliseconds.j,82 wholeRequestTimeoutProp = wholeRequestTimeoutProperty.map { it.j }83 )84 //retrofit support85 .enableRetrofitSupport()86 .addConverterFactory(JacksonConverterFactory.create(jacksonObjectMapper))87 .enableNamedBlockingResponseReadingExecutor(88 DynamicProperty.of(1),89 profiler,90 DynamicProperty.of(2.seconds.j)91 )92 .buildRetrofit().use { closeableRetrofit ->93 val testEntityApi = closeableRetrofit.retrofit.create<TestEntityCountingApi>()94 // Scenario 1. successful request to existing endpoint95 mockServer.enqueue {96 HttpResponse.of(TestEntity("return value").jsonStr)97 .delayedOn(250.milliseconds)98 }99 val inputTestEntity1 = TestEntity("input value")100 val result1 = testEntityApi.getTestEntity(inputTestEntity1)101 result1.strField shouldBe "return value"102 mockServer.pollRecordedRequest() should {103 it.shouldNotBeNull()104 it.request.contentUtf8() shouldMatchJson inputTestEntity1.jsonStr105 }106 eventually(500.milliseconds) {107 val wholeRequestMetricName = "$clientName.${Metrics.WHOLE_RETRY_SESSION_PREFIX}.http"108 val report = reporter.buildReportAndReset { metric, _ ->109 metric.name == wholeRequestMetricName110 }111 logger.trace { "Report: $report" }112 report.profiledCallReportWithName(wholeRequestMetricName) should {113 it.shouldNotBeNull()114 it.stopSum shouldBe 1115 it.identity.tags shouldContainAll mapOf(116 "remote_port" to mockServerAddress.port.toString(),117 "status" to "200"118 )119 }120 }121 logger.trace { "Full profiler report for Scenario 1: ${reporter.buildReportAndReset()}" }122 // Scenario 2. timeouted response123 wholeRequestTimeoutProperty.set(250.milliseconds)124 val inputTestEntity2 = TestEntity("return value 2")125 mockServer.enqueue {126 HttpResponse.of(inputTestEntity2.jsonStr)127 .delayedOn(400.milliseconds)128 }129 val thrownExc = shouldThrowAny {130 testEntityApi.getTestEntity(inputTestEntity2)131 }132 thrownExc should {133 it.shouldBeTypeOf<IOException>()134 it.shouldHaveCauseOfType<IOException>()135 val cause = it.cause136 cause.shouldNotBeNull()137 cause.shouldHaveCauseOfType<ResponseTimeoutException>()138 }139 mockServer.pollRecordedRequest() should {140 it.shouldNotBeNull()141 it.request.contentUtf8() shouldMatchJson inputTestEntity2.jsonStr142 }143 eventually(500.milliseconds) {144 val attemptErrorMetricName = "$clientName.${Metrics.EACH_RETRY_ATTEMPT_PREFIX}.http.error"145 val report = reporter.buildReportAndReset { metric, _ ->146 metric.name == attemptErrorMetricName && metric.hasTag("error_type", "response_timeout")147 }148 logger.trace { "Report: $report" }149 report.profiledCallReportWithName(attemptErrorMetricName) should {150 it.shouldNotBeNull()151 it.stopSum shouldBe 1152 it.identity.tags shouldContainAll mapOf(153 "remote_port" to mockServerAddress.port.toString()154 )155 }156 }157 eventually(500.milliseconds) {158 val wholeRequestMetricName = "$clientName.${Metrics.WHOLE_RETRY_SESSION_PREFIX}.http.error"159 val report = reporter.buildReportAndReset { metric, _ ->160 metric.name == wholeRequestMetricName && metric.hasTag("error_type", "response_timeout")161 }162 logger.trace { "Report: $report" }163 report.profiledCallReportWithName(wholeRequestMetricName) should {164 it.shouldNotBeNull()165 it.stopSum shouldBe 1166 it.identity.tags shouldContainAll mapOf(167 "remote_port" to mockServerAddress.port.toString()168 )169 }170 }171 logger.trace { "Full profiler report for Scenario 2: ${reporter.buildReportAndReset()}" }172 // Scenario 3. load-balancing and retrying on 503/connect_error173 wholeRequestTimeoutProperty.set(3.seconds)174 addressListProperty.set(listOf(mockServerAddress, nonExistingServerAddress))175 mockServer.enqueue {176 HttpResponse.of(HttpStatus.SERVICE_UNAVAILABLE).delayedOn(100.milliseconds)177 }178 mockServer.enqueue {179 HttpResponse.of(TestEntity("return value 3").jsonStr)180 }181 val inputTestEntity3 = TestEntity("input value 3")182 val result3 = testEntityApi.getTestEntity(inputTestEntity3)183 result3.strField shouldBe "return value 3"184 listOf(185 mockServer.pollRecordedRequest(),186 mockServer.pollRecordedRequest()187 ) should { recordedRequests ->188 recordedRequests.forAll {189 it.shouldNotBeNull()190 it.request.contentUtf8() shouldMatchJson inputTestEntity3.jsonStr191 }192 }193 eventually(500.milliseconds) {194 val attemptErrorMetricName = "$clientName.${Metrics.EACH_RETRY_ATTEMPT_PREFIX}.http.error"195 val report = reporter.buildReportAndReset { metric, _ ->196 metric.name == attemptErrorMetricName && metric.hasTag("error_type", "connect_refused")197 }198 logger.trace { "Report: $report" }199 report.profiledCallReportWithName(attemptErrorMetricName) should {200 it.shouldNotBeNull()201 // load balancing may route request to nonexisting server twice202 it.stopSum.shouldBeOneOf(1, 2)203 it.identity.tags shouldContainAll mapOf(204 "remote_port" to nonExistingServerPort.toString(),205 "error_type" to "connect_refused"206 )207 }208 }209 // TODO due to bug in ProfiledHttpClient remote_port of 1st touched endpoint210 // (possibly failed one) is written to metric211// eventually(500.milliseconds) {212// val wholeRequestMetricName = "$clientName.${Metrics.WHOLE_RETRY_SESSION_PREFIX}.http"213// val report = reporter.buildReportAndReset { metric, _ ->214// metric.name == wholeRequestMetricName215// && metric.hasTag("remote_port", mockServerAddress.port.toString())216// }217// logger.trace { "Report: $report" }218// report.profiledCallReportWithName(wholeRequestMetricName) should {219// it.shouldNotBeNull()220// it.stopSum shouldBe 1221// it.identity.tags shouldContainAll mapOf(222// "status" to "200",223// "remote_port" to mockServerAddress.port.toString()224// )225// }226// }227 }228 } finally {229 logger.trace { "Final profiler report: ${reporter.buildReportAndReset()}" }230 mockServer.stop()231 }232 }233 @Test234 suspend fun `not retrying client with next features - profiled, dynamically configured, rate limited`() {235 val mockServer = ArmeriaMockServer("test-armeria-mock-server", defaultServicePath = PATH).start()236 val mockServerAddress = SocketAddress("localhost", mockServer.httpPort())237 val nonExistingServerPort = SocketChecker.getAvailableRandomPort()238 val nonExistingServerAddress = SocketAddress("localhost", nonExistingServerPort)239 val addressProperty = AtomicProperty(mockServerAddress)240 val profiler = AggregatingProfiler()241 val reporter = profiler.createReporter()242 val responseTimeoutProperty = AtomicProperty(1.seconds)243 try {244 val clientName = "test-client"245 val rateLimitedDispatcher = RateLimitedDispatcher(246 clientName,247 ConfigurableRateLimiter("$clientName-rateLimiter", 10),248 profiler,249 DynamicProperty.of(20),250 DynamicProperty.of(1.seconds.toLongMilliseconds())251 )252 HttpClients.builder()253 .setClientName(clientName)254 //dynamic endpoints255 .setDynamicEndpoint(addressProperty)256 .setIoThreadsCount(2)257 //rate limiting258 .enableRateLimit(rateLimitedDispatcher)259 //connections profiling260 .enableConnectionsProfiling(profiler)261 //retrying262 .withoutRetries()263 //profiling264 .enableRequestsProfiling(profiler)265 //dynamic response timeouts266 .setResponseTimeout(responseTimeoutProperty.map { it.j })267 //retrofit support268 .enableRetrofitSupport()269 .addConverterFactory(JacksonConverterFactory.create(jacksonObjectMapper))270 .enableNamedBlockingResponseReadingExecutor(271 DynamicProperty.of(1),272 profiler,273 DynamicProperty.of(2.seconds.j)274 )275 .buildRetrofit().use { closeableRetrofit ->276 val testEntityApi = closeableRetrofit.retrofit.create<TestEntityCountingApi>()277 // Scenario 1. successful request to existing endpoint278 mockServer.enqueue {279 HttpResponse.of(TestEntity("return value").jsonStr)280 .delayedOn(250.milliseconds)281 }282 val inputTestEntity1 = TestEntity("input value")283 val result1 = testEntityApi.getTestEntity(inputTestEntity1)284 result1.strField shouldBe "return value"285 mockServer.pollRecordedRequest() should {286 it.shouldNotBeNull()287 it.request.contentUtf8() shouldMatchJson inputTestEntity1.jsonStr288 }289 eventually(500.milliseconds) {290 val requestMetricName = "$clientName.http"291 val report = reporter.buildReportAndReset { metric, _ ->292 metric.name == requestMetricName293 }294 logger.trace { "Report: $report" }295 report.profiledCallReportWithName(requestMetricName) should {296 it.shouldNotBeNull()297 it.stopSum shouldBe 1298 it.identity.tags shouldContainAll mapOf(299 "remote_port" to mockServerAddress.port.toString(),300 "status" to "200"301 )302 }303 }304 logger.trace { "Full profiler report for Scenario 1: ${reporter.buildReportAndReset()}" }305 // Scenario 2. timeouted response306 responseTimeoutProperty.set(250.milliseconds)307 val inputTestEntity2 = TestEntity("return value 2")308 mockServer.enqueue {309 HttpResponse.of(inputTestEntity2.jsonStr)310 .delayedOn(400.milliseconds)311 }312 val thrownExc2 = shouldThrowAny {313 testEntityApi.getTestEntity(inputTestEntity2)314 }315 thrownExc2 should {316 it.shouldBeTypeOf<IOException>()317 it.shouldHaveCauseOfType<IOException>()318 val cause = it.cause319 cause.shouldNotBeNull()320 cause.shouldHaveCauseOfType<ResponseTimeoutException>()321 }322 mockServer.pollRecordedRequest() should {323 it.shouldNotBeNull()324 it.request.contentUtf8() shouldMatchJson inputTestEntity2.jsonStr325 }326 eventually(500.milliseconds) {327 val requestMetricName = "$clientName.http.error"328 val report = reporter.buildReportAndReset { metric, _ ->329 metric.name == requestMetricName330 && metric.hasTag("error_type", "response_timeout")331 }332 logger.trace { "Report: $report" }333 report.profiledCallReportWithName(requestMetricName) should {334 it.shouldNotBeNull()335 it.stopSum shouldBe 1336 it.identity.tags shouldContainAll mapOf(337 "remote_port" to mockServerAddress.port.toString()338 )339 }340 }341 logger.trace { "Full profiler report for Scenario 2: ${reporter.buildReportAndReset()}" }342 // Scenario 3. change endpoint property to nonexisting server343 addressProperty.set(nonExistingServerAddress)344 responseTimeoutProperty.set(1.seconds)345 val inputTestEntity3 = TestEntity("return value 3")346 val thrownExc3 = shouldThrowAny {347 testEntityApi.getTestEntity(inputTestEntity3)348 }349 thrownExc3 should {350 it.shouldBeTypeOf<IOException>()351 it.shouldHaveCauseOfType<IOException>()352 it.cause should { cause ->353 cause.shouldNotBeNull()354 cause.shouldHaveCauseOfType<UnprocessedRequestException>()355 (cause.cause as UnprocessedRequestException).cause.shouldBeInstanceOf<ConnectException>()356 }357 }358 eventually(500.milliseconds) {359 val requestMetricName = "$clientName.http.error"360 val report = reporter.buildReportAndReset { metric, _ ->361 metric.name == requestMetricName362 && metric.hasTag("error_type", "connect_refused")363 }364 logger.trace { "Report: $report" }365 report.profiledCallReportWithName(requestMetricName) should {366 it.shouldNotBeNull()367 it.stopSum shouldBe 1368 it.identity.tags shouldContainAll mapOf(369 "remote_port" to nonExistingServerPort.toString()370 )371 }372 }373 logger.trace { "Full profiler report for Scenario 2: ${reporter.buildReportAndReset()}" }374 }375 } finally {376 logger.trace { "Final profiler report: ${reporter.buildReportAndReset()}" }377 mockServer.stop()378 }379 }380 interface TestEntityCountingApi {381 @POST(PATH)382 suspend fun getTestEntity(@Body testEntity: TestEntity): TestEntity383 }384 data class TestEntity(385 val strField: String386 ) {387 @JsonIgnore388 val jsonStr = """{"strField":"$strField"}"""389 }390 companion object: Logging {391 const val PATH = "/getTestEntity"392 val jacksonObjectMapper = ObjectMapper().findAndRegisterModules()393 fun createTestEntityCountedMockServer(mockServerNamePrefix: String): ArmeriaMockServer =394 ArmeriaMockServer(mockServerName = "$mockServerNamePrefix-armeria-mock-server", defaultServicePath = PATH) {395 decorator { delegate, ctx, req ->396 ctx.mutateAdditionalResponseHeaders {397 it.contentType(MediaType.JSON)398 }399 delegate.serve(ctx, req)400 }401 }402 }403}...
ThrowableMatchersTest.kt
Source:ThrowableMatchersTest.kt
1package com.sksamuel.kotest.matchers.throwable2import io.kotest.assertions.throwables.shouldThrow3import io.kotest.assertions.throwables.shouldThrowAny4import io.kotest.assertions.throwables.shouldThrowExactly5import io.kotest.assertions.throwables.shouldThrowWithMessage6import io.kotest.core.spec.style.FreeSpec7import io.kotest.matchers.throwable.shouldHaveCause8import io.kotest.matchers.throwable.shouldHaveCauseInstanceOf9import io.kotest.matchers.throwable.shouldHaveCauseOfType10import io.kotest.matchers.throwable.shouldHaveMessage11import io.kotest.matchers.throwable.shouldNotHaveCause12import io.kotest.matchers.throwable.shouldNotHaveCauseInstanceOf13import io.kotest.matchers.throwable.shouldNotHaveCauseOfType14import io.kotest.matchers.throwable.shouldNotHaveMessage15import java.io.FileNotFoundException16import java.io.IOException17class ThrowableMatchersTest : FreeSpec() {18 init {19 "shouldThrowAny" - {20 "shouldHaveMessage" {21 shouldThrowAny { throw FileNotFoundException("this_file.txt not found") } shouldHaveMessage "this_file.txt not found"22 shouldThrowAny { throw TestException() } shouldHaveMessage "This is a test exception"23 shouldThrowAny { throw CompleteTestException() } shouldHaveMessage "This is a complete test exception"24 }25 "shouldNotHaveMessage" {26 shouldThrowAny { throw FileNotFoundException("this_file.txt not found") } shouldNotHaveMessage "random message"27 shouldThrowAny { throw TestException() } shouldNotHaveMessage "This is a complete test exception"28 shouldThrowAny { throw CompleteTestException() } shouldNotHaveMessage "This is a test exception"29 }30 "shouldHaveCause" {31 shouldThrowAny { throw CompleteTestException() }.shouldHaveCause()32 shouldThrowAny { throw CompleteTestException() }.shouldHaveCause {33 it shouldHaveMessage "file.txt not found"34 }35 }36 "shouldNotHaveCause" {37 shouldThrowAny { throw TestException() }.shouldNotHaveCause()38 shouldThrowAny { throw FileNotFoundException("this_file.txt not found") }.shouldNotHaveCause()39 }40 "shouldHaveCauseInstanceOf" {41 shouldThrowAny { throw CompleteTestException() }.shouldHaveCauseInstanceOf<FileNotFoundException>()42 shouldThrowAny { throw CompleteTestException() }.shouldHaveCauseInstanceOf<IOException>()43 }44 "shouldNotHaveCauseInstanceOf" {45 shouldThrowAny { throw CompleteTestException() }.shouldNotHaveCauseInstanceOf<TestException>()46 }47 "shouldHaveCauseOfType" {48 shouldThrowAny { throw CompleteTestException() }.shouldHaveCauseOfType<FileNotFoundException>()49 }50 "shouldNotHaveCauseOfType" {51 shouldThrowAny { throw CompleteTestException() }.shouldNotHaveCauseOfType<IOException>()52 }53 }54 "shouldThrow" - {55 "shouldHaveMessage" {56 shouldThrow<IOException> { throw FileNotFoundException("this_file.txt not found") } shouldHaveMessage "this_file.txt not found"57 shouldThrow<TestException> { throw TestException() } shouldHaveMessage "This is a test exception"58 shouldThrow<CompleteTestException> { throw CompleteTestException() } shouldHaveMessage "This is a complete test exception"59 shouldThrow<AssertionError> { TestException() shouldHaveMessage "foo" }60 .shouldHaveMessage(61 """Throwable should have message:62"foo"63Actual was:64"This is a test exception"65expected:<"foo"> but was:<"This is a test exception">"""66 )67 }68 "shouldNotHaveMessage" {69 shouldThrow<IOException> { throw FileNotFoundException("this_file.txt not found") } shouldNotHaveMessage "random message"70 shouldThrow<TestException> { throw TestException() } shouldNotHaveMessage "This is a complete test exception"71 shouldThrow<CompleteTestException> { throw CompleteTestException() } shouldNotHaveMessage "This is a test exception"72 shouldThrow<AssertionError> { TestException() shouldNotHaveMessage "This is a test exception" }73 .shouldHaveMessage("Throwable should not have message:\n\"This is a test exception\"")74 }75 "shouldHaveCause" {76 shouldThrow<CompleteTestException> { throw CompleteTestException() }.shouldHaveCause()77 shouldThrow<CompleteTestException> { throw CompleteTestException() }.shouldHaveCause {78 it shouldHaveMessage "file.txt not found"79 }80 shouldThrow<AssertionError> { TestException().shouldHaveCause() }81 .shouldHaveMessage("Throwable should have a cause")82 }83 "shouldNotHaveCause" {84 shouldThrow<TestException> { throw TestException() }.shouldNotHaveCause()85 shouldThrow<IOException> { throw FileNotFoundException("this_file.txt not found") }.shouldNotHaveCause()86 shouldThrow<AssertionError> { CompleteTestException().shouldNotHaveCause() }87 .shouldHaveMessage("Throwable should not have a cause")88 }89 "shouldHaveCauseInstanceOf" {90 shouldThrow<CompleteTestException> { throw CompleteTestException() }.shouldHaveCauseInstanceOf<FileNotFoundException>()91 shouldThrow<CompleteTestException> { throw CompleteTestException() }.shouldHaveCauseInstanceOf<IOException>()92 shouldThrow<AssertionError> { CompleteTestException().shouldHaveCauseInstanceOf<RuntimeException>() }93 .shouldHaveMessage("Throwable cause should be of type java.lang.RuntimeException or it's descendant, but instead got java.io.FileNotFoundException")94 }95 "shouldNotHaveCauseInstanceOf" {96 shouldThrow<CompleteTestException> { throw CompleteTestException() }.shouldNotHaveCauseInstanceOf<TestException>()97 shouldThrow<AssertionError> { CompleteTestException().shouldNotHaveCauseInstanceOf<FileNotFoundException>() }98 .shouldHaveMessage("Throwable cause should not be of type java.io.FileNotFoundException or it's descendant")99 }100 "shouldHaveCauseOfType" {101 shouldThrow<CompleteTestException> { throw CompleteTestException() }.shouldHaveCauseOfType<FileNotFoundException>()102 shouldThrow<AssertionError> { CompleteTestException().shouldHaveCauseOfType<RuntimeException>() }103 .shouldHaveMessage("Throwable cause should be of type java.lang.RuntimeException, but instead got java.io.FileNotFoundException")104 }105 "shouldNotHaveCauseOfType" {106 shouldThrow<CompleteTestException> { throw CompleteTestException() }.shouldNotHaveCauseOfType<IOException>()107 shouldThrow<AssertionError> { CompleteTestException().shouldNotHaveCauseOfType<FileNotFoundException>() }108 .shouldHaveMessage("Throwable cause should not be of type java.io.FileNotFoundException")109 }110 }111 "shouldThrowExactly" - {112 "shouldHaveMessage" {113 shouldThrowExactly<FileNotFoundException> { throw FileNotFoundException("this_file.txt not found") } shouldHaveMessage "this_file.txt not found"114 shouldThrowExactly<TestException> { throw TestException() } shouldHaveMessage "This is a test exception"115 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() } shouldHaveMessage "This is a complete test exception"116 }117 "shouldNotHaveMessage" {118 shouldThrowExactly<FileNotFoundException> { throw FileNotFoundException("this_file.txt not found") } shouldNotHaveMessage "random message"119 shouldThrowExactly<TestException> { throw TestException() } shouldNotHaveMessage "This is a complete test exception"120 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() } shouldNotHaveMessage "This is a test exception"121 }122 "shouldHaveCause" {123 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() }.shouldHaveCause()124 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() }.shouldHaveCause {125 it shouldHaveMessage "file.txt not found"126 }127 }128 "shouldNotHaveCause" {129 shouldThrowExactly<TestException> { throw TestException() }.shouldNotHaveCause()130 shouldThrowExactly<FileNotFoundException> { throw FileNotFoundException("this_file.txt not found") }.shouldNotHaveCause()131 }132 "shouldHaveCauseInstanceOf" {133 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() }.shouldHaveCauseInstanceOf<FileNotFoundException>()134 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() }.shouldHaveCauseInstanceOf<IOException>()135 }136 "shouldNotHaveCauseInstanceOf" {137 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() }.shouldNotHaveCauseInstanceOf<TestException>()138 }139 "shouldHaveCauseOfType" {140 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() }.shouldHaveCauseOfType<FileNotFoundException>()141 }142 "shouldNotHaveCauseOfType" {143 shouldThrowExactly<CompleteTestException> { throw CompleteTestException() }.shouldNotHaveCauseOfType<IOException>()144 }145 }146 "result" - {147 "shouldHaveMessage" {148 Result.failure<Any>(FileNotFoundException("this_file.txt not found"))149 .exceptionOrNull()!! shouldHaveMessage "this_file.txt not found"150 Result.failure<Any>(TestException()).exceptionOrNull()!! shouldHaveMessage "This is a test exception"151 Result.failure<Any>(CompleteTestException())152 .exceptionOrNull()!! shouldHaveMessage "This is a complete test exception"153 }154 "shouldNotHaveMessage" {155 Result.failure<Any>(FileNotFoundException("this_file.txt not found"))156 .exceptionOrNull()!! shouldNotHaveMessage "random message"157 Result.failure<Any>(TestException())158 .exceptionOrNull()!! shouldNotHaveMessage "This is a complete test exception"159 Result.failure<Any>(CompleteTestException())160 .exceptionOrNull()!! shouldNotHaveMessage "This is a test exception"161 }162 "shouldHaveCause" {163 Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!.shouldHaveCause()164 Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!.shouldHaveCause {165 it shouldHaveMessage "file.txt not found"166 }167 }168 "shouldNotHaveCause" {169 Result.failure<Any>(TestException()).exceptionOrNull()!!.shouldNotHaveCause()170 Result.failure<Any>(FileNotFoundException("this_file.txt not found")).exceptionOrNull()!!171 .shouldNotHaveCause()172 }173 "shouldHaveCauseInstanceOf" {174 Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!175 .shouldHaveCauseInstanceOf<FileNotFoundException>()176 Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!.shouldHaveCauseInstanceOf<IOException>()177 }178 "shouldNotHaveCauseInstanceOf" {179 Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!180 .shouldNotHaveCauseInstanceOf<TestException>()181 }182 "shouldHaveCauseOfType" {183 Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!184 .shouldHaveCauseOfType<FileNotFoundException>()185 }186 "shouldNotHaveCauseOfType" {187 Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!.shouldNotHaveCauseOfType<IOException>()188 }189 }190 "shouldThrowWithMessage" {191 shouldThrowWithMessage<TestException>("This is a test exception") {192 throw TestException()193 } shouldHaveMessage "This is a test exception"194 }195 }196 class TestException : Throwable("This is a test exception")197 class CompleteTestException :198 Throwable("This is a complete test exception", FileNotFoundException("file.txt not found"))199}...
matchers.kt
Source:matchers.kt
1package io.kotest.matchers.throwable2import io.kotest.assertions.show.show3import io.kotest.matchers.Matcher4import io.kotest.matchers.MatcherResult5import io.kotest.matchers.should6import io.kotest.matchers.shouldNot7infix fun Throwable.shouldHaveMessage(message: String) = this should haveMessage(message)8infix fun Throwable.shouldNotHaveMessage(message: String) = this shouldNot haveMessage(message)9fun haveMessage(message: String) = object : Matcher<Throwable> {10 override fun test(value: Throwable) = MatcherResult(11 value.message == message,12 "Throwable should have message ${message.show().value}, but instead got ${value.message.show().value}",13 "Throwable should not have message ${message.show().value}"14 )15}16fun Throwable.shouldHaveCause(block: (Throwable) -> Unit = {}) {17 this should haveCause()18 block.invoke(cause!!)19}20fun Throwable.shouldNotHaveCause() = this shouldNot haveCause()21fun haveCause() = object : Matcher<Throwable> {22 override fun test(value: Throwable) = resultForThrowable(value.cause)23}24inline fun <reified T : Throwable> Throwable.shouldHaveCauseInstanceOf() = this should haveCauseInstanceOf<T>()25inline fun <reified T : Throwable> Throwable.shouldNotHaveCauseInstanceOf() = this shouldNot haveCauseInstanceOf<T>()26inline fun <reified T : Throwable> haveCauseInstanceOf() = object : Matcher<Throwable> {27 override fun test(value: Throwable) = when {28 value.cause == null -> resultForThrowable(value.cause)29 else -> MatcherResult(30 value.cause is T,31 "Throwable cause should be of type ${T::class}, but instead got ${value::class}",32 "Throwable cause should be of type ${T::class}"33 )34 }35}36inline fun <reified T : Throwable> Throwable.shouldHaveCauseOfType() = this should haveCauseOfType<T>()37inline fun <reified T : Throwable> Throwable.shouldNotHaveCauseOfType() = this shouldNot haveCauseOfType<T>()38inline fun <reified T : Throwable> haveCauseOfType() = object : Matcher<Throwable> {39 override fun test(value: Throwable) = when (value.cause) {40 null -> resultForThrowable(value.cause)41 else -> MatcherResult(42 value.cause!!::class == T::class,43 "Throwable cause should be of type ${T::class}, but instead got ${value::class}",44 "Throwable cause should be of type ${T::class}"45 )46 }47}48@PublishedApi49internal fun resultForThrowable(value: Throwable?) = MatcherResult(50 value != null,51 "Throwable should have a cause",52 "Throwable should not have a cause"53)...
Throwable.shouldHaveCauseOfType
Using AI Code Generation
1 Throwable.shouldHaveCauseOfType<IOException>()2 Throwable.shouldHaveCauseInstanceOf<IOException>()3 Throwable.shouldHaveCauseMessage("message")4 Throwable.shouldHaveCauseMessageContaining("message")5 Throwable.shouldHaveCauseMessageMatching("message")6 Throwable.shouldHaveCauseMessageStartingWith("message")7 Throwable.shouldHaveCauseMessageEndingWith("message")8 Throwable.shouldHaveCauseMessageMatching("message")9 Throwable.shouldHaveCauseMessageMatching("message")10 Throwable.shouldHaveCauseMessageMatching("message")11 Throwable.shouldHaveCauseMessageMatching("message")12 Throwable.shouldHaveCauseMessageMatching("message")13 Throwable.shouldHaveCauseMessageMatching("message")14 Throwable.shouldHaveCauseMessageMatching("message")15 Throwable.shouldHaveCauseMessageMatching("message
Throwable.shouldHaveCauseOfType
Using AI Code Generation
1 }2 fun `shouldHaveMessage`(){3 }4 fun `shouldHaveMessageContaining`(){5 }6 fun `shouldHaveMessageStartingWith`(){7 }8 fun `shouldHaveNoCause`(){9 }10 fun `shouldHaveSuppressedException`(){11 }12 fun `shouldHaveSuppressedExceptionOfType`(){13 }14 fun `shouldHaveSuppressedExceptionWithMessage`(){15 }16 fun `shouldHaveSuppressedExceptionWithMessageContaining`(){17 }18 fun `shouldHaveSuppressedExceptionWithMessageStartingWith`(){19 }20 fun `shouldHaveSuppressedExceptionWithMessageMatching`(){21 }22 fun `shouldHaveSuppressedExceptionWithMessageMatching`(){23 }
Throwable.shouldHaveCauseOfType
Using AI Code Generation
1 Throwable.shouldHaveCauseOfType<IllegalArgumentException>()2 Throwable.shouldHaveMessage("message")3 Throwable.shouldHaveMessageContaining("message")4 Throwable.shouldHaveMessageStartingWith("message")5 Throwable.shouldHaveMessageEndingWith("message")6 Throwable.shouldHaveMessageMatching("message")7 Throwable.shouldHaveMessage("message")8 Throwable.shouldHaveMessageContaining("message")9 Throwable.shouldHaveMessageStartingWith("message")10 Throwable.shouldHaveMessageEndingWith("message")11 Throwable.shouldHaveMessageMatching("message")12 Throwable.shouldHaveNoCause()13 Throwable.shouldHaveSuppressedException<IllegalArgumentException>()14 Throwable.shouldHaveSuppressedException<IllegalArgumentException>()15 Throwable.shouldHaveSuppressedException<IllegalArgumentException>()
Throwable.shouldHaveCauseOfType
Using AI Code Generation
1import io.kotest.matchers.throwable.*2import io.kotest.core.spec.style.StringSpec3import io.kotest.matchers.shouldBe4class ThrowableMatchersTest : StringSpec() {5 init {6 "should have cause of type" {7 val t = Throwable(Throwable("boom"))8 }9 }10}
Throwable.shouldHaveCauseOfType
Using AI Code Generation
1 val exception = shouldThrow<IllegalArgumentException> {2 }3 exception.shouldHaveCauseOfType<IllegalStateException>()4}5@DisplayName("should throw an exception with a cause of the specified type")6fun shouldThrowCauseOfType() {7 val exception = shouldThrow<IllegalArgumentException> {8 }9 exception.shouldHaveCauseOfType<IllegalStateException>()10}11@DisplayName("should throw an exception with a cause message")12fun shouldThrowCauseMessage() {13 val exception = shouldThrow<IllegalArgumentException> {14 }15 exception.shouldHaveCauseMessage("cause message")16}17@DisplayName("should throw an exception with a cause message that matches")18fun shouldThrowCauseMessageThatMatches() {19 val exception = shouldThrow<IllegalArgumentException> {20 }21 exception.shouldHaveCauseMessageThatMatches("cause message")22}23@DisplayName("should throw an exception with a cause message that matches")24fun shouldThrowCauseMessageThatMatches() {25 val exception = shouldThrow<IllegalArgumentException> {26 }27 exception.shouldHaveCauseMessageThatMatches("cause message")28}29@DisplayName("should throw an exception with a cause message that matches")30fun shouldThrowCauseMessageThatMatches() {31 val exception = shouldThrow<IllegalArgumentException> {32 }33 exception.shouldHaveCauseMessageThatMatches("cause message")34}35@DisplayName("should throw an exception with a cause message that matches")36fun shouldThrowCauseMessageThatMatches() {37 val exception = shouldThrow<IllegalArgumentException> {38 }39 exception.shouldHaveCauseMessageThatMatches("cause message")40}41@DisplayName("should throw an exception with a cause message that matches")
Throwable.shouldHaveCauseOfType
Using AI Code Generation
1 shouldThrow<InvalidSyntaxException> {2 parse("1 + ")3 }.shouldHaveCauseOfType<InvalidSyntaxException>()4 }5}6fun main() {7 test()8}9fun test() {10 shouldThrow<InvalidSyntaxException> {11 parse("1 + ")12 }.shouldHaveCauseInstanceOf<InvalidSyntaxException>()13}14fun main() {15 test()16}17fun test() {18 shouldThrow<InvalidSyntaxException> {19 parse("1 + ")20 }.shouldHaveCauseMessage("Invalid syntax")21}22fun main() {23 test()24}25fun test() {26 shouldThrow<InvalidSyntaxException> {27 parse("1 + ")28 }.shouldHaveCauseMessageContaining("syntax")29}
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!