Moved the CZ URL and node registration logic of the driver to be more internal, not available through the standard driver call, as these are not testing features for an app dev.

Also cleanup up some of the related tests.
This commit is contained in:
Shams Asari
2017-12-05 16:58:35 +00:00
parent 66515d24b5
commit 89256a7f16
13 changed files with 304 additions and 282 deletions

View File

@ -4,85 +4,96 @@ import net.corda.core.node.NodeInfo
import net.corda.core.utilities.seconds
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.driver.CompatibilityZoneParams
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.testing.driver.internalDriver
import net.corda.testing.node.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.net.URL
// TODO There is a unit test class with the same name. Rename this to something else.
class NetworkMapClientTest {
private val cacheTimeout = 1.seconds
private val portAllocation = PortAllocation.Incremental(10000)
private lateinit var networkMapServer: NetworkMapServer
private lateinit var compatibilityZone: CompatibilityZoneParams
@Before
fun start() {
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
val address = networkMapServer.start()
compatibilityZone = CompatibilityZoneParams(URL("http://$address"), rootCert = null)
}
@After
fun cleanUp() {
networkMapServer.close()
}
@Test
fun `nodes can see each other using the http network map`() {
NetworkMapServer(1.seconds, portAllocation.nextHostAndPort()).use {
val (host, port) = it.start()
driver(portAllocation = portAllocation, compatibilityZoneURL = URL("http://$host:$port")) {
val alice = startNode(providedName = ALICE.name)
val bob = startNode(providedName = BOB.name)
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) {
val alice = startNode(providedName = ALICE.name)
val bob = startNode(providedName = BOB.name)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
val bobNode = bob.get()
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
val bobNode = bob.get()
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
}
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
}
}
@Test
fun `nodes process network map add updates correctly when adding new node to network map`() {
NetworkMapServer(1.seconds, portAllocation.nextHostAndPort()).use {
val (host, port) = it.start()
driver(portAllocation = portAllocation, compatibilityZoneURL = URL("http://$host:$port")) {
val alice = startNode(providedName = ALICE.name)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) {
val alice = startNode(providedName = ALICE.name)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo)
val bob = startNode(providedName = BOB.name)
val bobNode = bob.get()
val bob = startNode(providedName = BOB.name)
val bobNode = bob.get()
// Wait for network map client to poll for the next update.
Thread.sleep(2.seconds.toMillis())
// Wait for network map client to poll for the next update.
Thread.sleep(cacheTimeout.toMillis() * 2)
bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
}
bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
}
}
@Test
fun `nodes process network map remove updates correctly`() {
NetworkMapServer(1.seconds, portAllocation.nextHostAndPort()).use {
val (host, port) = it.start()
driver(portAllocation = portAllocation, compatibilityZoneURL = URL("http://$host:$port")) {
val alice = startNode(providedName = ALICE.name)
val bob = startNode(providedName = BOB.name)
internalDriver(portAllocation = portAllocation, compatibilityZone = compatibilityZone) {
val alice = startNode(providedName = ALICE.name)
val bob = startNode(providedName = BOB.name)
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
val bobNode = bob.get()
val notaryNode = defaultNotaryNode.get()
val aliceNode = alice.get()
val bobNode = bob.get()
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
notaryNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
aliceNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
bobNode.onlySees(notaryNode.nodeInfo, aliceNode.nodeInfo, bobNode.nodeInfo)
it.removeNodeInfo(aliceNode.nodeInfo)
networkMapServer.removeNodeInfo(aliceNode.nodeInfo)
// Wait for network map client to poll for the next update.
Thread.sleep(2.seconds.toMillis())
// Wait for network map client to poll for the next update.
Thread.sleep(cacheTimeout.toMillis() * 2)
notaryNode.onlySees(notaryNode.nodeInfo, bobNode.nodeInfo)
bobNode.onlySees(notaryNode.nodeInfo, bobNode.nodeInfo)
}
notaryNode.onlySees(notaryNode.nodeInfo, bobNode.nodeInfo)
bobNode.onlySees(notaryNode.nodeInfo, bobNode.nodeInfo)
}
}

View File

@ -4,7 +4,7 @@ import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.cert
import net.corda.core.internal.toX509CertHolder
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.minutes
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
@ -13,12 +13,11 @@ import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.testing.ALICE_NAME
import net.corda.testing.driver.CompatibilityZoneParams
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.testing.driver.internalDriver
import net.corda.testing.node.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.After
@ -30,37 +29,26 @@ import java.net.URL
import java.security.KeyPair
import java.security.cert.CertPath
import java.security.cert.Certificate
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.ws.rs.*
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
private const val REQUEST_ID = "requestId"
private val x509CertificateFactory = X509CertificateFactory()
private val portAllocation = PortAllocation.Incremental(13000)
/**
* Driver based tests for [NetworkRegistrationHelper]
*/
// TODO Rename this to NodeRegistrationTest
class NetworkRegistrationHelperDriverTest {
val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate()
val rootCert = rootCertAndKeyPair.certificate
val handler = RegistrationHandler(rootCertAndKeyPair)
lateinit var server: NetworkMapServer
lateinit var host: String
var port: Int = 0
val compatibilityZoneUrl get() = URL("http", host, port, "")
private val portAllocation = PortAllocation.Incremental(13000)
private val rootCertAndKeyPair = createSelfKeyAndSelfSignedCertificate()
private val registrationHandler = RegistrationHandler(rootCertAndKeyPair)
private lateinit var server: NetworkMapServer
private lateinit var compatibilityZone: CompatibilityZoneParams
@Before
fun startServer() {
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), handler)
val (host, port) = server.start()
this.host = host
this.port = port
server = NetworkMapServer(1.minutes, portAllocation.nextHostAndPort(), registrationHandler)
val address = server.start()
compatibilityZone = CompatibilityZoneParams(URL("http://$address"), rootCertAndKeyPair.certificate.cert)
}
@After
@ -68,115 +56,84 @@ class NetworkRegistrationHelperDriverTest {
server.close()
}
// TODO Ideally this test should be checking that two nodes that register are able to transact with each other. However
// starting a second node hangs so that needs to be fixed.
@Test
fun `node registration correct root cert`() {
driver(portAllocation = portAllocation,
compatibilityZoneURL = compatibilityZoneUrl,
startNodesInProcess = true,
rootCertificate = rootCert
internalDriver(
portAllocation = portAllocation,
notarySpecs = emptyList(),
compatibilityZone = compatibilityZone
) {
startNode(providedName = ALICE_NAME, initialRegistration = true).get()
}
// We're getting:
// a request to sign the certificate then
// at least one poll request to see if the request has been approved.
// all the network map registration and download.
assertThat(handler.requests).startsWith("/certificate", "/certificate/" + REQUEST_ID)
}
@Test
fun `node registration without root cert`() {
driver(portAllocation = portAllocation,
compatibilityZoneURL = compatibilityZoneUrl,
startNodesInProcess = true
) {
assertThatThrownBy {
startNode(providedName = ALICE_NAME, initialRegistration = true).get()
}.isInstanceOf(java.nio.file.NoSuchFileException::class.java)
startNode(providedName = CordaX500Name("Alice", "London", "GB")).getOrThrow()
assertThat(registrationHandler.idsPolled).contains("Alice")
}
}
@Test
fun `node registration wrong root cert`() {
driver(portAllocation = portAllocation,
compatibilityZoneURL = compatibilityZoneUrl,
startNodesInProcess = true,
rootCertificate = createSelfKeyAndSelfSignedCertificate().certificate
) {
assertThatThrownBy {
startNode(providedName = ALICE_NAME, initialRegistration = true).get()
}.isInstanceOf(WrongRootCaCertificateException::class.java)
}
private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(
CordaX500Name(
commonName = "Integration Test Corda Node Root CA",
organisation = "R3 Ltd",
locality = "London",
country = "GB"),
rootCAKey)
return CertificateAndKeyPair(rootCACert, rootCAKey)
}
}
/**
* Simple registration handler which can handle a single request, which will be given request id [REQUEST_ID].
*/
@Path("certificate")
class RegistrationHandler(private val certificateAndKeyPair: CertificateAndKeyPair) {
val requests = mutableListOf<String>()
lateinit var certificationRequest: JcaPKCS10CertificationRequest
class RegistrationHandler(private val rootCertAndKeyPair: CertificateAndKeyPair) {
private val certPaths = HashMap<String, CertPath>()
val idsPolled = HashSet<String>()
@POST
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.TEXT_PLAIN)
fun registration(input: InputStream): Response {
requests += "/certificate"
certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) }
return Response.ok(REQUEST_ID).build()
val certificationRequest = input.use { JcaPKCS10CertificationRequest(it.readBytes()) }
val (certPath, name) = createSignedClientCertificate(
certificationRequest,
rootCertAndKeyPair.keyPair,
arrayOf(rootCertAndKeyPair.certificate.cert))
certPaths[name.organisation] = certPath
return Response.ok(name.organisation).build()
}
@GET
@Path(REQUEST_ID)
fun reply(): Response {
requests += "/certificate/" + REQUEST_ID
val certPath = createSignedClientCertificate(certificationRequest,
certificateAndKeyPair.keyPair, arrayOf(certificateAndKeyPair.certificate.cert))
return buildDoormanReply(certPath.certificates.toTypedArray())
@Path("{id}")
fun reply(@PathParam("id") id: String): Response {
idsPolled += id
return buildResponse(certPaths[id]!!.certificates)
}
}
// TODO this logic is shared with doorman itself, refactor this to be somewhere where both doorman and these tests
// can depend on
private fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest,
caKeyPair: KeyPair,
caCertPath: Array<Certificate>): CertPath {
val request = JcaPKCS10CertificationRequest(certificationRequest)
val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.CLIENT_CA,
caCertPath.first().toX509CertHolder(),
caKeyPair,
CordaX500Name.parse(request.subject.toString()).copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN),
request.publicKey,
nameConstraints = null)
return x509CertificateFactory.generateCertPath(x509CertificateHolder.cert, *caCertPath)
}
// TODO this logic is shared with doorman itself, refactor this to be somewhere where both doorman and these tests
// can depend on
private fun buildDoormanReply(certificates: Array<Certificate>): Response {
// Write certificate chain to a zip stream and extract the bit array output.
val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zip ->
// Client certificate must come first and root certificate should come last.
listOf(CORDA_CLIENT_CA, CORDA_INTERMEDIATE_CA, CORDA_ROOT_CA).zip(certificates).forEach {
zip.putNextEntry(ZipEntry("${it.first}.cer"))
zip.write(it.second.encoded)
zip.closeEntry()
private fun buildResponse(certificates: List<Certificate>): Response {
val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use { zip ->
listOf(CORDA_CLIENT_CA, CORDA_INTERMEDIATE_CA, CORDA_ROOT_CA).zip(certificates).forEach {
zip.putNextEntry(ZipEntry("${it.first}.cer"))
zip.write(it.second.encoded)
zip.closeEntry()
}
}
return Response.ok(baos.toByteArray())
.type("application/zip")
.header("Content-Disposition", "attachment; filename=\"certificates.zip\"").build()
}
return Response.ok(baos.toByteArray())
.type("application/zip")
.header("Content-Disposition", "attachment; filename=\"certificates.zip\"").build()
}
private fun createSelfKeyAndSelfSignedCertificate(): CertificateAndKeyPair {
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val rootCACert = X509Utilities.createSelfSignedCACertificate(
CordaX500Name(commonName = "Integration Test Corda Node Root CA",
organisation = "R3 Ltd", locality = "London",
country = "GB"), rootCAKey)
return CertificateAndKeyPair(rootCACert, rootCAKey)
private fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest,
caKeyPair: KeyPair,
caCertPath: Array<Certificate>): Pair<CertPath, CordaX500Name> {
val request = JcaPKCS10CertificationRequest(certificationRequest)
val name = CordaX500Name.parse(request.subject.toString())
val x509CertificateHolder = X509Utilities.createCertificate(CertificateType.CLIENT_CA,
caCertPath.first().toX509CertHolder(),
caKeyPair,
name,
request.publicKey,
nameConstraints = null)
val certPath = X509CertificateFactory().generateCertPath(x509CertificateHolder.cert, *caCertPath)
return Pair(certPath, name)
}
}

View File

@ -28,10 +28,18 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key"
}
init {
require(config.rootCertFile.exists()) {
"${config.rootCertFile} does not exist. This file must contain the root CA cert of your compatibility zone. " +
"Please contact your CZ operator."
}
}
private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt"
private val keystorePassword = config.keyStorePassword
// TODO: Use different password for private key.
private val privateKeyPassword = config.keyStorePassword
private val rootCert = X509Utilities.loadCertificateFromPEMFile(config.rootCertFile)
/**
* Ensure the initial keystore for a node is set up; note that this function may cause the process to exit under
@ -106,12 +114,11 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
/**
* Checks that the passed Certificate is the expected root CA.
* @throws WrongRootCaCertificateException if the certificates don't match.
* @throws WrongRootCertException if the certificates don't match.
*/
private fun checkReturnedRootCaMatchesExpectedCa(returnedRootCa: Certificate) {
val expected = X509Utilities.loadCertificateFromPEMFile(config.rootCaCertFile).cert
if (expected != returnedRootCa) {
throw WrongRootCaCertificateException(expected, returnedRootCa, config.rootCaCertFile)
if (rootCert != returnedRootCa) {
throw WrongRootCertException(rootCert, returnedRootCa, config.rootCertFile)
}
}
@ -173,9 +180,9 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
* Exception thrown when the doorman root certificate doesn't match the expected (out-of-band) root certificate.
* This usually means the has been a Man-in-the-middle attack when contacting the doorman.
*/
class WrongRootCaCertificateException(expected: Certificate,
actual: Certificate,
expectedFilePath: Path):
class WrongRootCertException(expected: Certificate,
actual: Certificate,
expectedFilePath: Path):
Exception("""
The Root CA returned back from the registration process does not match the expected Root CA
expected: $expected

View File

@ -1,53 +1,56 @@
package net.corda.node.utilities.registration
import com.nhaarman.mockito_kotlin.*
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.eq
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.*
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.getX509Certificate
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.ALICE
import net.corda.testing.rigorousMock
import net.corda.testing.testNodeConfiguration
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.security.cert.Certificate
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
val X500Name.commonName: String? get() = getRDNs(BCStyle.CN).firstOrNull()?.first?.value?.toString()
class NetworkRegistrationHelperTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test
fun buildKeyStore() {
val id = SecureHash.randomSHA256().toString()
private val requestId = SecureHash.randomSHA256().toString()
private lateinit var config: NodeConfiguration
@Before
fun init() {
config = testNodeConfiguration(baseDirectory = tempFolder.root.toPath(), myLegalName = ALICE.name)
}
@Test
fun `successful registration`() {
val identities = listOf("CORDA_CLIENT_CA",
"CORDA_INTERMEDIATE_CA",
"CORDA_ROOT_CA")
.map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") }
val certs = identities.stream().map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) }
.map { it.cert }.toTypedArray()
val certService = rigorousMock<NetworkRegistrationService>().also {
doReturn(id).whenever(it).submitRequest(any())
doReturn(certs).whenever(it).retrieveCertificates(eq(id))
}
val config = testNodeConfiguration(
baseDirectory = tempFolder.root.toPath(),
myLegalName = ALICE.name)
val certService = mockRegistrationResponse(*certs)
config.rootCaCertFile.parent.createDirectories()
X509Utilities.saveCertificateAsPEMFile(certs.last().toX509CertHolder(), config.rootCaCertFile)
config.rootCertFile.parent.createDirectories()
X509Utilities.saveCertificateAsPEMFile(certs.last(), config.rootCertFile)
assertFalse(config.nodeKeystore.exists())
assertFalse(config.sslKeystore.exists())
@ -92,4 +95,44 @@ class NetworkRegistrationHelperTest {
assertTrue(containsAlias(X509Utilities.CORDA_ROOT_CA))
}
}
@Test
fun `rootCertFile doesn't exist`() {
val certService = rigorousMock<NetworkRegistrationService>()
assertThatThrownBy {
NetworkRegistrationHelper(config, certService)
}.hasMessageContaining(config.rootCertFile.toString())
}
@Test
fun `root cert in response doesn't match expected`() {
val identities = listOf("CORDA_CLIENT_CA",
"CORDA_INTERMEDIATE_CA",
"CORDA_ROOT_CA")
.map { CordaX500Name(commonName = it, organisation = "R3 Ltd", locality = "London", country = "GB") }
val certs = identities.stream().map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) }
.map { it.cert }.toTypedArray()
val certService = mockRegistrationResponse(*certs)
config.rootCertFile.parent.createDirectories()
X509Utilities.saveCertificateAsPEMFile(
X509Utilities.createSelfSignedCACertificate(
CordaX500Name("CORDA_ROOT_CA", "R3 Ltd", "London", "GB"),
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)).cert,
config.rootCertFile
)
assertThatThrownBy {
NetworkRegistrationHelper(config, certService).buildKeystore()
}.isInstanceOf(WrongRootCertException::class.java)
}
private fun mockRegistrationResponse(vararg response: Certificate): NetworkRegistrationService {
return rigorousMock<NetworkRegistrationService>().also {
doReturn(requestId).whenever(it).submitRequest(any())
doReturn(response).whenever(it).retrieveCertificates(eq(requestId))
}
}
}