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:
Duke Greene 2023-07-21 07:43:56 -04:00 committed by GitHub
parent 88f8a84c4e
commit 9a6f04bd37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 558 additions and 349 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,4 @@
module Users
module Images
module ProfileImageGenerator
def self.call
Rails.root.join("app/assets/images/#{rand(1..40)}.png").open

View file

@ -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

View file

@ -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) }

View file

@ -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) }

View file

@ -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

View file

@ -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