Add details to error response for POST /api/v1/accounts in REST API (#15803)

This commit is contained in:
Eugen Rochko 2021-03-01 04:59:13 +01:00 committed by GitHub
parent b4cb8c3c83
commit 9aa37b32c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 72 additions and 23 deletions

View file

@ -27,6 +27,8 @@ class Api::V1::AccountsController < Api::BaseController
self.response_body = Oj.dump(response.body) self.response_body = Oj.dump(response.body)
self.status = response.status self.status = response.status
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity
end end
def follow def follow

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
class ValidationErrorFormatter
def initialize(error, aliases = {})
@error = error
@aliases = aliases
end
def as_json
{ error: @error.to_s, details: details }
end
private
def details
h = {}
errors.details.each_pair do |attribute_name, attribute_errors|
messages = errors.messages[attribute_name]
h[@aliases[attribute_name] || attribute_name] = attribute_errors.map.with_index do |error, index|
{ error: 'ERR_' + error[:error].to_s.upcase, description: messages[index] }
end
end
h
end
def errors
@errors ||= @error.record.errors
end
end

View file

@ -2,11 +2,11 @@
class BlacklistedEmailValidator < ActiveModel::Validator class BlacklistedEmailValidator < ActiveModel::Validator
def validate(user) def validate(user)
return if user.valid_invitation? return if user.valid_invitation? || user.email.blank?
@email = user.email @email = user.email
user.errors.add(:email, I18n.t('users.blocked_email_provider')) if blocked_email? user.errors.add(:email, :blocked) if blocked_email?
end end
private private

View file

@ -4,16 +4,19 @@ require 'resolv'
class EmailMxValidator < ActiveModel::Validator class EmailMxValidator < ActiveModel::Validator
def validate(user) def validate(user)
return if user.email.blank?
domain = get_domain(user.email) domain = get_domain(user.email)
if domain.nil? if domain.blank?
user.errors.add(:email, I18n.t('users.invalid_email')) user.errors.add(:email, :invalid)
else else
ips, hostnames = resolve_mx(domain) ips, hostnames = resolve_mx(domain)
if ips.empty? if ips.empty?
user.errors.add(:email, I18n.t('users.invalid_email_mx')) user.errors.add(:email, :unreachable)
elsif on_blacklist?(hostnames + ips) elsif on_blacklist?(hostnames + ips)
user.errors.add(:email, I18n.t('users.blocked_email_provider')) user.errors.add(:email, :blocked)
end end
end end
end end

View file

@ -2,7 +2,7 @@
class NoteLengthValidator < ActiveModel::EachValidator class NoteLengthValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
record.errors.add(attribute, I18n.t('statuses.over_character_limit', max: options[:maximum])) if too_long?(value) record.errors.add(attribute, :too_long, message: I18n.t('statuses.over_character_limit', max: options[:maximum]), count: options[:maximum]) if too_long?(value)
end end
private private

View file

@ -4,7 +4,7 @@
class UniqueUsernameValidator < ActiveModel::Validator class UniqueUsernameValidator < ActiveModel::Validator
def validate(account) def validate(account)
return if account.username.nil? return if account.username.blank?
normalized_username = account.username.downcase normalized_username = account.username.downcase
normalized_domain = account.domain&.downcase normalized_domain = account.domain&.downcase

View file

@ -3,9 +3,10 @@
class UnreservedUsernameValidator < ActiveModel::Validator class UnreservedUsernameValidator < ActiveModel::Validator
def validate(account) def validate(account)
@username = account.username @username = account.username
return if @username.nil?
account.errors.add(:username, I18n.t('accounts.reserved_username')) if reserved_username? return if @username.blank?
account.errors.add(:username, :reserved) if reserved_username?
end end
private private

View file

@ -5,13 +5,28 @@ en:
poll: poll:
expires_at: Deadline expires_at: Deadline
options: Choices options: Choices
user:
agreement: Service agreement
email: E-mail address
locale: Locale
password: Password
user/account:
username: Username
user/invite_request:
text: Reason
errors: errors:
models: models:
account: account:
attributes: attributes:
username: username:
invalid: only letters, numbers and underscores invalid: must contain only letters, numbers and underscores
reserved: is reserved
status: status:
attributes: attributes:
reblog: reblog:
taken: of status already exists taken: of status already exists
user:
attributes:
email:
blocked: uses a disallowed e-mail provider
unreachable: does not seem to exist

View file

@ -80,7 +80,6 @@ en:
other: Toots other: Toots
posts_tab_heading: Toots posts_tab_heading: Toots
posts_with_replies: Toots and replies posts_with_replies: Toots and replies
reserved_username: The username is reserved
roles: roles:
admin: Admin admin: Admin
bot: Bot bot: Bot
@ -1410,11 +1409,8 @@ en:
tips: Tips tips: Tips
title: Welcome aboard, %{name}! title: Welcome aboard, %{name}!
users: users:
blocked_email_provider: This e-mail provider isn't allowed
follow_limit_reached: You cannot follow more than %{limit} people follow_limit_reached: You cannot follow more than %{limit} people
generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance
invalid_email: The e-mail address is invalid
invalid_email_mx: The e-mail address does not seem to exist
invalid_otp_token: Invalid two-factor code invalid_otp_token: Invalid two-factor code
invalid_sign_in_token: Invalid security code invalid_sign_in_token: Invalid security code
otp_lost_help_html: If you lost access to both, you may get in touch with %{email} otp_lost_help_html: If you lost access to both, you may get in touch with %{email}

View file

@ -69,7 +69,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
end end
it 'shows a login error' do it 'shows a login error' do
expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: 'Email') expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: I18n.t('activerecord.attributes.user.email'))
end end
it "doesn't log the user in" do it "doesn't log the user in" do
@ -136,7 +136,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
end end
it 'shows a login error' do it 'shows a login error' do
expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: 'Email') expect(flash[:alert]).to match I18n.t('devise.failure.invalid', authentication_keys: I18n.t('activerecord.attributes.user.email'))
end end
it "doesn't log the user in" do it "doesn't log the user in" do

View file

@ -17,7 +17,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
let(:blocked_email) { true } let(:blocked_email) { true }
it 'calls errors.add' do it 'calls errors.add' do
expect(errors).to have_received(:add).with(:email, I18n.t('users.blocked_email_provider')) expect(errors).to have_received(:add).with(:email, :blocked)
end end
end end
@ -25,7 +25,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
let(:blocked_email) { false } let(:blocked_email) { false }
it 'not calls errors.add' do it 'not calls errors.add' do
expect(errors).not_to have_received(:add).with(:email, I18n.t('users.blocked_email_provider')) expect(errors).not_to have_received(:add).with(:email, :blocked)
end end
end end
end end

View file

@ -13,7 +13,7 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do
let(:account) { double(username: username, errors: errors) } let(:account) { double(username: username, errors: errors) }
let(:errors ) { double(add: nil) } let(:errors ) { double(add: nil) }
context '@username.nil?' do context '@username.blank?' do
let(:username) { nil } let(:username) { nil }
it 'not calls errors.add' do it 'not calls errors.add' do
@ -21,14 +21,14 @@ RSpec.describe UnreservedUsernameValidator, type: :validator do
end end
end end
context '!@username.nil?' do context '!@username.blank?' do
let(:username) { '' } let(:username) { 'f' }
context 'reserved_username?' do context 'reserved_username?' do
let(:reserved_username) { true } let(:reserved_username) { true }
it 'calls erros.add' do it 'calls erros.add' do
expect(errors).to have_received(:add).with(:username, I18n.t('accounts.reserved_username')) expect(errors).to have_received(:add).with(:username, :reserved)
end end
end end