The specs deleted a global authorizations:* pattern from the shared test Redis DB, so concurrent test processes could wipe each other's keys mid-example, causing intermittent empty/mismatched set failures. Drop all global pattern deletes and the now-unused redis_rs_delete_keys helper. Each spec now reads or asserts only the exact key derived from its own random token, so runs can no longer interfere regardless of ordering or concurrency. Also fixes a vacuous assertion in the #destroy spec, which checked authorizations:<user.address>:* (never matching the keys, which use user.cn), to assert the exact key is gone.
307 lines
10 KiB
Ruby
307 lines
10 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
|
|
after(:each) { clear_enqueued_jobs }
|
|
|
|
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 }
|
|
|
|
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.exists?("authorizations:#{user.cn}:#{auth.token}")).to be(false)
|
|
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
|
|
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
|
|
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 }
|
|
|
|
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
|
|
end
|