const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const source = fs.readFileSync('phone-sms/providers/sms-bower.js', 'utf8'); const api = new Function('self', `${source}; return self.PhoneSmsBowerProvider;`)({}); function createTextResponse(payload, ok = true, status = ok ? 200 : 400, statusText = ok ? 'OK' : 'Bad Request') { return { ok, status, statusText, text: async () => (typeof payload === 'string' ? payload : JSON.stringify(payload)), }; } test('SMS Bower provider defaults to Indonesia and documented OpenAI service code', async () => { const requests = []; const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { const parsed = new URL(url); requests.push({ url: parsed, options }); if (parsed.searchParams.get('action') === 'getNumberV2') { return createTextResponse('ACCESS_NUMBER:67890:6281234567890'); } throw new Error(`unexpected ${parsed.toString()}`); }, }); const activation = await provider.requestActivation({ smsBowerApiKey: 'demo-key' }); assert.equal(requests[0].url.searchParams.get('country'), '6'); assert.equal(requests[0].url.searchParams.get('service'), 'dr'); assert.equal(activation.countryId, 6); assert.equal(activation.countryLabel, 'Indonesia'); }); test('SMS Bower provider uses documented handler API methods for balance and price catalog', async () => { const requests = []; const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { const parsed = new URL(url); requests.push({ url: parsed, options }); if (parsed.searchParams.get('action') === 'getBalance') { return createTextResponse('ACCESS_BALANCE:12.34'); } if (parsed.searchParams.get('action') === 'getPricesV3') { return createTextResponse({ 52: { dr: { 3170: { price: 0.21, count: 7 } } } }); } throw new Error(`unexpected ${parsed.toString()}`); }, }); const balance = await provider.fetchBalance({ smsBowerApiKey: 'demo-key' }); const prices = await provider.fetchPrices({ smsBowerApiKey: 'demo-key', smsBowerCountryId: 52 }); const entries = provider.collectPriceEntries(prices, []); assert.equal(requests[0].url.origin + requests[0].url.pathname, 'https://smsbower.page/stubs/handler_api.php'); assert.equal(requests[0].url.searchParams.get('api_key'), 'demo-key'); assert.equal(requests[0].url.searchParams.get('action'), 'getBalance'); assert.equal(balance.balance, 12.34); assert.equal(requests[1].url.searchParams.get('action'), 'getPricesV3'); assert.equal(requests[1].url.searchParams.get('service'), 'dr'); assert.equal(requests[1].url.searchParams.get('country'), '52'); assert.deepStrictEqual(entries, [{ cost: 0.21, count: 7, inStock: true }]); }); test('SMS Bower provider treats JSON status 0 responses as API errors even when HTTP is ok', async () => { const provider = api.createProvider({ fetchImpl: async () => createTextResponse({ status: 0, message: 'No access', data: [] }, true, 200), }); await assert.rejects( () => provider.fetchBalance({ smsBowerApiKey: 'bad-key' }), /SMS Bower 查询余额失败:No access/ ); }); test('SMS Bower provider acquires number with getNumberV2, polls code, and updates activation statuses', async () => { const requests = []; const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { const parsed = new URL(url); const action = parsed.searchParams.get('action'); requests.push({ url: parsed, options }); if (action === 'getNumberV2') { return createTextResponse({ activationId: '12345', phoneNumber: '447700900123', activationCost: 0.3, countryCode: 16 }); } if (action === 'getStatus') { return createTextResponse('STATUS_OK:Your OpenAI code is 654321'); } if (action === 'setStatus') { return createTextResponse(parsed.searchParams.get('status') === '6' ? 'ACCESS_ACTIVATION' : 'ACCESS_CANCEL'); } throw new Error(`unexpected ${parsed.toString()}`); }, sleepWithStop: async () => {}, throwIfStopped: () => {}, }); const state = { smsBowerApiKey: 'demo-key', smsBowerServiceCode: 'dr', smsBowerCountryId: 16, smsBowerCountryLabel: 'United Kingdom', smsBowerMinPrice: '0.1', smsBowerMaxPrice: '0.3', }; const activation = await provider.requestActivation(state); const code = await provider.pollActivationCode(state, activation, { timeoutMs: 1000, intervalMs: 1, maxRounds: 1 }); await provider.finishActivation(state, activation); await provider.cancelActivation(state, activation); await provider.banActivation(state, activation); const reused = await provider.reuseActivation(state, activation); assert.equal(activation.provider, 'sms-bower'); assert.equal(activation.activationId, '12345'); assert.equal(activation.phoneNumber, '447700900123'); assert.equal(activation.countryId, 16); assert.equal(code, '654321'); assert.equal(reused.activationId, '12345'); const acquireUrl = requests[0].url; assert.equal(acquireUrl.searchParams.get('action'), 'getNumberV2'); assert.equal(acquireUrl.searchParams.get('service'), 'dr'); assert.equal(acquireUrl.searchParams.get('country'), '16'); assert.equal(acquireUrl.searchParams.get('maxPrice'), '0.3'); assert.equal(acquireUrl.searchParams.get('minPrice'), '0.1'); assert.equal(acquireUrl.searchParams.get('providerIds'), null); assert.equal(acquireUrl.searchParams.get('exceptProviderIds'), null); assert.equal(acquireUrl.searchParams.get('phoneException'), null); assert.deepStrictEqual( requests.map((entry) => [entry.url.searchParams.get('action'), entry.url.searchParams.get('status')]), [ ['getNumberV2', null], ['getStatus', null], ['setStatus', '6'], ['setStatus', '8'], ['setStatus', '8'], ['setStatus', '3'], ] ); }); test('SMS Bower rotate cancels rejected number then immediately acquires the next number', async () => { const requests = []; const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { const parsed = new URL(url); const action = parsed.searchParams.get('action'); requests.push({ url: parsed, options }); if (action === 'setStatus') { return createTextResponse('ACCESS_CANCEL'); } if (action === 'getNumberV2') { return createTextResponse({ activationId: 'next-activation', phoneNumber: '447700900456', activationCost: 0.22, countryCode: 16 }); } throw new Error(`unexpected ${parsed.toString()}`); }, sleepWithStop: async () => {}, throwIfStopped: () => {}, }); const state = { smsBowerApiKey: 'demo-key', smsBowerServiceCode: 'dr', smsBowerCountryId: 16, smsBowerCountryLabel: 'United Kingdom', smsBowerMinPrice: '0.1', smsBowerMaxPrice: '0.3', }; const rotated = await provider.rotateActivation( state, { activationId: 'old-activation', phoneNumber: '447700900123', provider: 'sms-bower', countryId: 16 }, { releaseAction: 'cancel', blockedCountryIds: [] } ); assert.equal(rotated.currentTicketId, 'old-activation'); assert.equal(rotated.nextActivation.activationId, 'next-activation'); assert.equal(rotated.nextActivation.phoneNumber, '447700900456'); assert.deepStrictEqual( requests.map((entry) => [ entry.url.searchParams.get('action'), entry.url.searchParams.get('id'), entry.url.searchParams.get('status'), ]), [ ['setStatus', 'old-activation', '8'], ['getNumberV2', null, null], ] ); });