Refactor devices controller and extract registration to model

- Remove Users namespace from DevicesController (now just DevicesController)
- Create ApplicationPushDevice model extending ActionPushNative::Device
- Move device registration logic (find_or_initialize + update) to model
- Update User::Devices concern to use ApplicationPushDevice
- Fix push notification tests (endpoint validation, job count expectations)
- Update push_config_test to use ActionPushNative.config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Rosa Gutierrez
2026-01-20 21:40:25 +01:00
parent 7e0470a692
commit 9d32f3e845
9 changed files with 82 additions and 48 deletions
@@ -1,6 +1,6 @@
require "test_helper"
class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
class DevicesControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:david)
sign_in_as @user
@@ -11,7 +11,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
test "index shows user devices" do
@user.devices.create!(token: "test_token_123", platform: "apple", name: "iPhone 15 Pro")
get users_devices_path
get devices_path
assert_response :success
assert_select "strong", "iPhone 15 Pro"
@@ -21,7 +21,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
test "index shows empty state when no devices" do
@user.devices.delete_all
get users_devices_path
get devices_path
assert_response :success
assert_select "p", /No devices registered/
@@ -30,7 +30,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
test "index requires authentication" do
sign_out
get users_devices_path
get devices_path
assert_response :redirect
end
@@ -41,7 +41,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
token = SecureRandom.hex(32)
assert_difference "ActionPushNative::Device.count", 1 do
post users_devices_path, params: {
post devices_path, params: {
token: token,
platform: "apple",
name: "iPhone 15 Pro"
@@ -58,7 +58,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
end
test "creates android device" do
post users_devices_path, params: {
post devices_path, params: {
token: SecureRandom.hex(32),
platform: "google",
name: "Pixel 8"
@@ -83,7 +83,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
# Current user registers the same token with their own device
assert_difference "ActionPushNative::Device.count", 1 do
post users_devices_path, params: {
post devices_path, params: {
token: shared_token,
platform: "apple",
name: "David's iPhone"
@@ -102,7 +102,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
end
test "rejects invalid platform" do
post users_devices_path, params: {
post devices_path, params: {
token: SecureRandom.hex(32),
platform: "windows",
name: "Surface"
@@ -112,7 +112,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
end
test "rejects missing token" do
post users_devices_path, params: {
post devices_path, params: {
platform: "apple",
name: "iPhone"
}, as: :json
@@ -123,7 +123,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
test "create requires authentication" do
sign_out
post users_devices_path, params: {
post devices_path, params: {
token: SecureRandom.hex(32),
platform: "apple"
}, as: :json
@@ -141,19 +141,19 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
)
assert_difference "ActionPushNative::Device.count", -1 do
delete users_device_path(device)
delete device_path(device)
end
assert_redirected_to users_devices_path
assert_redirected_to devices_path
assert_not ActionPushNative::Device.exists?(device.id)
end
test "does nothing when device not found by id" do
assert_no_difference "ActionPushNative::Device.count" do
delete users_device_path(id: "nonexistent")
delete device_path(id: "nonexistent")
end
assert_redirected_to users_devices_path
assert_redirected_to devices_path
end
test "cannot destroy another user's device by id" do
@@ -165,10 +165,10 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
)
assert_no_difference "ActionPushNative::Device.count" do
delete users_device_path(device)
delete device_path(device)
end
assert_redirected_to users_devices_path
assert_redirected_to devices_path
assert ActionPushNative::Device.exists?(device.id)
end
@@ -181,7 +181,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
sign_out
delete users_device_path(device)
delete device_path(device)
assert_response :redirect
assert ActionPushNative::Device.exists?(device.id)
@@ -197,7 +197,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
)
assert_difference "ActionPushNative::Device.count", -1 do
delete unregister_users_devices_path, params: { token: "token_to_unregister" }, as: :json
delete unregister_devices_path, params: { token: "token_to_unregister" }, as: :json
end
assert_response :no_content
@@ -206,7 +206,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
test "does nothing when device not found by token" do
assert_no_difference "ActionPushNative::Device.count" do
delete unregister_users_devices_path, params: { token: "nonexistent_token" }, as: :json
delete unregister_devices_path, params: { token: "nonexistent_token" }, as: :json
end
assert_response :no_content
@@ -221,7 +221,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
)
assert_no_difference "ActionPushNative::Device.count" do
delete unregister_users_devices_path, params: { token: "other_users_token" }, as: :json
delete unregister_devices_path, params: { token: "other_users_token" }, as: :json
end
assert_response :no_content
@@ -237,7 +237,7 @@ class Users::DevicesControllerTest < ActionDispatch::IntegrationTest
sign_out
delete unregister_users_devices_path, params: { token: "my_token" }, as: :json
delete unregister_devices_path, params: { token: "my_token" }, as: :json
assert_response :redirect
assert ActionPushNative::Device.exists?(device.id)
+17 -16
View File
@@ -59,14 +59,14 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
# === Has Any Push Destination ===
test "push_destination returns true when user has native devices" do
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
assert @pusher.send(:push_destination?)
end
test "push_destination returns true when user has web subscriptions" do
@user.push_subscriptions.create!(
endpoint: "https://example.com/push",
endpoint: "https://fcm.googleapis.com/fcm/send/test",
p256dh_key: "test_p256dh",
auth_key: "test_auth"
)
@@ -85,7 +85,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
test "push delivers to native devices when user has devices" do
stub_push_services
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
assert_native_push_delivery(count: 1) do
@pusher.push
@@ -102,7 +102,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
test "push does not deliver when creator is system user" do
stub_push_services
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
@notification.update!(creator: users(:system))
result = @pusher.push
@@ -112,10 +112,11 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
test "push delivers to multiple devices" do
stub_push_services
@user.devices.create!(uuid: SecureRandom.uuid, token: "token1", platform: "apple", name: "iPhone")
@user.devices.create!(uuid: SecureRandom.uuid, token: "token2", platform: "google", name: "Pixel")
@user.devices.delete_all
@user.devices.create!(token: "token1", platform: "apple", name: "iPhone")
@user.devices.create!(token: "token2", platform: "google", name: "Pixel")
assert_native_push_delivery(count: 1) do
assert_native_push_delivery(count: 2) do
@pusher.push
end
end
@@ -131,7 +132,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
)
# Set up native device
@user.devices.create!(uuid: SecureRandom.uuid, token: "native_token", platform: "apple", name: "iPhone")
@user.devices.create!(token: "native_token", platform: "apple", name: "iPhone")
# Mock web push pool to verify it receives the payload
web_push_pool = mock("web_push_pool")
@@ -149,7 +150,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
# === Native Notification Building ===
test "native notification includes required fields" do
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
payload = @pusher.send(:build_payload)
native = @pusher.send(:native_notification, payload)
@@ -159,7 +160,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
end
test "native notification sets thread_id from card" do
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
payload = @pusher.send(:build_payload)
native = @pusher.send(:native_notification, payload)
@@ -168,7 +169,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
test "native notification sets high_priority for assignments" do
notification = notifications(:logo_assignment_kevin)
notification.user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
notification.user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
pusher = NotificationPusher.new(notification)
payload = pusher.send(:build_payload)
@@ -178,7 +179,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
end
test "native notification sets normal priority for non-assignments" do
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
payload = @pusher.send(:build_payload)
native = @pusher.send(:native_notification, payload)
@@ -188,7 +189,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
# === Apple-specific Payload ===
test "native notification includes apple-specific fields" do
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
payload = @pusher.send(:build_payload)
native = @pusher.send(:native_notification, payload)
@@ -200,7 +201,7 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
# === Google-specific Payload ===
test "native notification sets android notification to nil for data-only" do
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
payload = @pusher.send(:build_payload)
native = @pusher.send(:native_notification, payload)
@@ -210,11 +211,11 @@ class NotificationPusherNativeTest < ActiveSupport::TestCase
# === Data Payload ===
test "native notification includes data payload" do
@user.devices.create!(uuid: SecureRandom.uuid, token: "test123", platform: "apple", name: "Test iPhone")
@user.devices.create!(token: "test123", platform: "apple", name: "Test iPhone")
payload = @pusher.send(:build_payload)
native = @pusher.send(:native_notification, payload)
assert_not_nil native.data[:path]
assert_not_nil native.data[:url]
assert_equal @notification.account.external_account_id, native.data[:account_id]
assert_equal @notification.creator.name, native.data[:creator_name]
end
+4 -4
View File
@@ -4,11 +4,11 @@ class PushConfigTest < ActiveSupport::TestCase
test "loads push config from the saas engine" do
skip unless Fizzy.saas?
config = Rails.application.config_for(:push)
config = ActionPushNative.config
apple_team_id = config.dig("apple", "team_id")
apple_topic = config.dig("apple", "topic")
google_project_id = config.dig("google", "project_id")
apple_team_id = config.dig(:apple, :team_id)
apple_topic = config.dig(:apple, :topic)
google_project_id = config.dig(:google, :project_id)
skip "Update test once APNS team_id is configured" if apple_team_id == "YOUR_TEAM_ID"
skip "Update test once APNS topic is configured" if apple_topic == "com.yourcompany.fizzy"