Skip to content

Commit 43287d2

Browse files
committed
Add multi-top-up and repeated exhaustion session coverage tests
1 parent c5852e2 commit 43287d2

File tree

1 file changed

+202
-1
lines changed

1 file changed

+202
-1
lines changed

src/tempo/server/Session.coverage.test.ts

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import * as Store from '../../Store.js'
2020
import * as ChannelStore from '../session/ChannelStore.js'
2121
import { signVoucher } from '../session/Voucher.js'
2222
import { sessionManager } from '../client/SessionManager.js'
23-
import { session } from './Session.js'
23+
import { charge, session } from './Session.js'
2424

2525
const isLocalnet = nodeEnv === 'localnet'
2626
const payer = accounts[2]
@@ -720,6 +720,133 @@ describe.runIf(isLocalnet)('session coverage gaps', () => {
720720
})
721721
})
722722

723+
describe('PR7: multi top-up continuity', () => {
724+
test('open -> topUp -> topUp -> voucher/charge -> close', async () => {
725+
const { channelId, serializedTransaction } = await createSignedOpenTransaction(4_000_000n)
726+
const server = createServer()
727+
728+
const openReceipt = await server.verify({
729+
credential: {
730+
challenge: makeChallenge({ id: 'open-multi-topup', channelId }),
731+
payload: {
732+
action: 'open' as const,
733+
type: 'transaction' as const,
734+
channelId,
735+
transaction: serializedTransaction,
736+
cumulativeAmount: '1000000',
737+
signature: await signVoucherFor(payer, channelId, 1_000_000n),
738+
},
739+
},
740+
request: makeRequest(),
741+
})
742+
expect(openReceipt.status).toBe('success')
743+
744+
await charge(store, channelId, 1_000_000n)
745+
await expect(charge(store, channelId, 1_000_000n)).rejects.toThrow('requested')
746+
747+
const topUp1Amount = 2_000_000n
748+
const { serializedTransaction: topUp1 } = await signTopUpChannel({
749+
escrow: escrowContract,
750+
payer,
751+
channelId,
752+
token: currency,
753+
amount: topUp1Amount,
754+
})
755+
756+
const topUp1Receipt = await server.verify({
757+
credential: {
758+
challenge: makeChallenge({ id: 'topup-1', channelId }),
759+
payload: {
760+
action: 'topUp' as const,
761+
type: 'transaction' as const,
762+
channelId,
763+
transaction: topUp1,
764+
additionalDeposit: topUp1Amount.toString(),
765+
},
766+
},
767+
request: makeRequest(),
768+
})
769+
expect(topUp1Receipt.status).toBe('success')
770+
expect((await store.getChannel(channelId))?.deposit).toBe(6_000_000n)
771+
772+
const voucher1 = await server.verify({
773+
credential: {
774+
challenge: makeChallenge({ id: 'voucher-after-topup-1', channelId }),
775+
payload: {
776+
action: 'voucher' as const,
777+
channelId,
778+
cumulativeAmount: '3000000',
779+
signature: await signVoucherFor(payer, channelId, 3_000_000n),
780+
},
781+
},
782+
request: makeRequest(),
783+
})
784+
expect(voucher1.acceptedCumulative).toBe('3000000')
785+
786+
await charge(store, channelId, 2_000_000n)
787+
await expect(charge(store, channelId, 1_000_000n)).rejects.toThrow('requested')
788+
789+
const topUp2Amount = 2_000_000n
790+
const { serializedTransaction: topUp2 } = await signTopUpChannel({
791+
escrow: escrowContract,
792+
payer,
793+
channelId,
794+
token: currency,
795+
amount: topUp2Amount,
796+
})
797+
798+
const topUp2Receipt = await server.verify({
799+
credential: {
800+
challenge: makeChallenge({ id: 'topup-2', channelId }),
801+
payload: {
802+
action: 'topUp' as const,
803+
type: 'transaction' as const,
804+
channelId,
805+
transaction: topUp2,
806+
additionalDeposit: topUp2Amount.toString(),
807+
},
808+
},
809+
request: makeRequest(),
810+
})
811+
expect(topUp2Receipt.status).toBe('success')
812+
expect((await store.getChannel(channelId))?.deposit).toBe(8_000_000n)
813+
814+
const voucher2 = await server.verify({
815+
credential: {
816+
challenge: makeChallenge({ id: 'voucher-after-topup-2', channelId }),
817+
payload: {
818+
action: 'voucher' as const,
819+
channelId,
820+
cumulativeAmount: '5000000',
821+
signature: await signVoucherFor(payer, channelId, 5_000_000n),
822+
},
823+
},
824+
request: makeRequest(),
825+
})
826+
expect(voucher2.acceptedCumulative).toBe('5000000')
827+
828+
await charge(store, channelId, 2_000_000n)
829+
830+
const closeReceipt = await server.verify({
831+
credential: {
832+
challenge: makeChallenge({ id: 'close-multi-topup', channelId }),
833+
payload: {
834+
action: 'close' as const,
835+
channelId,
836+
cumulativeAmount: '5000000',
837+
signature: await signVoucherFor(payer, channelId, 5_000_000n),
838+
},
839+
},
840+
request: makeRequest(),
841+
})
842+
expect(closeReceipt.status).toBe('success')
843+
844+
const finalized = await store.getChannel(channelId)
845+
expect(finalized?.spent).toBe(5_000_000n)
846+
expect(finalized?.finalized).toBe(true)
847+
})
848+
})
849+
723850
describe('PR7: e2e streaming loop', () => {
724851
test('open -> stream -> need-voucher -> resume -> close', async () => {
725852
const backingStore = Store.memory()
@@ -798,5 +925,79 @@ describe.runIf(isLocalnet)('session coverage gaps', () => {
798925
const persisted = await ChannelStore.fromStore(backingStore).getChannel(channelId!)
799926
expect(persisted?.finalized).toBe(true)
800927
})
928+
929+
test('handles repeated exhaustion/resume cycles within one stream', async () => {
930+
const backingStore = Store.memory()
931+
const routeHandler = Mppx_server.create({
932+
methods: [
933+
tempo_server.session({
934+
store: backingStore,
935+
getClient: () => client,
936+
account: recipient,
937+
currency,
938+
escrowContract,
939+
chainId: chain.id,
940+
sse: true,
941+
}),
942+
],
943+
realm: 'api.example.com',
944+
secretKey: 'secret',
945+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
946+
947+
let voucherPosts = 0
948+
const fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
949+
const request = new Request(input, init)
950+
let action: 'open' | 'topUp' | 'voucher' | 'close' | undefined
951+
952+
if (request.method === 'POST' && request.headers.has('Authorization')) {
953+
try {
954+
const credential = Credential.fromRequest<any>(request)
955+
action = credential.payload?.action
956+
if (action === 'voucher') voucherPosts++
957+
} catch {}
958+
}
959+
960+
const result = await routeHandler(request)
961+
if (result.status === 402) return result.challenge
962+
963+
if (action === 'voucher') {
964+
return new Response(null, { status: 200 })
965+
}
966+
967+
if (request.headers.get('Accept')?.includes('text/event-stream')) {
968+
return result.withReceipt(async function* (stream) {
969+
await stream.charge()
970+
yield 'chunk-1'
971+
await stream.charge()
972+
yield 'chunk-2'
973+
await stream.charge()
974+
yield 'chunk-3'
975+
await stream.charge()
976+
yield 'chunk-4'
977+
})
978+
}
979+
980+
return result.withReceipt(new Response('ok'))
981+
}
982+
983+
const manager = sessionManager({
984+
account: payer,
985+
client,
986+
escrowContract,
987+
fetch,
988+
maxDeposit: '4',
989+
})
990+
991+
const chunks: string[] = []
992+
const stream = await manager.sse('https://api.example.com/stream')
993+
for await (const chunk of stream) chunks.push(chunk)
994+
995+
expect(chunks).toEqual(['chunk-1', 'chunk-2', 'chunk-3', 'chunk-4'])
996+
expect(voucherPosts).toBeGreaterThanOrEqual(2)
997+
998+
const closeReceipt = await manager.close()
999+
expect(closeReceipt?.status).toBe('success')
1000+
expect(closeReceipt?.spent).toBe('4000000')
1001+
})
8011002
})
8021003
})

0 commit comments

Comments
 (0)