api v1 endpoint for creating an organization (#19778)
* api v1 endpoint for creating an organization * address failing specs, regenerate swagger docs * remove old comment in destroy now that woreker call takes third argument * refactor our services for profile images into images folder
This commit is contained in:
parent
88f8a84c4e
commit
9a6f04bd37
15 changed files with 558 additions and 349 deletions
|
|
@ -25,7 +25,7 @@ module Admin
|
|||
username = "#{email.split('@').first.gsub(/[^0-9a-z ]/i, '')}_#{rand(1000)}"
|
||||
User.invite!(email: email,
|
||||
username: username,
|
||||
profile_image: ::Users::ProfileImageGenerator.call,
|
||||
profile_image: ::Images::ProfileImageGenerator.call,
|
||||
registered: false)
|
||||
flash[:success] = I18n.t("admin.invitations_controller.create_success")
|
||||
redirect_to admin_invitations_path
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ module Api
|
|||
include Api::OrganizationsController
|
||||
before_action :find_organization, only: %i[users listings articles]
|
||||
before_action :authenticate!, only: %i[create update destroy]
|
||||
before_action :authorize_admin, only: %i[create update]
|
||||
before_action :authorize_super_admin, only: %i[destroy]
|
||||
before_action :authorize_admin, only: %i[update]
|
||||
before_action :authorize_super_admin, only: %i[create destroy]
|
||||
after_action :verify_authorized, only: %i[create update destroy]
|
||||
|
||||
INDEX_ATTRIBUTES_FOR_SERIALIZATION = %i[
|
||||
|
|
@ -41,6 +41,26 @@ module Api
|
|||
render json: { error: e }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def create
|
||||
organization = Organization.new params_for_create
|
||||
authorize organization
|
||||
if organization.save!
|
||||
render json: {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
profile_image: organization.profile_image_url,
|
||||
slug: organization.slug,
|
||||
summary: organization.summary,
|
||||
tag_line: organization.tag_line,
|
||||
url: organization.url
|
||||
}, status: :created
|
||||
else
|
||||
render json: { error: organization.errors_as_sentence, status: 422 }, status: :unprocessable_entity
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
render json: { error: e }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def update
|
||||
set_organization
|
||||
@organization.assign_attributes(organization_params)
|
||||
|
|
@ -65,8 +85,6 @@ module Api
|
|||
set_organization
|
||||
Organizations::DeleteWorker.perform_async(@organization.id, @user.id, false)
|
||||
|
||||
# A notification email will trigger once the async deletion is completed.
|
||||
# We do not currently appear to notify on a failed deletion but it is logged internally.
|
||||
render json: { message: "deletion scheduled for organization with ID #{@organization.id}", status: 200 }
|
||||
rescue ArgumentError => e
|
||||
render json: { error: e }, status: :unprocessable_entity
|
||||
|
|
@ -82,6 +100,19 @@ module Api
|
|||
params.require(:organization).permit(:id, :name, :profile_image, :slug, :summary, :tag_line, :url)
|
||||
end
|
||||
|
||||
# Accepts either an image file or a remote url for an image in the :profile_image attribute
|
||||
def params_for_create
|
||||
image = params.dig(:organization, :profile_image)
|
||||
|
||||
# If the user has given a url for the profile image, place it where it should be handled
|
||||
if image.is_a? String
|
||||
permitted_params = organization_params.to_h
|
||||
permitted_params.delete(:profile_image)
|
||||
permitted_params[:remote_profile_image_url] = Images::SafeRemoteProfileImageUrl.call(image)
|
||||
end
|
||||
permitted_params
|
||||
end
|
||||
|
||||
def set_organization
|
||||
@organization = Organization.find_by(id: params[:id])
|
||||
not_found unless @organization
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
build_resource(sign_up_params)
|
||||
resource.registered_at = Time.current
|
||||
resource.build_setting(editor_version: "v2")
|
||||
resource.profile_image = Users::ProfileImageGenerator.call if resource.profile_image.blank?
|
||||
resource.profile_image = Images::ProfileImageGenerator.call if resource.profile_image.blank?
|
||||
if Settings::General.waiting_on_first_user
|
||||
resource.password_confirmation = resource.password
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ module Authentication
|
|||
user_data[:profile_image] = if Rails.env.test?
|
||||
Settings::General.mascot_image_url
|
||||
else
|
||||
Users::ProfileImageGenerator.call
|
||||
Images::ProfileImageGenerator.call
|
||||
end
|
||||
|
||||
user_data
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module Authentication
|
|||
{
|
||||
name: @info.name,
|
||||
email: @info.email || "",
|
||||
remote_profile_image_url: Users::SafeRemoteProfileImageUrl.call(image_url),
|
||||
remote_profile_image_url: Images::SafeRemoteProfileImageUrl.call(image_url),
|
||||
facebook_username: user_nickname
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module Authentication
|
|||
email: info.email.to_s,
|
||||
github_username: info.nickname,
|
||||
name: name,
|
||||
remote_profile_image_url: Users::SafeRemoteProfileImageUrl.call(info.image.to_s)
|
||||
remote_profile_image_url: Images::SafeRemoteProfileImageUrl.call(info.image.to_s)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ module Authentication
|
|||
{
|
||||
name: info.name,
|
||||
email: info.email || "",
|
||||
remote_profile_image_url: Users::SafeRemoteProfileImageUrl.call(@info.image),
|
||||
remote_profile_image_url: Images::SafeRemoteProfileImageUrl.call(@info.image),
|
||||
google_oauth2_username: user_nickname
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module Authentication
|
|||
{
|
||||
email: info.email.to_s,
|
||||
name: name,
|
||||
remote_profile_image_url: Users::SafeRemoteProfileImageUrl.call(remote_profile_image_url),
|
||||
remote_profile_image_url: Images::SafeRemoteProfileImageUrl.call(remote_profile_image_url),
|
||||
twitter_username: info.nickname
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
module Users
|
||||
module Images
|
||||
module ProfileImageGenerator
|
||||
def self.call
|
||||
Rails.root.join("app/assets/images/#{rand(1..40)}.png").open
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
module Users
|
||||
module Images
|
||||
module SafeRemoteProfileImageUrl
|
||||
# Basic check for nil and blank URLs, alongside likely incomplete URLs, such as just "image.jpg".
|
||||
def self.call(url)
|
||||
|
|
@ -6,7 +6,7 @@ module Users
|
|||
url.sub!("http://", "https://")
|
||||
url
|
||||
else
|
||||
Users::ProfileImageGenerator.call
|
||||
Images::ProfileImageGenerator.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -159,6 +159,84 @@ It supports pagination, each page will contain `30` users by default."
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST /api/organizations" do
|
||||
let(:api_secret) { create(:api_secret, user: create(:user, :super_admin)) }
|
||||
let(:image_url) { "https://dummyimage.com/800x600.jpg" }
|
||||
let(:org_params) do
|
||||
{
|
||||
name: "New Test Org",
|
||||
summary: "a newly created test org",
|
||||
url: "https://testorg.io",
|
||||
profile_image: image_url,
|
||||
slug: "org10001",
|
||||
tag_line: "a test org's tagline"
|
||||
}
|
||||
end
|
||||
|
||||
path "/api/organizations" do
|
||||
post "Create an Organization" do
|
||||
tags "organizations"
|
||||
description "This endpoint allows the client to create an organization with the provided parameters.
|
||||
It requires a token from a user with `admin` privileges."
|
||||
operationId "createOrganization"
|
||||
produces "application/json"
|
||||
consumes "application/json"
|
||||
parameter name: :organization,
|
||||
in: :body,
|
||||
description: "Representation of Organization to be created",
|
||||
schema: { "$ref": "#/components/schemas/Organization" }
|
||||
|
||||
response "201", "Successful" do
|
||||
before do
|
||||
# mocking out the process where carrierwave takes the image at the url and uploads
|
||||
stub_request(:get, "https://dummyimage.com").to_return(body: "", status: 200)
|
||||
organization = Organization.new(org_params)
|
||||
allow(Organization).to receive(:new).and_return(organization)
|
||||
profile_image = Images::ProfileImageGenerator.call
|
||||
allow(organization).to receive(:profile_image).and_return(profile_image)
|
||||
# presence of image file allows save, mocking the upload in specs means file won't respond to :url
|
||||
allow(organization).to receive(:profile_image_url).and_return(
|
||||
"uploads/organization/profile_image/1/400x400.jpg",
|
||||
)
|
||||
end
|
||||
|
||||
let(:"api-key") { api_secret.secret }
|
||||
let(:organization) do
|
||||
{
|
||||
organization: {
|
||||
name: "New Test Org",
|
||||
summary: "a newly created test org",
|
||||
url: "https://testorg.io",
|
||||
profile_image: image_url,
|
||||
slug: "org10001",
|
||||
tag_line: "a test org's tagline"
|
||||
}
|
||||
}
|
||||
end
|
||||
add_examples
|
||||
run_test!
|
||||
end
|
||||
|
||||
response "401", "Unauthorized" do
|
||||
# Only site admins can create organizations through the v1 api
|
||||
let(:reg_admin_user) { create(:user, :admin) }
|
||||
let(:api_secret) { create(:api_secret, user: reg_admin_user) }
|
||||
let(:"api-key") { api_secret.secret }
|
||||
let(:organization) { {} }
|
||||
examples
|
||||
run_test!
|
||||
end
|
||||
|
||||
response "422", "Unprocessable Entity" do
|
||||
let(:"api-key") { api_secret.secret }
|
||||
let(:organization) { {} }
|
||||
add_examples
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/organizations/{id}" do
|
||||
let!(:org_admin) { create(:user, :org_admin, :admin) }
|
||||
let(:api_secret) { create(:api_secret, user: org_admin) }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ RSpec.describe "Api::V1::Organizations" do
|
|||
name: "Test Org",
|
||||
summary: "a test org",
|
||||
url: "https://testorg.io",
|
||||
profile_image: "https://remote.profileimage.jpg",
|
||||
profile_image: "https://dummyimage.com/400x400.png",
|
||||
slug: "test-org",
|
||||
tag_line: "a tagline"
|
||||
}
|
||||
|
|
@ -53,6 +53,58 @@ RSpec.describe "Api::V1::Organizations" do
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST /api/organizations" do
|
||||
let!(:user) { create(:user, :super_admin) }
|
||||
let(:api_secret) { create(:api_secret, user: user) }
|
||||
let(:admin_headers) { headers.merge({ "api-key" => api_secret.secret }) }
|
||||
|
||||
context "when user has site admin privileges and params are valid" do
|
||||
before do
|
||||
# mock around the remote image url retreival and upload
|
||||
profile_image = Images::ProfileImageGenerator.call
|
||||
organization = Organization.new(org_params.merge(profile_image: profile_image))
|
||||
# Organizations#create controller takes the image param as "profile_image".. if it's not a file,
|
||||
# the controller sets the remote_profile_image_url instead
|
||||
profile_image_url = org_params[:profile_image]
|
||||
allow(Organization).to receive(:new).and_return(organization)
|
||||
allow(organization).to receive(:profile_image).and_return(profile_image)
|
||||
allow(organization).to receive(:profile_image_url).and_return(profile_image_url)
|
||||
end
|
||||
|
||||
it "accepts request and creates the organization" do
|
||||
expect do
|
||||
post api_organizations_path, params: { organization: org_params }, headers: admin_headers
|
||||
end.to change(Organization, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user does not have site-admin-level privileges" do
|
||||
let!(:user) { create(:user, :admin) }
|
||||
let(:api_secret) { create(:api_secret, user: user) }
|
||||
let(:admin_headers) { headers.merge({ "api-key" => api_secret.secret }) }
|
||||
|
||||
it "returns a 401 and does not create the organization" do
|
||||
expect do
|
||||
post api_organizations_path, params: { organization: org_params }, headers: admin_headers
|
||||
end.not_to change(Organization, :count)
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context "when params are invalid" do
|
||||
it "returns a 422 and does not create the organization" do
|
||||
expect do
|
||||
post api_organizations_path, params: {}, headers: admin_headers
|
||||
end.not_to change(Organization, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/organizations/:id" do
|
||||
let!(:org_admin) { create(:user, :org_admin, :admin) }
|
||||
let(:api_secret) { create(:api_secret, user: org_admin) }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Users::ProfileImageGenerator, type: :service do
|
||||
RSpec.describe Images::ProfileImageGenerator, type: :service do
|
||||
it "returns an image file" do
|
||||
expect(described_class.call).to be_a(File)
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Users::SafeRemoteProfileImageUrl, type: :service do
|
||||
RSpec.describe Images::SafeRemoteProfileImageUrl, type: :service do
|
||||
it "returns the url if passed for proper URLs" do
|
||||
url = "https://image.com/image.png"
|
||||
expect(described_class.call(url)).to eq(url)
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue