// apps/hq/test/sms-gateway.test.ts — D28: parse the SMS gateway's balance response. import { describe, it, expect } from 'vitest' import { parseBalanceResponse, balanceUrl, assertSafeGatewayUrl } from '../src/sms-gateway' describe('parseBalanceResponse (D28)', () => { it('reads the balance from a success line (code|STATUS|balance)', () => { const r = parseBalanceResponse('001|SUCCESS|8200') expect(r.ok).toBe(true) expect(r.balance).toBe(8200) }) it('tolerates commas/spaces and a plain-number body', () => { expect(parseBalanceResponse('000|OK|1,25,000').balance).toBe(125000) expect(parseBalanceResponse('8200').balance).toBe(8200) }) it('flags the real error response as failed with its message', () => { const r = parseBalanceResponse('002|ERROR|Invalid User Id / Password') expect(r.ok).toBe(false) expect(r.balance).toBeNull() expect(r.message).toMatch(/Invalid User Id/) }) it('fails cleanly on empty or unreadable text (never a wrong number)', () => { expect(parseBalanceResponse('').ok).toBe(false) expect(parseBalanceResponse('welcome to the panel').ok).toBe(false) }) }) describe('balanceUrl (D28)', () => { it('substitutes credentials into the configured base URL', () => { const u = balanceUrl('http://164.52.195.161/API/BalAlert.aspx', 'bankuser', 'p@ss w/special') expect(u).toContain('uname=bankuser') expect(u).toContain('pass=p%40ss+w%2Fspecial') // URL-encoded }) }) describe('assertSafeGatewayUrl (D28 SSRF guard)', () => { it('allows the vendor panel and other public hosts', () => { expect(() => assertSafeGatewayUrl('http://164.52.195.161/API/BalAlert.aspx')).not.toThrow() expect(() => assertSafeGatewayUrl('https://sms.vendor.example/bal')).not.toThrow() }) it('blocks loopback, link-local metadata, and private ranges', () => { for (const bad of [ 'http://127.0.0.1/x', 'http://localhost/x', 'http://169.254.169.254/latest/meta-data', 'http://10.0.0.5/x', 'http://192.168.1.5/x', 'http://172.16.0.1/x', 'http://[::1]/x', 'http://box.internal/x', 'ftp://164.52.195.161/x', ]) { expect(() => assertSafeGatewayUrl(bad), bad).toThrow() } }) })