quickbooks-api
Version:
A modular TypeScript SDK for seamless integration with Intuit QuickBooks APIs. Provides robust authentication handling and future-ready foundation for accounting, payments, and commerce operations.
533 lines (434 loc) • 16.2 kB
text/typescript
import { ApiClient } from '../src/app';
import { AuthProvider, Environment, AuthScopes, type AccountQueryResponse, Account } from '../src/app';
import { describe, expect, it, beforeEach, afterEach } from 'bun:test';
import { mockFetch, mockAccountData, mockTokenData } from './helpers';
// Mock configuration
const TEST_CONFIG = {
clientId: 'test_client_id',
clientSecret: 'test_client_secret',
redirectUri: 'http://localhost:3000/auth-code',
scopes: [AuthScopes.Accounting],
};
// Describe the Account API
describe('Account API', () => {
// Declare the API Client
let apiClient: ApiClient;
// Declare the Global Fetch
let globalFetch: typeof fetch;
// Before Each
beforeEach(async () => {
// Set the Global Fetch
globalFetch = global.fetch;
// Create the Auth Provider
const authProvider = new AuthProvider(TEST_CONFIG.clientId, TEST_CONFIG.clientSecret, TEST_CONFIG.redirectUri, TEST_CONFIG.scopes);
// Set the Token for the Auth Provider
await authProvider.setToken(mockTokenData);
// Create the API Client
apiClient = new ApiClient(authProvider, Environment.Sandbox);
});
// After Each
afterEach(() => {
// Set the Global Fetch
global.fetch = globalFetch;
});
// Describe the getAllAccounts Method
describe('getAllAccounts', () => {
// After Each
afterEach(() => {
// Set the Global Fetch
global.fetch = globalFetch;
});
// Test handling search options
it('should handle search options', async () => {
// Get a subset of accounts
const accounts = mockAccountData.slice(0, 10);
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: accounts,
maxResults: 10,
startPosition: 1,
totalCount: accounts.length,
},
};
// Mock the Fetch with proper QueryResponse structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Accounts with search options
const searchResponse = await apiClient.accounts.getAllAccounts({
maxResults: 10,
page: 1,
});
// Assert the Accounts
expect(searchResponse.results).toBeArray();
expect(searchResponse.results.length).toBe(10);
// Assert the Intuit TID
expect(searchResponse.intuitTID).toBeDefined();
expect(typeof searchResponse.intuitTID).toBe('string');
expect(searchResponse.intuitTID).toBe('test-tid-12345-67890');
});
// Test fetching all accounts
it('should fetch all accounts', async () => {
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: mockAccountData,
maxResults: mockAccountData.length,
startPosition: 1,
totalCount: mockAccountData.length,
},
};
// Mock the Fetch with proper QueryResponse structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Accounts
const searchResponse = await apiClient.accounts.getAllAccounts();
// Assert the Accounts
expect(searchResponse.results).toBeArray();
expect(searchResponse.results.length).toBe(mockAccountData.length);
searchResponse.results.forEach((account, index) => {
expect(account.Id).toBe(mockAccountData[index].Id);
});
// Assert the Intuit TID
expect(searchResponse.intuitTID).toBeDefined();
expect(typeof searchResponse.intuitTID).toBe('string');
expect(searchResponse.intuitTID).toBe('test-tid-12345-67890');
});
});
// Describe the hasNextPage Method
describe('hasNextPage', () => {
it('should return true if there is a next page', async () => {
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: mockAccountData,
maxResults: mockAccountData.length,
startPosition: 1,
totalCount: mockAccountData.length,
},
};
// Mock the Fetch with proper QueryResponse structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Accounts
const searchResponse = await apiClient.accounts.getAllAccounts();
// Assert the Next Page
expect(searchResponse.hasNextPage).toBe(true);
// Assert the Intuit TID
expect(searchResponse.intuitTID).toBeDefined();
expect(typeof searchResponse.intuitTID).toBe('string');
expect(searchResponse.intuitTID).toBe('test-tid-12345-67890');
});
});
// Describe the getAccountById Method
describe('getAccountById', () => {
// After Each
afterEach(() => {
// Set the Global Fetch
global.fetch = globalFetch;
});
it('should fetch accounts by their IDs', async () => {
// Test each account in mock data
for (const account of mockAccountData) {
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: [account],
maxResults: 1,
startPosition: 1,
totalCount: 1,
},
};
// Mock single account response structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Account
const accountResponse = await apiClient.accounts.getAccountById(account.Id);
// Assert the Account Response Structure
expect(accountResponse).toBeObject();
expect(accountResponse).toHaveProperty('account');
expect(accountResponse).toHaveProperty('intuitTID');
expect(typeof accountResponse.intuitTID).toBe('string');
expect(accountResponse.intuitTID).toBe('test-tid-12345-67890');
// Assert the Account
expect(accountResponse.account).toBeObject();
expect(accountResponse.account?.Id).toBe(account.Id);
// Assert the Account is a class instance
expect(accountResponse.account).toBeInstanceOf(Account);
expect(typeof accountResponse.account?.setApiClient).toBe('function');
expect(typeof accountResponse.account?.reload).toBe('function');
expect(typeof accountResponse.account?.save).toBe('function');
}
});
it('should throw error for invalid account ID', async () => {
// Mock empty response with 400 status
global.fetch = mockFetch(
JSON.stringify({
QueryResponse: {},
fault: { error: [{ message: 'Account not found' }] },
}),
400,
);
// Assert the Error
expect(apiClient.accounts.getAccountById('-1')).rejects.toThrow('Failed to run request');
});
});
// Describe the getAccountsForDateRange Method
describe('getAccountsForDateRange', () => {
it('should fetch accounts within date range', async () => {
// Set the start Date
const startDate = new Date('2020-01-09');
// Set the end Date
const endDate = new Date('2025-01-12');
// Get the List of Accounts in that date range for the mock data
const accountsInDateRange = mockAccountData.filter((account) => {
// Skip if no metadata
if (!account.MetaData?.LastUpdatedTime) return false;
const accountDate = new Date(account.MetaData.LastUpdatedTime);
return accountDate >= startDate && accountDate <= endDate;
});
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: accountsInDateRange,
maxResults: accountsInDateRange.length,
startPosition: 1,
totalCount: accountsInDateRange.length,
},
};
// Mock response with proper structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Accounts
const searchResponse = await apiClient.accounts.getAccountsForDateRange(startDate, endDate);
// Assert the Accounts
expect(searchResponse.results).toBeArray();
expect(searchResponse.results.length).toBe(accountsInDateRange.length);
expect(searchResponse.results[0].Id).toBe(accountsInDateRange[0].Id);
// Assert the Intuit TID
expect(searchResponse.intuitTID).toBeDefined();
expect(typeof searchResponse.intuitTID).toBe('string');
expect(searchResponse.intuitTID).toBe('test-tid-12345-67890');
});
});
// Describe the getUpdatedAccounts Method
describe('getUpdatedAccounts', () => {
it('should fetch updated accounts', async () => {
// Get the Last Updated Time
const lastUpdatedTime = new Date('2024-01-09');
// Get the List of Updated Accounts from mock data
const updatedAccounts = mockAccountData.filter((account) => {
// Skip if no metadata
if (!account.MetaData?.LastUpdatedTime) return false;
const accountDate = new Date(account.MetaData.LastUpdatedTime);
return accountDate >= lastUpdatedTime;
});
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: updatedAccounts,
maxResults: updatedAccounts.length,
startPosition: 1,
totalCount: updatedAccounts.length,
},
};
// Mock the Fetch with proper QueryResponse structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Accounts
const searchResponse = await apiClient.accounts.getUpdatedAccounts(lastUpdatedTime);
// Assert the Accounts
expect(searchResponse.results).toBeArray();
expect(searchResponse.results.length).toBe(updatedAccounts.length);
expect(searchResponse.results[0].Id).toBe(updatedAccounts[0].Id);
// Assert the Intuit TID
expect(searchResponse.intuitTID).toBeDefined();
expect(typeof searchResponse.intuitTID).toBe('string');
expect(searchResponse.intuitTID).toBe('test-tid-12345-67890');
});
// Describe the getUpdatedCustomers Method
it('should return an empty array if no customers are updated', async () => {
// Setup the Account Query Response
const accountQueryResponse: { QueryResponse: {}; time: string } = {
QueryResponse: {},
time: '2025-03-04T05:46:36.933-08:00',
};
// Mock the Fetch with proper QueryResponse structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Accounts
const searchResponse = await apiClient.accounts.getUpdatedAccounts(new Date(new Date().getTime() + 68400000));
// Assert the Accounts
expect(searchResponse.results).toBeArray();
// Assert the Accounts Length
expect(searchResponse.results.length).toBe(0);
// Assert the Intuit TID
expect(searchResponse.intuitTID).toBeDefined();
expect(typeof searchResponse.intuitTID).toBe('string');
expect(searchResponse.intuitTID).toBe('test-tid-12345-67890');
});
});
// Describe the rawAccountQuery Method
describe('rawAccountQuery', () => {
it('should execute raw account query', async () => {
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: mockAccountData,
maxResults: mockAccountData.length,
startPosition: 1,
totalCount: mockAccountData.length,
},
};
// Mock the Fetch with proper QueryResponse structure
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Query Builder
const queryBuilder = await apiClient.accounts.getQueryBuilder();
// Execute Raw Query
const searchResponse = await apiClient.accounts.rawAccountQuery(queryBuilder);
// Assert the Accounts
expect(searchResponse.results).toBeArray();
expect(searchResponse.results.length).toBe(mockAccountData.length);
expect(searchResponse.results[0].Id).toBe(mockAccountData[0].Id);
// Assert the Intuit TID
expect(searchResponse.intuitTID).toBeDefined();
expect(typeof searchResponse.intuitTID).toBe('string');
expect(searchResponse.intuitTID).toBe('test-tid-12345-67890');
});
});
// Describe the Account Class Methods
describe('Account Class', () => {
// After Each
afterEach(() => {
// Set the Global Fetch
global.fetch = globalFetch;
});
// Test class instance methods
it('should have class instance methods', async () => {
// Get a test account
const testAccount = mockAccountData[0];
// Setup the Account Query Response
const accountQueryResponse = {
QueryResponse: {
Account: [testAccount],
maxResults: 1,
startPosition: 1,
totalCount: 1,
},
};
// Mock the Fetch
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Account
const accountResponse = await apiClient.accounts.getAccountById(testAccount.Id);
// Assert it's a class instance
expect(accountResponse.account).toBeInstanceOf(Account);
// Test setApiClient method exists and is callable
expect(typeof accountResponse.account?.setApiClient).toBe('function');
expect(() => accountResponse.account?.setApiClient(apiClient)).not.toThrow();
// Test reload method exists
expect(typeof accountResponse.account?.reload).toBe('function');
// Test save method exists
expect(typeof accountResponse.account?.save).toBe('function');
// Test delete method exists
expect(typeof accountResponse.account?.delete).toBe('function');
});
// Test reload method
it('should reload account data', async () => {
// Get a test account
const testAccount = mockAccountData[0];
const updatedAccount = { ...testAccount, Name: 'Updated Account Name' };
// Setup the Account Query Response for initial fetch
const accountQueryResponse = {
QueryResponse: {
Account: [testAccount],
maxResults: 1,
startPosition: 1,
totalCount: 1,
},
};
// Mock the Fetch for initial fetch
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Account
const accountResponse = await apiClient.accounts.getAccountById(testAccount.Id);
const account = accountResponse.account!;
// Modify the account locally
(account as any).Name = 'Local Change';
// Setup the Account Query Response for reload
const reloadQueryResponse = {
QueryResponse: {
Account: [updatedAccount],
maxResults: 1,
startPosition: 1,
totalCount: 1,
},
};
// Mock the Fetch for reload
global.fetch = mockFetch(JSON.stringify(reloadQueryResponse));
// Reload the Account
await account.reload();
// Assert the Account was reloaded
expect(account.Name).toBe(updatedAccount.Name);
});
// Test save method
it('should save account data', async () => {
// Get a test account
const testAccount = mockAccountData[0];
const savedAccount = { ...testAccount, SyncToken: '1', Id: testAccount.Id };
// Setup the Account Query Response for initial fetch
const accountQueryResponse = {
QueryResponse: {
Account: [testAccount],
maxResults: 1,
startPosition: 1,
totalCount: 1,
},
};
// Mock the Fetch for initial fetch
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Account
const accountResponse = await apiClient.accounts.getAccountById(testAccount.Id);
const account = accountResponse.account!;
// Modify the account
account.Name = 'Updated Account Name';
// Setup the response for save
const saveResponse = {
QueryResponse: {
Account: [savedAccount],
},
};
// Mock the Fetch for save
global.fetch = mockFetch(JSON.stringify(saveResponse));
// Save the Account
await account.save();
// Assert the save was called (mock was invoked)
expect(global.fetch).toBeDefined();
});
// Test delete method
it('should delete (deactivate) account by setting Active=false', async () => {
// Get a test account
const testAccount = mockAccountData[0];
const deletedAccount = { ...testAccount, Active: false, SyncToken: '1' };
// Setup the Account Query Response for initial fetch
const accountQueryResponse = {
QueryResponse: {
Account: [testAccount],
maxResults: 1,
startPosition: 1,
totalCount: 1,
},
};
// Mock the Fetch for initial fetch
global.fetch = mockFetch(JSON.stringify(accountQueryResponse));
// Get the Account
const accountResponse = await apiClient.accounts.getAccountById(testAccount.Id);
const account = accountResponse.account!;
// Ensure account is active
account.Active = true;
// Setup the response for delete (save with Active=false)
const deleteResponse = {
QueryResponse: {
Account: [deletedAccount],
},
};
// Mock the Fetch for delete
global.fetch = mockFetch(JSON.stringify(deleteResponse));
// Delete the Account
await account.delete();
// Assert the account was deactivated
expect(account.Active).toBe(false);
});
});
});