feat(ntfy): add Ntfy::Publisher service with HTTP POST + auth

This commit is contained in:
Min Zeya Phyo
2026-07-28 02:11:19 +08:00
parent 8f25dc057b
commit 76742d8791
5 changed files with 165 additions and 0 deletions

View File

@@ -72,6 +72,7 @@ group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara" gem "capybara"
gem "selenium-webdriver" gem "selenium-webdriver"
gem "webmock"
end end
gem "tailwindcss-rails", "~> 4.3" gem "tailwindcss-rails", "~> 4.3"

View File

@@ -98,6 +98,9 @@ GEM
chunky_png (1.4.0) chunky_png (1.4.0)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.5)
connection_pool (2.5.4) connection_pool (2.5.4)
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6) crass (1.0.6)
csv (3.3.5) csv (3.3.5)
date (3.4.1) date (3.4.1)
@@ -116,6 +119,7 @@ GEM
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.3.0) globalid (1.3.0)
activesupport (>= 6.1) activesupport (>= 6.1)
hashdiff (1.2.1)
httparty (0.23.2) httparty (0.23.2)
csv csv
mini_mime (>= 1.0.0) mini_mime (>= 1.0.0)
@@ -388,6 +392,10 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.26.2)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.11) websocket (1.2.11)
websocket-driver (0.8.0) websocket-driver (0.8.0)
base64 base64
@@ -442,6 +450,7 @@ DEPENDENCIES
turbo-rails turbo-rails
tzinfo-data tzinfo-data
web-console web-console
webmock
BUNDLED WITH BUNDLED WITH
2.6.9 2.6.9

View File

@@ -0,0 +1,49 @@
require "httparty"
module MySmsaPio
module Notifications
module Ntfy
class Publisher
def initialize(admin)
@admin = admin
end
def publish(title:, message:, priority: 3, tags: [], click: nil)
return false unless @admin.ntfy_configured?
body = {
topic: @admin.ntfy_topic,
title: title,
message: message,
priority: priority,
tags: Array(tags)
}
body[:click] = click if click.present?
url = "#{server_url}/#{@admin.ntfy_topic}"
response = HTTParty.post(
url,
body: body.to_json,
headers: {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{@admin.ntfy_token}"
},
timeout: 10
)
response.success?
rescue StandardError => e
Rails.logger.error("ntfy publish failed for admin #{@admin.id}: #{e.message}")
false
end
private
def server_url
@admin.ntfy_server_url.presence || ENV["NTFY_SERVER_URL"] || "https://ntfy.sh"
end
end
end
end
end

View File

@@ -0,0 +1,105 @@
require "test_helper"
module MySmsaPio
module Notifications
module Ntfy
class PublisherTest < ActiveSupport::TestCase
setup do
@admin = AdminUser.create!(
name: "Notifier", email: "notif@example.com",
password: "password123",
ntfy_topic: "mysmsa-test-topic",
ntfy_token: "tk_test_token",
ntfy_enabled: true,
ntfy_server_url: "https://ntfy.example.com"
)
end
test "publish sends POST to the correct ntfy URL with JSON body and auth header" do
stubbed = stub_request(:post, "https://ntfy.example.com/mysmsa-test-topic")
.with(
headers: {
"Authorization" => "Bearer tk_test_token",
"Content-Type" => "application/json"
},
body: hash_including(
"topic" => "mysmsa-test-topic",
"title" => "Gateway offline",
"message" => /gw-001 went offline/,
"priority" => 4,
"tags" => ->(tags) { tags.include?("rotating_light") }
)
)
.to_return(status: 200, body: "", headers: {})
result = Publisher.new(@admin).publish(
title: "Gateway offline",
message: "gw-001 went offline at #{Time.current}",
priority: 4,
tags: ["rotating_light"]
)
assert result
assert_requested stubbed
end
test "publish returns false on HTTP failure without raising" do
stub_request(:post, "https://ntfy.example.com/mysmsa-test-topic")
.to_return(status: 500, body: "error")
result = Publisher.new(@admin).publish(
title: "Test",
message: "body",
priority: 3,
tags: []
)
assert_not result
end
test "publish returns false if admin not ntfy_configured?" do
unconfigured = AdminUser.create!(
name: "No Ntfy", email: "none@example.com",
password: "password123"
)
result = Publisher.new(unconfigured).publish(
title: "X", message: "Y", priority: 3, tags: []
)
assert_not result
end
test "publish uses default server URL from ENV when ntfy_server_url is nil" do
ENV["NTFY_SERVER_URL"] = "https://default-ntfy.example.com"
admin = AdminUser.create!(
name: "Default", email: "default@example.com",
password: "password123",
ntfy_topic: "def-topic", ntfy_token: "tk_def",
ntfy_enabled: true
)
stub = stub_request(:post, "https://default-ntfy.example.com/def-topic")
.to_return(status: 200, body: "", headers: {})
Publisher.new(admin).publish(title: "T", message: "M", priority: 3, tags: [])
assert_requested stub
ensure
ENV.delete("NTFY_SERVER_URL")
end
test "publish includes click URL when provided" do
stub = stub_request(:post, "https://ntfy.example.com/mysmsa-test-topic")
.with(body: hash_including("click" => "https://app.example.com/admin/gateways"))
.to_return(status: 200, body: "", headers: {})
Publisher.new(@admin).publish(
title: "T", message: "M", priority: 3, tags: [],
click: "https://app.example.com/admin/gateways"
)
assert_requested stub
end
end
end
end
end

View File

@@ -1,6 +1,7 @@
ENV["RAILS_ENV"] ||= "test" ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment" require_relative "../config/environment"
require "rails/test_help" require "rails/test_help"
require "webmock/minitest"
module ActiveSupport module ActiveSupport
class TestCase class TestCase