Replace the hand-rolled user-agent regexes with the device_detector gem. Its UA database recognises many more clients (and, e.g., GNOME Web when its UA carries the Epiphany token). WebKit "Safari" user agents on Linux (WebKitGTK, which GNOME Web sends) are labelled "WebKit on Linux", since Safari never runs on Linux. Part of #254
384 lines
13 KiB
Ruby
384 lines
13 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 Mobile 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("Mobile Safari on iOS")
|
|
end
|
|
|
|
it "recognizes GNOME Web on Linux (Epiphany's Safari-like user agent)" do
|
|
ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) " \
|
|
"Version/16.0 Safari/605.1.15 Epiphany/45.0"
|
|
expect(auth_with_user_agent(ua).device_name).to eq("GNOME Web on Linux")
|
|
end
|
|
|
|
it "treats a WebKit Safari user agent on Linux as WebKit (WebKitGTK)" do
|
|
ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) " \
|
|
"Version/60.5 Safari/605.1.15"
|
|
expect(auth_with_user_agent(ua).device_name).to eq("WebKit on Linux")
|
|
end
|
|
|
|
it "keeps Safari on macOS as Safari" do
|
|
ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 " \
|
|
"(KHTML, like Gecko) Version/17.1 Safari/605.1.15"
|
|
expect(auth_with_user_agent(ua).device_name).to eq("Safari on macOS")
|
|
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
|