From aeb6d6a5e99fedc5cba281f5c620c5a03c8b251e Mon Sep 17 00:00:00 2001 From: catsika Date: Mon, 26 Oct 2026 02:48:38 +0000 Subject: [PATCH] test: retry rate-limited calls in the live suite The shared test workspace is rate-limited per minute and the live suite makes about 23 calls back to back. Retry a 429 up to 4 times, 10 s apart, like the Go and C# suites. A 429 is rejected before it is processed, so the retry is safe. --- tests/live.test.cjs | 73 +++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/tests/live.test.cjs b/tests/live.test.cjs index 970f664..2ba4255 100644 --- a/tests/live.test.cjs +++ b/tests/live.test.cjs @@ -8,6 +8,21 @@ function required(name) { return value; } +// The shared test workspace is rate-limited per minute. A 429 is rejected +// before it is processed, so retrying it is safe (same as the Go and C# suites). +const isRateLimited = (err) => err?.status === 429 || /too many requests/i.test(err?.message ?? ''); + +async function live(call) { + for (let attempt = 1; ; attempt++) { + try { + return await call(); + } catch (err) { + if (!isRateLimited(err) || attempt === 4) throw err; + await new Promise((resolve) => setTimeout(resolve, 10_000)); + } + } +} + test('current license and customer workflows work end to end', async () => { const productId = required('KEYMINT_TEST_PRODUCT_ID'); const baseUrl = process.env.KEYMINT_TEST_BASE_URL || 'https://api.keymint.dev'; @@ -21,58 +36,58 @@ test('current license and customer workflows work end to end', async () => { let customerId; try { - const created = await admin.createKey({ + const created = await live(() => admin.createKey({ productId, maxActivations: '2', metadata: { purpose: 'node-sdk-live-test', runId } - }); + })); nodeKey = created.key; assert.ok(nodeKey); - const lookup = await readOnly.getKey({ productId, licenseKey: nodeKey }); + const lookup = await live(() => readOnly.getKey({ productId, licenseKey: nodeKey })); assert.equal(lookup.data.license.productId, productId); - const activation = await client.activateKey({ + const activation = await live(() => client.activateKey({ productId, licenseKey: nodeKey, hostId: nodeHost, deviceTag: 'Node SDK CI' - }); + })); assert.equal(activation.metadata.hostId, nodeHost); - const deactivated = await client.deactivateKey({ productId, licenseKey: nodeKey, hostId: nodeHost }); + const deactivated = await live(() => client.deactivateKey({ productId, licenseKey: nodeKey, hostId: nodeHost })); assert.equal(deactivated.devicesRemoved, 1); - await admin.updateKey({ productId, licenseKey: nodeKey, maxActivations: 3 }); - await admin.blockKey({ productId, licenseKey: nodeKey }); - await admin.unblockKey({ productId, licenseKey: nodeKey }); + await live(() => admin.updateKey({ productId, licenseKey: nodeKey, maxActivations: 3 })); + await live(() => admin.blockKey({ productId, licenseKey: nodeKey })); + await live(() => admin.unblockKey({ productId, licenseKey: nodeKey })); - const floating = await admin.createKey({ + const floating = await live(() => admin.createKey({ productId, licenseType: 'floating', maxConcurrentSessions: 1, heartbeatInterval: 60, sessionLeaseDuration: 300, metadata: { purpose: 'node-sdk-floating-live-test', runId } - }); + })); floatingKey = floating.key; assert.ok(floatingKey); - const checkout = await client.floatingCheckout({ + const checkout = await live(() => client.floatingCheckout({ productId, licenseKey: floatingKey, hostId: `node-floating-${runId}` - }); - const heartbeat = await client.floatingHeartbeat({ + })); + const heartbeat = await live(() => client.floatingHeartbeat({ productId, licenseKey: floatingKey, sessionId: checkout.sessionId, timestamp: checkout.nextNonce, signature: KeyMint.generateSessionSignature(checkout.sessionId, checkout.nextNonce, checkout.sessionSecret) - }); - await client.floatingCheckin({ + })); + await live(() => client.floatingCheckin({ productId, licenseKey: floatingKey, sessionId: checkout.sessionId, timestamp: heartbeat.nextNonce, signature: KeyMint.generateSessionSignature(checkout.sessionId, heartbeat.nextNonce, checkout.sessionSecret) - }); + })); - const signed = await admin.signKey({ productId, licenseKey: nodeKey, hostId: nodeHost, ttl: 300 }); + const signed = await live(() => admin.signKey({ productId, licenseKey: nodeKey, hostId: nodeHost, ttl: 300 })); assert.equal(typeof signed.file, 'string'); assert.match(signed.file, /signedKey/); const publicKey = process.env.KEYMINT_TEST_PUBLIC_KEY; @@ -90,22 +105,22 @@ test('current license and customer workflows work end to end', async () => { console.log('KEYMINT_TEST_PUBLIC_KEY not set; skipping offline license verification'); } - const customer = await admin.createCustomer({ name: 'Node SDK CI', email: `node-ci-${runId}@example.com` }); + const customer = await live(() => admin.createCustomer({ name: 'Node SDK CI', email: `node-ci-${runId}@example.com` })); customerId = customer.data.id; assert.ok(customerId); - const fetched = await admin.getCustomerById({ customerId }); + const fetched = await live(() => admin.getCustomerById({ customerId })); assert.ok(fetched.data.some(item => item.id === customerId)); - await admin.updateCustomer({ customerId, name: 'Node SDK CI Updated' }); - const customers = await admin.getAllCustomers(); + await live(() => admin.updateCustomer({ customerId, name: 'Node SDK CI Updated' })); + const customers = await live(() => admin.getAllCustomers()); assert.ok(customers.data.some(item => item.id === customerId)); - assert.ok(Array.isArray(await admin.getCustomerWithKeys({ customerId }))); - assert.equal((await admin.toggleCustomerStatus({ customerId })).status, true); - assert.equal((await admin.toggleCustomerStatus({ customerId })).status, true); - assert.equal((await admin.deleteCustomer({ customerId })).status, true); + assert.ok(Array.isArray(await live(() => admin.getCustomerWithKeys({ customerId })))); + assert.equal((await live(() => admin.toggleCustomerStatus({ customerId }))).status, true); + assert.equal((await live(() => admin.toggleCustomerStatus({ customerId }))).status, true); + assert.equal((await live(() => admin.deleteCustomer({ customerId }))).status, true); customerId = undefined; } finally { - if (customerId) await admin.deleteCustomer({ customerId }).catch(() => {}); - if (nodeKey) await admin.blockKey({ productId, licenseKey: nodeKey }).catch(() => {}); - if (floatingKey) await admin.blockKey({ productId, licenseKey: floatingKey }).catch(() => {}); + if (customerId) await live(() => admin.deleteCustomer({ customerId })).catch(() => {}); + if (nodeKey) await live(() => admin.blockKey({ productId, licenseKey: nodeKey })).catch(() => {}); + if (floatingKey) await live(() => admin.blockKey({ productId, licenseKey: floatingKey })).catch(() => {}); } });