-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement GetBalance client call and server endpoint #217
Conversation
jiyoonie9
commented
Mar 29, 2024
•
edited
Loading
edited
- Implemented GetBalance client method (with auth token)
- Implemented GetBalance server endpoint and request handler (with auth token verification)
- Wrote bare bones BalancesApi interface with 1 method, GetBalances(requesterDid: String) and implemented FakeBalancesApi
- Wrote tests for the new client and server methods
…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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit/suggestion: Wrap this Balance.parse()
in a try/catch
that catches and re-throws as a TbdexResponseException
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
niceeee. thanks for setting this up. it'll be easy now to copy this pattern for other callback tests in this library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i love to see such thorough testing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few nits, but nothing blocking. great work.
Co-authored-by: Diane Huxley <[email protected]>
* @return A list of [Balance] matching the request. | ||
* @throws TbdexResponseException for request or response errors. | ||
*/ | ||
@Suppress("SwallowedException") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what exception gets swallowed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah does this throw an exception if the request fails? e.g. timeout, EHANGUP
etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes. there is a ton of repeat here for all protected endpoints' request handler. Issue #227
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice. had a similar issue in swift TBD54566975/tbdex-swift#85
js could use some drying up also. will create an issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here: TBD54566975/tbdex-js#223
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great work @jiyoontbd !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be an optional endpoint
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think it will be good for testing to still have balancesApi = config.balancesApi ?: FakeBalancesApi()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here: TBD54566975/tbdex-js#223