Files
akkounts/spec/models/remote_storage_authorization_spec.rb
T
raucao c8a0b5dbe9 Add storage dashboard card and per-app details modal
- Rework RsAuthComponent into a card that shows the app, its device, an
  access summary and the permission status
- Group connected apps by app and device, so repeated authorizations for
  the same app and device (e.g. after a browser update) are shown together,
  each with its own revoke action
- Add RsAuthDetailsModalComponent with the full scope list, device,
  connected tokens and permission expiry, plus Launch and Revoke actions
  (built on the existing ModalComponent)
- Add RemoteStorageAuthorization#device_name (parsed from the user agent)
  and #expired?

Part of #254
2026-11-03 08:07:44 +04:00

366 lines
12 KiB
Ruby

require 'rails_helper'
RSpec.describe RemoteStorageAuthorization, type: :model do
include ActiveJob::TestHelper
let(:user) { create :user }
before do
allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true)
allow_any_instance_of(RemoteStorageAuthorization).to receive(:remove_token_expiry_job).and_return(nil)
end
describe "#create" do
before(:each) { redis_rs_delete_keys("authorizations:*") }
after(:each) { clear_enqueued_jobs }
after(:all) { redis_rs_delete_keys("authorizations:*") }
let(:auth) do
user.remote_storage_authorizations.create!(
permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
client_id: "example.com",
redirect_uri: "https://example.com"
)
end
it "generates a token" do
expect(auth.token).to match(/[a-zA-Z0-9]+/)
end
it "stores a token in redis" do
auth
authorizations = redis_rs.smembers("authorizations:#{user.cn}:#{auth.token}")
expect(authorizations.sort).to eq(%w(documents photos contacts:rw videos:r tasks/work:r).sort)
end
context "with expiry set" do
it "enqueues an expiration job" do
auth_with_expiry = user.remote_storage_authorizations.create!(
permissions: %w(documents:rw), client_id: "example.com",
redirect_uri: "https://example.com",
expire_at: 1.month.from_now
)
job = enqueued_jobs.select{|j| j['job_class'] == "RemoteStorageExpireAuthorizationJob"}.first
expect(job['arguments'][0]).to eq(auth_with_expiry.id)
end
end
end
describe "#destroy" do
after(:each) { clear_enqueued_jobs }
after(:all) { redis_rs_delete_keys("authorizations:*") }
it "removes the token from redis" do
auth = user.remote_storage_authorizations.create!(
permissions: %w(shares:rw documents pictures:r),
client_id: "sharesome.5apps.com",
redirect_uri: "https://sharesome.5apps.com"
)
auth.destroy!
expect(redis_rs.keys("authorizations:#{user.address}:*")).to be_empty
end
context "with expiry set" do
it "removes the expiration job" do
auth_with_expiry = user.remote_storage_authorizations.create!(
permissions: %w(documents:rw), client_id: "example.com",
redirect_uri: "https://example.com",
expire_at: 1.month.from_now
)
# Cannot test for removal from the actual Sidekiq::Queue, because it is
# not used in specs, but the method directly removes jobs from there
expect(auth_with_expiry).to receive(:remove_token_expiry_job)
auth_with_expiry.destroy!
end
end
end
describe "#find_or_create_web_app" do
context "with origin that looks hosted" do
after(:all) { redis_rs_delete_keys("authorizations:*") }
let(:auth) do
user.remote_storage_authorizations.create!(
permissions: %w(documents:rw),
client_id: "example.com",
redirect_uri: "https://example.com"
)
end
it "generates a web_app" do
expect(auth.web_app).to be_a(AppCatalog::WebApp)
end
end
context "when creating two authorizations for the same app" do
let(:user_2) { create :user, id: 23, cn: "michiel", email: "michiel@example.com" }
let(:auth_1) do
user.remote_storage_authorizations.create!(
permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
client_id: "example.com",
redirect_uri: "https://example.com"
)
end
let(:auth_2) do
user_2.remote_storage_authorizations.create!(
permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
client_id: "example.com",
redirect_uri: "https://example.com"
)
end
after do
auth_1.destroy
auth_2.destroy
user_2.destroy
end
it "uses the same web app for both authorizations" do
expect(auth_1.web_app).to eq(auth_2.web_app)
end
end
describe "non-production app origins" do
context "when host is not an FQDN" do
let(:auth) do
user.remote_storage_authorizations.create!(
permissions: %w(recipes),
client_id: "localhost:4200",
redirect_uri: "http://localhost:4200"
)
end
it "does not create a web app" do
expect(auth.web_app).to be_nil
expect(auth.app_name).to eq("localhost:4200")
end
end
context "when host is an IP address" do
let(:auth) do
user.remote_storage_authorizations.create!(
permissions: %w(recipes),
client_id: "192.168.0.23:3000",
redirect_uri: "http://192.168.0.23:3000"
)
end
it "does not create a web app" do
expect(auth.web_app).to be_nil
expect(auth.app_name).to eq("192.168.0.23:3000")
end
end
context "when host is an extension URL" do
let(:auth) do
user.remote_storage_authorizations.create!(
permissions: %w(bookmarks),
client_id: "123.addons.allizom.org",
redirect_uri: "123.addons.allizom.org/foo"
)
end
it "does not create a web app" do
expect(auth.web_app).to be_nil
expect(auth.app_name).to eq("123.addons.allizom.org")
end
end
end
end
describe "#launch_url" do
after(:all) { redis_rs_delete_keys("authorizations:*") }
context "without start URL" do
before do
AppCatalog::WebApp.create!(
url: "https://webmarks.5apps.com", name: "Webmarks",
metadata: { name: "Webmarks", start_url: nil, scope: nil }
)
end
let(:auth) do
user.remote_storage_authorizations.create!(
permissions: %w(bookmarks:rw), client_id: "webmarks.5apps.com",
redirect_uri: "https://webmarks.5apps.com/connect"
)
end
it "uses the base URL (from client ID)" do
expect(auth.launch_url).to eq("https://webmarks.5apps.com")
end
end
context "with start URL" do
before do
AppCatalog::WebApp.create!(
url: "https://hyperdraft.rosano.ca", name: "Hyperdraft",
metadata: {
name: "Hyperdraft", scope: nil,
start_url: "https://hyperdraft.rosano.ca/start"
}
)
end
let(:auth) do
user.remote_storage_authorizations.create!(
permissions: %w(notes:rw), client_id: "hyperdraft.rosano.ca",
redirect_uri: "https://hyperdraft.rosano.ca/write/foo"
)
end
describe "full URL" do
it "respects the start URL" do
expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start")
end
it "does not respect URLs outside of the client ID scope" do
auth.web_app.metadata[:start_url] = "https://uberdraft.rosano.ca/write"
expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca")
end
end
describe "relative paths" do
it "includes the path relative from the base URL" do
auth.web_app.metadata[:start_url] = "start.html"
expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html")
auth.web_app.metadata[:start_url] = "./start.html"
expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html")
auth.web_app.metadata[:start_url] = "../start.html"
expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html")
end
end
describe "absolute path" do
it "includes the path relative from the base URL" do
auth.web_app.metadata[:start_url] = "/write"
expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/write")
end
end
end
end
describe "notifications" do
include ActiveJob::TestHelper
after(:each) { clear_enqueued_jobs }
after(:all) { redis_rs_delete_keys("authorizations:*") }
before { allow(user).to receive(:display_name).and_return("Jimmy") }
context "with notifications disabled" do
before do
user.preferences.merge!({ remotestorage_notify_auth_created: "off" })
user.save!
user.remote_storage_authorizations.create!(
:permissions => %w(photos), :client_id => "app.example.com",
:redirect_uri => "https://app.example.com"
)
end
it "does not notify the user via email about new RS app" do
expect(enqueued_jobs.size).to eq(0)
end
end
context "with email notifications enabled" do
before do
user.preferences.merge!({ remotestorage_notify_auth_created: "email" })
user.save!
user.remote_storage_authorizations.create!(
:permissions => %w(photos), :client_id => "app.example.com",
:redirect_uri => "https://app.example.com"
)
end
it "notifies the user via email" do
expect(enqueued_jobs.size).to eq(1)
job = enqueued_jobs.select{|j| j['job_class'] == "ActionMailer::MailDeliveryJob"}.first
expect(job['arguments'][0]).to eq('NotificationMailer')
expect(job['arguments'][1]).to eq('remotestorage_auth_created')
expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq('gid://akkounts/User/1')
expect(job['arguments'][3]['params']['auth']['_aj_globalid']).to eq('gid://akkounts/RemoteStorageAuthorization/1')
end
end
context "with XMPP notifications enabled" do
before do
Setting.xmpp_notifications_from_address = "botka@kosmos.org"
user.preferences.merge!({ remotestorage_notify_auth_created: "xmpp" })
user.save!
user.remote_storage_authorizations.create!(
:permissions => %w(photos), :client_id => "app.example.com",
:redirect_uri => "https://app.example.com"
)
end
it "sends an XMPP message to the account owner's JID" do
expect(enqueued_jobs.size).to eq(1)
expect(enqueued_jobs.first["job_class"]).to eq("XmppSendMessageJob")
msg = enqueued_jobs.first["arguments"].first
expect(msg["type"]).to eq("normal")
expect(msg["from"]).to eq("botka@kosmos.org")
expect(msg["to"]).to eq(user.address)
expect(msg["body"]).to match(/granted 'app\.example\.com' access to your Kosmos Storage/)
end
end
end
describe "#expired?" do
def auth_with_expiry(expire_at)
RemoteStorageAuthorization.new(
user: user, permissions: %w(documents:rw), client_id: "example.com",
redirect_uri: "https://example.com", expire_at: expire_at
)
end
it "is false without an expiry" do
expect(auth_with_expiry(nil)).not_to be_expired
end
it "is false before the expiry" do
expect(auth_with_expiry(1.day.from_now)).not_to be_expired
end
it "is true after the expiry" do
expect(auth_with_expiry(1.day.ago)).to be_expired
end
end
describe "#device_name" do
def auth_with_user_agent(user_agent)
RemoteStorageAuthorization.new(user: user, user_agent: user_agent)
end
it "returns nil without a user agent" do
expect(auth_with_user_agent(nil).device_name).to be_nil
end
it "recognizes Firefox on Linux" do
ua = "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0"
expect(auth_with_user_agent(ua).device_name).to eq("Firefox on Linux")
end
it "recognizes Chrome on macOS" do
ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " \
"(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
expect(auth_with_user_agent(ua).device_name).to eq("Chrome on macOS")
end
it "recognizes Safari on iOS" do
ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) " \
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"
expect(auth_with_user_agent(ua).device_name).to eq("Safari on iOS")
end
it "falls back to the browser when the OS is unknown" do
expect(auth_with_user_agent("Firefox/120.0").device_name).to eq("Firefox")
end
end
end