From e7301f17a957d922e4c0c1023ca94dcf53acfc29 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 5 Mar 2024 12:08:15 -0800 Subject: [PATCH] Split client sendMessage into separate methods (#189) --- .../tbdex/sdk/httpclient/TbdexHttpClient.kt | 106 +++++++----- .../kotlin/tbdex/sdk/httpclient/E2ETest.kt | 3 +- .../sdk/httpclient/TbdexHttpClientTest.kt | 163 +++++++++++++++--- 3 files changed, 208 insertions(+), 64 deletions(-) diff --git a/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt b/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt index 2acd5813..cc5f6837 100644 --- a/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt +++ b/httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt @@ -1,7 +1,6 @@ package tbdex.sdk.httpclient import com.fasterxml.jackson.module.kotlin.convertValue -import de.fxlae.typeid.TypeId import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -16,9 +15,11 @@ import tbdex.sdk.httpclient.models.GetExchangesFilter import tbdex.sdk.httpclient.models.GetOfferingsFilter import tbdex.sdk.httpclient.models.TbdexResponseException import tbdex.sdk.protocol.Validator +import tbdex.sdk.protocol.models.Close import tbdex.sdk.protocol.models.Message import tbdex.sdk.protocol.models.MessageKind import tbdex.sdk.protocol.models.Offering +import tbdex.sdk.protocol.models.Order import tbdex.sdk.protocol.models.Rfq import tbdex.sdk.protocol.serialization.Json import tbdex.sdk.protocol.serialization.Json.jsonMapper @@ -72,75 +73,100 @@ object TbdexHttpClient { } } + private fun sendMessage(pfiDid: String, path: String, requestBody: RequestBody) { + + val pfiServiceEndpoint = getPfiServiceEndpoint(pfiDid) + val url = pfiServiceEndpoint + path + + val request = buildRequest(url, requestBody) + + println("Attempting to send rfq message to: ${request.url}") + + executeRequest(request) + } + /** - * Sends a message to the PFI. You can also use this message to create an exchange without a replyTo URL. + * Send RFQ message to the PFI. * - * @param message The message to send. + * @param rfq The RFQ to send + * @param replyTo The callback URL for PFI to send messages to. * * @throws TbdexResponseException for response errors. */ - fun sendMessage(message: Message) { - validateMessage(message) + fun createExchange(rfq: Rfq) { + validateMessage(rfq) - val pfiDid = message.metadata.to - val exchangeId = message.metadata.exchangeId - val kind = message.metadata.kind + val pfiDid = rfq.metadata.to + val exchangeId = rfq.metadata.exchangeId - val pfiServiceEndpoint = getPfiServiceEndpoint(pfiDid) - - val body: RequestBody - val url: String - if (kind == MessageKind.rfq) { - body = Json.stringify(CreateExchangeRequest(message as Rfq)).toRequestBody(jsonMediaType) - url = "$pfiServiceEndpoint/exchanges/$exchangeId" - } else { - body = Json.stringify(message).toRequestBody(jsonMediaType) - url = "$pfiServiceEndpoint/exchanges/$exchangeId/$kind" - } - - val request = buildRequest(url, body) + val path = "/exchanges/$exchangeId" - println("Attempting to send $kind message to: ${request.url}") + val body: RequestBody = Json.stringify(CreateExchangeRequest(rfq)) + .toRequestBody(jsonMediaType) - executeRequest(request) + this.sendMessage(pfiDid, path, body) } /** * Send RFQ message and include a replyTo URL for the PFI to send a callback to. * - * @param message The message to send (is of type RFQ) + * @param rfq The RFQ to send * @param replyTo The callback URL for PFI to send messages to. * * @throws TbdexResponseException for response errors. - * */ - fun sendMessage(message: Rfq, replyTo: String) { - validateMessage(message) + fun createExchange(rfq: Rfq, replyTo: String) { + validateMessage(rfq) - val pfiDid = message.metadata.to - val exchangeId = message.metadata.exchangeId + val pfiDid = rfq.metadata.to + val exchangeId = rfq.metadata.exchangeId - val pfiServiceEndpoint = getPfiServiceEndpoint(pfiDid) - val url = "$pfiServiceEndpoint/exchanges/$exchangeId" + val path = "/exchanges/$exchangeId" - val body: RequestBody = Json.stringify(CreateExchangeRequest(message, replyTo)) + val body: RequestBody = Json.stringify(CreateExchangeRequest(rfq, replyTo)) .toRequestBody(jsonMediaType) - val request = buildRequest(url, body) + this.sendMessage(pfiDid, path, body) + } - println("Attempting to send Rfq message to: ${request.url}") + /** + * Send Order message to the PFI. + * + * @param order The Order to send + * + * @throws TbdexResponseException for response errors. + */ + fun submitOrder(order: Order) { + validateMessage(order) - executeRequest(request) + val pfiDid = order.metadata.to + val exchangeId = order.metadata.exchangeId + val path = "/exchanges/$exchangeId/order" + + val body: RequestBody = Json.stringify(order) + .toRequestBody(jsonMediaType) + + this.sendMessage(pfiDid, path, body) } /** - * Aliased method for sendMessage(Rfq, String) to create an exchange by sending an RFQ with a replyTo URL. + * Send Order message to the PFI. * - * @param message The message to send (is of type RFQ) - * @param replyTo The callback URL for PFI to send messages to. + * @param order The Order to send + * + * @throws TbdexResponseException for response errors. */ - fun createExchange(message: Rfq, replyTo: String) { - sendMessage(message, replyTo) + fun submitClose(close: Close) { + validateMessage(close) + + val pfiDid = close.metadata.to + val exchangeId = close.metadata.exchangeId + val path = "/exchanges/$exchangeId/close" + + val body: RequestBody = Json.stringify(close) + .toRequestBody(jsonMediaType) + + this.sendMessage(pfiDid, path, body) } /** diff --git a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt index 5a9375bf..1c2ef736 100644 --- a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt +++ b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/E2ETest.kt @@ -1,7 +1,6 @@ package tbdex.sdk.httpclient import com.nimbusds.jose.jwk.JWK -import de.fxlae.typeid.TypeId import foundation.identity.did.Service import org.junit.jupiter.api.Disabled import tbdex.sdk.httpclient.models.GetExchangesFilter @@ -127,7 +126,7 @@ class E2ETest { println("Sending order against Quote with exchangeId of ${order.metadata.exchangeId}") try { - client.sendMessage(order) + client.submitOrder(order) } catch (e: TbdexResponseException) { throw AssertionError( "Error returned from sending Order. " + diff --git a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TbdexHttpClientTest.kt b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TbdexHttpClientTest.kt index a1ab9c2b..62fb3f49 100644 --- a/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TbdexHttpClientTest.kt +++ b/httpclient/src/test/kotlin/tbdex/sdk/httpclient/TbdexHttpClientTest.kt @@ -10,6 +10,10 @@ import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import tbdex.sdk.httpclient.models.ErrorDetail import tbdex.sdk.httpclient.models.TbdexResponseException +import tbdex.sdk.protocol.models.Close +import tbdex.sdk.protocol.models.CloseData +import tbdex.sdk.protocol.models.Message +import tbdex.sdk.protocol.models.Order import tbdex.sdk.protocol.models.Quote import tbdex.sdk.protocol.models.Rfq import tbdex.sdk.protocol.serialization.Json @@ -17,6 +21,7 @@ import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.ion.CreateDidIonOptions import web5.sdk.dids.methods.ion.DidIon import web5.sdk.dids.methods.ion.models.Service +import web5.sdk.dids.methods.jwk.DidJwk import web5.sdk.dids.methods.key.DidKey import java.net.HttpURLConnection import kotlin.test.Test @@ -28,7 +33,7 @@ import kotlin.test.Test */ class TbdexHttpClientTest { private lateinit var server: MockWebServer - private val alice = DidKey.create(InMemoryKeyManager()) + private val aliceDid = DidKey.create(InMemoryKeyManager()) private val pfiDid = DidIon.create( InMemoryKeyManager(), @@ -84,41 +89,71 @@ class TbdexHttpClientTest { } @Test - fun `send RFQ without replyTo success via mockwebserver`() { + fun `createExchange(Rfq) submits RFQ`() { server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_ACCEPTED)) val rfq = TestData.getRfq(pfiDid.uri) - assertDoesNotThrow { TbdexHttpClient.sendMessage(rfq) } + assertDoesNotThrow { TbdexHttpClient.createExchange(rfq) } - val request1 = server.takeRequest() - assertEquals(request1.path, "/exchanges/${rfq.metadata.exchangeId}") + val request = server.takeRequest() + assertEquals(request.path, "/exchanges/${rfq.metadata.exchangeId}") assertEquals( - Json.jsonMapper.readTree(request1.body.readUtf8()), + Json.jsonMapper.readTree(request.body.readUtf8()), Json.jsonMapper.readTree(Json.stringify(mapOf("rfq" to rfq))) ) } @Test - fun `send RFQ with replyTo success via mockwebserver`() { - + fun `createExchange(Rfq, replyTo) submits RFQ and replyTo`() { server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_ACCEPTED)) val rfq = TestData.getRfq(pfiDid.uri) - assertDoesNotThrow { TbdexHttpClient.sendMessage(rfq, "https://tbdex.io/callback") } + val replyTo = "https://tbdex.io/callback" + assertDoesNotThrow { TbdexHttpClient.createExchange(rfq, replyTo) } + + val request = server.takeRequest() + assertEquals(request.path, "/exchanges/${rfq.metadata.exchangeId}") + assertEquals( + Json.jsonMapper.readTree(request.body.readUtf8()), + Json.jsonMapper.readTree(Json.stringify(mapOf("rfq" to rfq, "replyTo" to replyTo))) + ) } @Test - fun `send RFQ with createExchange success via mockwebserver`() { + fun `createExchange(Rfq) throws if server returns error`() { + val errorDetails = mapOf( + "errors" to listOf( + ErrorDetail( + id = "1", + status = "400", + code = "INVALID_INPUT", + title = "Invalid Input", + detail = "The request input is invalid.", + source = null, + meta = null + ) + ) + ) - server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_ACCEPTED)) + val mockResponseString = Json.jsonMapper.writeValueAsString(errorDetails) + server + .enqueue( + MockResponse() + .setBody(mockResponseString) + .setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) + ) val rfq = TestData.getRfq(pfiDid.uri) - assertDoesNotThrow { TbdexHttpClient.createExchange(rfq, "https://tbdex.io/callback") } + val exception = assertThrows { + TbdexHttpClient.createExchange(rfq) + } + assertEquals(1, exception.errors?.size) + assertEquals("400", exception.errors?.get(0)?.status) } @Test - fun `send RFQ fail via mockwebserver`() { + fun `createExchange(Rfq, replyTo) throws if server responds with error`() { val errorDetails = mapOf( "errors" to listOf( ErrorDetail( @@ -143,14 +178,34 @@ class TbdexHttpClientTest { val rfq = TestData.getRfq(pfiDid.uri) val exception = assertThrows { - TbdexHttpClient.sendMessage(rfq) + TbdexHttpClient.createExchange(rfq, "https://tbdex.io/callback") } assertEquals(1, exception.errors?.size) assertEquals("400", exception.errors?.get(0)?.status) } @Test - fun `send RFQ with createExchange() fail via mockwebserver`() { + fun `submitOrder(Order) submits Order`() { + server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_ACCEPTED)) + + val order = Order.create( + to = pfiDid.uri, + from = this.aliceDid.uri, + exchangeId = TypeId.generate("rfq").toString() + ) + order.sign(aliceDid) + assertDoesNotThrow { TbdexHttpClient.submitOrder(order) } + + val request = server.takeRequest() + assertEquals(request.path, "/exchanges/${order.metadata.exchangeId}/order") + assertEquals( + Json.jsonMapper.readTree(request.body.readUtf8()), + Json.jsonMapper.readTree(Json.stringify(order)) + ) + } + + @Test + fun `submitOrder(Order) throws if server responds with error`() { val errorDetails = mapOf( "errors" to listOf( ErrorDetail( @@ -173,9 +228,73 @@ class TbdexHttpClientTest { .setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) ) - val rfq = TestData.getRfq(pfiDid.uri) + val order = Order.create( + to = pfiDid.uri, + from = this.aliceDid.uri, + exchangeId = TypeId.generate("rfq").toString() + ) + order.sign(aliceDid) val exception = assertThrows { - TbdexHttpClient.createExchange(rfq, "https://tbdex.io/callback") + TbdexHttpClient.submitOrder(order) + } + assertEquals(1, exception.errors?.size) + assertEquals("400", exception.errors?.get(0)?.status) + } + + @Test + fun `submitClose(Close) submits Close`() { + server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_ACCEPTED)) + + val close = Close.create( + to = pfiDid.uri, + from = this.aliceDid.uri, + exchangeId = TypeId.generate("rfq").toString(), + closeData = CloseData(reason = "No more TBDex for you") + ) + close.sign(aliceDid) + assertDoesNotThrow { TbdexHttpClient.submitClose(close) } + + val request = server.takeRequest() + assertEquals(request.path, "/exchanges/${close.metadata.exchangeId}/close") + assertEquals( + Json.jsonMapper.readTree(request.body.readUtf8()), + Json.jsonMapper.readTree(Json.stringify(close)) + ) + } + + @Test + fun `submitClose(Close) throws if server responds with error`() { + val errorDetails = mapOf( + "errors" to listOf( + ErrorDetail( + id = "1", + status = "400", + code = "INVALID_INPUT", + title = "Invalid Input", + detail = "The request input is invalid.", + source = null, + meta = null + ) + ) + ) + + val mockResponseString = Json.jsonMapper.writeValueAsString(errorDetails) + server + .enqueue( + MockResponse() + .setBody(mockResponseString) + .setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) + ) + + val close = Close.create( + to = pfiDid.uri, + from = this.aliceDid.uri, + exchangeId = TypeId.generate("rfq").toString(), + closeData = CloseData(reason = "No more TBDex for you") + ) + close.sign(aliceDid) + val exception = assertThrows { + TbdexHttpClient.submitClose(close) } assertEquals(1, exception.errors?.size) assertEquals("400", exception.errors?.get(0)?.status) @@ -188,7 +307,7 @@ class TbdexHttpClientTest { val mockResponseString = Json.jsonMapper.writeValueAsString(mapOf("data" to exchange)) server.enqueue(MockResponse().setBody(mockResponseString).setResponseCode(HttpURLConnection.HTTP_OK)) - val response = TbdexHttpClient.getExchange(pfiDid.uri, alice, TypeId.generate("rfq").toString()) + val response = TbdexHttpClient.getExchange(pfiDid.uri, this.aliceDid, TypeId.generate("rfq").toString()) assertEquals(offeringId, (response[0] as Rfq).data.offeringId) } @@ -200,7 +319,7 @@ class TbdexHttpClientTest { val mockResponseString = Json.jsonMapper.writeValueAsString(mapOf("data" to exchange)) server.enqueue(MockResponse().setBody(mockResponseString).setResponseCode(HttpURLConnection.HTTP_OK)) - val response = TbdexHttpClient.getExchange(pfiDid.uri, alice, "exchange_1234") + val response = TbdexHttpClient.getExchange(pfiDid.uri, this.aliceDid, "exchange_1234") assertEquals(offeringId, (response[0] as Rfq).data.offeringId) } @@ -226,7 +345,7 @@ class TbdexHttpClientTest { server.enqueue(MockResponse().setBody(mockResponseString).setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST)) assertThrows { - TbdexHttpClient.getExchange(pfiDid.uri, alice, "exchange_1234") + TbdexHttpClient.getExchange(pfiDid.uri, this.aliceDid, "exchange_1234") } } @@ -237,7 +356,7 @@ class TbdexHttpClientTest { val mockResponseString = Json.jsonMapper.writeValueAsString(mapOf("data" to exchanges)) server.enqueue(MockResponse().setBody(mockResponseString).setResponseCode(HttpURLConnection.HTTP_OK)) - val response = TbdexHttpClient.getExchanges(pfiDid.uri, alice) + val response = TbdexHttpClient.getExchanges(pfiDid.uri, this.aliceDid) assertEquals(offeringId, (response[0][0] as Rfq).data.offeringId) } @@ -274,7 +393,7 @@ class TbdexHttpClientTest { val mockResponseString = Json.jsonMapper.writeValueAsString(errorDetails) server.enqueue(MockResponse().setBody(mockResponseString).setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST)) - assertThrows { TbdexHttpClient.getExchanges(pfiDid.uri, alice) } + assertThrows { TbdexHttpClient.getExchanges(pfiDid.uri, this.aliceDid) } } @AfterEach