Add admin notifications for new Mastodon versions (#26582)
parent
be991f1d18
commit
16681e0f20
39 changed files with 892 additions and 8 deletions
@ -0,0 +1,18 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
module Admin |
||||||
|
class SoftwareUpdatesController < BaseController |
||||||
|
before_action :check_enabled! |
||||||
|
|
||||||
|
def index |
||||||
|
authorize :software_update, :index? |
||||||
|
@software_updates = SoftwareUpdate.all.sort_by(&:gem_version) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def check_enabled! |
||||||
|
not_found unless SoftwareUpdate.check_enabled? |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,26 @@ |
|||||||
|
import { FormattedMessage } from 'react-intl'; |
||||||
|
|
||||||
|
export const CriticalUpdateBanner = () => ( |
||||||
|
<div className='warning-banner'> |
||||||
|
<div className='warning-banner__message'> |
||||||
|
<h1> |
||||||
|
<FormattedMessage |
||||||
|
id='home.pending_critical_update.title' |
||||||
|
defaultMessage='Critical security update available!' |
||||||
|
/> |
||||||
|
</h1> |
||||||
|
<p> |
||||||
|
<FormattedMessage |
||||||
|
id='home.pending_critical_update.body' |
||||||
|
defaultMessage='Please update your Mastodon server as soon as possible!' |
||||||
|
/>{' '} |
||||||
|
<a href='/admin/software_updates'> |
||||||
|
<FormattedMessage |
||||||
|
id='home.pending_critical_update.link' |
||||||
|
defaultMessage='See updates' |
||||||
|
/> |
||||||
|
</a> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
@ -0,0 +1,27 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
class Admin::SystemCheck::SoftwareVersionCheck < Admin::SystemCheck::BaseCheck |
||||||
|
include RoutingHelper |
||||||
|
|
||||||
|
def skip? |
||||||
|
!current_user.can?(:view_devops) || !SoftwareUpdate.check_enabled? |
||||||
|
end |
||||||
|
|
||||||
|
def pass? |
||||||
|
software_updates.empty? |
||||||
|
end |
||||||
|
|
||||||
|
def message |
||||||
|
if software_updates.any?(&:urgent?) |
||||||
|
Admin::SystemCheck::Message.new(:software_version_critical_check, nil, admin_software_updates_path, true) |
||||||
|
else |
||||||
|
Admin::SystemCheck::Message.new(:software_version_patch_check, nil, admin_software_updates_path) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def software_updates |
||||||
|
@software_updates ||= SoftwareUpdate.pending_to_a.filter { |update| update.urgent? || update.patch_type? } |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,40 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
# == Schema Information |
||||||
|
# |
||||||
|
# Table name: software_updates |
||||||
|
# |
||||||
|
# id :bigint(8) not null, primary key |
||||||
|
# version :string not null |
||||||
|
# urgent :boolean default(FALSE), not null |
||||||
|
# type :integer default("patch"), not null |
||||||
|
# release_notes :string default(""), not null |
||||||
|
# created_at :datetime not null |
||||||
|
# updated_at :datetime not null |
||||||
|
# |
||||||
|
|
||||||
|
class SoftwareUpdate < ApplicationRecord |
||||||
|
self.inheritance_column = nil |
||||||
|
|
||||||
|
enum type: { patch: 0, minor: 1, major: 2 }, _suffix: :type |
||||||
|
|
||||||
|
def gem_version |
||||||
|
Gem::Version.new(version) |
||||||
|
end |
||||||
|
|
||||||
|
class << self |
||||||
|
def check_enabled? |
||||||
|
ENV['UPDATE_CHECK_URL'] != '' |
||||||
|
end |
||||||
|
|
||||||
|
def pending_to_a |
||||||
|
return [] unless check_enabled? |
||||||
|
|
||||||
|
all.to_a.filter { |update| update.gem_version > Mastodon::Version.gem_version } |
||||||
|
end |
||||||
|
|
||||||
|
def urgent_pending? |
||||||
|
pending_to_a.any?(&:urgent?) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
class SoftwareUpdatePolicy < ApplicationPolicy |
||||||
|
def index? |
||||||
|
role.can?(:view_devops) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,82 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
class SoftwareUpdateCheckService < BaseService |
||||||
|
def call |
||||||
|
clean_outdated_updates! |
||||||
|
return unless SoftwareUpdate.check_enabled? |
||||||
|
|
||||||
|
process_update_notices!(fetch_update_notices) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def clean_outdated_updates! |
||||||
|
SoftwareUpdate.find_each do |software_update| |
||||||
|
software_update.delete if Mastodon::Version.gem_version >= software_update.gem_version |
||||||
|
rescue ArgumentError |
||||||
|
software_update.delete |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def fetch_update_notices |
||||||
|
Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res| |
||||||
|
return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200 |
||||||
|
end |
||||||
|
rescue HTTP::Error, OpenSSL::SSL::SSLError, Oj::ParseError |
||||||
|
nil |
||||||
|
end |
||||||
|
|
||||||
|
def api_url |
||||||
|
ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check') |
||||||
|
end |
||||||
|
|
||||||
|
def version |
||||||
|
@version ||= Mastodon::Version.to_s.split('+')[0] |
||||||
|
end |
||||||
|
|
||||||
|
def process_update_notices!(update_notices) |
||||||
|
return if update_notices.blank? || update_notices['updatesAvailable'].blank? |
||||||
|
|
||||||
|
# Clear notices that are not listed by the update server anymore |
||||||
|
SoftwareUpdate.where.not(version: update_notices['updatesAvailable'].pluck('version')).delete_all |
||||||
|
|
||||||
|
# Check if any of the notices is new, and issue notifications |
||||||
|
known_versions = SoftwareUpdate.where(version: update_notices['updatesAvailable'].pluck('version')).pluck(:version) |
||||||
|
new_update_notices = update_notices['updatesAvailable'].filter { |notice| known_versions.exclude?(notice['version']) } |
||||||
|
return if new_update_notices.blank? |
||||||
|
|
||||||
|
new_updates = new_update_notices.map do |notice| |
||||||
|
SoftwareUpdate.create!(version: notice['version'], urgent: notice['urgent'], type: notice['type'], release_notes: notice['releaseNotes']) |
||||||
|
end |
||||||
|
|
||||||
|
notify_devops!(new_updates) |
||||||
|
end |
||||||
|
|
||||||
|
def should_notify_user?(user, urgent_version, patch_version) |
||||||
|
case user.settings['notification_emails.software_updates'] |
||||||
|
when 'none' |
||||||
|
false |
||||||
|
when 'critical' |
||||||
|
urgent_version |
||||||
|
when 'patch' |
||||||
|
urgent_version || patch_version |
||||||
|
when 'all' |
||||||
|
true |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def notify_devops!(new_updates) |
||||||
|
has_new_urgent_version = new_updates.any?(&:urgent?) |
||||||
|
has_new_patch_version = new_updates.any?(&:patch_type?) |
||||||
|
|
||||||
|
User.those_who_can(:view_devops).includes(:account).find_each do |user| |
||||||
|
next unless should_notify_user?(user, has_new_urgent_version, has_new_patch_version) |
||||||
|
|
||||||
|
if has_new_urgent_version |
||||||
|
AdminMailer.with(recipient: user.account).new_critical_software_updates.deliver_later |
||||||
|
else |
||||||
|
AdminMailer.with(recipient: user.account).new_software_updates.deliver_later |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,29 @@ |
|||||||
|
- content_for :page_title do |
||||||
|
= t('admin.software_updates.title') |
||||||
|
|
||||||
|
.simple_form |
||||||
|
%p.lead |
||||||
|
= t('admin.software_updates.description') |
||||||
|
= link_to t('admin.software_updates.documentation_link'), 'https://docs.joinmastodon.org/admin/upgrading/#automated_checks', target: '_new' |
||||||
|
|
||||||
|
%hr.spacer |
||||||
|
|
||||||
|
- unless @software_updates.empty? |
||||||
|
.table-wrapper |
||||||
|
%table.table |
||||||
|
%thead |
||||||
|
%tr |
||||||
|
%th= t('admin.software_updates.version') |
||||||
|
%th= t('admin.software_updates.type') |
||||||
|
%th |
||||||
|
%th |
||||||
|
%tbody |
||||||
|
- @software_updates.each do |update| |
||||||
|
%tr |
||||||
|
%td= update.version |
||||||
|
%td= t("admin.software_updates.types.#{update.type}") |
||||||
|
- if update.urgent? |
||||||
|
%td.critical= t("admin.software_updates.critical_update") |
||||||
|
- else |
||||||
|
%td |
||||||
|
%td= table_link_to 'link', t('admin.software_updates.release_notes'), update.release_notes |
@ -0,0 +1,5 @@ |
|||||||
|
<%= raw t('application_mailer.salutation', name: display_name(@me)) %> |
||||||
|
|
||||||
|
<%= raw t('admin_mailer.new_critical_software_updates.body') %> |
||||||
|
|
||||||
|
<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %> |
@ -0,0 +1,5 @@ |
|||||||
|
<%= raw t('application_mailer.salutation', name: display_name(@me)) %> |
||||||
|
|
||||||
|
<%= raw t('admin_mailer.new_software_updates.body') %> |
||||||
|
|
||||||
|
<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %> |
@ -0,0 +1,11 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
class Scheduler::SoftwareUpdateCheckScheduler |
||||||
|
include Sidekiq::Worker |
||||||
|
|
||||||
|
sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.hour.to_i |
||||||
|
|
||||||
|
def perform |
||||||
|
SoftwareUpdateCheckService.new.call |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,16 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
class CreateSoftwareUpdates < ActiveRecord::Migration[7.0] |
||||||
|
def change |
||||||
|
create_table :software_updates do |t| |
||||||
|
t.string :version, null: false |
||||||
|
t.boolean :urgent, default: false, null: false |
||||||
|
t.integer :type, default: 0, null: false |
||||||
|
t.string :release_notes, default: '', null: false |
||||||
|
|
||||||
|
t.timestamps |
||||||
|
end |
||||||
|
|
||||||
|
add_index :software_updates, :version, unique: true |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
Fabricator(:software_update) do |
||||||
|
version '99.99.99' |
||||||
|
urgent false |
||||||
|
type 'patch' |
||||||
|
end |
@ -0,0 +1,23 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
describe 'finding software updates through the admin interface' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v99') |
||||||
|
|
||||||
|
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Owner')), scope: :user |
||||||
|
end |
||||||
|
|
||||||
|
it 'shows a link to the software updates page, which links to release notes' do |
||||||
|
visit settings_profile_path |
||||||
|
click_on I18n.t('admin.critical_update_pending') |
||||||
|
|
||||||
|
expect(page).to have_title(I18n.t('admin.software_updates.title')) |
||||||
|
|
||||||
|
expect(page).to have_content('99.99.99') |
||||||
|
|
||||||
|
click_on I18n.t('admin.software_updates.release_notes') |
||||||
|
expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,133 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
describe Admin::SystemCheck::SoftwareVersionCheck do |
||||||
|
include RoutingHelper |
||||||
|
|
||||||
|
subject(:check) { described_class.new(user) } |
||||||
|
|
||||||
|
let(:user) { Fabricate(:user) } |
||||||
|
|
||||||
|
describe 'skip?' do |
||||||
|
context 'when user cannot view devops' do |
||||||
|
before { allow(user).to receive(:can?).with(:view_devops).and_return(false) } |
||||||
|
|
||||||
|
it 'returns true' do |
||||||
|
expect(check.skip?).to be true |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when user can view devops' do |
||||||
|
before { allow(user).to receive(:can?).with(:view_devops).and_return(true) } |
||||||
|
|
||||||
|
it 'returns false' do |
||||||
|
expect(check.skip?).to be false |
||||||
|
end |
||||||
|
|
||||||
|
context 'when checks are disabled' do |
||||||
|
around do |example| |
||||||
|
ClimateControl.modify UPDATE_CHECK_URL: '' do |
||||||
|
example.run |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns true' do |
||||||
|
expect(check.skip?).to be true |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe 'pass?' do |
||||||
|
context 'when there is no known update' do |
||||||
|
it 'returns true' do |
||||||
|
expect(check.pass?).to be true |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when there is a non-urgent major release' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: false) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns true' do |
||||||
|
expect(check.pass?).to be true |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when there is an urgent major release' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns false' do |
||||||
|
expect(check.pass?).to be false |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when there is an urgent minor release' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'minor', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns false' do |
||||||
|
expect(check.pass?).to be false |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when there is an urgent patch release' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns false' do |
||||||
|
expect(check.pass?).to be false |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when there is a non-urgent patch release' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns false' do |
||||||
|
expect(check.pass?).to be false |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe 'message' do |
||||||
|
context 'when there is a non-urgent patch release pending' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false) |
||||||
|
end |
||||||
|
|
||||||
|
it 'sends class name symbol to message instance' do |
||||||
|
allow(Admin::SystemCheck::Message).to receive(:new) |
||||||
|
.with(:software_version_patch_check, anything, anything) |
||||||
|
|
||||||
|
check.message |
||||||
|
|
||||||
|
expect(Admin::SystemCheck::Message).to have_received(:new) |
||||||
|
.with(:software_version_patch_check, nil, admin_software_updates_path) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when there is an urgent patch release pending' do |
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'sends class name symbol to message instance' do |
||||||
|
allow(Admin::SystemCheck::Message).to receive(:new) |
||||||
|
.with(:software_version_critical_check, anything, anything, anything) |
||||||
|
|
||||||
|
check.message |
||||||
|
|
||||||
|
expect(Admin::SystemCheck::Message).to have_received(:new) |
||||||
|
.with(:software_version_critical_check, nil, admin_software_updates_path, true) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,87 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe SoftwareUpdate do |
||||||
|
describe '.pending_to_a' do |
||||||
|
before do |
||||||
|
allow(Mastodon::Version).to receive(:gem_version).and_return(Gem::Version.new(mastodon_version)) |
||||||
|
|
||||||
|
Fabricate(:software_update, version: '3.4.42', type: 'patch', urgent: true) |
||||||
|
Fabricate(:software_update, version: '3.5.0', type: 'minor', urgent: false) |
||||||
|
Fabricate(:software_update, version: '4.2.0', type: 'major', urgent: false) |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated release' do |
||||||
|
let(:mastodon_version) { '3.4.0' } |
||||||
|
|
||||||
|
it 'returns the expected versions' do |
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('3.4.42', '3.5.0', '4.2.0') |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the Mastodon version is more recent than anything last returned by the server' do |
||||||
|
let(:mastodon_version) { '5.0.0' } |
||||||
|
|
||||||
|
it 'returns the expected versions' do |
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to eq [] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated nightly' do |
||||||
|
let(:mastodon_version) { '4.3.0-nightly.2023-09-10' } |
||||||
|
|
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '4.3.0-nightly.2023-09-12', type: 'major', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns the expected versions' do |
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-nightly.2023-09-12') |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the Mastodon version is a very outdated nightly' do |
||||||
|
let(:mastodon_version) { '4.2.0-nightly.2023-07-10' } |
||||||
|
|
||||||
|
it 'returns the expected versions' do |
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.2.0') |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated dev version' do |
||||||
|
let(:mastodon_version) { '4.3.0-0.dev.0' } |
||||||
|
|
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '4.3.0-0.dev.2', type: 'major', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns the expected versions' do |
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-0.dev.2') |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated beta version' do |
||||||
|
let(:mastodon_version) { '4.3.0-beta1' } |
||||||
|
|
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '4.3.0-beta2', type: 'major', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns the expected versions' do |
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-beta2') |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the Mastodon version is an outdated beta version and there is a rc' do |
||||||
|
let(:mastodon_version) { '4.3.0-beta1' } |
||||||
|
|
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '4.3.0-rc1', type: 'major', urgent: true) |
||||||
|
end |
||||||
|
|
||||||
|
it 'returns the expected versions' do |
||||||
|
expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-rc1') |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,25 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
require 'rails_helper' |
||||||
|
require 'pundit/rspec' |
||||||
|
|
||||||
|
RSpec.describe SoftwareUpdatePolicy do |
||||||
|
subject { described_class } |
||||||
|
|
||||||
|
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')).account } |
||||||
|
let(:john) { Fabricate(:account) } |
||||||
|
|
||||||
|
permissions :index? do |
||||||
|
context 'when owner' do |
||||||
|
it 'permits' do |
||||||
|
expect(subject).to permit(admin, SoftwareUpdate) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when not owner' do |
||||||
|
it 'denies' do |
||||||
|
expect(subject).to_not permit(john, SoftwareUpdate) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,158 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe SoftwareUpdateCheckService, type: :service do |
||||||
|
subject { described_class.new } |
||||||
|
|
||||||
|
shared_examples 'when the feature is enabled' do |
||||||
|
let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.to_s.split('+')[0]}" } |
||||||
|
|
||||||
|
let(:devops_role) { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) } |
||||||
|
let(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) } |
||||||
|
let(:old_devops_user) { Fabricate(:user) } |
||||||
|
let(:none_user) { Fabricate(:user, role: devops_role) } |
||||||
|
let(:patch_user) { Fabricate(:user, role: devops_role) } |
||||||
|
let(:critical_user) { Fabricate(:user, role: devops_role) } |
||||||
|
|
||||||
|
around do |example| |
||||||
|
queue_adapter = ActiveJob::Base.queue_adapter |
||||||
|
ActiveJob::Base.queue_adapter = :test |
||||||
|
|
||||||
|
example.run |
||||||
|
|
||||||
|
ActiveJob::Base.queue_adapter = queue_adapter |
||||||
|
end |
||||||
|
|
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false) |
||||||
|
Fabricate(:software_update, version: '42.13.12', type: 'major', urgent: false) |
||||||
|
|
||||||
|
owner_user.settings.update('notification_emails.software_updates': 'all') |
||||||
|
owner_user.save! |
||||||
|
|
||||||
|
old_devops_user.settings.update('notification_emails.software_updates': 'all') |
||||||
|
old_devops_user.save! |
||||||
|
|
||||||
|
none_user.settings.update('notification_emails.software_updates': 'none') |
||||||
|
none_user.save! |
||||||
|
|
||||||
|
patch_user.settings.update('notification_emails.software_updates': 'patch') |
||||||
|
patch_user.save! |
||||||
|
|
||||||
|
critical_user.settings.update('notification_emails.software_updates': 'critical') |
||||||
|
critical_user.save! |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the update server errors out' do |
||||||
|
before do |
||||||
|
stub_request(:get, full_update_check_url).to_return(status: 404) |
||||||
|
end |
||||||
|
|
||||||
|
it 'deletes outdated update records but keeps valid update records' do |
||||||
|
expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['42.13.12']) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when the server returns new versions' do |
||||||
|
let(:server_json) do |
||||||
|
{ |
||||||
|
updatesAvailable: [ |
||||||
|
{ |
||||||
|
version: '4.2.1', |
||||||
|
urgent: false, |
||||||
|
type: 'patch', |
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.2.1', |
||||||
|
}, |
||||||
|
{ |
||||||
|
version: '4.3.0', |
||||||
|
urgent: false, |
||||||
|
type: 'minor', |
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.3.0', |
||||||
|
}, |
||||||
|
{ |
||||||
|
version: '5.0.0', |
||||||
|
urgent: false, |
||||||
|
type: 'minor', |
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0', |
||||||
|
}, |
||||||
|
], |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
before do |
||||||
|
stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json)) |
||||||
|
end |
||||||
|
|
||||||
|
it 'updates the list of known updates' do |
||||||
|
expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['4.2.1', '4.3.0', '5.0.0']) |
||||||
|
end |
||||||
|
|
||||||
|
context 'when no update is urgent' do |
||||||
|
it 'sends e-mail notifications according to settings', :aggregate_failures do |
||||||
|
expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_software_updates) |
||||||
|
.with(hash_including(params: { recipient: owner_user.account })).once |
||||||
|
.and(have_enqueued_mail(AdminMailer, :new_software_updates).with(hash_including(params: { recipient: patch_user.account })).once) |
||||||
|
.and(have_enqueued_mail.at_most(2)) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when an update is urgent' do |
||||||
|
let(:server_json) do |
||||||
|
{ |
||||||
|
updatesAvailable: [ |
||||||
|
{ |
||||||
|
version: '5.0.0', |
||||||
|
urgent: true, |
||||||
|
type: 'minor', |
||||||
|
releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0', |
||||||
|
}, |
||||||
|
], |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
it 'sends e-mail notifications according to settings', :aggregate_failures do |
||||||
|
expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_critical_software_updates) |
||||||
|
.with(hash_including(params: { recipient: owner_user.account })).once |
||||||
|
.and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: patch_user.account })).once) |
||||||
|
.and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: critical_user.account })).once) |
||||||
|
.and(have_enqueued_mail.at_most(3)) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when update checking is disabled' do |
||||||
|
around do |example| |
||||||
|
ClimateControl.modify UPDATE_CHECK_URL: '' do |
||||||
|
example.run |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
before do |
||||||
|
Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false) |
||||||
|
end |
||||||
|
|
||||||
|
it 'deletes outdated update records' do |
||||||
|
expect { subject.call }.to change(SoftwareUpdate, :count).from(1).to(0) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context 'when using the default update checking API' do |
||||||
|
let(:update_check_url) { 'https://api.joinmastodon.org/update-check' } |
||||||
|
|
||||||
|
it_behaves_like 'when the feature is enabled' |
||||||
|
end |
||||||
|
|
||||||
|
context 'when using a custom update check URL' do |
||||||
|
let(:update_check_url) { 'https://api.example.com/update_check' } |
||||||
|
|
||||||
|
around do |example| |
||||||
|
ClimateControl.modify UPDATE_CHECK_URL: 'https://api.example.com/update_check' do |
||||||
|
example.run |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it_behaves_like 'when the feature is enabled' |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,20 @@ |
|||||||
|
# frozen_string_literal: true |
||||||
|
|
||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
describe Scheduler::SoftwareUpdateCheckScheduler do |
||||||
|
subject { described_class.new } |
||||||
|
|
||||||
|
describe 'perform' do |
||||||
|
let(:service_double) { instance_double(SoftwareUpdateCheckService, call: nil) } |
||||||
|
|
||||||
|
before do |
||||||
|
allow(SoftwareUpdateCheckService).to receive(:new).and_return(service_double) |
||||||
|
end |
||||||
|
|
||||||
|
it 'calls SoftwareUpdateCheckService' do |
||||||
|
subject.perform |
||||||
|
expect(service_double).to have_received(:call) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue