Implement GetBalance client call and server endpoint#217
Conversation
…assing, but test vectors need to be fixed, also need to look at the new regex pattern decimalString and see if it still works with numbers with decimal
…en check on getbalances handler method. formatting
| * @param pfiDid The decentralized identifier of the PFI. | ||
| * @param requesterDid The decentralized identifier of the entity requesting the balances. | ||
| * @return A list of [Balance] matching the request. | ||
| * @throws TbdexResponseException for request or response errors. |
There was a problem hiding this comment.
Thinking out loud: in tbdex-js, we have both TbdexRequestError and TbdexResponseError, representing errors that happen before and after the client call respectively. It's a slight misnomer to throw a RequestException for both request and response errors, so I'm making a mental note that in my next tidy-up PR, I may explore introducing a TbdexRequestException class. In any case, it's relatively low priority and outside the scope of this PR.
| val jsonNode = jsonMapper.readTree(responseString) | ||
| return jsonNode.get("data").elements() | ||
| .asSequence() | ||
| .map { Balance.parse(it.toString()) } |
There was a problem hiding this comment.
Nit/suggestion: Wrap this Balance.parse() in a try/catch that catches and re-throws as a TbdexResponseException.
httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/GetBalances.kt
Outdated
Show resolved
Hide resolved
httpserver/src/main/kotlin/tbdex/sdk/httpserver/handlers/GetBalances.kt
Outdated
Show resolved
Hide resolved
httpserver/src/main/kotlin/tbdex/sdk/httpserver/models/BalancesApi.kt
Outdated
Show resolved
Hide resolved
| coVerify(exactly = 1) { callback.invoke(applicationCall) } | ||
| coVerify { applicationCall.respond(HttpStatusCode.OK, any<GetBalancesResponse>()) } |
There was a problem hiding this comment.
niceeee. thanks for setting this up. it'll be easy now to copy this pattern for other callback tests in this library.
diehuxx
left a comment
There was a problem hiding this comment.
a few nits, but nothing blocking. great work.
Co-authored-by: Diane Huxley <dhuxley@tbd.email>
| * @return A list of [Balance] matching the request. | ||
| * @throws TbdexResponseException for request or response errors. | ||
| */ | ||
| @Suppress("SwallowedException") |
There was a problem hiding this comment.
what exception gets swallowed?
There was a problem hiding this comment.
the exception thrown if Balance.parse(it.toString()) fails in the try/catch, line 123. it gets re-thrown as TbdexResponseException as implemented per this suggestion
| .get() | ||
| .build() | ||
|
|
||
| val response: Response = client.newCall(request).execute() |
There was a problem hiding this comment.
ah does this throw an exception if the request fails? e.g. timeout, EHANGUP etc.
There was a problem hiding this comment.
correct. from the docs:
Throws:
IOException - if the request could not be executed due to cancellation,
a connectivity problem or timeout. Because networks can fail during an exchange,
it is possible that the remote server accepted the request before the failure.
IllegalStateException - when the call has already been executed.
| val authzHeader = call.request.headers[HttpHeaders.Authorization] | ||
| if (authzHeader == null) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Authorization header required" | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| val arr = authzHeader.split("Bearer ") | ||
| if (arr.size != 2) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Malformed Authorization header. Expected: Bearer TOKEN_HERE" | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| val token = arr[1] | ||
| val requesterDid: String | ||
| try { | ||
| requesterDid = RequestToken.verify(token, pfiDid) | ||
| } catch (e: Exception) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Could not verify Authorization header." | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return |
There was a problem hiding this comment.
I haven't used ktor before but if it has the concept of middleware, maybe it'd be helpful to abstract this logic out into a verifyAuthzHeader middleware? and if ktor doesn't have the middleware concept, could just tuck this into a method and use it for all protected endpoints. (note for future PR)
There was a problem hiding this comment.
yes. there is a ton of repeat here for all protected endpoints' request handler. Issue #227
There was a problem hiding this comment.
nice. had a similar issue in swift TBD54566975/tbdex-swift#85
js could use some drying up also. will create an issue
mistermoe
left a comment
There was a problem hiding this comment.
great work @jiyoontbd !
kirahsapong
left a comment
There was a problem hiding this comment.
nice! couple comments, mostly just that balance endpoint is optional. once resolved lgtm
httpserver/src/main/kotlin/tbdex/sdk/httpserver/TbdexHttpServer.kt
Outdated
Show resolved
Hide resolved
| ) | ||
| } | ||
|
|
||
| get("/balances") { |
There was a problem hiding this comment.
this should be an optional endpoint
There was a problem hiding this comment.
ooh, i added the suggested change you posted earlier, but actually instead o that, i'd like to wrap this in a condition that checks for a new config value balancesEnabled
There was a problem hiding this comment.
i think it will be good for testing to still have balancesApi = config.balancesApi ?: FakeBalancesApi()
There was a problem hiding this comment.
i thought it might be helpful to share how the optional endpoint is implemented in js right now: https://github.com/TBD54566975/tbdex-js/blob/main/packages/http-server/src/http-server.ts#L45
and also this comment thread on the PR for it: TBD54566975/tbdex-js#212 (comment)
@KendallWeihe's proposal was simplifying it by keeping consistent with the other fields, and having the consumer explicitly pass in FakeBalancesApi() when instantiating the server to indicate they wanted to opt in / enable it
but either approach is cool, prob just good to make sure its aligned. i can open an issue in JS to add a balancesEnabled config value if we want to go with it, lmk!
There was a problem hiding this comment.
okey, i've aligned on essentially using balancesApi null check essentially the config on whether to enable balances. i like balancesEnabled config as it's slightly more explicit? but can see how things can go awry there too. let's revisit it if there's feedback from sdk consumers to change this
| val authzHeader = call.request.headers[HttpHeaders.Authorization] | ||
| if (authzHeader == null) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Authorization header required" | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| val arr = authzHeader.split("Bearer ") | ||
| if (arr.size != 2) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Malformed Authorization header. Expected: Bearer TOKEN_HERE" | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| val token = arr[1] | ||
| val requesterDid: String | ||
| try { | ||
| requesterDid = RequestToken.verify(token, pfiDid) | ||
| } catch (e: Exception) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Could not verify Authorization header." | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return |
There was a problem hiding this comment.
nice. had a similar issue in swift TBD54566975/tbdex-swift#85
js could use some drying up also. will create an issue
| val authzHeader = call.request.headers[HttpHeaders.Authorization] | ||
| if (authzHeader == null) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Authorization header required" | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| val arr = authzHeader.split("Bearer ") | ||
| if (arr.size != 2) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Malformed Authorization header. Expected: Bearer TOKEN_HERE" | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| val token = arr[1] | ||
| val requesterDid: String | ||
| try { | ||
| requesterDid = RequestToken.verify(token, pfiDid) | ||
| } catch (e: Exception) { | ||
| call.respond( | ||
| HttpStatusCode.Unauthorized, | ||
| ErrorResponse( | ||
| errors = listOf( | ||
| ErrorDetail( | ||
| detail = "Could not verify Authorization header." | ||
| ) | ||
| ) | ||
| ) | ||
| ) | ||
| return |
…r.kt Co-authored-by: kirahsapong <102400653+kirahsapong@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.