nortax-ts
Version:
Norwegian Tax Calculator - TypeScript Implementation
336 lines (335 loc) • 15.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tax_1 = require("../tax");
const mockFetch = jest.fn();
global.fetch = mockFetch;
describe('Tax', () => {
beforeEach(() => {
mockFetch.mockReset();
});
// ... existing constructor tests ...
describe('edge cases and validation', () => {
it('should handle negative gross income', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(0),
});
const tax = new tax_1.Tax(-5000);
const deduction = await tax.getDeduction();
expect(deduction).toBe(0);
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('bruttoloenn=-5000'));
});
it('should handle zero gross income', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(0),
});
const tax = new tax_1.Tax(0);
const deduction = await tax.getDeduction();
expect(deduction).toBe(0);
});
it('should handle very large income values', async () => {
const largeIncome = 999999999;
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(300000),
});
const tax = new tax_1.Tax(largeIncome);
const deduction = await tax.getDeduction();
expect(deduction).toBe(300000);
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining(`bruttoloenn=${largeIncome}`));
});
it('should handle decimal income values', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(5000),
});
const tax = new tax_1.Tax(25000.5);
await tax.getDeduction();
// Should use integer value in API call
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('bruttoloenn=25001'));
});
});
describe('API error handling', () => {
it('should handle empty response', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(null),
});
const tax = new tax_1.Tax(25000);
const deduction = await tax.getDeduction();
expect(deduction).toBe(0); // Should default to 0 for invalid response
});
it('should handle malformed JSON response', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.reject(new Error('Invalid JSON')),
});
const tax = new tax_1.Tax(25000);
await expect(tax.getDeduction()).rejects.toThrow('Invalid JSON response'); // Changed to match new error message
});
// Alternative approach with more specific testing:
it('should handle malformed JSON response with specific error', async () => {
// Set up mock for both calls
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.reject(new Error('Invalid JSON')),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.reject(new Error('Invalid JSON')),
});
const tax = new tax_1.Tax(25000);
// Test both error type and message in a single assertion
await expect(tax.getDeduction()).rejects.toThrow(expect.objectContaining({
name: 'TaxApiError',
message: expect.stringMatching(/Invalid JSON response/),
}));
});
it('should handle timeout errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Request timeout'));
const tax = new tax_1.Tax(25000);
await expect(tax.getDeduction()).rejects.toThrow('Failed to fetch tax data: Request timeout');
});
it('should handle 500 server error', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
statusText: 'Internal Server Error',
status: 500,
});
const tax = new tax_1.Tax(25000);
await expect(tax.getDeduction()).rejects.toThrow('API call failed: Internal Server Error');
});
it('should handle non-Error objects in catch block', async () => {
mockFetch.mockRejectedValueOnce('String error');
const tax = new tax_1.Tax(25000);
await expect(tax.getDeduction()).rejects.toThrow('Failed to fetch tax data: Unknown error');
});
it('should handle Error objects in JSON parsing', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.reject(new Error('Parse failed')),
});
const tax = new tax_1.Tax(25000);
await expect(tax.getDeduction()).rejects.toThrow('Invalid JSON response: Parse failed');
});
it('should handle undefined error in catch block', async () => {
// Mock fetch to reject with undefined
mockFetch.mockRejectedValueOnce(undefined);
const tax = new tax_1.Tax(25000);
await expect(tax.getDeduction()).rejects.toThrow('Failed to fetch tax data: Unknown error');
});
it('should handle null error in catch block', async () => {
// Mock fetch to reject with null
mockFetch.mockRejectedValueOnce(null);
const tax = new tax_1.Tax(25000);
await expect(tax.getDeduction()).rejects.toThrow('Failed to fetch tax data: Unknown error');
});
});
describe('period handling', () => {
it.each([
['1 day', 'PERIODE_1_DAG'],
['2 days', 'PERIODE_2_DAGER'],
['3 days', 'PERIODE_3_DAGER'],
['4 days', 'PERIODE_4_DAGER'],
['1 week', 'PERIODE_1_UKE'],
['2 weeks', 'PERIODE_14_DAGER'],
['Monthly', 'PERIODE_1_MAANED'],
])('should handle period %s correctly', async (period, expectedValue) => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(1000),
});
const tax = new tax_1.Tax(25000, '7100', 'Wage', period);
await tax.getDeduction();
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining(expectedValue));
});
});
describe('tax table handling', () => {
it.each([
['7100', 'standard'],
['7101', 'alternative'],
['7150', 'special'],
['6300', 'pension'],
])('should handle tax table %s correctly', async (table, _type) => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(1000),
});
const tax = new tax_1.Tax(25000, table);
await tax.getDeduction();
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining(`tabell=${table}`));
});
});
describe('income type handling', () => {
it('should handle Wage income type correctly', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(1000),
});
const tax = new tax_1.Tax(25000, '7100', 'Wage');
await tax.getDeduction();
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('LONN'));
});
it('should handle Pension income type correctly', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(1000),
});
const tax = new tax_1.Tax(25000, '7100', 'Pension');
await tax.getDeduction();
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('PENSJON'));
});
});
describe('whole table handling', () => {
it('should handle empty table response', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ alleTrekk: {} }),
});
const tax = new tax_1.Tax();
const table = await tax.getWholeTable();
expect(table).toEqual({});
});
it('should handle missing alleTrekk in response', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({}),
});
const tax = new tax_1.Tax();
const table = await tax.getWholeTable();
expect(table).toBeUndefined();
});
});
describe('tax table validation', () => {
it('should throw error for invalid table/year combination (8150 in 2024)', () => {
expect(() => new tax_1.Tax(50000, '8150', 'Wage', 'Monthly', 2024)).toThrow('Invalid tax table "8150" for year 2024');
});
it('should throw error for invalid table/year combination (7100 in 2025)', () => {
expect(() => new tax_1.Tax(50000, '7100', 'Wage', 'Monthly', 2025)).toThrow('Invalid tax table "7100" for year 2025');
});
it('should accept valid table for 2024', () => {
expect(() => new tax_1.Tax(50000, '7100', 'Wage', 'Monthly', 2024)).not.toThrow();
});
it('should accept valid table for 2025', () => {
expect(() => new tax_1.Tax(50000, '8150', 'Wage', 'Monthly', 2025)).not.toThrow();
});
});
describe('getFullDetails', () => {
it('should return formatted string with all tax details', async () => {
// Mock both fetch calls needed for getFullDetails
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(6735),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(6735),
});
const tax = new tax_1.Tax(25000, '7100', 'Pension', '2 weeks', 2024);
const details = await tax.getFullDetails();
expect(details).toContain(`URL: str = https://trekktabell.formueinntekt.skatt.skatteetaten.no/api/trekktabell/beregn`);
expect(details).toContain('Tax table: valid_tables = 7100');
expect(details).toContain('Income type: income_type = Pension');
expect(details).toContain('Period: period = 2 weeks');
expect(details).toContain('Year: int = 2024');
expect(details).toContain('Gross income: int = 25000');
expect(details).toContain('Tax deduction: int = 6735');
expect(details).toContain('Net income: int = 18265');
});
it('should handle API errors gracefully', async () => {
mockFetch.mockRejectedValueOnce(new Error('API Error'));
const tax = new tax_1.Tax(25000);
await expect(tax.getFullDetails()).rejects.toThrow('Failed to fetch tax data');
});
it('should handle null response', async () => {
// Mock both fetch calls to return null
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(null),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(null),
});
const tax = new tax_1.Tax(25000);
const details = await tax.getFullDetails();
expect(details).toContain('Tax deduction: int = 0');
expect(details).toContain('Net income: int = 25000'); // 25000 - 0
});
it('should throw error for invalid table 8150 in 2024', () => {
// Test the constructor directly since that's where the validation happens
expect(() => new tax_1.Tax(50000, // grossIncome
'8150', // invalid table for 2024
'Wage', 'Monthly', 2024)).toThrow('Invalid tax table "8150" for year 2024');
});
it('should work with table 8150 in 2025', async () => {
// Mock both fetch calls needed for getFullDetails
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(11333), // Known value for 50000 NOK
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(11333),
});
const tax = new tax_1.Tax(50000, // grossIncome
'8150', // valid table for 2025
'Wage', 'Monthly', 2025);
const details = await tax.getFullDetails();
expect(details).toContain('Tax table: valid_tables = 8150');
expect(details).toContain('Year: int = 2025');
expect(details).toContain('Tax deduction: int = 11333');
expect(details).toContain('Net income: int = 38667'); // 50000 - 11333
});
});
describe('toString', () => {
it('should return formatted string with basic tax details', () => {
const tax = new tax_1.Tax(25000, '7100', 'Pension', '2 weeks', 2024);
const result = tax.toString();
// Check each line individually
expect(result).toContain(`URL: str = `);
expect(result).toContain('Tax table: valid_tables = 7100');
expect(result).toContain('Income type: income_type = Pension');
expect(result).toContain('Period: period = 2 weeks');
expect(result).toContain('Year: int = 2024');
expect(result).toContain('Gross income: int = 25000');
});
it('should work with default values', () => {
const tax = new tax_1.Tax();
const result = tax.toString();
expect(result).toContain('Tax table: valid_tables = 7100');
expect(result).toContain('Income type: income_type = Wage');
expect(result).toContain('Period: period = Monthly');
expect(result).toContain('Year: int = 2024');
expect(result).toContain('Gross income: int = 0');
});
it('should handle 2025 tax table', () => {
const tax = new tax_1.Tax(50000, '8150', 'Wage', 'Monthly', 2025);
const result = tax.toString();
expect(result).toContain('Tax table: valid_tables = 8150');
expect(result).toContain('Year: int = 2025');
});
});
describe('Symbol.toStringTag', () => {
it('should return formatted string with tax instance details', () => {
const tax = new tax_1.Tax(25000, '7100', 'Pension', '2 weeks', 2024);
const result = tax[Symbol.toStringTag]();
expect(result).toBe('Tax(gross_income=25000, tax_table="7100", income_type="Pension", period="2 weeks", year=2024)');
});
it('should work with default values', () => {
const tax = new tax_1.Tax();
const result = tax[Symbol.toStringTag]();
expect(result).toBe('Tax(gross_income=0, tax_table="7100", income_type="Wage", period="Monthly", year=2024)');
});
it('should handle 2025 tax table', () => {
const tax = new tax_1.Tax(50000, '8150', 'Wage', 'Monthly', 2025);
const result = tax[Symbol.toStringTag]();
expect(result).toBe('Tax(gross_income=50000, tax_table="8150", income_type="Wage", period="Monthly", year=2025)');
});
});
});