This commit is contained in:
Eric Schultz 2020-04-10 14:51:34 -05:00
parent 25c7cd7c6a
commit 30b2df23e2
22 changed files with 459 additions and 33 deletions

View file

@ -116,7 +116,7 @@ GEM
debug_inspector (>= 0.0.1)
bootsnap (1.4.4)
msgpack (~> 1.0)
builder (3.2.4)
builder (3.2.3)
bunny (2.14.2)
amq-protocol (~> 2.3, >= 2.3.0)
byebug (11.0.1)
@ -132,7 +132,7 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colorize (0.8.1)
concurrent-ruby (1.1.6)
concurrent-ruby (1.1.5)
config (1.7.2)
activesupport (>= 3.0)
deep_merge (~> 1.2, >= 1.2.1)
@ -143,7 +143,7 @@ GEM
unicode_utils (~> 1.4)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.6)
crass (1.0.4)
css_parser (1.7.0)
addressable
dante (0.2.0)
@ -155,10 +155,10 @@ GEM
deep_merge (1.2.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.7.1)
devise (4.6.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
railties (>= 4.1.0, < 6.0)
responders
warden (~> 1.2.3)
devise-async (1.0.0)
@ -201,7 +201,7 @@ GEM
dry-logic (~> 0.5, >= 0.5.0)
dry-types (~> 0.14.0)
equalizer (0.0.11)
erubi (1.9.0)
erubi (1.8.0)
execjs (2.7.0)
factory_bot (5.0.2)
activesupport (>= 4.2.0)
@ -260,7 +260,7 @@ GEM
httparty (0.17.0)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
i18n (1.8.2)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
i18n-js (3.3.0)
i18n (>= 0.6.6)
@ -276,7 +276,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.5.0)
loofah (2.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -293,7 +293,7 @@ GEM
mini_magick (4.9.5)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
minitest (5.11.3)
msgpack (1.3.1)
multi_json (1.13.1)
multi_xml (0.6.0)
@ -307,7 +307,7 @@ GEM
require_all
netrc (0.11.0)
nio4r (2.4.0)
nokogiri (1.10.9)
nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0)
parallel (1.17.0)
@ -331,11 +331,7 @@ GEM
puma (>= 2.7, < 5)
rabl (0.14.1)
activesupport (>= 2.3.14)
<<<<<<< HEAD
rack (2.0.9)
=======
rack (2.2.2)
>>>>>>> Update to Rack
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (5.4.2)
@ -361,8 +357,8 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails-html-sanitizer (1.2.0)
loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.3)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
@ -398,13 +394,13 @@ GEM
rspec-mocks (~> 3.8.0)
rspec-core (3.8.2)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.6)
rspec-expectations (3.8.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-mocks (3.8.2)
rspec-mocks (3.8.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-rails (3.8.3)
rspec-rails (3.8.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@ -412,7 +408,7 @@ GEM
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.3)
rspec-support (3.8.2)
rubocop (0.72.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
@ -475,7 +471,7 @@ GEM
timecop (0.9.1)
traceroute (0.8.0)
rails (>= 3.0.0)
tzinfo (1.2.7)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uglifier (4.1.20)
execjs (>= 0.3.0, < 3)

View file

@ -0,0 +1,81 @@
class Api::NonprofitsController < ApplicationController
# requires :nonprofit, type: Hash do
# requires :name, type: String, desc: 'Organization Name', allow_blank: false, documentation: { param_type: 'body' }
# requires :zip_code, type: String, allow_blank: false, desc: 'Organization Address ZIP Code', documentation: { param_type: 'body' }
# requires :state_code, type: String, allow_blank: false, desc: 'Organization Address State Code', documentation: { param_type: 'body' }
# requires :city, type: String, allow_blank: false, desc: 'Organization Address City', documentation: { param_type: 'body' }
# end
# requires :user, type: Hash do
# requires :name, type: String, desc: 'Full name', allow_blank: false, documentation: { param_type: 'body' }
# requires :email, type: String, desc: 'Username', allow_blank: false, documentation: { param_type: 'body' }
# requires :password, type: String, desc: 'Password', allow_blank: false, is_equal_to: :password_confirmation, documentation: { param_type: 'body' }
def create
model = CreateModel.new(clean_params)
np = nil
u = nil
raise ActiveRecord::RecordInvalid
Qx.transaction do
raise Errors::MessageInvalid.new(model) unless model.valid?
model.save!
end
# Qx.transaction do
# byebug
# np = ::Nonprofit.new(OnboardAccounts.set_nonprofit_defaults(clean_params[:nonprofit]))
# begin
# np.save!
# rescue ActiveRecord::RecordInvalid => e
# if e.record.errors[:slug]
# begin
# slug = SlugNonprofitNamingAlgorithm.new(np.state_code_slug, np.city_slug).create_copy_name(np.slug)
# np.slug = slug
# np.save!
# rescue UnableToCreateNameCopyError
# raise Grape::Exceptions::ValidationErrors.new(errors: [Grape::Exceptions::Validation.new(
# params: ['nonprofit[name]'],
# message: 'has an invalid slug. Contact support for help.'
# )])
# end
# else
# raise e
# end
# end
# u = User.new(clean_params[:user])
# u.save!
# role = u.roles.build(host: np, name: 'nonprofit_admin')
# role.save!
# billing_plan = BillingPlan.find(Settings.default_bp.id)
# b_sub = np.build_billing_subscription(billing_plan: billing_plan, status: 'active')
# b_sub.save!
# rescue ActiveRecord::RecordInvalid => e
# class_to_name = { Nonprofit => 'nonprofit', User => 'user' }
# if class_to_name[e.record.class]
# errors = e.record.errors.keys.map do |k|
# errors = e.record.errors[k].uniq
# errors.map do |error|
# Grape::Exceptions::Validation.new(
# params: ["#{class_to_name[e.record.class]}[#{k}]"],
# message: error
# )
# end
# end
# raise Grape::Exceptions::ValidationErrors.new(errors: errors.flatten)
# else
# raise e
# end
# end
end
def clean_params
params.permit(nonprofit: [:name, :zip_code, :state_code, :city], user: [:name, :email, :password])
end
end

41
app/models/base.rb Normal file
View file

@ -0,0 +1,41 @@
class Base
include ActiveModel::Model
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
def self.validate_nested_attribute(*attributes)
validates_with NestedAttributesValidator, _merge_attributes(attributes)
end
private
def _merge_attributes(attr_names)
options = attr_names.extract_options!.symbolize_keys
attr_names.flatten!
options[:attributes] = attr_names
options
end
class NestedAttributesValidator < ActiveModel::EachValidator
def initialize(options)
@model_class = options[:model_class]
super
end
def validate_each(record, attribute, value)
inner_validator = @model_class.new(value) unless value.is_a? @model_class
return if inner_validator.valid?
add_nested_errors_for(record, attribute, inner_validator)
end
def add_nested_errors_for(record, attribute, other_validator)
record.errors.messages[attribute] = other_validator.errors.messages
record.errors.details[attribute] = other_validator.errors.details
end
end
end

View file

@ -0,0 +1,48 @@
class CreateModel < Base
attr_accessor :nonprofit, :user
validates_presence_of :user
validates_presence_of :nonprofit
validate_nested_attribute :user, model_class: User
validate_nested_attribute :nonprofit, model_class: Nonprofit
before_validation do
nonprofit = Nonprofit.create(nonprofit) if !nonprofit.is_a? Nonprofit
user = User.create(user) if !nonprofit.is_a? Nonprofit
end
def save
if valid?
if nonprofit.save!
if user.save!
role = user.roles.build(host: nonprofit, name: 'nonprofit_admin')
role.save!
billing_plan = BillingPlan.find(Settings.default_bp.id)
b_sub = nonprofit.build_billing_subscription(billing_plan: billing_plan, status: 'active')
b_sub.save!
end
end
end
# rescue ActiveRecord::RecordInvalid => e
# class_to_name = { Nonprofit => 'nonprofit', User => 'user' }
# if class_to_name[e.record.class]
# errors = e.record.errors.keys.map do |k|
# errors = e.record.errors[k].uniq
# errors.map do |error|
# Grape::Exceptions::Validation.new(
# params: ["#{class_to_name[e.record.class]}[#{k}]"],
# value message: error
# )
# end
# end
# raise Grape::Exceptions::ValidationErrors.new(errors: errors.flatten)
# else
# raise e
# end
# end
end
def save!
raise 'runtime' unless save
end
end

View file

@ -0,0 +1,5 @@
class Errors::ActiveModelError < StandardError
end

View file

@ -0,0 +1,17 @@
class Errors::MessageInvalid < Errors::ActiveModelError
attr_reader :record
def initialize(record=nil)
if record
@record = record
errors = @record.errors.full_messages.join(", ")
message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
else
message = "Record invalid"
end
super(message)
end
end

View file

@ -106,6 +106,10 @@ class Nonprofit < ApplicationRecord
self
end
after_validation(on: :create) do
correct_nonunique_slug
end
# Register (create) a nonprofit with an initial admin
def self.register(user, params)
np = create ConstructNonprofit.construct(user, params)
@ -148,6 +152,16 @@ class Nonprofit < ApplicationRecord
end
self
end
def correct_nonunique_slug
if errors[:slug]
begin
slug = SlugNonprofitNamingAlgorithm.new(self.state_code_slug, self.city_slug).create_copy_name(self.slug)
self.slug = slug
rescue UnableToCreateNameCopyError
end
end
end
def full_address
Format::Address.full_address(address, city, state_code)

View file

@ -14,7 +14,7 @@ require "action_mailer/railtie"
require "action_view/railtie"
# require "action_cable/engine"
# require "sprockets/railtie"
# require "rails/test_unit/railtie"
require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
@ -24,6 +24,7 @@ Bundler.require(*Rails.groups)
module Commitchange
class Application < Rails::Application
config.load_defaults '5.0'
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
@ -32,9 +33,9 @@ module Commitchange
# config.autoload_paths += %W(#{config.root}/extras)
config.eager_load_paths += Dir["#{config.root}/lib/**/", ""]
config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
config.paths.add File.join('app', 'listeners'), glob: File.join('**', '*.rb')
config.eager_load_paths += Dir[Rails.root.join('app', 'api', '*'), Rails.root.join('app', 'listeners', '*')]
# config.eager_load_paths += Dir[Rails.root.join('app', 'api', '*'), Rails.root.join('app', 'listeners', '*')]
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.

View file

@ -2,7 +2,6 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
Rails.application.routes.draw do
mount Houdini::API => '/api'
if Rails.env == 'development'
get '/button_debug/embedded' => 'button_debug#embedded'
@ -12,6 +11,10 @@ Rails.application.routes.draw do
end
get 'onboard' => 'onboard#index'
namespace(:api) do
resources(:nonprofits)
end
resources(:emails, only: [:create])
resources(:settings, only: [:index])
resources(:campaign_gifts, only: [:create])
@ -254,5 +257,7 @@ Rails.application.routes.draw do
get '/static/terms_and_privacy' => 'static#terms_and_privacy'
get '/static/ccs' => 'static#ccs'
root to: 'front#index'
end

View file

@ -3,7 +3,7 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
require 'rails_helper'
describe Houdini::V1::Nonprofit, type: :request do
describe 'HOUDINI NOnprofit_spec', type: :request do
describe 'get' do
end
@ -53,14 +53,14 @@ describe Houdini::V1::Nonprofit, type: :request do
end
it 'rejects csrf' do
post '/api/v1/nonprofit', params: {}, xhr: true
post '/api/nonprofits', params: {}, xhr: true
expect(response.code).to eq '400'
end
end
it 'validates nothing' do
input = {}
post '/api/v1/nonprofit', params: input, xhr: true
post '/api/nonprofits', params: input, xhr: true
expect(response.code).to eq '400'
expect_validation_errors(JSON.parse(response.body), create_errors('nonprofit', 'user'))
end
@ -73,7 +73,7 @@ describe Houdini::V1::Nonprofit, type: :request do
url: ''
}
}
post '/api/v1/nonprofit', params: input, xhr: true
post '/api/nonprofits', params: input, xhr: true
expect(response.code).to eq '400'
expected = create_errors('user')
expected[:errors].push(h(params: ['nonprofit[email]'], messages: gr_e('regexp')))
@ -93,7 +93,7 @@ describe Houdini::V1::Nonprofit, type: :request do
password_confirmation: 'doesn\'t match'
}
}
post '/api/v1/nonprofit', params: input, xhr: true
post '/api/nonprofits', params: input, xhr: true
expect(response.code).to eq '400'
expect(JSON.parse(response.body)['errors']).to include(h(params: ['user[password]', 'user[password_confirmation]'], messages: gr_e('is_equal_to')))
end
@ -107,7 +107,7 @@ describe Houdini::V1::Nonprofit, type: :request do
expect_any_instance_of(SlugNonprofitNamingAlgorithm).to receive(:create_copy_name).and_raise(UnableToCreateNameCopyError.new)
post '/api/v1/nonprofit', params: input, xhr: true
post '/api/nonprofits', params: input, xhr: true
expect(response.code).to eq '400'
expect_validation_errors(JSON.parse(response.body),
@ -127,7 +127,7 @@ describe Houdini::V1::Nonprofit, type: :request do
user: { name: 'Name', email: 'em@em.com', password: '12345678', password_confirmation: '12345678' }
}
post '/api/v1/nonprofit', params: input, xhr: true
post '/api/nonprofits', params: input, xhr: true
expect(response.code).to eq '400'
expect_validation_errors(JSON.parse(response.body),
@ -151,7 +151,7 @@ describe Houdini::V1::Nonprofit, type: :request do
# expect(Houdini::V1::Nonprofit).to receive(:sign_in)
post '/api/v1/nonprofit', params: input, xhr: true
post '/api/nonprofits', params: input, xhr: true
expect(response.code).to eq '201'
our_np = Nonprofit.all[1]

View file

@ -0,0 +1,218 @@
require 'rails_helper'
describe Api::NonprofitsController, type: :request do
it 'do things' do
post '/api/nonprofits', params: {nonprofit: {name: 'hathatoh'}, user: {email: 'thoahtoa'}}
byebug
expect(response.code).to eq 400
end
describe 'get' do
end
describe 'post' do
around(:each) do |example|
@old_bp = Settings.default_bp
example.run
Settings.default_bp = @old_bp
end
def expect_validation_errors(actual, input)
expected_errors = input.with_indifferent_access[:errors]
expect(actual['errors']).to match_array expected_errors
end
def create_errors(*wrapper_params)
output = totally_empty_errors
wrapper_params.each { |i| output[:errors].push(h(params: [i], messages: gr_e('presence'))) }
output
end
def h(h = {})
h.with_indifferent_access
end
let(:totally_empty_errors) do
{
errors:
[
h(params: ['nonprofit[name]'], messages: gr_e('presence', 'blank')),
h(params: ['nonprofit[zip_code]'], messages: gr_e('presence', 'blank')),
h(params: ['nonprofit[state_code]'], messages: gr_e('presence', 'blank')),
h(params: ['nonprofit[city]'], messages: gr_e('presence', 'blank')),
h(params: ['user[name]'], messages: gr_e('presence', 'blank')),
h(params: ['user[email]'], messages: gr_e('presence', 'blank')),
h(params: ['user[password]'], messages: gr_e('presence', 'blank')),
h(params: ['user[password_confirmation]'], messages: gr_e('presence', 'blank'))
]
}.with_indifferent_access
end
describe 'authorization' do
around(:each) do |e|
Rails.configuration.action_controller.allow_forgery_protection = true
e.run
Rails.configuration.action_controller.allow_forgery_protection = false
end
it 'rejects csrf' do
post :create, params: {}, xhr: true
expect(response.code).to eq '400'
end
end
it 'validates nothing' do
input = {}
post :create, params: input, xhr: true
expect(response.code).to eq '400'
expect_validation_errors(JSON.parse(response.body), create_errors('nonprofit', 'user'))
end
it 'validates url, email, phone ' do
input = {
nonprofit: {
email: 'noemeila',
phone: 'notphone',
url: ''
}
}
post :create, params: input, xhr: true
expect(response.code).to eq '400'
expected = create_errors('user')
expected[:errors].push(h(params: ['nonprofit[email]'], messages: gr_e('regexp')))
# expected[:errors].push(h(params:["nonprofit[phone]"], messages: gr_e("regexp")))
# expected[:errors].push(h(params:["nonprofit[url]"], messages: gr_e("regexp")))
expect_validation_errors(JSON.parse(response.body), expected)
end
it 'should reject unmatching passwords ' do
input = {
user: {
email: 'wmeil@email.com',
name: 'name',
password: 'password',
password_confirmation: 'doesn\'t match'
}
}
post :create, params: input, xhr: true
expect(response.code).to eq '400'
expect(JSON.parse(response.body)['errors']).to include(h(params: ['user[password]', 'user[password_confirmation]'], messages: gr_e('is_equal_to')))
end
it 'attempts to make a slug copy and returns the proper errors' do
force_create(:nonprofit, slug: 'n', state_code_slug: 'wi', city_slug: 'appleton')
input = {
nonprofit: { name: 'n', state_code: 'WI', city: 'appleton', zip_code: 54_915 },
user: { name: 'Name', email: 'em@em.com', password: '12345678', password_confirmation: '12345678' }
}
expect_any_instance_of(SlugNonprofitNamingAlgorithm).to receive(:create_copy_name).and_raise(UnableToCreateNameCopyError.new)
post :create, params: input, xhr: true
expect(response.code).to eq '400'
expect_validation_errors(JSON.parse(response.body),
errors: [
h(
params: ['nonprofit[name]'],
messages: ['has an invalid slug. Contact support for help.']
)
])
end
it 'errors on attempt to add user with email that already exists' do
force_create(:user, email: 'em@em.com')
input = {
nonprofit: { name: 'n', state_code: 'WI', city: 'appleton', zip_code: 54_915 },
user: { name: 'Name', email: 'em@em.com', password: '12345678', password_confirmation: '12345678' }
}
expect do
post :create, params: input, xhr: true
end.to raise_error {|error|
expect(error).to be_a Errors::MessageInvalid
}
byebug
expect(response.code).to eq '400'
expect_validation_errors(JSON.parse(response.body),
errors: [
h(
params: ['user[email]'],
messages: ['has already been taken']
)
])
end
it 'succeeds' do
force_create(:nonprofit, slug: 'n', state_code_slug: 'wi', city_slug: 'appleton')
input = {
nonprofit: { name: 'n', state_code: 'WI', city: 'appleton', zip_code: 54_915, url: 'www.cs.c', website: 'www.cs.c' },
user: { name: 'Name', email: 'em@em.com', password: '12345678', password_confirmation: '12345678' }
}
bp = force_create(:billing_plan)
Settings.default_bp.id = bp.id
# expect(Houdini::V1::Nonprofit).to receive(:sign_in)
post :create, params: input, xhr: true
expect(response.code).to eq '201'
our_np = Nonprofit.all[1]
expected_np = {
name: 'n',
state_code: 'WI',
city: 'appleton',
zip_code: '54915',
state_code_slug: 'wi',
city_slug: 'appleton',
slug: 'n-00',
website: 'http://www.cs.c'
}.with_indifferent_access
expected_np = our_np.attributes.with_indifferent_access.merge(expected_np)
expect(our_np.attributes).to eq expected_np
expect(our_np.billing_subscription.billing_plan).to eq bp
response_body = {
id: our_np.id
}.with_indifferent_access
expect(JSON.parse(response.body)).to eq response_body
user = User.first
expected_user = {
email: 'em@em.com',
name: 'Name'
}
expected_user = user.attributes.with_indifferent_access.merge(expected_user)
expect(our_np.roles.nonprofit_admins.count).to eq 1
expect(our_np.roles.nonprofit_admins.first.user.attributes).to eq expected_user
end
end
end
def find_error_message(json, field_name)
errors = json['errors']
error = errors.select { |i| i['params'].any? { |j| j == field_name } }.first
return error unless error
error['messages']
end
def gr_e(*keys)
keys.map { |i| I18n.translate('grape.errors.messages.' + i, locale: 'en') }
end