From d54f227bfb54e11d204d2e93791c0424b1589d78 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Tue, 28 Nov 2023 13:02:45 -0800 Subject: [PATCH 1/5] Add some initial api tests for budgets and accounts - Adds jest config for the api package - Reuse setup from loot-core, as we are loading it's server/main anyways - Cover some basic budget and account tests to prove it out --- .../api/__snapshots__/methods.test.js.snap | 21 +++ packages/api/jest.config.js | 24 +++ packages/api/methods.test.js | 176 ++++++++++++++++++ packages/api/mocks/budgets/.gitkeep | 0 packages/api/package.json | 7 +- packages/api/tsconfig.dist.json | 2 +- .../server/connection/index.testing.ts | 4 + packages/loot-core/src/server/main.ts | 3 - upcoming-release-notes/1991.md | 6 + yarn.lock | 4 + 10 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 packages/api/__snapshots__/methods.test.js.snap create mode 100644 packages/api/jest.config.js create mode 100644 packages/api/methods.test.js create mode 100644 packages/api/mocks/budgets/.gitkeep create mode 100644 upcoming-release-notes/1991.md diff --git a/packages/api/__snapshots__/methods.test.js.snap b/packages/api/__snapshots__/methods.test.js.snap new file mode 100644 index 00000000000..806d2194337 --- /dev/null +++ b/packages/api/__snapshots__/methods.test.js.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/jest.config.js b/packages/api/jest.config.js new file mode 100644 index 00000000000..343a818015d --- /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/'], + setupFilesAfterEnv: ['/../loot-core/src/mocks/setup.ts'], + transformIgnorePatterns: ['/node_modules/'], + transform: { + '\\.pegjs$': '/../loot-core/peg-transform.mjs', + '^.+\\.(t|j)sx?$': '@swc/jest', + }, +}; diff --git a/packages/api/methods.test.js b/packages/api/methods.test.js new file mode 100644 index 00000000000..dafb57b6048 --- /dev/null +++ b/packages/api/methods.test.js @@ -0,0 +1,176 @@ +import * as connection from '../loot-core/src/platform/server/connection'; +import * as fs from '../loot-core/src/platform/server/fs'; +import * as db from '../loot-core/src/server/db'; +import * as actualApp from '../loot-core/src/server/main'; +import { + runHandler, + runMutator, + disableGlobalMutations, + enableGlobalMutations, +} from '../loot-core/src/server/mutators'; +import * as prefs from '../loot-core/src/server/prefs'; + +import * as injected from './injected'; +import * as api from './methods'; + +let budgetName; +beforeAll(async () => { + budgetName = 'test-budget'; +}); + +beforeEach(async () => { + await global.emptyDatabase()(); + disableGlobalMutations(); + + // Inject the actual API + injected.override(actualApp.lib.send); + + // we need real datetime if we are going to mix new timestamps with our mock data + global.restoreDateNow(); +}); + +afterEach(async () => { + await runHandler(actualApp.handlers['close-budget']); + connection.resetEvents(); + enableGlobalMutations(); + global.currentMonth = null; + global.resetTime(); + + fs._setDocumentDir(null); + const budgetPath = fs.join(__dirname, '/mocks/budgets/', budgetName); + + if (await fs.exists(budgetPath)) { + await fs.removeDirRecursively(budgetPath); + } +}); + +async function createTestBudget(templateName, name) { + const templatePath = fs.join( + __dirname, + '/../loot-core/src/mocks/files', + templateName, + ); + const budgetPath = fs.join(__dirname, '/mocks/budgets/', name); + fs._setDocumentDir(fs.join(budgetPath, '..')); + + await fs.mkdir(budgetPath); + await fs.copyFile( + fs.join(templatePath, 'metadata.json'), + fs.join(budgetPath, 'metadata.json'), + ); + await fs.copyFile( + fs.join(templatePath, 'db.sqlite'), + fs.join(budgetPath, 'db.sqlite'), + ); +} + +describe('API setup and teardown', () => { + // apis: loadBudget, getBudgetMonths + test('successfully loads budget', async () => { + await createTestBudget('default-budget-template', budgetName); + + await expect(api.loadBudget(budgetName)).resolves.toBeUndefined(); + + // Make sure the prefs were loaded + expect(prefs.getPrefs().id).toBe(budgetName); + + await expect(api.getBudgetMonths()).resolves.toMatchSnapshot(); + }); +}); + +describe('API CRUD operations', () => { + beforeEach(async () => { + // load test budget + await createTestBudget('default-budget-template', budgetName); + await runHandler(actualApp.handlers['load-budget'], { id: 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 + await runMutator(async () => { + await db.insertCategoryGroup({ id: 'tests', name: 'tests' }); + await db.insertCategory({ + id: 'test-budget', + name: 'test-budget', + cat_group: 'tests', + }); + }); + + await api.setBudgetAmount(month, 'test-budget', 100); + await api.setBudgetCarryover(month, 'test-budget', true); + + const budgetMonth = await api.getBudgetMonth(month); + expect(budgetMonth.categoryGroups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'tests', + categories: expect.arrayContaining([ + expect.objectContaining({ + id: 'test-budget', + 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 00000000000..e69de29bb2d diff --git a/packages/api/package.json b/packages/api/package.json index 6f876dc3383..436026fa405 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": "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/packages/api/tsconfig.dist.json b/packages/api/tsconfig.dist.json index 6704cb1fb7c..f95cf7a2873 100644 --- a/packages/api/tsconfig.dist.json +++ b/packages/api/tsconfig.dist.json @@ -11,5 +11,5 @@ "outDir": "dist" }, "include": ["."], - "exclude": ["dist"] + "exclude": ["dist", "**/*.test.js"] } diff --git a/packages/loot-core/src/platform/server/connection/index.testing.ts b/packages/loot-core/src/platform/server/connection/index.testing.ts index 80a1bc71c78..a0c2dd0532b 100644 --- a/packages/loot-core/src/platform/server/connection/index.testing.ts +++ b/packages/loot-core/src/platform/server/connection/index.testing.ts @@ -15,3 +15,7 @@ export const getEvents: T.GetEvents = function () { export const resetEvents: T.ResetEvents = function () { events = []; }; + +export const getNumClients: T.GetNumClients = function () { + return 1; +}; diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 1a4baaee73c..c3691bb263d 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -2170,7 +2170,6 @@ async function setupDocumentsDir() { fs._setDocumentDir(documentDir); } -// eslint-disable-next-line import/no-unused-modules export async function initApp(isDev, socketName) { await sqlite.init(); await Promise.all([asyncStorage.init(), fs.init()]); @@ -2233,7 +2232,6 @@ export async function initApp(isDev, socketName) { } } -// eslint-disable-next-line import/no-unused-modules export async function init(config) { // Get from build @@ -2272,7 +2270,6 @@ export async function init(config) { } // Export a few things required for the platform -// eslint-disable-next-line import/no-unused-modules export const lib = { getDataDir: fs.getDataDir, sendMessage: (msg, args) => connection.send(msg, args), diff --git a/upcoming-release-notes/1991.md b/upcoming-release-notes/1991.md new file mode 100644 index 00000000000..5fa4b94bddd --- /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 1dd1f7c5915..2251b1091e1 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" From 062f86f923aad8b9b8789681c9ab62410ff1c2b0 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 30 Nov 2023 16:01:03 -0800 Subject: [PATCH 2/5] Address feedback - Switch test to typescript - Drop loot-core imports, use only api and node modules --- packages/api/.gitignore | 1 + ...hods.test.js.snap => methods.test.ts.snap} | 0 packages/api/index.js | 2 - packages/api/jest.config.js | 1 - .../api/{methods.test.js => methods.test.ts} | 93 +++++++------------ packages/api/tsconfig.dist.json | 2 +- packages/loot-core/src/server/main.ts | 3 + 7 files changed, 37 insertions(+), 65 deletions(-) rename packages/api/__snapshots__/{methods.test.js.snap => methods.test.ts.snap} (100%) rename packages/api/{methods.test.js => methods.test.ts} (58%) diff --git a/packages/api/.gitignore b/packages/api/.gitignore index 1682676f0b6..25df8197844 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.js.snap b/packages/api/__snapshots__/methods.test.ts.snap similarity index 100% rename from packages/api/__snapshots__/methods.test.js.snap rename to packages/api/__snapshots__/methods.test.ts.snap diff --git a/packages/api/index.js b/packages/api/index.js index aebc7c061d4..4473e540c86 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 index 343a818015d..f076b9da9d8 100644 --- a/packages/api/jest.config.js +++ b/packages/api/jest.config.js @@ -18,7 +18,6 @@ module.exports = { setupFilesAfterEnv: ['/../loot-core/src/mocks/setup.ts'], transformIgnorePatterns: ['/node_modules/'], transform: { - '\\.pegjs$': '/../loot-core/peg-transform.mjs', '^.+\\.(t|j)sx?$': '@swc/jest', }, }; diff --git a/packages/api/methods.test.js b/packages/api/methods.test.ts similarity index 58% rename from packages/api/methods.test.js rename to packages/api/methods.test.ts index dafb57b6048..f5284966b98 100644 --- a/packages/api/methods.test.js +++ b/packages/api/methods.test.ts @@ -1,79 +1,52 @@ -import * as connection from '../loot-core/src/platform/server/connection'; -import * as fs from '../loot-core/src/platform/server/fs'; -import * as db from '../loot-core/src/server/db'; -import * as actualApp from '../loot-core/src/server/main'; -import { - runHandler, - runMutator, - disableGlobalMutations, - enableGlobalMutations, -} from '../loot-core/src/server/mutators'; -import * as prefs from '../loot-core/src/server/prefs'; - -import * as injected from './injected'; -import * as api from './methods'; - -let budgetName; -beforeAll(async () => { - budgetName = 'test-budget'; -}); +import * as fs from 'fs/promises'; +import * as path from 'path'; -beforeEach(async () => { - await global.emptyDatabase()(); - disableGlobalMutations(); +import * as api from './index'; - // Inject the actual API - injected.override(actualApp.lib.send); +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 () => { - await runHandler(actualApp.handlers['close-budget']); - connection.resetEvents(); - enableGlobalMutations(); global.currentMonth = null; - global.resetTime(); - - fs._setDocumentDir(null); - const budgetPath = fs.join(__dirname, '/mocks/budgets/', budgetName); - - if (await fs.exists(budgetPath)) { - await fs.removeDirRecursively(budgetPath); - } + await api.shutdown(); }); -async function createTestBudget(templateName, name) { - const templatePath = fs.join( +async function createTestBudget(templateName: string, name: string) { + const templatePath = path.join( __dirname, '/../loot-core/src/mocks/files', templateName, ); - const budgetPath = fs.join(__dirname, '/mocks/budgets/', name); - fs._setDocumentDir(fs.join(budgetPath, '..')); + const budgetPath = path.join(__dirname, '/mocks/budgets/', name); await fs.mkdir(budgetPath); await fs.copyFile( - fs.join(templatePath, 'metadata.json'), - fs.join(budgetPath, 'metadata.json'), + path.join(templatePath, 'metadata.json'), + path.join(budgetPath, 'metadata.json'), ); await fs.copyFile( - fs.join(templatePath, 'db.sqlite'), - fs.join(budgetPath, 'db.sqlite'), + path.join(templatePath, 'db.sqlite'), + path.join(budgetPath, 'db.sqlite'), ); } describe('API setup and teardown', () => { // apis: loadBudget, getBudgetMonths test('successfully loads budget', async () => { - await createTestBudget('default-budget-template', budgetName); - await expect(api.loadBudget(budgetName)).resolves.toBeUndefined(); - // Make sure the prefs were loaded - expect(prefs.getPrefs().id).toBe(budgetName); - await expect(api.getBudgetMonths()).resolves.toMatchSnapshot(); }); }); @@ -81,8 +54,7 @@ describe('API setup and teardown', () => { describe('API CRUD operations', () => { beforeEach(async () => { // load test budget - await createTestBudget('default-budget-template', budgetName); - await runHandler(actualApp.handlers['load-budget'], { id: budgetName }); + await api.loadBudget(budgetName); }); // apis: setBudgetAmount, setBudgetCarryover, getBudgetMonth @@ -91,26 +63,25 @@ describe('API CRUD operations', () => { global.currentMonth = month; // create some new categories to test with - await runMutator(async () => { - await db.insertCategoryGroup({ id: 'tests', name: 'tests' }); - await db.insertCategory({ - id: 'test-budget', - name: 'test-budget', - cat_group: 'tests', - }); + const groupId = await api.createCategoryGroup({ + name: 'tests', + }); + const categoryId = await api.createCategory({ + name: 'test-budget', + group_id: groupId, }); - await api.setBudgetAmount(month, 'test-budget', 100); - await api.setBudgetCarryover(month, 'test-budget', true); + 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: 'tests', + id: groupId, categories: expect.arrayContaining([ expect.objectContaining({ - id: 'test-budget', + id: categoryId, budgeted: 100, carryover: true, }), diff --git a/packages/api/tsconfig.dist.json b/packages/api/tsconfig.dist.json index f95cf7a2873..6704cb1fb7c 100644 --- a/packages/api/tsconfig.dist.json +++ b/packages/api/tsconfig.dist.json @@ -11,5 +11,5 @@ "outDir": "dist" }, "include": ["."], - "exclude": ["dist", "**/*.test.js"] + "exclude": ["dist"] } diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index c3691bb263d..1a4baaee73c 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -2170,6 +2170,7 @@ async function setupDocumentsDir() { fs._setDocumentDir(documentDir); } +// eslint-disable-next-line import/no-unused-modules export async function initApp(isDev, socketName) { await sqlite.init(); await Promise.all([asyncStorage.init(), fs.init()]); @@ -2232,6 +2233,7 @@ export async function initApp(isDev, socketName) { } } +// eslint-disable-next-line import/no-unused-modules export async function init(config) { // Get from build @@ -2270,6 +2272,7 @@ export async function init(config) { } // Export a few things required for the platform +// eslint-disable-next-line import/no-unused-modules export const lib = { getDataDir: fs.getDataDir, sendMessage: (msg, args) => connection.send(msg, args), From 2ba235c13fd4c3b931b787a7419bdce7254c9763 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 30 Nov 2023 16:06:16 -0800 Subject: [PATCH 3/5] Ensure test --watch doesn't loop --- packages/api/jest.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/jest.config.js b/packages/api/jest.config.js index f076b9da9d8..2d98fde77d2 100644 --- a/packages/api/jest.config.js +++ b/packages/api/jest.config.js @@ -15,6 +15,7 @@ module.exports = { ], testEnvironment: 'node', testPathIgnorePatterns: ['/node_modules/'], + watchPathIgnorePatterns: ['/mocks/budgets/'], setupFilesAfterEnv: ['/../loot-core/src/mocks/setup.ts'], transformIgnorePatterns: ['/node_modules/'], transform: { From f03fe5ceaf6c70dc6b18ec58d7b462968ad6669d Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 30 Nov 2023 16:21:52 -0800 Subject: [PATCH 4/5] Remove unneeded index.testing.js change --- .../loot-core/src/platform/server/connection/index.testing.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/loot-core/src/platform/server/connection/index.testing.ts b/packages/loot-core/src/platform/server/connection/index.testing.ts index a0c2dd0532b..80a1bc71c78 100644 --- a/packages/loot-core/src/platform/server/connection/index.testing.ts +++ b/packages/loot-core/src/platform/server/connection/index.testing.ts @@ -15,7 +15,3 @@ export const getEvents: T.GetEvents = function () { export const resetEvents: T.ResetEvents = function () { events = []; }; - -export const getNumClients: T.GetNumClients = function () { - return 1; -}; From 865fc62f1bb94e26edf131d1e51e33eea9c7a14c Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 30 Nov 2023 16:34:54 -0800 Subject: [PATCH 5/5] Ensure the api bundle is build prior to running test --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 436026fa405..efd4df1b185 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -17,7 +17,7 @@ "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", - "test": "jest -c jest.config.js" + "test": "yarn run build:app && jest -c jest.config.js" }, "dependencies": { "better-sqlite3": "^9.1.1",