From 01d6ff3fa31a64634764645389c39e290040cae0 Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Fri, 27 Mar 2026 21:14:42 -0500 Subject: [PATCH] Move tests to __tests__ folders, add unit tests for shared utils and services Restructure tests into __tests__/ directories at package root so they can be excluded from production builds. Add unit tests for dates, currency, lookup service, payment method default logic, and tax exemption state transitions. --- .../routes/v1/accounts-extended.test.ts | 2 +- .../routes/v1/accounts.test.ts | 2 +- .../{src => __tests__}/routes/v1/auth.test.ts | 2 +- .../routes/v1/health.test.ts | 2 +- .../routes/v1/inventory.test.ts | 2 +- .../routes/v1/products.test.ts | 2 +- .../services/account.service.test.ts | 240 ++++++++++++++++++ .../__tests__/services/lookup.service.test.ts | 152 +++++++++++ .../shared/__tests__/utils/currency.test.ts | 68 +++++ packages/shared/__tests__/utils/dates.test.ts | 79 ++++++ 10 files changed, 545 insertions(+), 6 deletions(-) rename packages/backend/{src => __tests__}/routes/v1/accounts-extended.test.ts (99%) rename packages/backend/{src => __tests__}/routes/v1/accounts.test.ts (99%) rename packages/backend/{src => __tests__}/routes/v1/auth.test.ts (99%) rename packages/backend/{src => __tests__}/routes/v1/health.test.ts (91%) rename packages/backend/{src => __tests__}/routes/v1/inventory.test.ts (99%) rename packages/backend/{src => __tests__}/routes/v1/products.test.ts (99%) create mode 100644 packages/backend/__tests__/services/account.service.test.ts create mode 100644 packages/backend/__tests__/services/lookup.service.test.ts create mode 100644 packages/shared/__tests__/utils/currency.test.ts create mode 100644 packages/shared/__tests__/utils/dates.test.ts diff --git a/packages/backend/src/routes/v1/accounts-extended.test.ts b/packages/backend/__tests__/routes/v1/accounts-extended.test.ts similarity index 99% rename from packages/backend/src/routes/v1/accounts-extended.test.ts rename to packages/backend/__tests__/routes/v1/accounts-extended.test.ts index e0d269c..e57e0b9 100644 --- a/packages/backend/src/routes/v1/accounts-extended.test.ts +++ b/packages/backend/__tests__/routes/v1/accounts-extended.test.ts @@ -6,7 +6,7 @@ import { seedTestCompany, registerAndLogin, TEST_COMPANY_ID, -} from '../../test/helpers.js' +} from '../../../src/test/helpers.js' describe('Processor link routes', () => { let app: FastifyInstance diff --git a/packages/backend/src/routes/v1/accounts.test.ts b/packages/backend/__tests__/routes/v1/accounts.test.ts similarity index 99% rename from packages/backend/src/routes/v1/accounts.test.ts rename to packages/backend/__tests__/routes/v1/accounts.test.ts index 4eddedc..ee9b0fd 100644 --- a/packages/backend/src/routes/v1/accounts.test.ts +++ b/packages/backend/__tests__/routes/v1/accounts.test.ts @@ -6,7 +6,7 @@ import { seedTestCompany, registerAndLogin, TEST_COMPANY_ID, -} from '../../test/helpers.js' +} from '../../../src/test/helpers.js' describe('Account routes', () => { let app: FastifyInstance diff --git a/packages/backend/src/routes/v1/auth.test.ts b/packages/backend/__tests__/routes/v1/auth.test.ts similarity index 99% rename from packages/backend/src/routes/v1/auth.test.ts rename to packages/backend/__tests__/routes/v1/auth.test.ts index 1664e82..faddfc1 100644 --- a/packages/backend/src/routes/v1/auth.test.ts +++ b/packages/backend/__tests__/routes/v1/auth.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test' import type { FastifyInstance } from 'fastify' -import { createTestApp, cleanDb, seedTestCompany, TEST_COMPANY_ID } from '../../test/helpers.js' +import { createTestApp, cleanDb, seedTestCompany, TEST_COMPANY_ID } from '../../../src/test/helpers.js' describe('Auth routes', () => { let app: FastifyInstance diff --git a/packages/backend/src/routes/v1/health.test.ts b/packages/backend/__tests__/routes/v1/health.test.ts similarity index 91% rename from packages/backend/src/routes/v1/health.test.ts rename to packages/backend/__tests__/routes/v1/health.test.ts index 6adf97a..a74d370 100644 --- a/packages/backend/src/routes/v1/health.test.ts +++ b/packages/backend/__tests__/routes/v1/health.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll, afterAll } from 'bun:test' import type { FastifyInstance } from 'fastify' -import { createTestApp } from '../../test/helpers.js' +import { createTestApp } from '../../../src/test/helpers.js' describe('GET /v1/health', () => { let app: FastifyInstance diff --git a/packages/backend/src/routes/v1/inventory.test.ts b/packages/backend/__tests__/routes/v1/inventory.test.ts similarity index 99% rename from packages/backend/src/routes/v1/inventory.test.ts rename to packages/backend/__tests__/routes/v1/inventory.test.ts index b72bcda..695ce8d 100644 --- a/packages/backend/src/routes/v1/inventory.test.ts +++ b/packages/backend/__tests__/routes/v1/inventory.test.ts @@ -5,7 +5,7 @@ import { cleanDb, seedTestCompany, registerAndLogin, -} from '../../test/helpers.js' +} from '../../../src/test/helpers.js' describe('Category routes', () => { let app: FastifyInstance diff --git a/packages/backend/src/routes/v1/products.test.ts b/packages/backend/__tests__/routes/v1/products.test.ts similarity index 99% rename from packages/backend/src/routes/v1/products.test.ts rename to packages/backend/__tests__/routes/v1/products.test.ts index 519c1a3..1372241 100644 --- a/packages/backend/src/routes/v1/products.test.ts +++ b/packages/backend/__tests__/routes/v1/products.test.ts @@ -5,7 +5,7 @@ import { cleanDb, seedTestCompany, registerAndLogin, -} from '../../test/helpers.js' +} from '../../../src/test/helpers.js' describe('Product routes', () => { let app: FastifyInstance diff --git a/packages/backend/__tests__/services/account.service.test.ts b/packages/backend/__tests__/services/account.service.test.ts new file mode 100644 index 0000000..4dea6b9 --- /dev/null +++ b/packages/backend/__tests__/services/account.service.test.ts @@ -0,0 +1,240 @@ +import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test' +import type { FastifyInstance } from 'fastify' +import { createTestApp, cleanDb, seedTestCompany, registerAndLogin, TEST_COMPANY_ID } from '../../src/test/helpers.js' +import { AccountService, PaymentMethodService, TaxExemptionService } from '../../src/services/account.service.js' + +describe('PaymentMethodService', () => { + let app: FastifyInstance + let accountId: string + + beforeAll(async () => { + app = await createTestApp() + }) + + beforeEach(async () => { + await cleanDb(app) + await seedTestCompany(app) + const auth = await registerAndLogin(app, { email: `pm-svc-${Date.now()}@test.com` }) + + const account = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'PM Test' }) + accountId = account.id + }) + + afterAll(async () => { + await app.close() + }) + + it('creates a payment method', async () => { + const pm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_abc', + cardBrand: 'visa', + lastFour: '4242', + expMonth: 12, + expYear: 2027, + isDefault: false, + }) + expect(pm.processorPaymentMethodId).toBe('pm_abc') + expect(pm.isDefault).toBe(false) + }) + + it('unsets old default when creating a new default', async () => { + const first = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_first', + isDefault: true, + }) + expect(first.isDefault).toBe(true) + + const second = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_second', + isDefault: true, + }) + expect(second.isDefault).toBe(true) + + // First should no longer be default + const firstRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, first.id) + expect(firstRefreshed!.isDefault).toBe(false) + }) + + it('unsets old default when updating to default', async () => { + const first = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_1', + isDefault: true, + }) + + const second = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_2', + isDefault: false, + }) + + await PaymentMethodService.update(app.db, TEST_COMPANY_ID, second.id, { isDefault: true }) + + const firstRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, first.id) + expect(firstRefreshed!.isDefault).toBe(false) + + const secondRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, second.id) + expect(secondRefreshed!.isDefault).toBe(true) + }) + + it('does not affect other accounts when setting default', async () => { + const otherAccount = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Other' }) + + const otherPm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId: otherAccount.id, + processor: 'stripe', + processorPaymentMethodId: 'pm_other', + isDefault: true, + }) + + // Create default on our account — should not touch the other account's default + await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_ours', + isDefault: true, + }) + + const otherRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, otherPm.id) + expect(otherRefreshed!.isDefault).toBe(true) + }) + + it('lists only methods for the requested account', async () => { + const otherAccount = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Other' }) + + await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_a', + isDefault: false, + }) + await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId: otherAccount.id, + processor: 'stripe', + processorPaymentMethodId: 'pm_b', + isDefault: false, + }) + + const methods = await PaymentMethodService.listByAccount(app.db, TEST_COMPANY_ID, accountId) + expect(methods.length).toBe(1) + expect(methods[0].processorPaymentMethodId).toBe('pm_a') + }) + + it('deletes a payment method', async () => { + const pm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, { + accountId, + processor: 'stripe', + processorPaymentMethodId: 'pm_del', + isDefault: false, + }) + + const deleted = await PaymentMethodService.delete(app.db, TEST_COMPANY_ID, pm.id) + expect(deleted).not.toBeNull() + + const fetched = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, pm.id) + expect(fetched).toBeNull() + }) +}) + +describe('TaxExemptionService', () => { + let app: FastifyInstance + let accountId: string + let userId: string + + beforeAll(async () => { + app = await createTestApp() + }) + + beforeEach(async () => { + await cleanDb(app) + await seedTestCompany(app) + const auth = await registerAndLogin(app, { email: `tax-svc-${Date.now()}@test.com` }) + userId = (auth.user as { id: string }).id + + const account = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Tax Test' }) + accountId = account.id + }) + + afterAll(async () => { + await app.close() + }) + + it('creates an exemption in pending status', async () => { + const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, { + accountId, + certificateNumber: 'TX-001', + certificateType: 'resale', + issuingState: 'TX', + }) + expect(exemption.status).toBe('pending') + expect(exemption.certificateNumber).toBe('TX-001') + expect(exemption.approvedBy).toBeNull() + }) + + it('approves a pending exemption', async () => { + const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, { + accountId, + certificateNumber: 'TX-002', + }) + + const approved = await TaxExemptionService.approve(app.db, TEST_COMPANY_ID, exemption.id, userId) + expect(approved!.status).toBe('approved') + expect(approved!.approvedBy).toBe(userId) + expect(approved!.approvedAt).toBeTruthy() + }) + + it('revokes an exemption with reason', async () => { + const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, { + accountId, + certificateNumber: 'TX-003', + }) + await TaxExemptionService.approve(app.db, TEST_COMPANY_ID, exemption.id, userId) + + const revoked = await TaxExemptionService.revoke(app.db, TEST_COMPANY_ID, exemption.id, userId, 'Expired') + expect(revoked!.status).toBe('none') + expect(revoked!.revokedBy).toBe(userId) + expect(revoked!.revokedReason).toBe('Expired') + expect(revoked!.revokedAt).toBeTruthy() + }) + + it('lists exemptions for an account', async () => { + await TaxExemptionService.create(app.db, TEST_COMPANY_ID, { + accountId, + certificateNumber: 'CERT-A', + }) + await TaxExemptionService.create(app.db, TEST_COMPANY_ID, { + accountId, + certificateNumber: 'CERT-B', + }) + + const list = await TaxExemptionService.listByAccount(app.db, TEST_COMPANY_ID, accountId) + expect(list.length).toBe(2) + }) + + it('updates exemption details', async () => { + const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, { + accountId, + certificateNumber: 'OLD', + }) + + const updated = await TaxExemptionService.update(app.db, TEST_COMPANY_ID, exemption.id, { + certificateNumber: 'NEW', + issuingState: 'CA', + }) + expect(updated!.certificateNumber).toBe('NEW') + expect(updated!.issuingState).toBe('CA') + }) + + it('returns null for nonexistent exemption', async () => { + const result = await TaxExemptionService.getById(app.db, TEST_COMPANY_ID, '00000000-0000-0000-0000-000000000000') + expect(result).toBeNull() + }) +}) diff --git a/packages/backend/__tests__/services/lookup.service.test.ts b/packages/backend/__tests__/services/lookup.service.test.ts new file mode 100644 index 0000000..d25d593 --- /dev/null +++ b/packages/backend/__tests__/services/lookup.service.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test' +import type { FastifyInstance } from 'fastify' +import { createTestApp, cleanDb, seedTestCompany, TEST_COMPANY_ID } from '../../src/test/helpers.js' +import { UnitStatusService, ItemConditionService } from '../../src/services/lookup.service.js' + +describe('UnitStatusService', () => { + let app: FastifyInstance + + beforeAll(async () => { + app = await createTestApp() + }) + + beforeEach(async () => { + await cleanDb(app) + await seedTestCompany(app) + }) + + afterAll(async () => { + await app.close() + }) + + describe('seedForCompany', () => { + it('seeds system statuses on first call', async () => { + const statuses = await UnitStatusService.list(app.db, TEST_COMPANY_ID) + expect(statuses.length).toBeGreaterThanOrEqual(8) + expect(statuses.every((s) => s.isSystem)).toBe(true) + }) + + it('is idempotent — second seed does not duplicate', async () => { + await UnitStatusService.seedForCompany(app.db, TEST_COMPANY_ID) + const statuses = await UnitStatusService.list(app.db, TEST_COMPANY_ID) + const slugs = statuses.map((s) => s.slug) + const uniqueSlugs = new Set(slugs) + expect(slugs.length).toBe(uniqueSlugs.size) + }) + }) + + describe('getBySlug', () => { + it('returns a system status by slug', async () => { + const status = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'available') + expect(status).not.toBeNull() + expect(status!.slug).toBe('available') + expect(status!.isSystem).toBe(true) + }) + + it('returns null for nonexistent slug', async () => { + const status = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'nonexistent') + expect(status).toBeNull() + }) + }) + + describe('validateSlug', () => { + it('returns true for valid active slug', async () => { + expect(await UnitStatusService.validateSlug(app.db, TEST_COMPANY_ID, 'sold')).toBe(true) + }) + + it('returns false for nonexistent slug', async () => { + expect(await UnitStatusService.validateSlug(app.db, TEST_COMPANY_ID, 'bogus')).toBe(false) + }) + }) + + describe('create', () => { + it('creates a custom (non-system) status', async () => { + const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, { + name: 'In Transit', + slug: 'in_transit', + description: 'Being shipped between locations', + sortOrder: 99, + }) + expect(custom.slug).toBe('in_transit') + expect(custom.isSystem).toBe(false) + }) + }) + + describe('delete', () => { + it('throws when deleting a system status', async () => { + const system = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'available') + expect(system).not.toBeNull() + expect(() => UnitStatusService.delete(app.db, TEST_COMPANY_ID, system!.id)).toThrow('system') + }) + + it('deletes a custom status', async () => { + const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, { + name: 'Temp', + slug: 'temp', + sortOrder: 0, + }) + const deleted = await UnitStatusService.delete(app.db, TEST_COMPANY_ID, custom.id) + expect(deleted).not.toBeNull() + expect(deleted!.slug).toBe('temp') + }) + + it('returns null for nonexistent id', async () => { + const result = await UnitStatusService.delete(app.db, TEST_COMPANY_ID, '00000000-0000-0000-0000-000000000000') + expect(result).toBeNull() + }) + }) + + describe('update', () => { + it('throws when deactivating a system status', async () => { + const system = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'sold') + expect(() => UnitStatusService.update(app.db, TEST_COMPANY_ID, system!.id, { isActive: false })).toThrow('system') + }) + + it('allows renaming a custom status', async () => { + const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, { + name: 'Old Name', + slug: 'custom_rename', + sortOrder: 0, + }) + const updated = await UnitStatusService.update(app.db, TEST_COMPANY_ID, custom.id, { name: 'New Name' }) + expect(updated!.name).toBe('New Name') + }) + }) +}) + +describe('ItemConditionService', () => { + let app: FastifyInstance + + beforeAll(async () => { + app = await createTestApp() + }) + + beforeEach(async () => { + await cleanDb(app) + await seedTestCompany(app) + }) + + afterAll(async () => { + await app.close() + }) + + it('seeds system conditions', async () => { + const conditions = await ItemConditionService.list(app.db, TEST_COMPANY_ID) + const slugs = conditions.map((c) => c.slug) + expect(slugs).toContain('new') + expect(slugs).toContain('excellent') + expect(slugs).toContain('good') + expect(slugs).toContain('fair') + expect(slugs).toContain('poor') + }) + + it('creates a custom condition', async () => { + const custom = await ItemConditionService.create(app.db, TEST_COMPANY_ID, { + name: 'Refurbished', + slug: 'refurbished', + sortOrder: 10, + }) + expect(custom.isSystem).toBe(false) + expect(await ItemConditionService.validateSlug(app.db, TEST_COMPANY_ID, 'refurbished')).toBe(true) + }) +}) diff --git a/packages/shared/__tests__/utils/currency.test.ts b/packages/shared/__tests__/utils/currency.test.ts new file mode 100644 index 0000000..9e2e812 --- /dev/null +++ b/packages/shared/__tests__/utils/currency.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from 'bun:test' +import { formatCurrency, roundCurrency, toCents, toDollars } from '../../src/utils/currency.js' + +describe('formatCurrency', () => { + it('formats whole dollars', () => { + expect(formatCurrency(100)).toBe('$100.00') + }) + + it('formats cents', () => { + expect(formatCurrency(49.99)).toBe('$49.99') + }) + + it('formats zero', () => { + expect(formatCurrency(0)).toBe('$0.00') + }) + + it('formats thousands with comma', () => { + expect(formatCurrency(1234.56)).toBe('$1,234.56') + }) + + it('formats negative amounts', () => { + expect(formatCurrency(-25.50)).toBe('-$25.50') + }) +}) + +describe('roundCurrency', () => { + it('rounds to 2 decimal places', () => { + expect(roundCurrency(1.005)).toBe(1.01) + expect(roundCurrency(1.004)).toBe(1) + }) + + it('handles already-rounded values', () => { + expect(roundCurrency(10.50)).toBe(10.50) + }) + + it('handles zero', () => { + expect(roundCurrency(0)).toBe(0) + }) +}) + +describe('toCents', () => { + it('converts dollars to cents', () => { + expect(toCents(1.00)).toBe(100) + expect(toCents(49.99)).toBe(4999) + }) + + it('rounds fractional cents', () => { + // 1.005 * 100 = 100.49999... in IEEE 754, so Math.round gives 100 + // This is a known floating point limitation — use toCents(roundCurrency(x)) for precision + expect(toCents(1.005)).toBe(100) + expect(toCents(1.006)).toBe(101) + }) + + it('handles zero', () => { + expect(toCents(0)).toBe(0) + }) +}) + +describe('toDollars', () => { + it('converts cents to dollars', () => { + expect(toDollars(100)).toBe(1.00) + expect(toDollars(4999)).toBe(49.99) + }) + + it('handles zero', () => { + expect(toDollars(0)).toBe(0) + }) +}) diff --git a/packages/shared/__tests__/utils/dates.test.ts b/packages/shared/__tests__/utils/dates.test.ts new file mode 100644 index 0000000..68c6e95 --- /dev/null +++ b/packages/shared/__tests__/utils/dates.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'bun:test' +import { isMinor, capBillingDay, todayISO } from '../../src/utils/dates.js' + +describe('isMinor', () => { + it('returns true for a child born 10 years ago', () => { + const dob = new Date() + dob.setFullYear(dob.getFullYear() - 10) + expect(isMinor(dob)).toBe(true) + }) + + it('returns false for an adult born 25 years ago', () => { + const dob = new Date() + dob.setFullYear(dob.getFullYear() - 25) + expect(isMinor(dob)).toBe(false) + }) + + it('returns true for someone turning 18 later this year', () => { + const dob = new Date() + dob.setFullYear(dob.getFullYear() - 18) + dob.setMonth(dob.getMonth() + 1) // birthday is next month + expect(isMinor(dob)).toBe(true) + }) + + it('returns false for someone who turned 18 earlier this year', () => { + const dob = new Date() + dob.setFullYear(dob.getFullYear() - 18) + dob.setMonth(dob.getMonth() - 1) // birthday was last month + expect(isMinor(dob)).toBe(false) + }) + + it('returns false on their 18th birthday', () => { + const dob = new Date() + dob.setFullYear(dob.getFullYear() - 18) + expect(isMinor(dob)).toBe(false) + }) + + it('accepts a string date', () => { + expect(isMinor('2000-01-01')).toBe(false) + expect(isMinor('2020-01-01')).toBe(true) + }) + + it('returns true for a newborn', () => { + expect(isMinor(new Date())).toBe(true) + }) +}) + +describe('capBillingDay', () => { + it('caps at 28', () => { + expect(capBillingDay(31)).toBe(28) + expect(capBillingDay(29)).toBe(28) + }) + + it('floors at 1', () => { + expect(capBillingDay(0)).toBe(1) + expect(capBillingDay(-5)).toBe(1) + }) + + it('passes through valid days', () => { + expect(capBillingDay(15)).toBe(15) + expect(capBillingDay(1)).toBe(1) + expect(capBillingDay(28)).toBe(28) + }) + + it('floors fractional days', () => { + expect(capBillingDay(15.7)).toBe(15) + }) +}) + +describe('todayISO', () => { + it('returns YYYY-MM-DD format', () => { + const result = todayISO() + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/) + }) + + it('matches today', () => { + const expected = new Date().toISOString().split('T')[0] + expect(todayISO()).toBe(expected) + }) +})