The exact issuance and expiry times in the device rows' hover titles were rendered in UTC, which reads as wrong to anyone not living there. - Mark the dates with a <time> element carrying the absolute (UTC) timestamp, rather than a custom data attribute whose name claimed to hold local time - Rewrite those titles in the browser's own zone, keeping the server's UTC rendering as the fallback for when JavaScript is unavailable - Only convert values that carry an explicit offset, so a date-only <time> is not read as UTC midnight and shifted onto the previous day - Give the revoke button a little more room from the device name and dates Part of #254
207 lines
6.3 KiB
Ruby
207 lines
6.3 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe "Storage connected apps", type: :feature do
|
|
let(:user) { create :user }
|
|
|
|
let(:firefox_ua) do
|
|
"Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0"
|
|
end
|
|
|
|
let(:chrome_ua) do
|
|
"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"
|
|
end
|
|
|
|
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(User).to receive(:ldap_entry).and_return(
|
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
|
services_enabled: ["remotestorage"]
|
|
)
|
|
login_as user, scope: :user
|
|
end
|
|
|
|
def create_auth(client:, device:, permissions: %w(documents:rw))
|
|
user.remote_storage_authorizations.create!(
|
|
permissions: permissions,
|
|
client_id: client,
|
|
redirect_uri: "https://#{client}",
|
|
expire_at: 1.month.from_now,
|
|
user_agent: device
|
|
)
|
|
end
|
|
|
|
context "with a connected app" do
|
|
before do
|
|
AppCatalog::WebApp.create!(url: "https://notes.example.com", name: "Notes", metadata: {})
|
|
end
|
|
|
|
let!(:auth) do
|
|
create_auth(client: "notes.example.com", device: firefox_ua,
|
|
permissions: %w(documents:rw photos:r))
|
|
end
|
|
|
|
after { auth.destroy }
|
|
|
|
it "shows the app with device and category counts" do
|
|
visit apps_services_storage_path
|
|
|
|
within ".rs-auth-card" do
|
|
expect(page).to have_content("Notes")
|
|
expect(page).to have_content("notes.example.com")
|
|
expect(page).to have_content("1 device")
|
|
expect(page).to have_content("2 categories")
|
|
end
|
|
end
|
|
|
|
it "does not show the permission expiry on the card" do
|
|
visit apps_services_storage_path
|
|
|
|
within ".rs-auth-card" do
|
|
expect(page).not_to have_content("Expires")
|
|
end
|
|
end
|
|
|
|
it "renders access and per-token details in the modal" do
|
|
visit apps_services_storage_path
|
|
|
|
expect(page).to have_selector("p.scope", text: "documents", visible: false)
|
|
expect(page).to have_selector("p.scope", text: "photos", visible: false)
|
|
expect(page).to have_selector("h5", text: "Connected devices", visible: false)
|
|
expect(page).to have_selector("p", text: "Firefox on Linux", visible: false)
|
|
expect(page).to have_selector("p", text: /Expires/, visible: false)
|
|
expect(page).to have_button("Revoke access", visible: false)
|
|
expect(page).to have_link("Launch app", visible: false)
|
|
end
|
|
|
|
it "renders the ids that the revoke stream targets" do
|
|
visit apps_services_storage_path
|
|
|
|
expect(page).to have_selector(".rs-auth-card[id^='rs_app_card_']")
|
|
expect(page).to have_selector("div[id^='rs_app_access_']", visible: false)
|
|
expect(page).to have_selector(
|
|
"li##{ActionView::RecordIdentifier.dom_id(auth, :device)}", visible: false
|
|
)
|
|
end
|
|
|
|
it "marks the device timestamps with machine-readable UTC times" do
|
|
visit apps_services_storage_path
|
|
|
|
timestamps = page.all("time[datetime]", visible: false)
|
|
.map { |element| Time.iso8601(element["datetime"]) }
|
|
|
|
expect(timestamps).to contain_exactly(
|
|
be_within(1.second).of(auth.created_at),
|
|
be_within(1.second).of(auth.expire_at)
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with multiple apps" do
|
|
let!(:notes) { create_auth(client: "notes.example.com", device: firefox_ua) }
|
|
let!(:photos) { create_auth(client: "photos.example.com", device: firefox_ua) }
|
|
|
|
after do
|
|
notes.destroy
|
|
photos.destroy
|
|
end
|
|
|
|
it "shows one card per app" do
|
|
visit apps_services_storage_path
|
|
|
|
expect(page).to have_selector(".rs-auth-card", count: 2)
|
|
end
|
|
end
|
|
|
|
context "with one app on multiple devices" do
|
|
let!(:laptop) { create_auth(client: "notes.example.com", device: firefox_ua) }
|
|
let!(:phone) { create_auth(client: "notes.example.com", device: chrome_ua) }
|
|
|
|
after do
|
|
laptop.destroy
|
|
phone.destroy
|
|
end
|
|
|
|
it "shows a single card with the device count" do
|
|
visit apps_services_storage_path
|
|
|
|
expect(page).to have_selector(".rs-auth-card", count: 1)
|
|
|
|
within ".rs-auth-card" do
|
|
expect(page).to have_content("2 devices")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with one app on multiple devices and differing scopes" do
|
|
let!(:laptop) do
|
|
create_auth(client: "notes.example.com", device: firefox_ua, permissions: %w(documents:rw))
|
|
end
|
|
|
|
let!(:phone) do
|
|
create_auth(client: "notes.example.com", device: chrome_ua,
|
|
permissions: %w(documents:rw photos:r))
|
|
end
|
|
|
|
after do
|
|
laptop.destroy
|
|
phone.destroy
|
|
end
|
|
|
|
it "summarises the union of the app's scopes in the card and the modal" do
|
|
visit apps_services_storage_path
|
|
|
|
within ".rs-auth-card" do
|
|
expect(page).to have_content("2 devices")
|
|
expect(page).to have_content("2 categories")
|
|
end
|
|
|
|
expect(page).to have_selector("p.scope", text: "documents", visible: false)
|
|
expect(page).to have_selector("p.scope", text: "photos", visible: false)
|
|
end
|
|
end
|
|
|
|
context "with an authorization from before user agents were recorded" do
|
|
let!(:legacy) { create_auth(client: "notes.example.com", device: nil) }
|
|
|
|
after { legacy.destroy }
|
|
|
|
it "only claims a lower-bound device count and labels the device as unknown" do
|
|
visit apps_services_storage_path
|
|
|
|
within ".rs-auth-card" do
|
|
expect(page).to have_content("1+ device")
|
|
end
|
|
|
|
expect(page).to have_selector("p", text: "Unknown device(s)", visible: false)
|
|
end
|
|
end
|
|
|
|
context "with an unrecorded authorization alongside a recorded device" do
|
|
let!(:legacy) { create_auth(client: "notes.example.com", device: nil) }
|
|
let!(:laptop) { create_auth(client: "notes.example.com", device: firefox_ua) }
|
|
|
|
after do
|
|
legacy.destroy
|
|
laptop.destroy
|
|
end
|
|
|
|
it "counts the unrecorded authorization as a possible extra device" do
|
|
visit apps_services_storage_path
|
|
|
|
within ".rs-auth-card" do
|
|
expect(page).to have_content("2+ devices")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "without connected apps" do
|
|
it "shows the empty state" do
|
|
visit apps_services_storage_path
|
|
|
|
expect(page).to have_content("No apps connected")
|
|
end
|
|
end
|
|
end
|