Handle rejected jira issues in doorman (#371)

* handle reject status from jira - WIP

* fix up after rebase

* address PR issue and fix build error after rebase
This commit is contained in:
Patrick Kuo 2018-01-22 10:45:25 +00:00 committed by GitHub
parent 3094e44115
commit 43604ed212
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 231 additions and 66 deletions

View File

@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro
projectCode = "TD"
username = "username"
password = "password"
doneTransitionCode = 41
}
.
.
.
}
```
#### JIRA project configuration
* The JIRA project should setup as "Business Project" with "Task" workflow.
* Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without these custom fields.
### Auto approval
When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a test environment)
@ -118,7 +120,6 @@ doormanConfig {
projectCode = "TD"
username = "username"
password = "password"
doneTransitionCode = 41
}
}

View File

@ -88,7 +88,7 @@ dependencies {
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
testCompile "com.spotify:docker-client:8.9.1"
compile('com.atlassian.jira:jira-rest-java-client-core:4.0.0') {
compile('com.atlassian.jira:jira-rest-java-client-core:5.0.4') {
// The jira client includes jersey-core 1.5 which breaks everything.
exclude module: 'jersey-core'
}

View File

@ -23,7 +23,6 @@ doormanConfig{
projectCode = "TD"
username = "username"
password = "password"
doneTransitionCode = 41
}
}

View File

@ -59,7 +59,7 @@ interface CertificationRequestStorage {
* @param rejectedBy authority (its identifier) rejecting this request.
* @param rejectReason brief description of the rejection reason
*/
fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String)
fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?)
/**
* Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED].

View File

@ -93,7 +93,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
}
}
override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) {
override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) {
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = findRequest(requestId)
request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.")

View File

@ -65,8 +65,7 @@ data class JiraConfig(
val address: String,
val projectCode: String,
val username: String,
val password: String,
val doneTransitionCode: Int
val password: String
)
/**

View File

@ -1,13 +1,15 @@
package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.api.IssueRestClient
import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.Comment
import com.atlassian.jira.rest.client.api.domain.Field
import com.atlassian.jira.rest.client.api.domain.Issue
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
@ -17,15 +19,20 @@ import java.io.StringWriter
import java.security.cert.CertPath
import javax.security.auth.x500.X500Principal
class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) {
class JiraClient(private val restClient: JiraRestClient, private val projectCode: String) {
companion object {
val logger = loggerFor<JiraClient>()
val logger = contextLogger()
}
// The JIRA project must have a Request ID field and the Task issue type.
// The JIRA project must have a Request ID and reject reason field, and the Task issue type.
private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'")
private val rejectReasonField: Field = restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" } ?: throw IllegalArgumentException("Reject Reason field not found in JIRA '$projectCode'")
private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'")
private var doneTransitionId: Int = -1
private var canceledTransitionId: Int = -1
private var startProgressTransitionId: Int = -1
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Check there isn't already a ticket for this request.
if (getIssueById(requestId) != null) {
@ -54,14 +61,26 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
fun getApprovedRequests(): List<Pair<String, String>> {
fun getApprovedRequests(): List<ApprovedRequest> {
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
return issues.map { issue ->
issue.getField(requestIdField.id)?.value?.toString().let {
val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.")
val approvedBy = issue.assignee?.displayName ?: "Unknown"
Pair(requestId, approvedBy)
}
return issues.mapNotNull { issue ->
val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
// Issue retrieved via search doesn't contain change logs.
val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } }
ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown")
}
}
fun getRejectedRequests(): List<RejectedRequest> {
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Rejected").claim().issues
return issues.mapNotNull { issue ->
val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
val rejectedReason = issue.getField(rejectReasonField.id)?.value?.toString()
// Issue retrieved via search doesn't contain comments.
val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
val rejectedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Rejected" } }
RejectedRequest(requestId, rejectedBy?.author?.displayName ?: "Unknown", rejectedReason)
}
}
@ -69,16 +88,41 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
// Retrieving certificates for signed CSRs to attach to the jira tasks.
signedRequests.forEach { (id, certPath) ->
val certificate = certPath.certificates.first()
// Jira only support ~ (contains) search for custom textfield.
val issue = getIssueById(id)
if (issue != null) {
restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
if (doneTransitionId == -1) {
doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id
}
restClient.issueClient.transition(issue, TransitionInput(doneTransitionId)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate.encoded.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Error processing request '${issue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
}
}
}
private fun getIssueById(requestId: String): Issue? =
restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
fun updateRejectedRequests(rejectedRequests: List<String>) {
rejectedRequests.mapNotNull { getIssueById(it) }
.forEach { issue ->
// Move status to in progress.
if (startProgressTransitionId == -1) {
startProgressTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Start Progress" }.id
}
restClient.issueClient.transition(issue, TransitionInput(startProgressTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
// Move status to cancelled.
if (canceledTransitionId == -1) {
canceledTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Stop Progress" }.id
}
restClient.issueClient.transition(issue, TransitionInput(canceledTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
restClient.issueClient.addComment(issue.commentsUri, Comment.valueOf("Request cancelled by doorman.")).claim()
}
}
private fun getIssueById(requestId: String): Issue? {
// Jira only support ~ (contains) search for custom textfield.
return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
}
}
data class ApprovedRequest(val requestId: String, val approvedBy: String)
data class RejectedRequest(val requestId: String, val rejectedBy: String, val reason: String?)

View File

@ -91,7 +91,7 @@ class NetworkManagementServer : Closeable {
val jiraConfig = config.jiraConfig
val requestProcessor = if (jiraConfig != null) {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode)
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey))
} else {
DefaultCsrHandler(requestService, csrCertPathAndKey)
@ -101,10 +101,8 @@ class NetworkManagementServer : Closeable {
val approvalThread = Runnable {
try {
serverStatus.lastRequestCheckTime = Instant.now()
// Create tickets for requests which don't have one yet.
requestProcessor.createTickets()
// Process Jira approved tickets.
requestProcessor.processApprovedRequests()
requestProcessor.processRequests()
} catch (e: Exception) {
// Log the error and carry on.
logger.error("Error encountered when approving request.", e)

View File

@ -18,25 +18,23 @@ import javax.security.auth.x500.X500Principal
interface CsrHandler {
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
fun createTickets()
fun processApprovedRequests()
fun processRequests()
fun getResponse(requestId: String): CertificateResponse
}
class DefaultCsrHandler(private val storage: CertificationRequestStorage,
private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
override fun processApprovedRequests() {
override fun processRequests() {
if (csrCertPathAndKey == null) return
storage.getRequests(RequestStatus.APPROVED).forEach {
val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey)
// Since Doorman is deployed in the auto-signing mode, we use DOORMAN_SIGNATURE as the signer.
// Since Doorman is deployed in the auto-signing mode (i.e. signer != null),
// we use DOORMAN_SIGNATURE as the signer.
storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE))
}
}
override fun createTickets() {}
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
override fun getResponse(requestId: String): CertificateResponse {

View File

@ -4,7 +4,9 @@ import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.doorman.ApprovedRequest
import com.r3.corda.networkmanage.doorman.JiraClient
import com.r3.corda.networkmanage.doorman.RejectedRequest
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.loggerFor
import org.bouncycastle.pkcs.PKCS10CertificationRequest
@ -29,21 +31,34 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
}
}
override fun processApprovedRequests() {
override fun processRequests() {
createTickets()
val (approvedRequests, rejectedRequests) = updateRequestStatus()
delegate.processRequests()
updateJiraTickets(approvedRequests, rejectedRequests)
}
private fun updateRequestStatus(): Pair<List<ApprovedRequest>, List<RejectedRequest>> {
// Update local request statuses.
val approvedRequest = jiraClient.getApprovedRequests()
approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
delegate.processApprovedRequests()
val rejectedRequest = jiraClient.getRejectedRequests()
rejectedRequest.forEach { (id, rejectedBy, reason) -> storage.rejectRequest(id, rejectedBy, reason) }
return Pair(approvedRequest, rejectedRequest)
}
val signedRequests = approvedRequest.mapNotNull { (id, _) ->
val request = storage.getRequest(id)
if (request != null && request.status == RequestStatus.SIGNED) {
request.certData?.certPath?.let { certs -> id to certs }
} else {
null
}
}.toMap()
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
// Reconfirm request status and update jira status
val signedRequests = approvedRequest.mapNotNull { storage.getRequest(it.requestId) }
.filter { it.status == RequestStatus.SIGNED && it.certData != null }
.associateBy { it.requestId }
.mapValues { it.value.certData!!.certPath }
jiraClient.updateSignedRequests(signedRequests)
val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) }
.filter { it.status == RequestStatus.REJECTED }
.map { it.requestId }
jiraClient.updateRejectedRequests(rejectedRequestIDs)
}
/**
@ -52,13 +67,13 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
* Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately,
* they might be left in the [RequestStatus.NEW] state if Jira is down.
*/
override fun createTickets() {
try {
for (signingRequest in storage.getRequests(RequestStatus.NEW)) {
createTicket(signingRequest)
private fun createTickets() {
storage.getRequests(RequestStatus.NEW).forEach {
try {
createTicket(it)
} catch (e: Exception) {
log.warn("There were errors while creating Jira tickets for request '${it.requestId}'", e)
}
} catch (e: Exception) {
log.warn("There were errors while creating Jira tickets", e)
}
}

View File

@ -86,7 +86,6 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
@GET
@Path("my-ip")
fun myIp(@Context request: HttpServletRequest): Response {
// TODO: Verify this returns IP correctly.
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
}

View File

@ -52,6 +52,5 @@ class DoormanParametersTest {
assertEquals("TD", parameter.jiraConfig?.projectCode)
assertEquals("username", parameter.jiraConfig?.username)
assertEquals("password", parameter.jiraConfig?.password)
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
}
}

View File

@ -0,0 +1,55 @@
package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.r3.corda.networkmanage.common.utils.buildCertPath
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import java.net.URI
@Ignore
// This is manual test for testing Jira API.
class JiraClientTest {
private lateinit var jiraClient: JiraClient
@Before
fun init() {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password")
jiraClient = JiraClient(jiraWebAPI, "DOOR")
}
@Test
fun createRequestTicket() {
val request = X509Utilities.createCertificateSigningRequest(CordaX500Name("JiraAPITest", "R3 Ltd 3", "London", "GB").x500Principal, "test@test.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
jiraClient.createRequestTicket(SecureHash.randomSHA256().toString(), request)
}
@Test
fun getApprovedRequests() {
jiraClient.getApprovedRequests().forEach { println(it) }
}
@Test
fun getRejectedRequests() {
val requests = jiraClient.getRejectedRequests()
requests.forEach { println(it) }
}
@Test
fun updateSignedRequests() {
val requests = jiraClient.getApprovedRequests()
val selfSignedCA = X509Utilities.createSelfSignedCACertificate(CordaX500Name("test", "london", "GB").x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
jiraClient.updateSignedRequests(requests.map { it.requestId to buildCertPath(selfSignedCA) }.toMap())
}
@Test
fun updateRejectedRequests() {
val requests = jiraClient.getRejectedRequests()
jiraClient.updateRejectedRequests(requests.map { it.requestId })
assert(jiraClient.getRejectedRequests().isEmpty())
}
}

View File

@ -62,7 +62,7 @@ class DefaultCsrHandlerTest : TestBase() {
val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private)
val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey)
requestProcessor.processApprovedRequests()
requestProcessor.processRequests()
val certPathCapture = argumentCaptor<CertPath>()

View File

@ -1,12 +1,12 @@
package com.r3.corda.networkmanage.doorman.signer
import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.common.persistence.*
import com.r3.corda.networkmanage.doorman.ApprovedRequest
import com.r3.corda.networkmanage.doorman.JiraClient
import com.r3.corda.networkmanage.doorman.RejectedRequest
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.junit.Before
@ -16,9 +16,9 @@ import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import java.security.cert.CertPath
import kotlin.test.assertEquals
class JiraCsrHandlerTest {
@Rule
@JvmField
val mockitoRule: MockitoRule = MockitoJUnit.rule()
@ -80,9 +80,67 @@ class JiraCsrHandlerTest {
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
// Test
jiraCsrHandler.createTickets()
jiraCsrHandler.processRequests()
verify(jiraClient).createRequestTicket(requestId, csr.request)
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
}
@Test
fun `sync tickets status`() {
val id1 = SecureHash.randomSHA256().toString()
val id2 = SecureHash.randomSHA256().toString()
val csr1 = CertificateSigningRequest(id1, "name1", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
val csr2 = CertificateSigningRequest(id2, "name2", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
val requests = mutableMapOf(id1 to csr1, id2 to csr2)
// Mocking storage behaviour.
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(requests.values.filter { it.status == RequestStatus.NEW })
whenever(certificationRequestStorage.getRequest(any())).thenAnswer { requests[it.getArgument(0)] }
whenever(certificationRequestStorage.approveRequest(any(), any())).then {
val id = it.getArgument<String>(0)
if (requests[id]?.status == RequestStatus.NEW) {
requests[id] = requests[id]!!.copy(status = RequestStatus.APPROVED, modifiedBy = listOf(it.getArgument(1)))
}
null
}
whenever(certificationRequestStorage.rejectRequest(any(), any(), any())).then {
val id = it.getArgument<String>(0)
requests[id] = requests[id]!!.copy(status = RequestStatus.REJECTED, modifiedBy = listOf(it.getArgument(1)), remark = it.getArgument(2))
null
}
// Status change from jira.
whenever(jiraClient.getApprovedRequests()).thenReturn(listOf(ApprovedRequest(id1, "Me")))
whenever(jiraClient.getRejectedRequests()).thenReturn(listOf(RejectedRequest(id2, "Me", "Test reject")))
// Test.
jiraCsrHandler.processRequests()
verify(jiraClient).createRequestTicket(id1, csr1.request)
verify(jiraClient).createRequestTicket(id2, csr2.request)
verify(certificationRequestStorage).markRequestTicketCreated(id1)
verify(certificationRequestStorage).markRequestTicketCreated(id2)
// Verify request has the correct status in DB.
assertEquals(RequestStatus.APPROVED, requests[id1]!!.status)
assertEquals(RequestStatus.REJECTED, requests[id2]!!.status)
// Verify jira client get the correct call.
verify(jiraClient).updateRejectedRequests(listOf(id2))
verify(jiraClient).updateSignedRequests(emptyMap())
// Sign request 1
val certPath = mock<CertPath>()
val certData = CertificateData("", CertificateStatus.VALID, certPath)
requests[id1] = requests[id1]!!.copy(status = RequestStatus.SIGNED, certData = certData)
// Process request again.
jiraCsrHandler.processRequests()
// Update signed request should be called.
verify(jiraClient).updateSignedRequests(mapOf(id1 to certPath))
}
}

View File

@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() {
CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady
}
on { processApprovedRequests() }.then {
on { processRequests() }.then {
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
val tlsCert = X509Utilities.createCertificate(
@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() {
startSigningServer(requestProcessor)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
requestProcessor.processApprovedRequests()
requestProcessor.processRequests()
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(requestProcessor, times(2)).getResponse(any())
@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() {
CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady
}
on { processApprovedRequests() }.then {
on { processRequests() }.then {
val request = X509Utilities.createCertificateSigningRequest(
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
"my@mail.com",
@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() {
startSigningServer(storage)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
storage.processApprovedRequests()
storage.processRequests()
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(storage, times(2)).getResponse(any())