Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Sendethics possibility to the sms gateway #605

Merged
merged 3 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to define a dedicated class implementing an interface SmsGateway with method request callable rather than if / else condition

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
Loading