diff --git a/packages/api/.gitignore b/packages/api/.gitignore index 1682676f0b6695791c4e95fd8e17cb33fb3906ca..25df81978441e97c5d2bf32d382e53512c39f809 100644 --- a/packages/api/.gitignore +++ b/packages/api/.gitignore @@ -2,3 +2,4 @@ app/bundle.api.js* app/stats.json migrations default-db.sqlite +mocks/budgets/**/* diff --git a/packages/api/__snapshots__/methods.test.ts.snap b/packages/api/__snapshots__/methods.test.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..806d2194337f0c529bd63f7e7966ed959243e0d4 --- /dev/null +++ b/packages/api/__snapshots__/methods.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`API setup and teardown successfully loads budget 1`] = ` +Array [ + "2016-10", + "2016-11", + "2016-12", + "2017-01", + "2017-02", + "2017-03", + "2017-04", + "2017-05", + "2017-06", + "2017-07", + "2017-08", + "2017-09", + "2017-10", + "2017-11", + "2017-12", +] +`; diff --git a/packages/api/index.js b/packages/api/index.js index aebc7c061d44a81b6794c8e2629c0b6e8032b1f0..4473e540c86a0ef82967dd3e383b485d94ee297d 100644 --- a/packages/api/index.js +++ b/packages/api/index.js @@ -1,5 +1,3 @@ -/* eslint-disable import/no-unused-modules */ - // eslint-disable-next-line import/extensions import * as bundle from './app/bundle.api.js'; import * as injected from './injected'; diff --git a/packages/api/jest.config.js b/packages/api/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..2d98fde77d2434e64499f59487aa54a6ee6a5bf8 --- /dev/null +++ b/packages/api/jest.config.js @@ -0,0 +1,24 @@ +module.exports = { + moduleFileExtensions: [ + 'testing.js', + 'testing.ts', + 'api.js', + 'api.ts', + 'api.tsx', + 'electron.js', + 'electron.ts', + 'mjs', + 'js', + 'ts', + 'tsx', + 'json', + ], + testEnvironment: 'node', + testPathIgnorePatterns: ['/node_modules/'], + watchPathIgnorePatterns: ['<rootDir>/mocks/budgets/'], + setupFilesAfterEnv: ['<rootDir>/../loot-core/src/mocks/setup.ts'], + transformIgnorePatterns: ['/node_modules/'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, +}; diff --git a/packages/api/methods.test.ts b/packages/api/methods.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5284966b98de0874683f20ca9a493c77f248db7 --- /dev/null +++ b/packages/api/methods.test.ts @@ -0,0 +1,147 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; + +import * as api from './index'; + +const budgetName = 'test-budget'; + +beforeEach(async () => { + // we need real datetime if we are going to mix new timestamps with our mock data + global.restoreDateNow(); + + const budgetPath = path.join(__dirname, '/mocks/budgets/', budgetName); + await fs.rm(budgetPath, { force: true, recursive: true }); + + await createTestBudget('default-budget-template', budgetName); + await api.init({ + dataDir: path.join(__dirname, '/mocks/budgets/'), + }); +}); + +afterEach(async () => { + global.currentMonth = null; + await api.shutdown(); +}); + +async function createTestBudget(templateName: string, name: string) { + const templatePath = path.join( + __dirname, + '/../loot-core/src/mocks/files', + templateName, + ); + const budgetPath = path.join(__dirname, '/mocks/budgets/', name); + + await fs.mkdir(budgetPath); + await fs.copyFile( + path.join(templatePath, 'metadata.json'), + path.join(budgetPath, 'metadata.json'), + ); + await fs.copyFile( + path.join(templatePath, 'db.sqlite'), + path.join(budgetPath, 'db.sqlite'), + ); +} + +describe('API setup and teardown', () => { + // apis: loadBudget, getBudgetMonths + test('successfully loads budget', async () => { + await expect(api.loadBudget(budgetName)).resolves.toBeUndefined(); + + await expect(api.getBudgetMonths()).resolves.toMatchSnapshot(); + }); +}); + +describe('API CRUD operations', () => { + beforeEach(async () => { + // load test budget + await api.loadBudget(budgetName); + }); + + // apis: setBudgetAmount, setBudgetCarryover, getBudgetMonth + test('Budgets: successfully update budgets', async () => { + const month = '2023-10'; + global.currentMonth = month; + + // create some new categories to test with + const groupId = await api.createCategoryGroup({ + name: 'tests', + }); + const categoryId = await api.createCategory({ + name: 'test-budget', + group_id: groupId, + }); + + await api.setBudgetAmount(month, categoryId, 100); + await api.setBudgetCarryover(month, categoryId, true); + + const budgetMonth = await api.getBudgetMonth(month); + expect(budgetMonth.categoryGroups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: groupId, + categories: expect.arrayContaining([ + expect.objectContaining({ + id: categoryId, + budgeted: 100, + carryover: true, + }), + ]), + }), + ]), + ); + }); + + //apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount + test('Accounts: successfully complete account operators', async () => { + const accountId1 = await api.createAccount( + { name: 'test-account1', offbudget: true }, + 1000, + ); + const accountId2 = await api.createAccount({ name: 'test-account2' }, 0); + let accounts = await api.getAccounts(); + + // accounts successfully created + expect(accounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: accountId1, + name: 'test-account1', + offbudget: true, + }), + expect.objectContaining({ id: accountId2, name: 'test-account2' }), + ]), + ); + + await api.updateAccount(accountId1, { offbudget: false }); + await api.closeAccount(accountId1, accountId2, null); + await api.deleteAccount(accountId2); + + // accounts successfully updated, and one of them deleted + accounts = await api.getAccounts(); + expect(accounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: accountId1, + name: 'test-account1', + closed: true, + offbudget: false, + }), + expect.not.objectContaining({ id: accountId2 }), + ]), + ); + + await api.reopenAccount(accountId1); + + // the non-deleted account is reopened + accounts = await api.getAccounts(); + expect(accounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: accountId1, + name: 'test-account1', + closed: false, + }), + ]), + ); + }); +}); diff --git a/packages/api/mocks/budgets/.gitkeep b/packages/api/mocks/budgets/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/api/package.json b/packages/api/package.json index 0ef8b7de68ea5a93bb5952bc1b61f992a4302995..e3c71255ff59f35a7c6da3de0660424d9fca8e35 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -16,7 +16,8 @@ "build:node": "tsc --p tsconfig.dist.json", "build:migrations": "cp migrations/*.sql dist/migrations", "build:default-db": "cp default-db.sqlite dist/", - "build": "rm -rf dist && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db" + "build": "rm -rf dist && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db", + "test": "yarn run build:app && jest -c jest.config.js" }, "dependencies": { "better-sqlite3": "^9.1.1", @@ -25,7 +26,11 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@swc/core": "^1.3.82", + "@swc/jest": "^0.2.29", + "@types/jest": "^27.5.0", "@types/uuid": "^9.0.2", + "jest": "^27.0.0", "typescript": "^5.0.2" } } diff --git a/upcoming-release-notes/1991.md b/upcoming-release-notes/1991.md new file mode 100644 index 0000000000000000000000000000000000000000..5fa4b94bdddd2d4537fa6f0ffd5285dd729a3d9b --- /dev/null +++ b/upcoming-release-notes/1991.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [twk3] +--- + +Add some initial api tests for budgets and accounts diff --git a/yarn.lock b/yarn.lock index 1dd1f7c5915e2898726d09dfc7fca8f5baf05c66..2251b1091e102e77f3dc75263b8010f40614d082 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,9 +23,13 @@ __metadata: version: 0.0.0-use.local resolution: "@actual-app/api@workspace:packages/api" dependencies: + "@swc/core": "npm:^1.3.82" + "@swc/jest": "npm:^0.2.29" + "@types/jest": "npm:^27.5.0" "@types/uuid": "npm:^9.0.2" better-sqlite3: "npm:^9.1.1" compare-versions: "npm:^6.1.0" + jest: "npm:^27.0.0" node-fetch: "npm:^3.3.2" typescript: "npm:^5.0.2" uuid: "npm:^9.0.0"