Skip to content

Commit

Permalink
feat: Add Sendethics possibility to the sms gateway (#605)
Browse files Browse the repository at this point in the history
Co-authored-by: Lucie Grau <[email protected]>
  • Loading branch information
AyakorK and luciegrau authored Nov 7, 2024
1 parent be3a496 commit b331c3b
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 7 deletions.
3 changes: 3 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ RAILS_LOG_LEVEL=warn
# SMS_GATEWAY_PASSWORD=
## Set to replace the organization name
# SMS_GATEWAY_PLATFORM="hashimoto.local"
## In case you're using Sendethics service
SMS_GATEWAY_MB_API_KEY=
SMS_GATEWAY_MB_ACCOUNT_ID=

#Timeout for the unsubscribe link of the newsletter
# NEWSLETTERS_UNSUBSCRIBE_TIMEOUT=
Expand Down
32 changes: 25 additions & 7 deletions app/services/decidim/sms_gateway_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,46 @@ class SmsGatewayService
attr_reader :mobile_phone_number, :code

def initialize(mobile_phone_number, code, sms_gateway_context = {})
Rails.logger.debug { "#{mobile_phone_number} - #{code}" }

@mobile_phone_number = mobile_phone_number
@code = code
@organization_name = sms_gateway_context[:organization]&.name
@url = fetch_configuration(:url)
@username = fetch_configuration(:username)
@password = fetch_configuration(:password)
@username = fetch_configuration(:username, required: false)
@password = fetch_configuration(:password, required: false)
@mb_account_id = fetch_configuration(:mb_account_id, required: false)
@mb_api_key = fetch_configuration(:mb_api_key, required: false)
@message = sms_message
@type = "sms"
end

def deliver_code
url = URI("#{@url}?u=#{@username}&p=#{@password}&t=#{@message}&n=#{@mobile_phone_number}&f=#{@type}")
url, request = build_request
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Get.new(url)
https.request(request)

true
end

def build_request
if @url.include?("services.message-business.com")
url = URI("#{@url}/sms/send")
request = Net::HTTP::Post.new(url)

request["Content-Type"] = "application/json"
request["Authorization"] = "Basic #{Base64.strict_encode64("#{@mb_account_id}:#{@mb_api_key}")}"

request.body = JSON.dump({
"mobile" => @mobile_phone_number,
"message" => @message
})
else
url = URI("#{@url}?u=#{@username}&p=#{@password}&t=#{@message}&n=#{@mobile_phone_number}&f=#{@type}")
request = Net::HTTP::Get.new(url)
end

[url, request]
end

# Ensure '@code' is not a full i18n keys rather than a verification code.
def sms_message
return code if code.to_s.length > Decidim::HalfSignup.auth_code_length
Expand Down
2 changes: 2 additions & 0 deletions config/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ default: &default
username: <%= ENV["SMS_GATEWAY_USERNAME"] %>
password: <%= ENV["SMS_GATEWAY_PASSWORD"] %>
platform: <%= ENV["SMS_GATEWAY_PLATFORM"] %>
mb_api_key: <%= ENV["SMS_GATEWAY_MB_API_KEY"] %>
mb_account_id: <%= ENV["SMS_GATEWAY_MB_ACCOUNT_ID"] %>
newsletters_unsubscribe_timeout: <%= ENV.fetch("NEWSLETTERS_UNSUBSCRIBE_TIMEOUT", 365).to_i %>
modules:
gallery:
Expand Down
226 changes: 226 additions & 0 deletions spec/services/decidim/sms_gateway_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Decidim::SmsGatewayService, type: :service do
let(:mobile_phone_number) { "+1234567890" }
let(:code) { "1234" }
let(:sms_gateway_context) { { organization: double("organization", name: "Test Organization") } }
let(:url) { "https://services.message-business.com" }
let(:username) { "test_user" }
let(:password) { "test_password" }
let(:mb_account_id) { "test_account" }
let(:mb_api_key) { "test_key" }
let(:platform) { "Test Platform" }
let(:service) { described_class.new(mobile_phone_number, code, sms_gateway_context) }

before do
allow(Rails.application).to receive(:secrets).and_return(
decidim: {
sms_gateway: {
url: url,
username: username,
password: password,
mb_account_id: mb_account_id,
mb_api_key: mb_api_key,
platform: platform
}
}
)
end

describe "#initialize" do
it "initializes with the correct attributes" do
expect(service.mobile_phone_number).to eq(mobile_phone_number)
expect(service.code).to eq(code)
expect(service.instance_variable_get(:@organization_name)).to eq("Test Organization")
expect(service.instance_variable_get(:@url)).to eq(url)
expect(service.instance_variable_get(:@username)).to eq(username)
expect(service.instance_variable_get(:@password)).to eq(password)
expect(service.instance_variable_get(:@mb_account_id)).to eq(mb_account_id)
expect(service.instance_variable_get(:@mb_api_key)).to eq(mb_api_key)
expect(service.instance_variable_get(:@type)).to eq("sms")
end
end

describe "#sms_message" do
context "when code length is greater than auth code length" do
before do
allow(Decidim::HalfSignup).to receive(:auth_code_length).and_return(3)
end

it "returns the code as the message" do
expect(service.sms_message).to eq(code)
end
end

context "when code length is less than or equal to auth code length" do
before do
allow(Decidim::HalfSignup).to receive(:auth_code_length).and_return(6)
end

it "returns the localized message" do
localized_message = "Your verification code is 1234 for Test Platform"
allow(I18n).to receive(:t).and_return(localized_message)
expect(service.sms_message).to eq(localized_message)
end
end
end

describe "#build_request" do
context "when URL is for Message Business service" do
let(:url) { "https://services.message-business.com" }
let(:mb_account_id) { "fake_account_id" }
let(:mb_api_key) { "fake_api_key" }

before do
# Override with specific test values for headers verification
allow(Rails.application).to receive(:secrets).and_return(
decidim: {
sms_gateway: {
url: url,
username: username,
password: password,
mb_account_id: mb_account_id,
mb_api_key: mb_api_key,
platform: platform
}
}
)
end

it "builds a POST request with the correct headers and body" do
request_url, request = service.build_request

expect(request_url.to_s).to eq("https://services.message-business.com/sms/send")
expect(request.body).to eq({
"mobile" => mobile_phone_number,
"message" => service.sms_message
}.to_json)

expect(request["Content-Type"]).to eq("application/json")

# Verify the exact value of the Authorization header
encoded_credentials = Base64.strict_encode64("#{mb_account_id}:#{mb_api_key}")
expect(request["Authorization"]).to eq("Basic #{encoded_credentials}")
end
end

context "when URL is for a generic SMS gateway" do
let(:url) { "https://generic-sms-gateway.com" }

it "builds a GET request with the correct query parameters" do
request_url, request = service.build_request

expect(request_url.to_s).to include("https://generic-sms-gateway.com?u=test_user&p=test_password")
sms_message_encoded = service.sms_message.gsub(" ", "%20")
expect(request_url.to_s).to include("&t=#{sms_message_encoded}&n=#{mobile_phone_number}&f=sms")
expect(request.method).to eq("GET")
end
end

context "when the message contains unsupported characters" do
let(:code) { "Invalid 😃 Characters" }

it "encodes unsupported characters correctly in the request" do
request_body = service.build_request[1].body
expect(request_body.to_s).to include(code)
end
end
end

describe "#deliver_code" do
context "when using Message Business service" do
let(:url) { "https://services.message-business.com" }
let(:message_url) { "#{url}/sms/send" }

before do
stub_request(:post, message_url)
.with(
body: {
"mobile" => mobile_phone_number,
"message" => service.sms_message
}.to_json,
headers: {
"Content-Type" => "application/json",
"Authorization" => "Basic #{Base64.strict_encode64("#{mb_account_id}:#{mb_api_key}")}"
}
)
.to_return(status: 200, body: "", headers: {})
end

it "makes a POST request to send the SMS" do
expect(service.deliver_code).to be true
end

context "when using valid credentials" do
before do
stub_request(:post, message_url)
.with(
body: {
"mobile" => mobile_phone_number,
"message" => service.sms_message
}.to_json,
headers: {
"Content-Type" => "application/json",
"Authorization" => "Basic #{Base64.strict_encode64("#{mb_account_id}:#{mb_api_key}")}"
}
)
.to_return(status: 200, body: '{"success": true}', headers: {})
end

it "delivers the SMS successfully" do
expect(service.deliver_code).to be true
expect(WebMock).to have_requested(:post, message_url)
end
end
end

context "when using a generic SMS gateway" do
let(:url) { "https://generic-sms-gateway.com" }

before do
stub_request(:get, /generic-sms-gateway.com.*/)
.to_return(status: 200, body: "", headers: {})
end

it "makes a GET request to send the SMS" do
expect(service.deliver_code).to be true
end
end

context "when using Message Business service with invalid credentials" do
let(:url) { "https://services.message-business.com" }
let(:message_url) { "#{url}/sms/send" }

before do
stub_request(:post, message_url)
.with(
body: {
"mobile" => mobile_phone_number,
"message" => service.sms_message
}.to_json,
headers: {
"Content-Type" => "application/json",
"MB_ACCOUNT_ID" => "invalid_account",
"MB_API_KEY" => "invalid_key"
}
)
.to_return(status: 401, body: "Unauthorized")
end

it "fails to deliver SMS and returns unauthorized error" do
expect { service.deliver_code }.to raise_error(WebMock::NetConnectNotAllowedError)
end
end
end

describe "#fetch_configuration" do
context "when a configuration value is missing" do
it "logs an error and returns nil" do
expect(Rails.logger).to receive(:error).with(/is missing a configuration value for :missing_key/)
expect(service.fetch_configuration(:missing_key, required: true)).to be_nil
end
end
end
end

0 comments on commit b331c3b

Please sign in to comment.