mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-22 12:12:11 -05:00
* refactor(api): defineConfig vitest, api-helpers, drop vite.api build - Wrap api vitest.config with defineConfig for typing/IDE - Add loot-core api-helpers, use in YNAB4/YNAB5 importers - Remove vite.api.config, build-api, injected.js; simplify api package * refactor(api): update package structure and build scripts - Change main entry point and types definition paths in package.json to reflect new structure. - Simplify build script by removing migration and default database copy commands. - Adjust tsconfig.dist.json to maintain declaration directory. - Add typings for external modules in a new typings.ts file. - Update comments in schedules.ts to improve clarity and maintainability. * chore(api): update dependencies and build configuration - Replace tsc-alias with rollup-plugin-visualizer in package.json. - Update build script to use vite for building the API package. - Add vite configuration file for improved build process and visualization. - Adjust tsconfig.dist.json to exclude additional configuration files from the build. * fix(api): update visualizer output path in vite configuration - Change the output filename for the visualizer plugin from 'dist/stats.json' to 'app/stats.json' to align with the new directory structure. * refactor(api): streamline Vite configuration and remove vitest.config.ts - Remove vitest.config.ts as its configuration is now integrated into vite.config.ts. - Update vite.config.ts to include sourcemap generation and adjust CRDT path resolution. - Modify vitest.setup.ts to correct the import path for the CRDT proto file. * feat(api): enhance build scripts and add file system utilities - Update build scripts in package.json to include separate commands for building node, migrations, and default database. - Introduce a new file system utility module in loot-core to handle file operations such as reading, writing, and directory management. - Implement error handling and logging for file operations to improve robustness. * Refactor typecheck script in api package and enhance api-helpers with new schedule and rule update functions. The typecheck command was simplified by removing the strict check, and new API methods for creating schedules and updating rules were added to improve functionality. * Refactor API integration in loot-core by removing api-helpers and directly invoking handlers. Update typecheck script in api package to include strict checks, and refine TypeScript configurations across multiple packages for improved type safety and build processes. * Refactor imports and enhance code readability across multiple files in loot-core. Simplified import statements in the API and adjusted formatting in YNAB importers for consistency. Updated type annotations to improve type safety and maintainability. * Refactor handler invocation in YNAB importers to use the new send function from main-app. This change improves code consistency and readability by standardizing the method of invoking handlers across different modules. * Refactor schedule configuration in loot-core to enhance type safety by introducing a new ScheduleRuleOptions type. This change improves the clarity of the recurring schedule configuration and ensures better type checking for frequency and interval properties. * Update TypeScript configuration in api package to include path mapping for loot-core. This change enhances module resolution and improves type safety by allowing direct imports from the loot-core source directory. * Update TypeScript configuration in api package to reposition the typescript-strict-plugin entry. This change improves the organization of the tsconfig.json file while maintaining the existing path mapping for loot-core, ensuring consistent type checking across the project. * Update TypeScript configurations across multiple packages to enable noEmit option. This change enhances build processes by preventing unnecessary output files during compilation. Additionally, remove the obsolete tsconfig.api.json file from loot-core to streamline project structure. * Update TypeScript configuration in sync-server package to enable noEmit option. This change allows for the generation of output files during compilation, facilitating the build process. * Update api package configuration to streamline build process and enhance type safety. Removed unnecessary build scripts, integrated vite-plugin-dts for type declaration generation, and added migration and default database copying functionality. Adjusted vitest setup to comment out CRDT proto file import for improved test isolation. * Update TypeScript configurations in desktop-client and desktop-electron packages to enable noEmit option, allowing for output file generation during compilation. Additionally, add ts-strict-ignore comments in YNAB importers to suppress strict type checking, improving compatibility with embedded API usage. * Refactor api package configuration to update type declaration paths and enhance build process. Changed type definitions reference in package.json, streamlined tsconfig.json exclusions, and added functionality to copy inlined types during the build. Removed obsolete vitest setup file for improved test isolation. * Revert to solution without types * Update TypeScript configuration in API package to use ES2022 module and bundler resolution. This change enhances compatibility with modern JavaScript features and improves the build process. * Update yarn.lock and API package to enhance TypeScript build process and add new dependencies * Refactor inline-loot-core-types script to streamline TypeScript declaration handling and improve output organization. Remove legacy code and directly copy loot-core declaration tree, updating index.d.ts to reference local imports. * Add internal export to API and enhance Vite configuration for migration handling * Update Vite configuration in API package to target Node 18, enhancing compatibility with the latest Node features. * Enhance inline-loot-core-types script to improve TypeScript declaration handling by separating source and typings directories. Update the copy process to include emitted typings, ensuring no declarations are dropped and maintaining better organization of loot-core types. * Enhance migration handling by allowing both .sql and .js files to be copied during the migration process. Refactor file system operations in loot-core to improve error handling and streamline file management, including new methods for reading, writing, and removing files and directories. * Refactor rootPath determination in Electron file system module by removing legacy case for 'bundle.api.js'. This simplifies the path management for the Electron app. * Update API tests to mock file system paths for migration handling and change Vite configuration to target Node 20 for improved compatibility. * Add promise-retry dependency to loot-core package and update yarn.lock * Fix lint * Refactor build script order in package.json for improved execution flow * Feedback: API changes for "internal"
1019 lines
27 KiB
TypeScript
1019 lines
27 KiB
TypeScript
import * as fs from 'fs/promises';
|
|
import * as path from 'path';
|
|
|
|
import { vi } from 'vitest';
|
|
|
|
import type { RuleEntity } from 'loot-core/types/models';
|
|
|
|
import * as api from './index';
|
|
|
|
// In tests we run from source; loot-core's API fs uses __dirname (for the built dist/).
|
|
// Mock the fs so path constants point at loot-core package root where migrations live.
|
|
vi.mock(
|
|
'../loot-core/src/platform/server/fs/index.api',
|
|
async importOriginal => {
|
|
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
const pathMod = await import('path');
|
|
const lootCoreRoot = pathMod.join(__dirname, '..', 'loot-core');
|
|
return {
|
|
...actual,
|
|
migrationsPath: pathMod.join(lootCoreRoot, 'migrations'),
|
|
bundledDatabasePath: pathMod.join(lootCoreRoot, 'default-db.sqlite'),
|
|
demoBudgetPath: pathMod.join(lootCoreRoot, 'demo-budget'),
|
|
};
|
|
},
|
|
);
|
|
|
|
const budgetName = 'test-budget';
|
|
|
|
global.IS_TESTING = true;
|
|
|
|
beforeEach(async () => {
|
|
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);
|
|
});
|
|
|
|
// api: getBudgets
|
|
test('getBudgets', async () => {
|
|
const budgets = await api.getBudgets();
|
|
expect(budgets).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: 'test-budget',
|
|
name: 'Default Test Db',
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
// apis: getCategoryGroups, createCategoryGroup, updateCategoryGroup, deleteCategoryGroup
|
|
test('CategoryGroups: successfully update category groups', async () => {
|
|
const month = '2023-10';
|
|
global.currentMonth = month;
|
|
|
|
// get existing category groups
|
|
const groups = await api.getCategoryGroups();
|
|
expect(groups).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
hidden: false,
|
|
id: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
is_income: false,
|
|
name: 'Usual Expenses',
|
|
}),
|
|
expect.objectContaining({
|
|
hidden: false,
|
|
id: 'a137772f-cf2f-4089-9432-822d2ddc1466',
|
|
is_income: false,
|
|
name: 'Investments and Savings',
|
|
}),
|
|
expect.objectContaining({
|
|
hidden: false,
|
|
id: '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00',
|
|
is_income: true,
|
|
name: 'Income',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// create our test category group
|
|
const mainGroupId = await api.createCategoryGroup({
|
|
name: 'test-group',
|
|
});
|
|
|
|
let budgetMonth = await api.getBudgetMonth(month);
|
|
expect(budgetMonth.categoryGroups).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: mainGroupId,
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// update group
|
|
await api.updateCategoryGroup(mainGroupId, {
|
|
name: 'update-tests',
|
|
});
|
|
|
|
budgetMonth = await api.getBudgetMonth(month);
|
|
expect(budgetMonth.categoryGroups).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: mainGroupId,
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// delete group
|
|
await api.deleteCategoryGroup(mainGroupId);
|
|
|
|
budgetMonth = await api.getBudgetMonth(month);
|
|
expect(budgetMonth.categoryGroups).toEqual(
|
|
expect.arrayContaining([
|
|
expect.not.objectContaining({
|
|
id: mainGroupId,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
// apis: createCategory, getCategories, updateCategory, deleteCategory
|
|
test('Categories: successfully update categories', async () => {
|
|
const month = '2023-10';
|
|
global.currentMonth = month;
|
|
|
|
// create our test category group
|
|
const mainGroupId = await api.createCategoryGroup({
|
|
name: 'test-group',
|
|
});
|
|
const secondaryGroupId = await api.createCategoryGroup({
|
|
name: 'test-secondary-group',
|
|
});
|
|
const categoryId = await api.createCategory({
|
|
name: 'test-budget',
|
|
group_id: mainGroupId,
|
|
});
|
|
const categoryIdHidden = await api.createCategory({
|
|
name: 'test-budget-hidden',
|
|
group_id: mainGroupId,
|
|
hidden: true,
|
|
});
|
|
|
|
let categories = await api.getCategories();
|
|
expect(categories).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: categoryId,
|
|
name: 'test-budget',
|
|
hidden: false,
|
|
group_id: mainGroupId,
|
|
}),
|
|
expect.objectContaining({
|
|
id: categoryIdHidden,
|
|
name: 'test-budget-hidden',
|
|
hidden: true,
|
|
group_id: mainGroupId,
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// update/move category
|
|
await api.updateCategory(categoryId, {
|
|
name: 'updated-budget',
|
|
group_id: secondaryGroupId,
|
|
});
|
|
|
|
await api.updateCategory(categoryIdHidden, {
|
|
name: 'updated-budget-hidden',
|
|
group_id: secondaryGroupId,
|
|
hidden: false,
|
|
});
|
|
|
|
categories = await api.getCategories();
|
|
expect(categories).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: categoryId,
|
|
name: 'updated-budget',
|
|
hidden: false,
|
|
group_id: secondaryGroupId,
|
|
}),
|
|
expect.objectContaining({
|
|
id: categoryIdHidden,
|
|
name: 'updated-budget-hidden',
|
|
hidden: false,
|
|
group_id: secondaryGroupId,
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// delete categories
|
|
await api.deleteCategory(categoryId);
|
|
|
|
expect(categories).toEqual(
|
|
expect.arrayContaining([
|
|
expect.not.objectContaining({
|
|
id: categoryId,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
// 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, getAccountBalance
|
|
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' }),
|
|
]),
|
|
);
|
|
|
|
expect(await api.getAccountBalance(accountId1)).toEqual(1000);
|
|
expect(await api.getAccountBalance(accountId2)).toEqual(0);
|
|
|
|
await api.updateAccount(accountId1, { offbudget: false });
|
|
await api.closeAccount(accountId1, accountId2);
|
|
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,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
// apis: createPayee, getPayees, updatePayee, deletePayee
|
|
test('Payees: successfully update payees', async () => {
|
|
const payeeId1 = await api.createPayee({ name: 'test-payee1' });
|
|
const payeeId2 = await api.createPayee({ name: 'test-payee2' });
|
|
let payees = await api.getPayees();
|
|
|
|
// payees successfully created
|
|
expect(payees).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: payeeId1,
|
|
name: 'test-payee1',
|
|
}),
|
|
expect.objectContaining({
|
|
id: payeeId2,
|
|
name: 'test-payee2',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
await api.updatePayee(payeeId1, { name: 'test-updated-payee' });
|
|
await api.deletePayee(payeeId2);
|
|
|
|
// confirm update and delete were successful
|
|
payees = await api.getPayees();
|
|
expect(payees).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: payeeId1,
|
|
name: 'test-updated-payee',
|
|
}),
|
|
expect.not.objectContaining({
|
|
name: 'test-payee1',
|
|
}),
|
|
expect.not.objectContaining({
|
|
id: payeeId2,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
// apis: createTag, getTags, updateTag, deleteTag
|
|
test('Tags: successfully complete tag operations', async () => {
|
|
// Create tags
|
|
const tagId1 = await api.createTag({ tag: 'test-tag1', color: '#ff0000' });
|
|
const tagId2 = await api.createTag({
|
|
tag: 'test-tag2',
|
|
description: 'A test tag',
|
|
});
|
|
|
|
let tags = await api.getTags();
|
|
expect(tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: tagId1,
|
|
tag: 'test-tag1',
|
|
color: '#ff0000',
|
|
}),
|
|
expect.objectContaining({
|
|
id: tagId2,
|
|
tag: 'test-tag2',
|
|
description: 'A test tag',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// Update tag
|
|
await api.updateTag(tagId1, { tag: 'updated-tag', color: '#00ff00' });
|
|
tags = await api.getTags();
|
|
expect(tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: tagId1,
|
|
tag: 'updated-tag',
|
|
color: '#00ff00',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// Delete tag
|
|
await api.deleteTag(tagId2);
|
|
tags = await api.getTags();
|
|
expect(tags).not.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ id: tagId2 })]),
|
|
);
|
|
});
|
|
|
|
test('Tags: create tag with minimal fields', async () => {
|
|
const tagId = await api.createTag({ tag: 'minimal-tag' });
|
|
const tags = await api.getTags();
|
|
expect(tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: tagId,
|
|
tag: 'minimal-tag',
|
|
color: null,
|
|
description: null,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
test('Tags: update single field only', async () => {
|
|
const tagId = await api.createTag({ tag: 'original', color: '#ff0000' });
|
|
|
|
// Update only color, tag and description should remain unchanged
|
|
await api.updateTag(tagId, { color: '#00ff00' });
|
|
|
|
const tags = await api.getTags();
|
|
expect(tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: tagId,
|
|
tag: 'original',
|
|
color: '#00ff00',
|
|
description: null,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
test('Tags: handle null values correctly', async () => {
|
|
const tagId = await api.createTag({
|
|
tag: 'with-nulls',
|
|
color: null,
|
|
description: null,
|
|
});
|
|
|
|
const tags = await api.getTags();
|
|
expect(tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: tagId,
|
|
color: null,
|
|
description: null,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
test('Tags: clear optional field', async () => {
|
|
const tagId = await api.createTag({
|
|
tag: 'clearable',
|
|
color: '#ff0000',
|
|
description: 'will be cleared',
|
|
});
|
|
|
|
// Clear color by setting to null
|
|
await api.updateTag(tagId, { color: null });
|
|
|
|
let tags = await api.getTags();
|
|
expect(tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: tagId,
|
|
tag: 'clearable',
|
|
color: null,
|
|
description: 'will be cleared',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// Clear description by setting to null
|
|
await api.updateTag(tagId, { description: null });
|
|
|
|
tags = await api.getTags();
|
|
expect(tags).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: tagId,
|
|
tag: 'clearable',
|
|
color: null,
|
|
description: null,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
|
|
test('Rules: successfully update rules', async () => {
|
|
await api.createPayee({ name: 'test-payee' });
|
|
await api.createPayee({ name: 'test-payee2' });
|
|
|
|
// create our test rules
|
|
const rule = await api.createRule({
|
|
stage: 'pre',
|
|
conditionsOp: 'and',
|
|
conditions: [
|
|
{
|
|
field: 'payee',
|
|
op: 'is',
|
|
value: 'test-payee',
|
|
},
|
|
],
|
|
actions: [
|
|
{
|
|
op: 'set',
|
|
field: 'category',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
},
|
|
],
|
|
});
|
|
const rule2 = await api.createRule({
|
|
stage: 'pre',
|
|
conditionsOp: 'and',
|
|
conditions: [
|
|
{
|
|
field: 'payee',
|
|
op: 'is',
|
|
value: 'test-payee2',
|
|
},
|
|
],
|
|
actions: [
|
|
{
|
|
op: 'set',
|
|
field: 'category',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
},
|
|
],
|
|
});
|
|
|
|
// get existing rules
|
|
const rules = await api.getRules();
|
|
expect(rules).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
actions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'category',
|
|
op: 'set',
|
|
type: 'id',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
}),
|
|
]),
|
|
conditions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'payee',
|
|
op: 'is',
|
|
type: 'id',
|
|
value: 'test-payee2',
|
|
}),
|
|
]),
|
|
conditionsOp: 'and',
|
|
id: rule2.id,
|
|
stage: 'pre',
|
|
}),
|
|
expect.objectContaining({
|
|
actions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'category',
|
|
op: 'set',
|
|
type: 'id',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
}),
|
|
]),
|
|
conditions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'payee',
|
|
op: 'is',
|
|
type: 'id',
|
|
value: 'test-payee',
|
|
}),
|
|
]),
|
|
conditionsOp: 'and',
|
|
id: rule.id,
|
|
stage: 'pre',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// get by payee
|
|
expect(await api.getPayeeRules('test-payee')).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
actions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'category',
|
|
op: 'set',
|
|
type: 'id',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
}),
|
|
]),
|
|
conditions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'payee',
|
|
op: 'is',
|
|
type: 'id',
|
|
value: 'test-payee',
|
|
}),
|
|
]),
|
|
conditionsOp: 'and',
|
|
id: rule.id,
|
|
stage: 'pre',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
expect(await api.getPayeeRules('test-payee2')).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
actions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'category',
|
|
op: 'set',
|
|
type: 'id',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
}),
|
|
]),
|
|
conditions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'payee',
|
|
op: 'is',
|
|
type: 'id',
|
|
value: 'test-payee2',
|
|
}),
|
|
]),
|
|
conditionsOp: 'and',
|
|
id: rule2.id,
|
|
stage: 'pre',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// update one rule
|
|
const updatedRule = {
|
|
...rule,
|
|
stage: 'post',
|
|
conditionsOp: 'or',
|
|
} satisfies RuleEntity;
|
|
expect(await api.updateRule(updatedRule)).toEqual(updatedRule);
|
|
|
|
expect(await api.getRules()).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
actions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'category',
|
|
op: 'set',
|
|
type: 'id',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
}),
|
|
]),
|
|
conditions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'payee',
|
|
op: 'is',
|
|
type: 'id',
|
|
value: 'test-payee',
|
|
}),
|
|
]),
|
|
conditionsOp: 'or',
|
|
id: rule.id,
|
|
stage: 'post',
|
|
}),
|
|
expect.objectContaining({
|
|
actions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'category',
|
|
op: 'set',
|
|
type: 'id',
|
|
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
|
}),
|
|
]),
|
|
conditions: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
field: 'payee',
|
|
op: 'is',
|
|
type: 'id',
|
|
value: 'test-payee2',
|
|
}),
|
|
]),
|
|
conditionsOp: 'and',
|
|
id: rule2.id,
|
|
stage: 'pre',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
// delete rules
|
|
await api.deleteRule(rules[1].id);
|
|
expect(await api.getRules()).toHaveLength(1);
|
|
|
|
await api.deleteRule(rules[0].id);
|
|
expect(await api.getRules()).toHaveLength(0);
|
|
});
|
|
|
|
// apis: addTransactions, getTransactions, importTransactions, updateTransaction, deleteTransaction
|
|
test('Transactions: successfully update transactions', async () => {
|
|
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
|
|
|
let newTransaction = [
|
|
{
|
|
account: accountId,
|
|
date: '2023-11-03',
|
|
imported_id: '11',
|
|
amount: 100,
|
|
notes: 'notes',
|
|
},
|
|
{
|
|
account: accountId,
|
|
date: '2023-11-03',
|
|
imported_id: '12',
|
|
amount: 100,
|
|
notes: '',
|
|
},
|
|
];
|
|
|
|
const addResult = await api.addTransactions(accountId, newTransaction, {
|
|
learnCategories: true,
|
|
runTransfers: true,
|
|
});
|
|
expect(addResult).toBe('ok');
|
|
|
|
expect(await api.getAccountBalance(accountId)).toEqual(200);
|
|
expect(
|
|
await api.getAccountBalance(accountId, new Date(2023, 10, 2)),
|
|
).toEqual(0);
|
|
|
|
// confirm added transactions exist
|
|
let transactions = await api.getTransactions(
|
|
accountId,
|
|
'2023-11-01',
|
|
'2023-11-30',
|
|
);
|
|
expect(transactions).toEqual(
|
|
expect.arrayContaining(
|
|
newTransaction.map(trans => expect.objectContaining(trans)),
|
|
),
|
|
);
|
|
expect(transactions).toHaveLength(2);
|
|
|
|
newTransaction = [
|
|
{
|
|
account: accountId,
|
|
date: '2023-12-03',
|
|
imported_id: '11',
|
|
amount: 100,
|
|
notes: 'notes',
|
|
},
|
|
{
|
|
account: accountId,
|
|
date: '2023-12-03',
|
|
imported_id: '12',
|
|
amount: 100,
|
|
notes: 'notes',
|
|
},
|
|
{
|
|
account: accountId,
|
|
date: '2023-12-03',
|
|
imported_id: '22',
|
|
amount: 200,
|
|
notes: '',
|
|
},
|
|
];
|
|
|
|
const reconciled = await api.importTransactions(accountId, newTransaction);
|
|
|
|
// Expect it to reconcile and to have updated one of the previous transactions
|
|
expect(reconciled.added).toHaveLength(1);
|
|
expect(reconciled.updated).toHaveLength(1);
|
|
|
|
// confirm imported transactions exist
|
|
transactions = await api.getTransactions(
|
|
accountId,
|
|
'2023-12-01',
|
|
'2023-12-31',
|
|
);
|
|
expect(transactions).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ imported_id: '22', amount: 200 }),
|
|
]),
|
|
);
|
|
expect(transactions).toHaveLength(1);
|
|
|
|
// confirm imported transactions update perfomed
|
|
transactions = await api.getTransactions(
|
|
accountId,
|
|
'2023-11-01',
|
|
'2023-11-30',
|
|
);
|
|
expect(transactions).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ notes: 'notes', amount: 100 }),
|
|
]),
|
|
);
|
|
expect(transactions).toHaveLength(2);
|
|
|
|
const idToUpdate = reconciled.added[0];
|
|
const idToDelete = reconciled.updated[0];
|
|
await api.updateTransaction(idToUpdate, { amount: 500 });
|
|
await api.deleteTransaction(idToDelete);
|
|
|
|
// confirm updates and deletions work
|
|
transactions = await api.getTransactions(
|
|
accountId,
|
|
'2023-12-01',
|
|
'2023-12-31',
|
|
);
|
|
expect(transactions).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ id: idToUpdate, amount: 500 }),
|
|
expect.not.objectContaining({ id: idToDelete }),
|
|
]),
|
|
);
|
|
expect(transactions).toHaveLength(1);
|
|
});
|
|
|
|
test('Transactions: import notes are preserved when importing', async () => {
|
|
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
|
|
|
// Test with notes
|
|
const transactionsWithNotes = [
|
|
{
|
|
date: '2023-11-03',
|
|
imported_id: '11',
|
|
amount: 100,
|
|
notes: 'test note',
|
|
},
|
|
];
|
|
|
|
const addResultWithNotes = await api.addTransactions(
|
|
accountId,
|
|
transactionsWithNotes,
|
|
{
|
|
learnCategories: true,
|
|
runTransfers: true,
|
|
},
|
|
);
|
|
expect(addResultWithNotes).toBe('ok');
|
|
|
|
let transactions = await api.getTransactions(
|
|
accountId,
|
|
'2023-11-01',
|
|
'2023-11-30',
|
|
);
|
|
expect(transactions[0].notes).toBe('test note');
|
|
|
|
// Clear transactions
|
|
await api.deleteTransaction(transactions[0].id);
|
|
|
|
// Test without notes
|
|
const transactionsWithoutNotes = [
|
|
{ date: '2023-11-03', imported_id: '11', amount: 100 },
|
|
];
|
|
|
|
const addResultWithoutNotes = await api.addTransactions(
|
|
accountId,
|
|
transactionsWithoutNotes,
|
|
{
|
|
learnCategories: true,
|
|
runTransfers: true,
|
|
},
|
|
);
|
|
expect(addResultWithoutNotes).toBe('ok');
|
|
|
|
transactions = await api.getTransactions(
|
|
accountId,
|
|
'2023-11-01',
|
|
'2023-11-30',
|
|
);
|
|
expect(transactions[0].notes).toBeNull();
|
|
});
|
|
});
|
|
|
|
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule
|
|
test('Schedules: successfully complete schedules operations', async () => {
|
|
await api.loadBudget(budgetName);
|
|
//test a schedule with a recuring configuration
|
|
const ScheduleId1 = await api.createSchedule({
|
|
name: 'test-schedule 1',
|
|
posts_transaction: true,
|
|
// amount: -5000,
|
|
amountOp: 'is',
|
|
date: {
|
|
frequency: 'monthly',
|
|
interval: 1,
|
|
start: '2025-06-13',
|
|
patterns: [],
|
|
skipWeekend: false,
|
|
weekendSolveMode: 'after',
|
|
endMode: 'never',
|
|
},
|
|
});
|
|
//test the creation of non recurring schedule
|
|
const ScheduleId2 = await api.createSchedule({
|
|
name: 'test-schedule 2',
|
|
posts_transaction: false,
|
|
amount: 4000,
|
|
amountOp: 'is',
|
|
date: '2025-06-13',
|
|
});
|
|
let schedules = await api.getSchedules();
|
|
|
|
// Schedules successfully created
|
|
expect(schedules).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: 'test-schedule 1',
|
|
posts_transaction: true,
|
|
// amount: -5000,
|
|
amountOp: 'is',
|
|
date: {
|
|
frequency: 'monthly',
|
|
interval: 1,
|
|
start: '2025-06-13',
|
|
patterns: [],
|
|
skipWeekend: false,
|
|
weekendSolveMode: 'after',
|
|
endMode: 'never',
|
|
},
|
|
}),
|
|
expect.objectContaining({
|
|
name: 'test-schedule 2',
|
|
posts_transaction: false,
|
|
amount: 4000,
|
|
amountOp: 'is',
|
|
date: '2025-06-13',
|
|
}),
|
|
]),
|
|
);
|
|
//check getIDByName works on schedules
|
|
expect(await api.getIDByName('schedules', 'test-schedule 1')).toEqual(
|
|
ScheduleId1,
|
|
);
|
|
expect(await api.getIDByName('schedules', 'test-schedule 2')).toEqual(
|
|
ScheduleId2,
|
|
);
|
|
|
|
//check getIDByName works on accounts
|
|
const schedAccountId1 = await api.createAccount(
|
|
{ name: 'sched-test-account1', offbudget: true },
|
|
1000,
|
|
);
|
|
|
|
expect(await api.getIDByName('accounts', 'sched-test-account1')).toEqual(
|
|
schedAccountId1,
|
|
);
|
|
|
|
//check getIDByName works on payees
|
|
const schedPayeeId1 = await api.createPayee({ name: 'sched-test-payee1' });
|
|
|
|
expect(await api.getIDByName('payees', 'sched-test-payee1')).toEqual(
|
|
schedPayeeId1,
|
|
);
|
|
await api.updateSchedule(ScheduleId1, {
|
|
amount: -10000,
|
|
account: schedAccountId1,
|
|
});
|
|
await api.deleteSchedule(ScheduleId2);
|
|
|
|
// schedules successfully updated, and one of them deleted
|
|
await api.updateSchedule(ScheduleId1, {
|
|
amount: -10000,
|
|
account: schedAccountId1,
|
|
payee: schedPayeeId1,
|
|
});
|
|
await api.deleteSchedule(ScheduleId2);
|
|
|
|
schedules = await api.getSchedules();
|
|
expect(schedules).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: ScheduleId1,
|
|
posts_transaction: true,
|
|
amount: -10000,
|
|
account: schedAccountId1,
|
|
payee: schedPayeeId1,
|
|
amountOp: 'is',
|
|
date: {
|
|
frequency: 'monthly',
|
|
interval: 1,
|
|
start: '2025-06-13',
|
|
patterns: [],
|
|
skipWeekend: false,
|
|
weekendSolveMode: 'after',
|
|
endMode: 'never',
|
|
},
|
|
}),
|
|
expect.not.objectContaining({ id: ScheduleId2 }),
|
|
]),
|
|
);
|
|
});
|