-
-
Notifications
You must be signed in to change notification settings - Fork 591
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
refactor app-simplefin.ts to make it easily testable #440
base: master
Are you sure you want to change the base?
Conversation
13e1104
to
8bc98de
Compare
"eslint": "^8.33.0", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"jest": "^29.3.1", | ||
"prettier": "^2.8.3", | ||
"supertest": "^6.3.1", | ||
"typescript": "^4.9.5" | ||
"typescript": "^5.5.4" |
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.
Needed to update typescript to have certain flags in tsconfig.json
work.
const simplefinService = new SimpleFinService( | ||
new SimplefinApi(new HttpsClient()), | ||
); |
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.
dependency injection, makes it easier to test
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 tried to mock simplefinService
to be able to test app-simplefin.js
as an integration test, but I couldn't get jest.spyOn
to work, so I didn't. I'm relying on someone checking out this branch and manually testing that I haven't broken anything.
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 was able to test that, if the SimplefinAPI
returns a valid account/transaction, the code in app-simplefin.js
works. I just need someone to test with a real SimplefinAPI.
} catch (e) { | ||
serverDown(e, res); | ||
return; | ||
} | ||
|
||
try { | ||
const account = | ||
!results?.accounts || results.accounts.find((a) => a.id === accountId); | ||
const account = results.accounts.find((a) => a.id === accountId); |
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.
If no error is caught on line 100, results
should always be set, as well as results.accounts
(albeit it being an empty list) if there are any error. In that case, results.errors
will have an error.
@@ -164,20 +177,20 @@ app.post( | |||
|
|||
let dateToUse = 0; | |||
|
|||
if (trans.posted == 0) { | |||
if (trans.isPending()) { |
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.
trans.posted
is now a Date
, so couldn't compare it to 0
. Added a method on Transaction
to do this.
} | ||
|
||
newTrans.bookingDate = new Date(dateToUse * 1000) | ||
.toISOString() | ||
.split('T')[0]; | ||
|
||
newTrans.date = new Date(dateToUse * 1000).toISOString().split('T')[0]; | ||
newTrans.payeeName = trans.payee; | ||
// newTrans.payeeName = trans.payee; TODO: Is this used? |
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 payee
field appears nowhere in the Simplefin documentation, so I think newTrans.payeeName
ends up undefined.
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.
Hmm, seems this was changed not too long ago, so I guess the Simplefin docs are not quite up to date :/
newTrans.booked = false; | ||
dateToUse = trans.transacted_at; | ||
dateToUse = trans.transacted_at.getTime() / 1000; |
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 didn't want to change the logic further down in this function, so I just reverted the transacted_at
to use seconds
instead of Date
objects. getTime()
returns milliseconds, so needed to divide by 1000.
}); | ||
|
||
return AccountSet.fromJson(result); | ||
} |
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.
Separated the api call (fetchAccessKey()
) from the state (context
), so that the SimplefinAPI
remains stateless, and the only state is in SimplefinContextData
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.
One would reuse context when trying to fetch accounts right after getting an access key, as the access key would live in context
, and could be reused for further calls to the SimplefinAPI.
The SimplefinContextData
class is really a proof of concept here, some fields may need to be removed, e.g. method
and port
, but that can happen at a later date.
const context = new SimplefinContextData(
'POST',
443,
{ 'Content-Length': 0 },
base64Token,
);
this.simplefinApi.setContext(context);
// Fetch access key
const accessKey = this.simplefinApi.fetchAccessKey().then(() => context.accessKey)
// Modify the context to add authentication
context.parseAccessKey(accessKey);
// Use the authentified context to make further calls
const accounts = await this.simplefinApi
.fetchAccounts(startDate, endDate)
.then((accounts) => accounts);
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.
Right now, I could only test that our API still works and returns invalid token errors in an end-to-end way.
"docker:artifacts": "chmod +x docker/download-artifacts.sh && ./docker/download-artifacts.sh", | ||
"docker:build-common": "yarn docker:artifacts && docker build --build-arg GITHUB_TOKEN=$(gh auth token) -t actual-server-dev", | ||
"docker:build-edge": "yarn docker:build-common -f docker/edge-ubuntu.Dockerfile .", | ||
"docker:build-edge-alpine": "yarn docker:build-common -f docker/edge-alpine.Dockerfile .", | ||
"docker:build-stable": "yarn docker:build-common -f docker/stable-ubuntu.Dockerfile .", | ||
"docker:build-stable-alpine": "yarn docker:build-common -f docker/stable-alpine.Dockerfile .", | ||
"docker:run": "docker run --rm -p 5006:5006 actual-server-dev", | ||
"lint": "eslint . --max-warnings 0", |
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 built all the docker images and ran them, making sure the server ran and was accessible with curl
.
accessKey, | ||
startDate, | ||
endDate, | ||
); | ||
|
||
res.send({ | ||
status: 'ok', |
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've gone over the code in the frontend that uses the accounts from the server, and made sure that the variables used there had the same name as the ones in Account
(from models/account.ts
)
@tcrasset that other PR was merged in. |
Changes
splits app-simplefin.js into multiple components:
HTTPSClient
which is responsible for calling any external URL, using PromisesSimplefinAPI
which uses the injectedhttp_client
to call the simplefin API, and parse its response into one of the objects inmodels/
SimplefinService
which uses the injectedSimplefinAPI
to do all the business logic for Actual. Currently, I haven't moved any of the business logic fromapp-simplefin.js
, because I haven't tested these, so I did not want to break stuff. However, the end goal is to have everything except the routing in this service, to that we can test inner logic of the service, with calling our API, nor Simplefin'sSimplefinAPI
implementation withFakeSimplefinAPI
when integration testing theSimplefinService
. This will help us in the long run, so that we don't run into mocking hell where everything is mocked and we're not testing the actual code, but the mocks.cleans up the standard testing output:
--silent
flag injest.config.json
so that the test output is not littered will all the transactions logging fromapp-gocardless
NODE_NO_WARNINGS=1
to disable the ExperimentalWarnings from VM modules coming from the--experimental-vm-modules
flag. Currently, in Node 18 and 20, there is no way to disable this specific warning (or other warning as a matter of fact) directly from the node. [Source]. Instead, some people create jest setup scripts that mockconsole.warn
and filter these warnings. I tried it, it didn't work, so I resorted to the nuclear option that disables all warnings. This is not ideal, but I'm not knowledgeable enough in the JS ecosystem to make it work.Now, a standard
npm run test
output looks all green and clean:allows us to run typescript files directly by calling
npx tsx app.js
.node app.js
does not allow importing.ts
files likesimplefin-service.ts
. I noticed that we were not running the files from thebuild/
folder either way (as we were runningnode app.js
), so JavaScript code built from the Typescript code was not used. Might as well run typescript directly.This required changing the Dockerfiles too.
jest
(which usests-node
under the hood).I admit, this was a mess to deal with, lots of flags changed in
jest.config.json
andtsconfig.json
. I would have preferred changing only the code, but I desperately wanted to introduce typescript in the repo, and now it's done. I'm sure this can be improved.I will need someone to manually test the simplefin integration, to make sure that the current refactor hasn't broken anything. I'm not able to do it, as I don't have a Simplefin Account.
Future
app-simplefin.js
toapp-simplefin/services/simplefin-service.js
. I prefer waiting until @psybers merges his PR Sync multiple accounts in a single SimpleFIN API call. #384 so that I don't break any existing/upcoming features.