The service will allow a user to monitor a currency ticker, and notify a webhook upon a tick where certain conditions are met, such as the price falling below or going above a given threshold. The API must allow the user to specify the base currency (for simplicity, if the base is not EURO, your service should respond that this is not yet implemented), the target currency (arbitrary, from all currencies supported by fixer.io), and the min and max price for the event to trigger the notification. The notification will be provided via a webhook specified by the user, and multiple webhooks should be provided (predefined types).
In addition, the service will be able to monitor the currencies (all from the http://api.fixer.io/latest?base=EUR query) at regular time intervals (once a day) and store the results in a MongoDB database. The system will allow the user to query for the "latest" ticker of given currency pair between EUR/xxx(See Highlights), and also, to query for the "running average" of the last 3 days.
Clarification: The service will only need to support the use of EUR as base currency. Other currencies could be supported, if you want, but are not required. If the user specifies base currency other than EURO, your service can respond: "not implemented", along with the corresponding status code.
Base URL: http://<app_name>.herokuapps.com/
New webhooks can be registered using POST requests with the following schema.
Request:
Method: POST
Path: api/
**Payload specification: **
{
"webhookURL": {"type": "string"},
"baseCurrency": {"type": "string"},
"targetCurrency": {"type": "string"},
"minTriggerValue": {"type": "number"},
"maxTriggerValue": {"type": "number"}
}
Example:
{
"webhookURL": "http://remoteUrl:8080/randomWebhookPath",
"baseCurrency": "EUR",
"targetCurrency": "NOK",
"minTriggerValue": 1.50,
"maxTriggerValue": 2.55
}
Response:
The response body contains the id of the created webhook, as string. Note, the response body will contain only the created id, as string, not the entire path; no json encoding. Response code upon success is 201 - Created.
When the service invokes a registered webhook, it uses following payload specification:
Request:
Method: POST
URL: <webhookUrl>
Payload Specification:
{
"baseCurrency": {"type": "string"},
"targetCurrency": {"type": "string"},
"currentRate": {"type": "number"},
"minTriggerValue": {"type": "number"},
"maxTriggerValue": {"type": "number"}
}
Example:
{
"baseCurrency": "EUR",
"targetCurrency": "NOK",
"currentRate": 2.75,
"minTriggerValue": 1.50,
"maxTriggerValue": 2.55
}
Response:
Upon successful notification you will receive either status code 200 (for trigger) or 204 (when no trigger).
Registered webhooks can be accessed with the webhook ID generated during registration**.**
Request:
Method: GET
Path: /api/{id}
Response:
Upon entered invalid ID
Status 404 - Not Found
Upon entered valid ID
Status 200 - OK
Body:
{
"webhookURL": "http://remoteUrl:8080/randomWebhookPath",
"baseCurrency": "EUR",
"targetCurrency": "NOK",
"minTriggerValue": 1.50,
"maxTriggerValue": 2.55
}
Registered webhooks can also be deleted using the webhook id.
Method: DELETE
Path: /api/{id}
Response:
Upon deletion:
Status 202 - Accepted
Upon failed deletion:
Status 404 - Not Found
Request:
Method: POST
Path: /api/latest
Payload Specification:
{
"baseCurrency": {"type": "string"},
"targetCurrency": {"type": "string"},
}
Example:
{
"baseCurrency": "USD",
"targetCurrency": "NZD",
}
Response:
The response contains only the latest exchange rate value (no json tags).
Example: 1.56
Request:
Method: POST
Path: /api/average
Payload Specification:
{
"baseCurrency": {"type": "string"},
"targetCurrency": {"type": "string"},
}
Example:
{
"baseCurrency": "USD",
"targetCurrency": "NZD",
}
Response:
The response contains only the average (of the last three days) exchange rate value (no json tags).
Example: 1.89
This trigger invokes all webhooks (i.e. bypasses the timed trigger) and sends the payload as specified under 'Invoking a registered webhook'. This functionality is meant for testing and evaluation purposes.
Request:
Method: GET
Path: /api/evaluationtrigger
Response:
If all invocations ran successfully
Status 200 - OK
Parts of the assignment I'm particularly happy about
-
Testing standards - taking use of the same type of testing generally throughout all of my code:
I'm taking use of nested testing patterns. First of all I've taken use of
TestMain
func TestMain(m *testing.M) { // ---------- "global" setup -------------- //insert new database credentials to use a test databas e -> confining tests code := m.Run() // ---------- "global" teardown ----------- // Drop database to clean up after tests've ran os.Exit(code) }
This is massively helpful and works like a charm when the database variable is global, as I can simply insert other credentials to my database connector, further ensuring that my tests won't interfere with the actual service. The next nested testing pattern I've used is called TDT (Table Driven Testing) Which allows for grouping similar tests within one test to use the same setup/teardown, but still run them as individual tests. It even lists them in hierarchical order when running go test. This allowed me to test large pieces of code where some logic within said code will split in multiple cases. in other words: it allowed me to easily gain coverage by utilizing essentially the same test twice, thrice, etc. (n-ice?) with some minor changes to divert into the different cases.
-
Allowing "All" Currencies as Base currency.
I've taken use of a little hack to get results regardless of what you post as
baseCurrency
, The hack is nothing more advanced than a little piece of math. Though this is not totally representative of the actual conversion an end user would request (because of how money work), it's a proof of concept for now. As my system is obtaining currency data from fixer with baseCurrency = EUR, I simply do the calculation conversionRate = targetCurrencyRate/baseCurrencyRate -
Using multiple "users" when connecting with my MongoDB hosted on Mongo Atlas
As I had trouble with understanding how to handle the sensitive data (Posted an issue on the issuetracker on this topic), I ended up essentially posting credentials to connect to the test database (as I wasn't able to load environment variables from the test environment, later I've found gotenv to be useful here), however I realized that it is a major security flaw to allow anyone to connect to the mongo cluster, especially when the credentials would allow them to enter the actual database the service too would use. This is why I created a very basic user that was only allowed to read/write on the "test" database and it's one collection.
-
High test coverage. If I've done my calculations right, I'm at 75% test coverage in total, almost the required coverage doubled. |
-
Dockerized project. just run
docker build --tag ass2:latest --file ./cmd/currencytrackr/Dockerfile.
aka. Issues Revolving Around my Submission
I, as many others started getting timeouts on my tests when verifying against the submission verification service, as testing some times tool fractionally more than 60 seconds to complete. I've now set it to use localhost as host for the daemon instead, and I'm getting test times closer to 2-8 seconds.
I ended up in a situation where I was never able to get gofmt up to 100% with the verification service, although running gofmt (even after updating go) on my side never gave me any warnings, trying gofmt -d ./.. returned nothing at all. Even running gofmt -w ./.. did not fix this issue. In other words, this still persists, and I don't understand why.
I touched on this briefly above, but feel like the discussion on sensitive data to connect to a mongoDB, or if we at all should connect to a mongoDB when testing. Is an honorable mention as we gained a lot of insight having it.