Files
akkounts/spec/controllers/services/rs_auths_controller_spec.rb
T
raucao faa7bf6146 Revoke device authorizations in place with Turbo Streams
Revoking a device redirected away from the storage dashboard, which closed the
details modal and reloaded the page. Refreshing the affected parts instead
keeps the modal open and shows the change where it happened.

- Extract the card and the modal's access list into their own components, so
  either can be re-rendered without touching the modal's open state, and share
  the group summaries between the components
- Derive the app's DOM ids from its client_id (the grouping key) and the device
  row ids from the authorization, through one helper used by both the templates
  and the controller
- Revoke one of several devices as a Turbo Stream: remove the row and refresh
  the card behind the modal and the access list
- Revoke an app's last device with a redirect, so the dashboard re-renders
  itself and grouping, sorting and the empty state stay in one place
- Fade the removed row out, deferring Turbo's own removal until the animation
  has ended

Part of #254
2026-11-03 17:45:37 +04:00

165 lines
5.5 KiB
Ruby

require 'rails_helper'
RSpec.describe Services::RsAuthsController, type: :controller do
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)
allow_any_instance_of(LdapService).to receive(:fetch_users).and_return([
{ services_enabled: ["remotestorage"] }
])
end
describe "GET /services/storage/apps/:client_id/launch (storage-first)" do
context "when user is signed in" do
before do
sign_in user
end
context "authorization exists" do
before do
@auth = user.remote_storage_authorizations.create!(
permissions: %w(documents), client_id: "app.example.com",
redirect_uri: "https://app.example.com",
expire_at: 2.days.from_now
)
get :launch_app, params: { client_id: @auth.client_id }
end
after do
@auth.destroy
end
it "hands the app the user address in the URL fragment" do
launch_url = "https://app.example.com#remotestorage=#{user.address}"
expect(response).to redirect_to(launch_url)
end
it "does not hand over an access token" do
expect(response.location).not_to include("access_token")
end
end
context "when remoteStorage is not enabled for the user" do
before do
allow_any_instance_of(LdapService).to receive(:fetch_users).and_return([
{ services_enabled: [] }
])
get :launch_app, params: { client_id: "app.example.com" }
end
it "responds with forbidden" do
expect(response).to have_http_status(:forbidden)
end
end
end
end
describe "GET /services/storage/apps/:client_id/launch for an unknown app" do
before { sign_in user }
it "returns 404 instead of raising" do
get :launch_app, params: { client_id: "nonexistent.example.com" }
expect(response).to have_http_status(:not_found)
end
end
describe "DELETE /services/storage/rs_auths/:id" do
context "when user is signed in" do
before { sign_in user }
it "revokes the authorization" do
auth = user.remote_storage_authorizations.create!(
permissions: %w(documents), client_id: "app.example.com",
redirect_uri: "https://app.example.com", expire_at: 2.days.from_now
)
expect {
delete :destroy, params: { id: auth.id }
}.to change(RemoteStorageAuthorization, :count).by(-1)
expect(response).to redirect_to(apps_services_storage_url)
end
it "returns 404 for an unknown authorization" do
delete :destroy, params: { id: 0 }
expect(response).to have_http_status(:not_found)
end
it "returns 404 for another user's authorization" do
other = create :user, id: 99, cn: "otheruser", email: "other@example.com"
auth = other.remote_storage_authorizations.create!(
permissions: %w(documents), client_id: "app.example.com",
redirect_uri: "https://app.example.com"
)
delete :destroy, params: { id: auth.id }
expect(response).to have_http_status(:not_found)
expect(RemoteStorageAuthorization.exists?(auth.id)).to be(true)
end
context "when another device remains connected to the app" do
let!(:revoked) do
user.remote_storage_authorizations.create!(
permissions: %w(documents), client_id: "app.example.com",
redirect_uri: "https://app.example.com", expire_at: 2.days.from_now
)
end
let!(:remaining) do
user.remote_storage_authorizations.create!(
permissions: %w(documents photos), client_id: "app.example.com",
redirect_uri: "https://app.example.com", expire_at: 2.days.from_now
)
end
after do
revoked.destroy
remaining.destroy
end
it "streams the removed device row and the refreshed card and access list" do
delete :destroy, params: { id: revoked.id }, format: :turbo_stream
expect(response.media_type).to eq("text/vnd.turbo-stream.html")
expect(response.body).to include(
%(action="remove" target="#{ActionView::RecordIdentifier.dom_id(revoked, :device)}")
)
expect(response.body).to include(
%(action="replace" target="#{controller.helpers.rs_app_dom_id('app.example.com', :card)}")
)
expect(response.body).to include(
%(action="replace" target="#{controller.helpers.rs_app_dom_id('app.example.com', :access)}")
)
end
it "leaves the other device connected" do
delete :destroy, params: { id: revoked.id }, format: :turbo_stream
expect(RemoteStorageAuthorization.exists?(remaining.id)).to be(true)
end
end
context "when the app's last device is revoked" do
it "redirects so that the dashboard re-renders itself" do
auth = user.remote_storage_authorizations.create!(
permissions: %w(documents), client_id: "app.example.com",
redirect_uri: "https://app.example.com", expire_at: 2.days.from_now
)
delete :destroy, params: { id: auth.id }, format: :turbo_stream
expect(response).to redirect_to(apps_services_storage_url)
expect(flash[:success]).to eq("App authorization revoked")
end
end
end
end
end