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("messageThrowable.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!!
