mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-02-02 21:13:02 -05:00
Merge commit '5b291fcbe41564264954618fb1f4086a3be1c600' into glitch-soc/merge-upstream
Conflicts: - `app/validators/poll_options_validator.rb`: Upstream split `PollValidator` in two, and glitch-soc had local changes to make the options configurable. Refactored as upstream did, keeping glitch-soc's configurable limits.
This commit is contained in:
commit
353e5f797c
@ -370,7 +370,7 @@ GEM
|
|||||||
marcel (~> 1.0.1)
|
marcel (~> 1.0.1)
|
||||||
mime-types
|
mime-types
|
||||||
terrapin (>= 0.6.0, < 2.0)
|
terrapin (>= 0.6.0, < 2.0)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.4)
|
||||||
launchy (3.0.1)
|
launchy (3.0.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
childprocess (~> 5.0)
|
childprocess (~> 5.0)
|
||||||
@ -716,7 +716,7 @@ GEM
|
|||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.2)
|
||||||
rubocop (1.70.0)
|
rubocop (1.71.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
@ -726,14 +726,14 @@ GEM
|
|||||||
rubocop-ast (>= 1.36.2, < 2.0)
|
rubocop-ast (>= 1.36.2, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.37.0)
|
rubocop-ast (1.38.0)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.21.0)
|
rubocop-capybara (2.21.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-performance (1.23.1)
|
rubocop-performance (1.23.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.28.0)
|
rubocop-rails (2.29.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.52.0, < 2.0)
|
rubocop (>= 1.52.0, < 2.0)
|
||||||
|
@ -117,7 +117,7 @@ module SignatureVerification
|
|||||||
|
|
||||||
def verify_signature_strength!
|
def verify_signature_strength!
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest')
|
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
|
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
||||||
end
|
end
|
||||||
@ -155,14 +155,14 @@ module SignatureVerification
|
|||||||
def build_signed_string(include_query_string: true)
|
def build_signed_string(include_query_string: true)
|
||||||
signed_headers.map do |signed_header|
|
signed_headers.map do |signed_header|
|
||||||
case signed_header
|
case signed_header
|
||||||
when Request::REQUEST_TARGET
|
when HttpSignatureDraft::REQUEST_TARGET
|
||||||
if include_query_string
|
if include_query_string
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
||||||
else
|
else
|
||||||
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
||||||
# Therefore, temporarily support such incorrect signatures for compatibility.
|
# Therefore, temporarily support such incorrect signatures for compatibility.
|
||||||
# TODO: remove eventually some time after release of the fixed version
|
# TODO: remove eventually some time after release of the fixed version
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
end
|
end
|
||||||
when '(created)'
|
when '(created)'
|
||||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
|
@ -86,6 +86,13 @@
|
|||||||
"alert.unexpected.message": "Objevila se neočekávaná chyba.",
|
"alert.unexpected.message": "Objevila se neočekávaná chyba.",
|
||||||
"alert.unexpected.title": "Jejda!",
|
"alert.unexpected.title": "Jejda!",
|
||||||
"alt_text_badge.title": "Popisek",
|
"alt_text_badge.title": "Popisek",
|
||||||
|
"alt_text_modal.add_alt_text": "Přidat alt text",
|
||||||
|
"alt_text_modal.add_text_from_image": "Přidat text z obrázku",
|
||||||
|
"alt_text_modal.cancel": "Zrušit",
|
||||||
|
"alt_text_modal.change_thumbnail": "Změnit miniaturu",
|
||||||
|
"alt_text_modal.describe_for_people_with_hearing_impairments": "Popište to pro osoby se sluchovým postižením…",
|
||||||
|
"alt_text_modal.describe_for_people_with_visual_impairments": "Popište to pro osoby se zrakovým postižením…",
|
||||||
|
"alt_text_modal.done": "Hotovo",
|
||||||
"announcement.announcement": "Oznámení",
|
"announcement.announcement": "Oznámení",
|
||||||
"annual_report.summary.archetype.booster": "Lovec obsahu",
|
"annual_report.summary.archetype.booster": "Lovec obsahu",
|
||||||
"annual_report.summary.archetype.lurker": "Špión",
|
"annual_report.summary.archetype.lurker": "Špión",
|
||||||
@ -407,6 +414,8 @@
|
|||||||
"ignore_notifications_modal.not_followers_title": "Ignorovat oznámení od lidí, kteří vás nesledují?",
|
"ignore_notifications_modal.not_followers_title": "Ignorovat oznámení od lidí, kteří vás nesledují?",
|
||||||
"ignore_notifications_modal.not_following_title": "Ignorovat oznámení od lidí, které nesledujete?",
|
"ignore_notifications_modal.not_following_title": "Ignorovat oznámení od lidí, které nesledujete?",
|
||||||
"ignore_notifications_modal.private_mentions_title": "Ignorovat oznámení z nevyžádaných soukromých zmínek?",
|
"ignore_notifications_modal.private_mentions_title": "Ignorovat oznámení z nevyžádaných soukromých zmínek?",
|
||||||
|
"info_button.label": "Nápověda",
|
||||||
|
"info_button.what_is_alt_text": "<h1>Co je to alt text?</h1> <p>Alt text poskytuje popis obrázků pro lidi se zrakovými postižením, špatným připojením něbo těm, kteří potřebují více kontextu.</p> <p>Můžete zlepšit přístupnost a porozumění napsáním jasného, stručného a objektivního alt textu.</p> <ul> <li>Zachyťte důležité prvky</li> <li>Shrňte text v obrázku</li> <li>Použijte pravidelnou větnou skladbu</li> <li>Vyhněte se nadbytečným informacím</li> <li>U komplexních vizualizací (diagramy, mapy...) se zaměřte na trendy a klíčová zjištění</li> </ul>",
|
||||||
"interaction_modal.action.favourite": "Chcete-li pokračovat, musíte oblíbit z vašeho účtu.",
|
"interaction_modal.action.favourite": "Chcete-li pokračovat, musíte oblíbit z vašeho účtu.",
|
||||||
"interaction_modal.action.follow": "Chcete-li pokračovat, musíte sledovat z vašeho účtu.",
|
"interaction_modal.action.follow": "Chcete-li pokračovat, musíte sledovat z vašeho účtu.",
|
||||||
"interaction_modal.action.reblog": "Chcete-li pokračovat, musíte dát boost z vašeho účtu.",
|
"interaction_modal.action.reblog": "Chcete-li pokračovat, musíte dát boost z vašeho účtu.",
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"account.enable_notifications": "Notificarme cuando @{name} publique algo",
|
"account.enable_notifications": "Notificarme cuando @{name} publique algo",
|
||||||
"account.endorse": "Destacar en mi perfil",
|
"account.endorse": "Destacar en mi perfil",
|
||||||
"account.featured_tags.last_status_at": "Última publicación el {date}",
|
"account.featured_tags.last_status_at": "Última publicación el {date}",
|
||||||
"account.featured_tags.last_status_never": "No hay publicaciones",
|
"account.featured_tags.last_status_never": "Sin publicaciones",
|
||||||
"account.featured_tags.title": "Etiquetas destacadas de {name}",
|
"account.featured_tags.title": "Etiquetas destacadas de {name}",
|
||||||
"account.follow": "Seguir",
|
"account.follow": "Seguir",
|
||||||
"account.follow_back": "Seguir también",
|
"account.follow_back": "Seguir también",
|
||||||
@ -146,7 +146,7 @@
|
|||||||
"column.about": "Acerca de",
|
"column.about": "Acerca de",
|
||||||
"column.blocks": "Usuarios bloqueados",
|
"column.blocks": "Usuarios bloqueados",
|
||||||
"column.bookmarks": "Marcadores",
|
"column.bookmarks": "Marcadores",
|
||||||
"column.community": "Línea de tiempo local",
|
"column.community": "Cronología local",
|
||||||
"column.create_list": "Crear lista",
|
"column.create_list": "Crear lista",
|
||||||
"column.direct": "Menciones privadas",
|
"column.direct": "Menciones privadas",
|
||||||
"column.directory": "Buscar perfiles",
|
"column.directory": "Buscar perfiles",
|
||||||
|
@ -414,6 +414,7 @@
|
|||||||
"ignore_notifications_modal.not_followers_title": "나를 팔로우하지 않는 사람들의 알림을 무시할까요?",
|
"ignore_notifications_modal.not_followers_title": "나를 팔로우하지 않는 사람들의 알림을 무시할까요?",
|
||||||
"ignore_notifications_modal.not_following_title": "내가 팔로우하지 않는 사람들의 알림을 무시할까요?",
|
"ignore_notifications_modal.not_following_title": "내가 팔로우하지 않는 사람들의 알림을 무시할까요?",
|
||||||
"ignore_notifications_modal.private_mentions_title": "요청하지 않은 개인 멘션 알림을 무시할까요?",
|
"ignore_notifications_modal.private_mentions_title": "요청하지 않은 개인 멘션 알림을 무시할까요?",
|
||||||
|
"info_button.label": "도움말",
|
||||||
"interaction_modal.action.favourite": "계속하려면 내 계정으로 즐겨찾기해야 합니다.",
|
"interaction_modal.action.favourite": "계속하려면 내 계정으로 즐겨찾기해야 합니다.",
|
||||||
"interaction_modal.action.follow": "계속하려면 내 계정으로 팔로우해야 합니다.",
|
"interaction_modal.action.follow": "계속하려면 내 계정으로 팔로우해야 합니다.",
|
||||||
"interaction_modal.action.reblog": "계속하려면 내 계정으로 리블로그해야 합니다.",
|
"interaction_modal.action.reblog": "계속하려면 내 계정으로 리블로그해야 합니다.",
|
||||||
|
31
app/lib/http_signature_draft.rb
Normal file
31
app/lib/http_signature_draft.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This implements an older draft of HTTP Signatures:
|
||||||
|
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures
|
||||||
|
|
||||||
|
class HttpSignatureDraft
|
||||||
|
REQUEST_TARGET = '(request-target)'
|
||||||
|
|
||||||
|
def initialize(keypair, key_id, full_path: true)
|
||||||
|
@keypair = keypair
|
||||||
|
@key_id = key_id
|
||||||
|
@full_path = full_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_target(verb, url)
|
||||||
|
if url.query.nil? || !@full_path
|
||||||
|
"#{verb} #{url.path}"
|
||||||
|
else
|
||||||
|
"#{verb} #{url.path}?#{url.query}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign(signed_headers, verb, url)
|
||||||
|
signed_headers = signed_headers.merge(REQUEST_TARGET => request_target(verb, url))
|
||||||
|
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
|
||||||
|
algorithm = 'rsa-sha256'
|
||||||
|
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
|
||||||
|
|
||||||
|
"keyId=\"#{@key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
|
||||||
|
end
|
||||||
|
end
|
@ -61,8 +61,6 @@ class PerOperationWithDeadline < HTTP::Timeout::PerOperation
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Request
|
class Request
|
||||||
REQUEST_TARGET = '(request-target)'
|
|
||||||
|
|
||||||
# We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
|
# We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
|
||||||
# and 5s timeout on the TLS handshake, meaning the worst case should take
|
# and 5s timeout on the TLS handshake, meaning the worst case should take
|
||||||
# about 15s in total
|
# about 15s in total
|
||||||
@ -78,11 +76,18 @@ class Request
|
|||||||
@http_client = options.delete(:http_client)
|
@http_client = options.delete(:http_client)
|
||||||
@allow_local = options.delete(:allow_local)
|
@allow_local = options.delete(:allow_local)
|
||||||
@full_path = !options.delete(:omit_query_string)
|
@full_path = !options.delete(:omit_query_string)
|
||||||
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
|
@options = {
|
||||||
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
|
follow: {
|
||||||
|
max_hops: 3,
|
||||||
|
on_redirect: ->(response, request) { re_sign_on_redirect(response, request) },
|
||||||
|
},
|
||||||
|
socket_class: use_proxy? || @allow_local ? ProxySocket : Socket,
|
||||||
|
}.merge(options)
|
||||||
@options = @options.merge(proxy_url) if use_proxy?
|
@options = @options.merge(proxy_url) if use_proxy?
|
||||||
@headers = {}
|
@headers = {}
|
||||||
|
|
||||||
|
@signing = nil
|
||||||
|
|
||||||
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
|
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
|
||||||
|
|
||||||
set_common_headers!
|
set_common_headers!
|
||||||
@ -92,8 +97,9 @@ class Request
|
|||||||
def on_behalf_of(actor, sign_with: nil)
|
def on_behalf_of(actor, sign_with: nil)
|
||||||
raise ArgumentError, 'actor must not be nil' if actor.nil?
|
raise ArgumentError, 'actor must not be nil' if actor.nil?
|
||||||
|
|
||||||
@actor = actor
|
key_id = ActivityPub::TagManager.instance.key_uri_for(actor)
|
||||||
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
|
keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : actor.keypair
|
||||||
|
@signing = HttpSignatureDraft.new(keypair, key_id, full_path: @full_path)
|
||||||
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
@ -119,7 +125,7 @@ class Request
|
|||||||
end
|
end
|
||||||
|
|
||||||
def headers
|
def headers
|
||||||
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
|
(@signing ? @headers.merge('Signature' => signature) : @headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
@ -134,14 +140,13 @@ class Request
|
|||||||
end
|
end
|
||||||
|
|
||||||
def http_client
|
def http_client
|
||||||
HTTP.use(:auto_inflate).follow(max_hops: 3)
|
HTTP.use(:auto_inflate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_common_headers!
|
def set_common_headers!
|
||||||
@headers[REQUEST_TARGET] = request_target
|
|
||||||
@headers['User-Agent'] = Mastodon::Version.user_agent
|
@headers['User-Agent'] = Mastodon::Version.user_agent
|
||||||
@headers['Host'] = @url.host
|
@headers['Host'] = @url.host
|
||||||
@headers['Date'] = Time.now.utc.httpdate
|
@headers['Date'] = Time.now.utc.httpdate
|
||||||
@ -152,31 +157,28 @@ class Request
|
|||||||
@headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
|
@headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_target
|
|
||||||
if @url.query.nil? || !@full_path
|
|
||||||
"#{@verb} #{@url.path}"
|
|
||||||
else
|
|
||||||
"#{@verb} #{@url.path}?#{@url.query}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def signature
|
def signature
|
||||||
algorithm = 'rsa-sha256'
|
@signing.sign(@headers.without('User-Agent', 'Accept-Encoding'), @verb, @url)
|
||||||
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
|
|
||||||
|
|
||||||
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def signed_string
|
def re_sign_on_redirect(_response, request)
|
||||||
signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
|
# Delete existing signature if there is one, since it will be invalid
|
||||||
|
request.headers.delete('Signature')
|
||||||
|
|
||||||
|
return unless @signing.present? && @verb == :get
|
||||||
|
|
||||||
|
signed_headers = request.headers.to_h.slice(*@headers.keys)
|
||||||
|
unless @headers.keys.all? { |key| signed_headers.key?(key) }
|
||||||
|
# We have lost some headers in the process, so don't sign the new
|
||||||
|
# request, in order to avoid issuing a valid signature with fewer
|
||||||
|
# conditions than expected.
|
||||||
|
|
||||||
|
Rails.logger.warn { "Some headers (#{@headers.keys - signed_headers.keys}) have been lost on redirect from {@uri} to #{request.uri}, this should not happen. Skipping signatures" }
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
def signed_headers
|
signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding'), @verb, Addressable::URI.parse(request.uri))
|
||||||
@headers.without('User-Agent', 'Accept-Encoding')
|
request.headers['Signature'] = signature_value
|
||||||
end
|
|
||||||
|
|
||||||
def key_id
|
|
||||||
ActivityPub::TagManager.instance.key_uri_for(@actor)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def http_client
|
def http_client
|
||||||
|
@ -37,7 +37,8 @@ class Poll < ApplicationRecord
|
|||||||
|
|
||||||
validates :options, presence: true
|
validates :options, presence: true
|
||||||
validates :expires_at, presence: true, if: :local?
|
validates :expires_at, presence: true, if: :local?
|
||||||
validates_with PollValidator, on: :create, if: :local?
|
validates_with PollOptionsValidator, if: :local?
|
||||||
|
validates_with PollExpirationValidator, if: -> { local? && expires_at_changed? }
|
||||||
|
|
||||||
before_validation :prepare_options, if: :local?
|
before_validation :prepare_options, if: :local?
|
||||||
before_validation :prepare_votes_count
|
before_validation :prepare_votes_count
|
||||||
|
@ -19,10 +19,10 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||||||
|
|
||||||
def poll_limits
|
def poll_limits
|
||||||
{
|
{
|
||||||
max_options: PollValidator::MAX_OPTIONS,
|
max_options: PollOptionsValidator::MAX_OPTIONS,
|
||||||
max_option_chars: PollValidator::MAX_OPTION_CHARS,
|
max_option_chars: PollOptionsValidator::MAX_OPTION_CHARS,
|
||||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
|
||||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -88,10 +88,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||||||
},
|
},
|
||||||
|
|
||||||
polls: {
|
polls: {
|
||||||
max_options: PollValidator::MAX_OPTIONS,
|
max_options: PollOptionsValidator::MAX_OPTIONS,
|
||||||
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
|
max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
|
||||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
|
||||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
|
||||||
},
|
},
|
||||||
|
|
||||||
translation: {
|
translation: {
|
||||||
|
@ -42,10 +42,10 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
|||||||
|
|
||||||
def poll_limits
|
def poll_limits
|
||||||
{
|
{
|
||||||
max_options: PollValidator::MAX_OPTIONS,
|
max_options: PollOptionsValidator::MAX_OPTIONS,
|
||||||
max_option_chars: PollValidator::MAX_OPTION_CHARS,
|
max_option_chars: PollOptionsValidator::MAX_OPTION_CHARS,
|
||||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
|
||||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -84,10 +84,10 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
|||||||
},
|
},
|
||||||
|
|
||||||
polls: {
|
polls: {
|
||||||
max_options: PollValidator::MAX_OPTIONS,
|
max_options: PollOptionsValidator::MAX_OPTIONS,
|
||||||
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
|
max_characters_per_option: PollOptionsValidator::MAX_OPTION_CHARS,
|
||||||
min_expiration: PollValidator::MIN_EXPIRATION,
|
min_expiration: PollExpirationValidator::MIN_EXPIRATION,
|
||||||
max_expiration: PollValidator::MAX_EXPIRATION,
|
max_expiration: PollExpirationValidator::MAX_EXPIRATION,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
13
app/validators/poll_expiration_validator.rb
Normal file
13
app/validators/poll_expiration_validator.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PollExpirationValidator < ActiveModel::Validator
|
||||||
|
MAX_EXPIRATION = 1.month.freeze
|
||||||
|
MIN_EXPIRATION = 5.minutes.freeze
|
||||||
|
|
||||||
|
def validate(poll)
|
||||||
|
current_time = Time.now.utc
|
||||||
|
|
||||||
|
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
|
||||||
|
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
|
||||||
|
end
|
||||||
|
end
|
@ -1,19 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PollValidator < ActiveModel::Validator
|
class PollOptionsValidator < ActiveModel::Validator
|
||||||
MAX_OPTIONS = (ENV['MAX_POLL_OPTIONS'] || 5).to_i
|
MAX_OPTIONS = (ENV['MAX_POLL_OPTIONS'] || 5).to_i
|
||||||
MAX_OPTION_CHARS = (ENV['MAX_POLL_OPTION_CHARS'] || 100).to_i
|
MAX_OPTION_CHARS = (ENV['MAX_POLL_OPTION_CHARS'] || 100).to_i
|
||||||
MAX_EXPIRATION = 1.month.freeze
|
|
||||||
MIN_EXPIRATION = 5.minutes.freeze
|
|
||||||
|
|
||||||
def validate(poll)
|
def validate(poll)
|
||||||
current_time = Time.now.utc
|
|
||||||
|
|
||||||
poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
|
poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1
|
||||||
poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
|
poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS
|
||||||
poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
|
poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS }
|
||||||
poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
|
poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size
|
||||||
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_long')) if poll.expires_at.nil? || poll.expires_at - current_time > MAX_EXPIRATION
|
|
||||||
poll.errors.add(:expires_at, I18n.t('polls.errors.duration_too_short')) if poll.expires_at.present? && (poll.expires_at - current_time).ceil < MIN_EXPIRATION
|
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -1247,6 +1247,7 @@ cs:
|
|||||||
too_fast: Formulář byl odeslán příliš rychle, zkuste to znovu.
|
too_fast: Formulář byl odeslán příliš rychle, zkuste to znovu.
|
||||||
use_security_key: Použít bezpečnostní klíč
|
use_security_key: Použít bezpečnostní klíč
|
||||||
user_agreement_html: Přečetl jsem si a souhlasím s <a href="%{terms_of_service_path}" target="_blank">podmínkami služby</a> a <a href="%{privacy_policy_path}" target="_blank">ochranou osobních údajů</a>
|
user_agreement_html: Přečetl jsem si a souhlasím s <a href="%{terms_of_service_path}" target="_blank">podmínkami služby</a> a <a href="%{privacy_policy_path}" target="_blank">ochranou osobních údajů</a>
|
||||||
|
user_privacy_agreement_html: Četl jsem a souhlasím se zásadami <a href="%{privacy_policy_path}" target="_blank">ochrany osobních údajů</a>
|
||||||
author_attribution:
|
author_attribution:
|
||||||
example_title: Ukázkový text
|
example_title: Ukázkový text
|
||||||
hint_html: Píšete novinové články nebo blog mimo Mastodon? Kontrolujte, jak Vám bude připisováno autorství, když jsou sdíleny na Mastodonu.
|
hint_html: Píšete novinové články nebo blog mimo Mastodon? Kontrolujte, jak Vám bude připisováno autorství, když jsou sdíleny na Mastodonu.
|
||||||
|
@ -302,7 +302,7 @@ ko:
|
|||||||
deleted_account: 계정을 삭제했습니다
|
deleted_account: 계정을 삭제했습니다
|
||||||
empty: 로그를 찾을 수 없습니다
|
empty: 로그를 찾을 수 없습니다
|
||||||
filter_by_action: 동작 별 필터
|
filter_by_action: 동작 별 필터
|
||||||
filter_by_user: 사용자로 거르기
|
filter_by_user: 사용자 기준으로 필터
|
||||||
title: 감사 로그
|
title: 감사 로그
|
||||||
unavailable_instance: "(도메인네임 사용불가)"
|
unavailable_instance: "(도메인네임 사용불가)"
|
||||||
announcements:
|
announcements:
|
||||||
@ -1192,6 +1192,7 @@ ko:
|
|||||||
too_fast: 너무 빠르게 양식이 제출되었습니다, 다시 시도하세요.
|
too_fast: 너무 빠르게 양식이 제출되었습니다, 다시 시도하세요.
|
||||||
use_security_key: 보안 키 사용
|
use_security_key: 보안 키 사용
|
||||||
user_agreement_html: <a href="%{terms_of_service_path}" target="_blank">이용 약관</a>과 <a href="%{privacy_policy_path}" target="_blank">개인정보처리방침</a>을 읽었고 동의합니다
|
user_agreement_html: <a href="%{terms_of_service_path}" target="_blank">이용 약관</a>과 <a href="%{privacy_policy_path}" target="_blank">개인정보처리방침</a>을 읽었고 동의합니다
|
||||||
|
user_privacy_agreement_html: <a href="%{privacy_policy_path}" target="_blank">개인정보처리방침</a>을 읽었고 동의합니다
|
||||||
author_attribution:
|
author_attribution:
|
||||||
example_title: 예시 텍스트
|
example_title: 예시 텍스트
|
||||||
hint_html: 마스토돈 밖에서 뉴스나 블로그 글을 쓰시나요? 마스토돈에 공유되었을 때 어떻게 표시될지를 제어하세요.
|
hint_html: 마스토돈 밖에서 뉴스나 블로그 글을 쓰시나요? 마스토돈에 공유되었을 때 어떻게 표시될지를 제어하세요.
|
||||||
|
@ -3,6 +3,7 @@ cs:
|
|||||||
simple_form:
|
simple_form:
|
||||||
hints:
|
hints:
|
||||||
account:
|
account:
|
||||||
|
attribution_domains: Jeden na řádek. Chrání před falešným připisování autorství.
|
||||||
discoverable: Vaše veřejné příspěvky a profil mohou být zobrazeny nebo doporučeny v různých oblastech Mastodonu a váš profil může být navrhován ostatním uživatelům.
|
discoverable: Vaše veřejné příspěvky a profil mohou být zobrazeny nebo doporučeny v různých oblastech Mastodonu a váš profil může být navrhován ostatním uživatelům.
|
||||||
display_name: Vaše celé jméno nebo přezdívka.
|
display_name: Vaše celé jméno nebo přezdívka.
|
||||||
fields: Vaše domovská stránka, zájmena, věk, cokoliv chcete.
|
fields: Vaše domovská stránka, zájmena, věk, cokoliv chcete.
|
||||||
@ -155,6 +156,7 @@ cs:
|
|||||||
url: Kam budou události odesílány
|
url: Kam budou události odesílány
|
||||||
labels:
|
labels:
|
||||||
account:
|
account:
|
||||||
|
attribution_domains: Webové stránky s povolením Vám připsat autorství
|
||||||
discoverable: Zobrazovat profil a příspěvky ve vyhledávacích algoritmech
|
discoverable: Zobrazovat profil a příspěvky ve vyhledávacích algoritmech
|
||||||
fields:
|
fields:
|
||||||
name: Označení
|
name: Označení
|
||||||
|
@ -23,7 +23,7 @@ module Devise
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def valid_params?
|
def valid_params?
|
||||||
params[scope] && params[scope][:password].present?
|
params[scope].is_a?(Hash) && params[scope][:password].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -22,7 +22,7 @@ module Devise
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def valid_params?
|
def valid_params?
|
||||||
params[scope].respond_to?(:[]) && params[scope][:password].present?
|
params[scope].is_a?(Hash) && params[scope][:password].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -48,15 +48,11 @@ RSpec.describe Admin::DomainBlocksController do
|
|||||||
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } }
|
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'records a block' do
|
it 'records a block, calls a worker, redirects' do
|
||||||
expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true
|
expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true
|
||||||
end
|
|
||||||
|
|
||||||
it 'calls DomainBlockWorker' do
|
|
||||||
expect(DomainBlockWorker).to have_received(:perform_async)
|
expect(DomainBlockWorker).to have_received(:perform_async)
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects with a success message' do
|
|
||||||
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
|
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
|
||||||
expect(response).to redirect_to(admin_instances_path(limited: '1'))
|
expect(response).to redirect_to(admin_instances_path(limited: '1'))
|
||||||
end
|
end
|
||||||
@ -68,15 +64,11 @@ RSpec.describe Admin::DomainBlocksController do
|
|||||||
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } }
|
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not record a block' do
|
it 'does not record a block or call worker, renders new' do
|
||||||
expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be false
|
expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be false
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not call DomainBlockWorker' do
|
|
||||||
expect(DomainBlockWorker).to_not have_received(:perform_async)
|
expect(DomainBlockWorker).to_not have_received(:perform_async)
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders new' do
|
|
||||||
expect(response).to render_template :new
|
expect(response).to render_template :new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -87,15 +79,11 @@ RSpec.describe Admin::DomainBlocksController do
|
|||||||
post :create, params: { domain_block: { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true } }
|
post :create, params: { domain_block: { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not record a block' do
|
it 'does not record a block or call worker, renders confirm suspension' do
|
||||||
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be false
|
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be false
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not call DomainBlockWorker' do
|
|
||||||
expect(DomainBlockWorker).to_not have_received(:perform_async)
|
expect(DomainBlockWorker).to_not have_received(:perform_async)
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders confirm_suspension' do
|
|
||||||
expect(response).to render_template :confirm_suspension
|
expect(response).to render_template :confirm_suspension
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -105,15 +93,11 @@ RSpec.describe Admin::DomainBlocksController do
|
|||||||
post :create, params: { :domain_block => { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true }, 'confirm' => '' }
|
post :create, params: { :domain_block => { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true }, 'confirm' => '' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'records a block' do
|
it 'records a block and calls worker and redirects' do
|
||||||
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true
|
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true
|
||||||
end
|
|
||||||
|
|
||||||
it 'calls DomainBlockWorker' do
|
|
||||||
expect(DomainBlockWorker).to have_received(:perform_async)
|
expect(DomainBlockWorker).to have_received(:perform_async)
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects with a success message' do
|
|
||||||
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
|
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
|
||||||
expect(response).to redirect_to(admin_instances_path(limited: '1'))
|
expect(response).to redirect_to(admin_instances_path(limited: '1'))
|
||||||
end
|
end
|
||||||
@ -130,15 +114,11 @@ RSpec.describe Admin::DomainBlocksController do
|
|||||||
post :create, params: { domain_block: { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true } }
|
post :create, params: { domain_block: { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true } }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not record a block' do
|
it 'does not record a block or call worker, renders confirm suspension' do
|
||||||
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be false
|
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be false
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not call DomainBlockWorker' do
|
|
||||||
expect(DomainBlockWorker).to_not have_received(:perform_async)
|
expect(DomainBlockWorker).to_not have_received(:perform_async)
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders confirm_suspension' do
|
|
||||||
expect(response).to render_template :confirm_suspension
|
expect(response).to render_template :confirm_suspension
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -148,15 +128,11 @@ RSpec.describe Admin::DomainBlocksController do
|
|||||||
post :create, params: { :domain_block => { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true }, 'confirm' => '' }
|
post :create, params: { :domain_block => { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true }, 'confirm' => '' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the record' do
|
it 'updates the record and calls worker, redirects' do
|
||||||
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true
|
expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true
|
||||||
end
|
|
||||||
|
|
||||||
it 'calls DomainBlockWorker' do
|
|
||||||
expect(DomainBlockWorker).to have_received(:perform_async)
|
expect(DomainBlockWorker).to have_received(:perform_async)
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects with a success message' do
|
|
||||||
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
|
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
|
||||||
expect(response).to redirect_to(admin_instances_path(limited: '1'))
|
expect(response).to redirect_to(admin_instances_path(limited: '1'))
|
||||||
end
|
end
|
||||||
|
@ -58,11 +58,9 @@ RSpec.describe Admin::EmailDomainBlocksController do
|
|||||||
post :create, params: { email_domain_block: { domain: 'example.com' }, save: '' }
|
post :create, params: { email_domain_block: { domain: 'example.com' }, save: '' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'blocks the domain' do
|
it 'blocks the domain and redirects to email domain blocks' do
|
||||||
expect(EmailDomainBlock.find_by(domain: 'example.com')).to_not be_nil
|
expect(EmailDomainBlock.find_by(domain: 'example.com')).to_not be_nil
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects to e-mail domain blocks' do
|
|
||||||
expect(response).to redirect_to(admin_email_domain_blocks_path)
|
expect(response).to redirect_to(admin_email_domain_blocks_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -68,11 +68,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
let(:selected_position) { 1 }
|
let(:selected_position) { 1 }
|
||||||
let(:selected_permissions_as_keys) { %w(manage_roles) }
|
let(:selected_permissions_as_keys) { %w(manage_roles) }
|
||||||
|
|
||||||
it 'redirects to roles page' do
|
it 'redirects to roles page and creates role' do
|
||||||
expect(response).to redirect_to(admin_roles_path)
|
expect(response).to redirect_to(admin_roles_path)
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates new role' do
|
|
||||||
expect(UserRole.find_by(name: 'Bar')).to_not be_nil
|
expect(UserRole.find_by(name: 'Bar')).to_not be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -81,11 +79,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
let(:selected_position) { 100 }
|
let(:selected_position) { 100 }
|
||||||
let(:selected_permissions_as_keys) { %w(manage_roles) }
|
let(:selected_permissions_as_keys) { %w(manage_roles) }
|
||||||
|
|
||||||
it 'renders new template' do
|
it 'renders new template and does not create role' do
|
||||||
expect(response).to render_template(:new)
|
expect(response).to render_template(:new)
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create new role' do
|
|
||||||
expect(UserRole.find_by(name: 'Bar')).to be_nil
|
expect(UserRole.find_by(name: 'Bar')).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -94,11 +90,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
let(:selected_position) { 1 }
|
let(:selected_position) { 1 }
|
||||||
let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
|
let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
|
||||||
|
|
||||||
it 'renders new template' do
|
it 'renders new template and does not create role' do
|
||||||
expect(response).to render_template(:new)
|
expect(response).to render_template(:new)
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create new role' do
|
|
||||||
expect(UserRole.find_by(name: 'Bar')).to be_nil
|
expect(UserRole.find_by(name: 'Bar')).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -109,11 +103,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
let(:selected_position) { 1 }
|
let(:selected_position) { 1 }
|
||||||
let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
|
let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
|
||||||
|
|
||||||
it 'redirects to roles page' do
|
it 'redirects to roles page and creates new role' do
|
||||||
expect(response).to redirect_to(admin_roles_path)
|
expect(response).to redirect_to(admin_roles_path)
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates new role' do
|
|
||||||
expect(UserRole.find_by(name: 'Bar')).to_not be_nil
|
expect(UserRole.find_by(name: 'Bar')).to_not be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -166,11 +158,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when user does not have permission to manage roles' do
|
context 'when user does not have permission to manage roles' do
|
||||||
it 'returns http forbidden' do
|
it 'returns http forbidden and does not update role' do
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not update the role' do
|
|
||||||
expect(role.reload.name).to eq 'Bar'
|
expect(role.reload.name).to eq 'Bar'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -179,11 +169,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
let(:permissions) { UserRole::FLAGS[:manage_roles] }
|
let(:permissions) { UserRole::FLAGS[:manage_roles] }
|
||||||
|
|
||||||
context 'when role has permissions the user doesn\'t' do
|
context 'when role has permissions the user doesn\'t' do
|
||||||
it 'renders edit template' do
|
it 'renders edit template and does not update role' do
|
||||||
expect(response).to render_template(:edit)
|
expect(response).to render_template(:edit)
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not update the role' do
|
|
||||||
expect(role.reload.name).to eq 'Bar'
|
expect(role.reload.name).to eq 'Bar'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -192,11 +180,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
let(:permissions) { UserRole::FLAGS[:manage_roles] | UserRole::FLAGS[:manage_users] }
|
let(:permissions) { UserRole::FLAGS[:manage_roles] | UserRole::FLAGS[:manage_users] }
|
||||||
|
|
||||||
context 'when user outranks the role' do
|
context 'when user outranks the role' do
|
||||||
it 'redirects to roles page' do
|
it 'redirects to roles page and updates role' do
|
||||||
expect(response).to redirect_to(admin_roles_path)
|
expect(response).to redirect_to(admin_roles_path)
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the role' do
|
|
||||||
expect(role.reload.name).to eq 'Baz'
|
expect(role.reload.name).to eq 'Baz'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -204,11 +190,9 @@ RSpec.describe Admin::RolesController do
|
|||||||
context 'when role outranks user' do
|
context 'when role outranks user' do
|
||||||
let(:role_position) { current_role.position + 1 }
|
let(:role_position) { current_role.position + 1 }
|
||||||
|
|
||||||
it 'returns http forbidden' do
|
it 'returns http forbidden and does not update role' do
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not update the role' do
|
|
||||||
expect(role.reload.name).to eq 'Bar'
|
expect(role.reload.name).to eq 'Bar'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -44,11 +44,9 @@ RSpec.describe Admin::Users::RolesController do
|
|||||||
let(:permissions) { UserRole::FLAGS[:manage_roles] }
|
let(:permissions) { UserRole::FLAGS[:manage_roles] }
|
||||||
let(:position) { 1 }
|
let(:position) { 1 }
|
||||||
|
|
||||||
it 'updates user role' do
|
it 'updates user role and redirects' do
|
||||||
expect(user.reload.role_id).to eq selected_role&.id
|
expect(user.reload.role_id).to eq selected_role&.id
|
||||||
end
|
|
||||||
|
|
||||||
it 'redirects back to account page' do
|
|
||||||
expect(response).to redirect_to(admin_account_path(user.account_id))
|
expect(response).to redirect_to(admin_account_path(user.account_id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -57,11 +55,9 @@ RSpec.describe Admin::Users::RolesController do
|
|||||||
let(:permissions) { UserRole::FLAGS[:administrator] }
|
let(:permissions) { UserRole::FLAGS[:administrator] }
|
||||||
let(:position) { 100 }
|
let(:position) { 100 }
|
||||||
|
|
||||||
it 'does not update user role' do
|
it 'does not update user role and renders edit' do
|
||||||
expect(user.reload.role_id).to eq previous_role&.id
|
expect(user.reload.role_id).to eq previous_role&.id
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders edit form' do
|
|
||||||
expect(response).to render_template(:show)
|
expect(response).to render_template(:show)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -71,11 +67,9 @@ RSpec.describe Admin::Users::RolesController do
|
|||||||
let(:permissions) { UserRole::FLAGS[:manage_roles] }
|
let(:permissions) { UserRole::FLAGS[:manage_roles] }
|
||||||
let(:position) { 1 }
|
let(:position) { 1 }
|
||||||
|
|
||||||
it 'does not update user role' do
|
it 'does not update user role and returns http forbidden' do
|
||||||
expect(user.reload.role_id).to eq previous_role&.id
|
expect(user.reload.role_id).to eq previous_role&.id
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http forbidden' do
|
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -60,16 +60,12 @@ RSpec.describe Request do
|
|||||||
expect(a_request(:get, 'http://example.com')).to have_been_made.once
|
expect(a_request(:get, 'http://example.com')).to have_been_made.once
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets headers' do
|
it 'makes a request with expected headers, yields, and closes the underlying connection' do
|
||||||
expect { |block| subject.perform(&block) }.to yield_control
|
|
||||||
expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'closes underlying connection' do
|
|
||||||
allow(subject.send(:http_client)).to receive(:close)
|
allow(subject.send(:http_client)).to receive(:close)
|
||||||
|
|
||||||
expect { |block| subject.perform(&block) }.to yield_control
|
expect { |block| subject.perform(&block) }.to yield_control
|
||||||
|
|
||||||
|
expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
|
||||||
expect(subject.send(:http_client)).to have_received(:close)
|
expect(subject.send(:http_client)).to have_received(:close)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -80,6 +76,29 @@ RSpec.describe Request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a redirect and HTTP signatures' do
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'http://example.com').to_return(status: 301, headers: { Location: 'http://redirected.example.com/foo' })
|
||||||
|
stub_request(:get, 'http://redirected.example.com/foo').to_return(body: 'lorem ipsum')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'makes a request with expected headers and follows redirects' do
|
||||||
|
expect { |block| subject.on_behalf_of(account).perform(&block) }.to yield_control
|
||||||
|
|
||||||
|
# request.headers includes the `Signature` sent for the first request
|
||||||
|
expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made.once
|
||||||
|
|
||||||
|
# request.headers includes the `Signature`, but it has changed
|
||||||
|
expect(a_request(:get, 'http://redirected.example.com/foo').with(headers: subject.headers.merge({ 'Host' => 'redirected.example.com' }))).to_not have_been_made
|
||||||
|
|
||||||
|
# `with(headers: )` matching tests for inclusion, so strip `Signature`
|
||||||
|
# This doesn't actually test that there is a signature, but it tests that the original signature is not passed
|
||||||
|
expect(a_request(:get, 'http://redirected.example.com/foo').with(headers: subject.headers.without('Signature').merge({ 'Host' => 'redirected.example.com' }))).to have_been_made.once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with private host' do
|
context 'with private host' do
|
||||||
around do |example|
|
around do |example|
|
||||||
WebMock.disable!
|
WebMock.disable!
|
||||||
|
@ -59,7 +59,7 @@ RSpec.describe 'Instances' do
|
|||||||
description_limit: MediaAttachment::MAX_DESCRIPTION_LENGTH
|
description_limit: MediaAttachment::MAX_DESCRIPTION_LENGTH
|
||||||
),
|
),
|
||||||
polls: include(
|
polls: include(
|
||||||
max_options: PollValidator::MAX_OPTIONS
|
max_options: PollOptionsValidator::MAX_OPTIONS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe PollValidator do
|
RSpec.describe PollExpirationValidator do
|
||||||
describe '#validate' do
|
describe '#validate' do
|
||||||
before do
|
before do
|
||||||
validator.validate(poll)
|
validator.validate(poll)
|
||||||
@ -14,16 +14,24 @@ RSpec.describe PollValidator do
|
|||||||
let(:options) { %w(foo bar) }
|
let(:options) { %w(foo bar) }
|
||||||
let(:expires_at) { 1.day.from_now }
|
let(:expires_at) { 1.day.from_now }
|
||||||
|
|
||||||
it 'have no errors' do
|
it 'has no errors' do
|
||||||
expect(errors).to_not have_received(:add)
|
expect(errors).to_not have_received(:add)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when expires is just 5 min ago' do
|
context 'when the poll expires in 5 min from now' do
|
||||||
let(:expires_at) { 5.minutes.from_now }
|
let(:expires_at) { 5.minutes.from_now }
|
||||||
|
|
||||||
it 'not calls errors add' do
|
it 'has no errors' do
|
||||||
expect(errors).to_not have_received(:add)
|
expect(errors).to_not have_received(:add)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the poll expires in the past' do
|
||||||
|
let(:expires_at) { 5.minutes.ago }
|
||||||
|
|
||||||
|
it 'has errors' do
|
||||||
|
expect(errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
45
spec/validators/poll_options_validator_spec.rb
Normal file
45
spec/validators/poll_options_validator_spec.rb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe PollOptionsValidator do
|
||||||
|
describe '#validate' do
|
||||||
|
before do
|
||||||
|
validator.validate(poll)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:validator) { described_class.new }
|
||||||
|
let(:poll) { instance_double(Poll, options: options, expires_at: expires_at, errors: errors) }
|
||||||
|
let(:errors) { instance_double(ActiveModel::Errors, add: nil) }
|
||||||
|
let(:options) { %w(foo bar) }
|
||||||
|
let(:expires_at) { 1.day.from_now }
|
||||||
|
|
||||||
|
it 'has no errors' do
|
||||||
|
expect(errors).to_not have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the poll has duplicate options' do
|
||||||
|
let(:options) { %w(foo foo) }
|
||||||
|
|
||||||
|
it 'adds errors' do
|
||||||
|
expect(errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the poll has no options' do
|
||||||
|
let(:options) { [] }
|
||||||
|
|
||||||
|
it 'adds errors' do
|
||||||
|
expect(errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the poll has too many options' do
|
||||||
|
let(:options) { Array.new(described_class::MAX_OPTIONS + 1) { |i| "option #{i}" } }
|
||||||
|
|
||||||
|
it 'adds errors' do
|
||||||
|
expect(errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user