From cd67fbd4d49dc47c937fe4fedbefcad4002e01f9 Mon Sep 17 00:00:00 2001 From: James Xian Date: Wed, 28 Oct 2026 12:05:06 -0700 Subject: [PATCH] Use configured full URLs for Infobip endpoints Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- TECHNICAL.md | 24 ++++--------------- docs/ONBOARDING.md | 5 ---- dotnet/Src/Providers/InfobipProvider.cs | 5 +--- dotnet/tests/ContractTests.cs | 8 +++---- dotnet/tests/SendOtpTests.cs | 2 +- javascript/src/functions/providers/infobip.js | 4 ++-- javascript/test/provider-flow.test.js | 12 ++++------ javascript/test/sendotp.test.js | 2 +- python/src/providers/infobip.py | 4 ++-- python/tests/test_contract.py | 12 ++++------ python/tests/test_function_app.py | 2 +- setup/docs/README.md | 7 +++--- setup/providers/infobip.json | 8 +++---- setup/tests/Providers.Tests.ps1 | 2 -- 14 files changed, 34 insertions(+), 63 deletions(-) diff --git a/TECHNICAL.md b/TECHNICAL.md index 38a05ae..ca23e87 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -337,30 +337,16 @@ Infobip uses OAuth only, with the same managed-identity client-assertion exchang tenant and API application ID; its OAuth scope is that application ID followed by `/.default`. Provider-side authorization of the customer application is still required. -Both channel settings are **base URLs**, without a message path or trailing slash: +Both channel settings are **complete HTTPS URLs**, used unchanged by the adapters: -| Provider route | `EPP_PROVIDER_SMS_ENDPOINT` and `EPP_PROVIDER_VOICE_ENDPOINT` | -| --- | --- | -| Global | `https://api.infobip.com` | -| EU | `https://api-eu.infobip.com` | - -The adapters preserve the API-key-tested request and response contracts: append `/sms/3/messages` -for SMS or `/tts/3/advanced` for voice, and send the existing `messages` payload with OAuth -`Authorization: Bearer ` instead of `App `. Voice does **not** switch to -the manifest's `/tts/3/single` contract, and neither channel uses plain HTTP. -Message text, destinations, correlation IDs, locale, and voice options are unchanged. -Recognized success groups remain `ACCEPTED`, `PENDING`, and `DELIVERED` on successful HTTP responses; -unknown statuses fail closed. +| Provider route | `EPP_PROVIDER_SMS_ENDPOINT` | `EPP_PROVIDER_VOICE_ENDPOINT` | +| --- | --- | --- | +| Global | `https://api.infobip.com/sms/3/messages` | `https://api.infobip.com/tts/3/advanced` | +| EU | `https://api-eu.infobip.com/sms/3/messages` | `https://api-eu.infobip.com/tts/3/advanced` | Set `EPP_PROVIDER_ACCOUNT_NAME` to an Infobip-approved sender/caller ID and confirm it works for both intended channels. The adapter's existing `Verify` fallback is not proof of sender approval. -For an older API-key deployment, deploy a package containing this OAuth update and rerun setup -with `-Provider infobip` to configure federation and the provider tenant/scope. Review -`EPP_PROVIDER_AUTH_MODE=oauth` and the regional base URLs. Updating settings alone does not upgrade -an older adapter; this version has no API-key fallback. Complete -[provider authorization](docs/ONBOARDING.md#complete-provider-authentication) before live use. - ### Soprano Soprano uses OAuth client-assertion exchange with: diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index b84e087..ef78b27 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -116,11 +116,6 @@ Guided setup supports **Infobip, Soprano, and Telesign**. Sinch is bundled but does not have a guided setup profile. Twilio requires the [Add Twilio](BRING-YOUR-OWN-PROVIDER.md) development process. -Infobip retains the previously tested SMS and advanced voice API contracts, using OAuth rather -than API-key authentication. Its channel settings are HTTPS regional base URLs, not the complete -message paths in the Security Store manifest. See -[Infobip routing and migration](../TECHNICAL.md#infobip). - ### Telesign: OAuth configuration and handoff The provider owner must complete `deployment.AccessTokenScopeTenant` and both Global/EU routes' diff --git a/dotnet/Src/Providers/InfobipProvider.cs b/dotnet/Src/Providers/InfobipProvider.cs index 34555ee..ffcdb17 100644 --- a/dotnet/Src/Providers/InfobipProvider.cs +++ b/dotnet/Src/Providers/InfobipProvider.cs @@ -38,7 +38,6 @@ private static HttpRequestMessage CreateRequest( var senderId = env.Get("EPP_PROVIDER_ACCOUNT_NAME") ?? "Verify"; var messageId = delivery.CorrelationId ?? delivery.MessageId; object body; - string url; if (channel == "voice") { @@ -51,7 +50,6 @@ [new Destination(delivery.PhoneNumber, messageId)], delivery.Locale ?? "en", new Voice("Joanna", "female")) ]); - url = $"{endpoint}/tts/3/advanced"; } else { @@ -62,10 +60,9 @@ [new Destination(delivery.PhoneNumber, messageId)], [new Destination(delivery.PhoneNumber, messageId)], new SmsContent(delivery.Message)) ]); - url = $"{endpoint}/sms/3/messages"; } - var request = new HttpRequestMessage(HttpMethod.Post, url) + var request = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = JsonContent.Create(body), }; diff --git a/dotnet/tests/ContractTests.cs b/dotnet/tests/ContractTests.cs index ca9b350..9ecdb87 100644 --- a/dotnet/tests/ContractTests.cs +++ b/dotnet/tests/ContractTests.cs @@ -161,8 +161,8 @@ public void ProviderResultKeepsExistingConstructorAndDeconstructionShape() } [Theory] - [InlineData("https://api.infobip.com")] - [InlineData("https://api-eu.infobip.com")] + [InlineData("https://provider.example/custom/messages")] + [InlineData("https://eu.provider.example/another/path")] public async Task InfobipOAuthRequestsKeepExactSmsAndVoiceWireContracts(string endpoint) { var env = new TestEnv { ["EPP_PROVIDER_ACCOUNT_NAME"] = "Verify" }; @@ -171,7 +171,7 @@ public async Task InfobipOAuthRequestsKeepExactSmsAndVoiceWireContracts(string e var sms = await new InfobipProvider().CaptureRequestAsync( "sms", endpoint, Delivery(), credential, env); - AssertJsonRequest(sms, $"{endpoint}/sms/3/messages", "Bearer test-token"); + AssertJsonRequest(sms, endpoint, "Bearer test-token"); var expectedSms = new { messages = new[] @@ -188,7 +188,7 @@ public async Task InfobipOAuthRequestsKeepExactSmsAndVoiceWireContracts(string e var voice = await new InfobipProvider().CaptureRequestAsync( "voice", endpoint, Delivery("voice"), credential, env); - AssertJsonRequest(voice, $"{endpoint}/tts/3/advanced", "Bearer test-token"); + AssertJsonRequest(voice, endpoint, "Bearer test-token"); var expectedVoice = new { messages = new[] diff --git a/dotnet/tests/SendOtpTests.cs b/dotnet/tests/SendOtpTests.cs index 7ed0c30..80696c2 100644 --- a/dotnet/tests/SendOtpTests.cs +++ b/dotnet/tests/SendOtpTests.cs @@ -179,7 +179,7 @@ public async Task RequestSelectsEitherChannelRegardlessOfLegacySetting(string pr [Theory] [InlineData("telesign", "", "")] [InlineData("soprano", "", "")] - [InlineData("infobip", "/sms/3/messages", "/tts/3/advanced")] + [InlineData("infobip", "", "")] [InlineData("sinch", "/xms/v1/plan/batches", "/calling/v1/callouts")] public async Task RequestChannelSelectsDistinctConfiguredEndpoints(string provider, string smsPath, string voicePath) { diff --git a/javascript/src/functions/providers/infobip.js b/javascript/src/functions/providers/infobip.js index fafca5a..a6f497a 100644 --- a/javascript/src/functions/providers/infobip.js +++ b/javascript/src/functions/providers/infobip.js @@ -20,7 +20,7 @@ const provider = Object.freeze({ }; if (channel === 'voice') { return { - url: `${endpoint}/tts/3/advanced`, + url: endpoint, method: 'POST', headers, body: JSON.stringify({ @@ -35,7 +35,7 @@ const provider = Object.freeze({ }; } return { - url: `${endpoint}/sms/3/messages`, + url: endpoint, method: 'POST', headers, body: JSON.stringify({ diff --git a/javascript/test/provider-flow.test.js b/javascript/test/provider-flow.test.js index d4d64be..dca6145 100644 --- a/javascript/test/provider-flow.test.js +++ b/javascript/test/provider-flow.test.js @@ -230,24 +230,22 @@ test('Soprano accepts root or first array object with strict status and flexible } }); -test('Infobip uses OAuth with the tested SMS and advanced voice contracts on both setup routes', () => { +test('Infobip uses configured URLs unchanged with OAuth on both setup routes', () => { const infobip = selectProvider('infobip'); const profile = require('../../setup/providers/infobip.json'); assert.equal(infobip.authenticationMode, 'oauth'); assert.deepEqual(infobip.credentialSpec, { mode: 'oauth' }); - for (const [region, baseUrl] of [ - ['global', 'https://api.infobip.com'], - ['eu', 'https://api-eu.infobip.com'], - ]) { + for (const region of ['global', 'eu']) { for (const channel of ['sms', 'voice']) { + const endpoint = profile.deployment.routes[region][`${channel}Endpoint`]; const request = infobip.createRequest({ ...input, channel, - endpoint: profile.deployment.routes[region][`${channel}Endpoint`], + endpoint, credential: { mode: 'oauth', accessToken: 'provider-token' }, env: { EPP_PROVIDER_ACCOUNT_NAME: 'EPP' }, }); - assert.equal(request.url, baseUrl + (channel === 'voice' ? '/tts/3/advanced' : '/sms/3/messages')); + assert.equal(request.url, endpoint); assert.equal(request.method, 'POST'); assert.deepEqual(request.headers, { Authorization: ['Bearer', 'provider-token'].join(' '), diff --git a/javascript/test/sendotp.test.js b/javascript/test/sendotp.test.js index 38ba55f..cf8424d 100644 --- a/javascript/test/sendotp.test.js +++ b/javascript/test/sendotp.test.js @@ -627,7 +627,7 @@ for (const provider of ['telesign', 'soprano']) { for (const [provider, response, smsPath, voicePath] of [ ['telesign', { status: { code: 3001 } }, '', ''], ['soprano', { status: 'ENROUTE' }, '', ''], - ['infobip', { messages: [{ status: { groupName: 'PENDING' } }] }, '/sms/3/messages', '/tts/3/advanced'], + ['infobip', { messages: [{ status: { groupName: 'PENDING' } }] }, '', ''], ['sinch', { id: 'provider-id' }, '/xms/v1/plan/batches', '/calling/v1/callouts'], ]) { test(`${provider}: request channel selects distinct configured endpoints`, async () => { diff --git a/python/src/providers/infobip.py b/python/src/providers/infobip.py index 91b809b..79becb2 100644 --- a/python/src/providers/infobip.py +++ b/python/src/providers/infobip.py @@ -26,14 +26,14 @@ def build_request(self, channel, endpoint, delivery, credential, env): "language": delivery.locale or "en", "voice": {"name": "Joanna", "gender": "female"}, }]} - return OutboundRequest("POST", f"{endpoint}/tts/3/advanced", headers, json.dumps(body)) + return OutboundRequest("POST", endpoint, headers, json.dumps(body)) body = {"messages": [{ "sender": sender_id, "destinations": [{"to": delivery.phone_number, "messageId": message_id}], "content": {"text": delivery.message}, }]} - return OutboundRequest("POST", f"{endpoint}/sms/3/messages", headers, json.dumps(body)) + return OutboundRequest("POST", endpoint, headers, json.dumps(body)) def map_response(self, payload, http_status): messages = payload.get("messages") if isinstance(payload, dict) else None diff --git a/python/tests/test_contract.py b/python/tests/test_contract.py index 0e8b7ab..cb2577d 100644 --- a/python/tests/test_contract.py +++ b/python/tests/test_contract.py @@ -88,20 +88,18 @@ def test_soprano_preserves_protocol_specific_mixed_response_shapes(body, expecte assert result.failure_reason == (None if recognized else "unrecognized_provider_status") -@pytest.mark.parametrize("region,base_url", [ - ("global", "https://api.infobip.com"), - ("eu", "https://api-eu.infobip.com"), -]) +@pytest.mark.parametrize("region", ["global", "eu"]) @pytest.mark.parametrize("channel", ["sms", "voice"]) -def test_infobip_oauth_preserves_tested_contract_on_both_setup_routes(region, base_url, channel): +def test_infobip_uses_configured_urls_unchanged_with_oauth_on_both_setup_routes(region, channel): profile = json.loads((Path(__file__).parents[2] / "setup" / "providers" / "infobip.json").read_text()) provider = InfobipProvider() assert provider.authentication_mode == "oauth" assert provider.credential_spec == {"mode": "oauth"} + endpoint = profile["deployment"]["routes"][region][f"{channel}Endpoint"] request = provider.build_request( - channel, profile["deployment"]["routes"][region][f"{channel}Endpoint"], _delivery(channel), + channel, endpoint, _delivery(channel), {"mode": "oauth", "access_token": "provider-token"}, {"EPP_PROVIDER_ACCOUNT_NAME": "EPP"}) - assert request.url == base_url + ("/tts/3/advanced" if channel == "voice" else "/sms/3/messages") + assert request.url == endpoint assert request.method == "POST" assert request.headers == { "Authorization": " ".join(("Bearer", "provider-token")), diff --git a/python/tests/test_function_app.py b/python/tests/test_function_app.py index 47db2a0..f6171df 100644 --- a/python/tests/test_function_app.py +++ b/python/tests/test_function_app.py @@ -413,7 +413,7 @@ def test_telesign_oauth_live_send_never_forwards_inbound_credentials(monkeypatch @pytest.mark.parametrize("provider,payload,sms_path,voice_path", [ ("telesign", {"status": {"code": 3001}}, "", ""), ("soprano", {"status": "ENROUTE"}, "", ""), - ("infobip", {"messages": [{"status": {"groupName": "PENDING"}}]}, "/sms/3/messages", "/tts/3/advanced"), + ("infobip", {"messages": [{"status": {"groupName": "PENDING"}}]}, "", ""), ("sinch", {"id": "provider-id"}, "/xms/v1/plan/batches", "/calling/v1/callouts"), ]) def test_request_channel_selects_distinct_configured_endpoints(monkeypatch, provider, payload, sms_path, voice_path): diff --git a/setup/docs/README.md b/setup/docs/README.md index cbf1998..14bc88e 100644 --- a/setup/docs/README.md +++ b/setup/docs/README.md @@ -36,13 +36,12 @@ Both channels are configured. Profile objects `deployment.routes.global` and `deployment.routes.eu` define `smsEndpoint`, `voiceEndpoint`, tenant, authentication, and timing values. Setup writes the endpoints to `EPP_PROVIDER_SMS_ENDPOINT` and `EPP_PROVIDER_VOICE_ENDPOINT`; OAuth profiles also supply their API app ID and scope. -Infobip's settings are HTTPS base URLs: its adapters append the tested `/sms/3/messages` and -`/tts/3/advanced` paths without changing their request or response contracts. +All guided providers use the complete HTTPS URLs from the selected profile without appending paths. Use a package that supports channel-specific settings; neither channel uses the other channel's URL. -Soprano and Telesign currently use identical Global/EU URLs. Infobip uses `https://api.infobip.com` -for Global and `https://api-eu.infobip.com` for EU. Each OAuth provider uses the same API app ID/scope +Soprano and Telesign currently use identical Global/EU URLs. Infobip uses the `api.infobip.com` +host for Global and `api-eu.infobip.com` for EU. Each OAuth provider uses the same API app ID/scope across its two routes, except that Telesign's OAuth values are pending provider confirmation. The route label is not a residency or Azure-region selection. Sinch remains a bundled adapter without a guided profile. diff --git a/setup/providers/infobip.json b/setup/providers/infobip.json index db58578..4f8f191 100644 --- a/setup/providers/infobip.json +++ b/setup/providers/infobip.json @@ -8,16 +8,16 @@ }, "routes": { "global": { - "smsEndpoint": "https://api.infobip.com", - "voiceEndpoint": "https://api.infobip.com", + "smsEndpoint": "https://api.infobip.com/sms/3/messages", + "voiceEndpoint": "https://api.infobip.com/tts/3/advanced", "appId": "d1131a30-5ed5-49b7-8602-d978c8eb1677", "scope": "d1131a30-5ed5-49b7-8602-d978c8eb1677/.default", "timeoutMilliseconds": 1500, "retryIntervalSeconds": 30 }, "eu": { - "smsEndpoint": "https://api-eu.infobip.com", - "voiceEndpoint": "https://api-eu.infobip.com", + "smsEndpoint": "https://api-eu.infobip.com/sms/3/messages", + "voiceEndpoint": "https://api-eu.infobip.com/tts/3/advanced", "appId": "d1131a30-5ed5-49b7-8602-d978c8eb1677", "scope": "d1131a30-5ed5-49b7-8602-d978c8eb1677/.default", "timeoutMilliseconds": 1500, diff --git a/setup/tests/Providers.Tests.ps1 b/setup/tests/Providers.Tests.ps1 index e101d40..c7b2e5f 100644 --- a/setup/tests/Providers.Tests.ps1 +++ b/setup/tests/Providers.Tests.ps1 @@ -239,8 +239,6 @@ try { Assert ($settings.EPP_PROVIDER_APP_ID -ceq $route.appId -and $settings.EPP_PROVIDER_SCOPE -ceq $route.scope) 'Preserve regional OAuth settings.' } if ($id -eq 'infobip') { - $baseUrl = if ($region -eq 'eu') { 'https://api-eu.infobip.com' } else { 'https://api.infobip.com' } - Assert ($settings.EPP_PROVIDER_SMS_ENDPOINT -ceq $baseUrl -and $settings.EPP_PROVIDER_VOICE_ENDPOINT -ceq $baseUrl) 'Infobip adapters append the tested SMS and advanced voice paths to HTTPS regional base URLs.' Assert ($settings.EPP_PROVIDER_AUTH_MODE -ceq 'oauth' -and $settings.EPP_PROVIDER_TENANT_ID -ceq 'a7d072c3-504e-4385-9029-89283084be5d' -and $settings.EPP_PROVIDER_APP_ID -ceq 'd1131a30-5ed5-49b7-8602-d978c8eb1677' -and