You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/test/aws-costs.test.ts

82 lines
4.4 KiB
TypeScript

// apps/hq/test/aws-costs.test.ts
import { describe, it, expect } from 'vitest'
import { openDb } from '../src/db'
import { seedIfEmpty } from '../src/seed'
import { createClient } from '../src/repos-clients'
import { getSetting } from '../src/repos-reminders'
import { previousMonth } from '../src/repos-aws'
import { signAwsRequest, pullMonthlyCosts, maybePullAwsCosts, type AwsCostDeps } from '../src/aws-costs'
const CREDS = { accessKeyId: 'AKIDEXAMPLE', secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' }
describe('signAwsRequest (SigV4)', () => {
it('produces a deterministic, well-formed authorization header', () => {
const now = new Date('2026-02-01T00:00:00.000Z')
const a = signAwsRequest({ creds: CREDS, region: 'us-east-1', service: 'ce', target: 'AWSInsightsIndexService.GetCostAndUsage', body: '{}', now })
const b = signAwsRequest({ creds: CREDS, region: 'us-east-1', service: 'ce', target: 'AWSInsightsIndexService.GetCostAndUsage', body: '{}', now })
expect(a.headers['authorization']).toBe(b.headers['authorization']) // deterministic for a fixed clock
expect(a.headers['authorization']).toMatch(
/^AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE\/20260201\/us-east-1\/ce\/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=[0-9a-f]{64}$/,
)
expect(a.headers['x-amz-date']).toBe('20260201T000000Z')
expect(a.url).toBe('https://ce.us-east-1.amazonaws.com/')
})
it('changes the signature when the secret changes', () => {
const now = new Date('2026-02-01T00:00:00.000Z')
const a = signAwsRequest({ creds: CREDS, region: 'us-east-1', service: 'ce', target: 't', body: '{}', now })
const b = signAwsRequest({ creds: { ...CREDS, secretAccessKey: 'other' }, region: 'us-east-1', service: 'ce', target: 't', body: '{}', now })
expect(a.headers['authorization']).not.toBe(b.headers['authorization'])
})
})
const CE_RESPONSE = {
ResultsByTime: [{
Groups: [
{ Keys: ['client$ACME'], Metrics: { UnblendedCost: { Amount: '1234.56', Unit: 'INR' } } },
{ Keys: ['client$GHOST'], Metrics: { UnblendedCost: { Amount: '10.00', Unit: 'INR' } } },
{ Keys: ['client$'], Metrics: { UnblendedCost: { Amount: '5.00', Unit: 'INR' } } },
],
}],
}
function deps(response: unknown, capture?: (url: string, init?: RequestInit) => void): AwsCostDeps {
const f = (async (url: string, init?: RequestInit) => {
capture?.(String(url), init)
return new Response(JSON.stringify(response), { status: 200 })
}) as unknown as typeof fetch
return { f, creds: CREDS, now: () => '2026-07-01T00:00:00Z' }
}
describe('pullMonthlyCosts', () => {
it('maps client tags to clients, upserts cost rows, and collects unknown tags', async () => {
const db = openDb(':memory:'); await seedIfEmpty(db)
const acme = await createClient(db, 'u1', { name: 'Acme', code: 'ACME', stateCode: '32' })
let sentUrl = ''
const out = await pullMonthlyCosts(db, deps(CE_RESPONSE, (u) => { sentUrl = u }), '2026-06')
expect(sentUrl).toBe('https://ce.us-east-1.amazonaws.com/')
expect(out.upserted).toBe(1) // only ACME matched
expect(out.unknownTags.map((u) => u.tag).sort()).toEqual(['', 'GHOST']) // ghost client + untagged
expect(out.unknownTags.find((u) => u.tag === 'GHOST')?.costPaise).toBe(10_00)
const row = await db.get(`SELECT cost_paise, source FROM aws_usage WHERE client_id=?`, acme.id)
expect(row).toMatchObject({ cost_paise: 123456, source: 'auto' })
})
it('throws on a non-INR billing currency rather than mis-converting', async () => {
const db = openDb(':memory:'); await seedIfEmpty(db)
const usd = { ResultsByTime: [{ Groups: [{ Keys: ['client$ACME'], Metrics: { UnblendedCost: { Amount: '10.00', Unit: 'USD' } } }] }] }
await expect(pullMonthlyCosts(db, deps(usd), '2026-06')).rejects.toThrow(/INR/i)
})
})
describe('maybePullAwsCosts', () => {
it('pulls the previous month once per calendar month', async () => {
const db = openDb(':memory:'); await seedIfEmpty(db)
await createClient(db, 'u1', { name: 'Acme', code: 'ACME', stateCode: '32' })
expect(previousMonth('2026-07-10')).toBe('2026-06')
const first = await maybePullAwsCosts(db, deps(CE_RESPONSE), '2026-07-10')
expect(first).not.toBeNull()
expect(await getSetting(db, 'aws.last_pull_month')).toBe('2026-06')
const second = await maybePullAwsCosts(db, deps(CE_RESPONSE), '2026-07-20')
expect(second).toBeNull() // already pulled 2026-06 this run
})
})