Listings creation and update API (#4014)

* Listings creation and update API

  - new listings controller
  - refactored code to share core functionnality between api and web controllers
  - code in common in classified listing concerns
  - added tests for index/create/update
  - json output as views
  - updated a factory

* Listings creation and update API: code update

  - refactored json calls
  - added test cases
  - added pagination for list of listings
  - refactored jbuilder files
  - typos

* Listings creation and update API: code update

  - tags and tag_list in controller
  - changed view to display both
  - refactored and added test

* Listings creation and update API: code update

  - small change to resolve conflit

* Listings creation and update API: code update

  - added test case
  - deleted unneeded test case
This commit is contained in:
cyrillefr 2019-09-24 16:03:52 +02:00 committed by Ben Halpern
parent f172856ec1
commit 70631ad2f7
16 changed files with 796 additions and 144 deletions

View file

@ -0,0 +1,76 @@
module Api
module V0
class ClassifiedListingsController < ApiController
include ClassifiedListingsToolkit
respond_to :json
before_action :set_classified_listing, only: %i[show update]
before_action :authenticate!, only: %i[create update]
# skip CSRF checks for create and update
skip_before_action :verify_authenticity_token, only: %i[create update]
def index
@classified_listings = ClassifiedListing.published.
order("bumped_at DESC").
includes(:user, :organization, :taggings)
@classified_listings = @classified_listings.where(category: params[:category]) if params[:category].present?
per_page = (params[:per_page] || 30).to_i
num = [per_page, 100].min
page = params[:page] || 1
@classified_listings = @classified_listings.page(page).per(num)
set_surrogate_key_header "classified-listings-#{params[:category]}-#{page}-#{num}"
end
def show
# rendering with json builder
end
def create
super
end
def update
super
end
private
attr_accessor :user
alias current_user user
def process_no_credit_left
msg = "Not enough available credits"
render json: { error: msg, status: 402 }, status: :payment_required
end
def process_successful_draft
render "show", status: :created
end
def process_unsuccessful_draft
render json: { errors: @classified_listing.errors }, status: :unprocessable_entity
end
def process_successful_creation
render "show", status: :created
end
def process_unsuccessful_creation
render json: { errors: @classified_listing.errors }, status: :unprocessable_entity
end
def process_after_update
render "show", status: :ok
end
def process_after_unpublish
render "show", status: :ok
end
end
end
end

View file

@ -31,75 +31,20 @@ class ClassifiedListingsController < ApplicationController
@credits = current_user.credits.unspent
end
def create
super
end
def update
super
end
def edit
authorize @classified_listing
@organizations = current_user.organizations
@credits = current_user.credits.unspent
end
def create
@classified_listing = ClassifiedListing.new(listing_params)
# this will 500 for now if they don't belong in the org
authorize @classified_listing, :authorized_organization_poster? if @classified_listing.organization_id.present?
@classified_listing.user_id = current_user.id
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
org = Organization.find_by(id: @classified_listing.organization_id)
if listing_params[:action] == "draft"
@classified_listing.published = false
if @classified_listing.save
redirect_to "/listings/dashboard"
else
@credits = current_user.credits.unspent
@classified_listing.cached_tag_list = listing_params[:tag_list]
@organizations = current_user.organizations
render :new
end
return
end
available_org_credits = org.credits.unspent if org
available_user_credits = current_user.credits.unspent
# we use the org's credits if available, otherwise we default to the user's
if org && available_org_credits.size >= cost
create_listing(org, cost)
elsif available_user_credits.size >= cost
create_listing(current_user, cost)
else
redirect_to credits_path, notice: "Not enough available credits"
end
end
def update
authorize @classified_listing
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
# NOTE: this should probably be split in three different actions: bump, unpublish, publish
return bump_listing(cost) if listing_params[:action] == "bump"
if listing_params[:action] == "unpublish"
unpublish_listing
redirect_to "/listings/dashboard"
return
elsif listing_params[:action] == "publish"
unless @classified_listing.bumped_at?
first_publish(cost)
return
end
publish_listing
elsif listing_params[:body_markdown].present? && ((@classified_listing.bumped_at && @classified_listing.bumped_at > 24.hours.ago) || !@classified_listing.published)
update_listing_details
end
clear_listings_cache
redirect_to "/listings"
end
def dashboard
@classified_listings = current_user.classified_listings.
includes(:organization, :taggings)
@ -125,90 +70,31 @@ class ClassifiedListingsController < ApplicationController
private
def create_listing(purchaser, cost)
successful_transaction = false
ActiveRecord::Base.transaction do
# subtract credits
Credits::Buyer.call(
purchaser: purchaser,
purchase: @classified_listing,
cost: cost,
)
# save the listing
@classified_listing.bumped_at = Time.current
@classified_listing.published = true
# since we can't raise active record errors in this transaction
# due to the fact that we need to display them in the :new view,
# we manually rollback the transaction if there are validation errors
raise ActiveRecord::Rollback unless @classified_listing.save
successful_transaction = true
end
if successful_transaction
clear_listings_cache
@classified_listing.index!
redirect_to classified_listings_path
else
@credits = current_user.credits.unspent
@classified_listing.cached_tag_list = listing_params[:tag_list]
@organizations = current_user.organizations
render :new
end
def process_no_credit_left
redirect_to credits_path, notice: "Not enough available credits"
end
def first_publish(cost)
available_author_credits = @classified_listing.author.credits.unspent
available_user_credits = []
if @classified_listing.author.is_a?(Organization)
available_user_credits = current_user.credits.unspent
end
if available_author_credits.size >= cost
create_listing(@classified_listing.author, cost)
elsif available_user_credits.size >= cost
create_listing(current_user, cost)
else
redirect_to credits_path, notice: "Not enough available credits"
end
def process_successful_draft
redirect_to "/listings/dashboard"
end
def bump_listing(cost)
org = Organization.find_by(id: @classified_listing.organization_id)
available_org_credits = org.credits.unspent if org
available_user_credits = current_user.credits.unspent
if org && available_org_credits.size >= cost
charge_credits_before_bump(org, cost)
elsif available_user_credits.size >= cost
charge_credits_before_bump(current_user, cost)
else
redirect_to(credits_path, notice: "Not enough available credits") && return
end
def process_unsuccessful_draft
render :new
end
def charge_credits_before_bump(purchaser, cost)
ActiveRecord::Base.transaction do
Credits::Buyer.call(
purchaser: purchaser,
purchase: @classified_listing,
cost: cost,
)
raise ActiveRecord::Rollback unless bump_listing_success
end
def process_successful_creation
redirect_to classified_listings_path
end
def set_classified_listing
@classified_listing = ClassifiedListing.find(params[:id])
def process_unsuccessful_creation
render :new
end
# Never trust parameters from the scary internet, only allow a specific list through.
def listing_params
accessible = %i[title body_markdown category tag_list expires_at contact_via_connect location organization_id action]
params.require(:classified_listing).permit(accessible)
def process_after_update
redirect_to "/listings"
end
def process_after_unpublish
redirect_to "/listings/dashboard"
end
end

View file

@ -1,6 +1,8 @@
module ClassifiedListingsToolkit
extend ActiveSupport::Concern
MANDATORY_FIELDS_FOR_UPDATE = %i[body_markdown title tag_list].freeze
def unpublish_listing
@classified_listing.published = false
@classified_listing.save
@ -34,4 +36,174 @@ module ClassifiedListingsToolkit
def clear_listings_cache
ClassifiedListings::BustCacheJob.perform_now(@classified_listing.id)
end
def set_classified_listing
@classified_listing = ClassifiedListing.find(params[:id])
end
def create
@classified_listing = ClassifiedListing.new(listing_params)
# this will 500 for now if they don't belong in the org
authorize @classified_listing, :authorized_organization_poster? if @classified_listing.organization_id.present?
@classified_listing.user_id = current_user.id
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
org = Organization.find_by(id: @classified_listing.organization_id)
if listing_params[:action] == "draft"
create_draft
return
end
available_org_credits = org.credits.unspent if org
available_user_credits = current_user.credits.unspent
# we use the org's credits if available, otherwise we default to the user's
if org && available_org_credits.size >= cost
create_listing(org, cost)
elsif available_user_credits.size >= cost
create_listing(current_user, cost)
else
process_no_credit_left
end
end
# Never trust parameters from the scary internet, only allow a specific list through.
def listing_params
if params["classified_listing"]["tags"].present?
params["classified_listing"]["tags"] = params["classified_listing"]["tags"].join(", ")
params["classified_listing"]["tag_list"] = params["classified_listing"].delete "tags"
end
accessible = %i[title body_markdown category tag_list expires_at contact_via_connect location organization_id action]
params.require(:classified_listing).permit(accessible)
end
def create_draft
@classified_listing.published = false
if @classified_listing.save
process_successful_draft
else
@credits = current_user.credits.unspent
@classified_listing.cached_tag_list = listing_params[:tag_list]
@organizations = current_user.organizations
process_unsuccessful_draft
end
end
def create_listing(purchaser, cost)
successful_transaction = false
ActiveRecord::Base.transaction do
# subtract credits
Credits::Buyer.call(
purchaser: purchaser,
purchase: @classified_listing,
cost: cost,
)
# save the listing
@classified_listing.bumped_at = Time.current
@classified_listing.published = true
# since we can't raise active record errors in this transaction
# due to the fact that we need to display them in the :new view,
# we manually rollback the transaction if there are validation errors
raise ActiveRecord::Rollback unless @classified_listing.save
successful_transaction = true
end
if successful_transaction
clear_listings_cache
@classified_listing.index!
process_successful_creation
else
@credits = current_user.credits.unspent
@classified_listing.cached_tag_list = listing_params[:tag_list]
@organizations = current_user.organizations
process_unsuccessful_creation
end
end
def update
authorize @classified_listing
cost = ClassifiedListing.cost_by_category(@classified_listing.category)
# NOTE: this should probably be split in three different actions: bump, unpublish, publish
return bump_listing(cost) if listing_params[:action] == "bump"
if listing_params[:action] == "unpublish"
unpublish_listing
process_after_unpublish
return
elsif listing_params[:action] == "publish"
unless @classified_listing.bumped_at?
first_publish(cost)
return
end
publish_listing
elsif listing_updatable?
update_listing_details
end
clear_listings_cache
process_after_update
end
def bump_listing(cost)
org = Organization.find_by(id: @classified_listing.organization_id)
available_org_credits = org.credits.unspent if org
available_user_credits = current_user.credits.unspent
if org && available_org_credits.size >= cost
charge_credits_before_bump(org, cost)
elsif available_user_credits.size >= cost
charge_credits_before_bump(current_user, cost)
else
process_no_credit_left && return
end
end
def charge_credits_before_bump(purchaser, cost)
ActiveRecord::Base.transaction do
Credits::Buyer.call(
purchaser: purchaser,
purchase: @classified_listing,
cost: cost,
)
raise ActiveRecord::Rollback unless bump_listing_success
end
end
def first_publish(cost)
available_author_credits = @classified_listing.author.credits.unspent
available_user_credits = []
if @classified_listing.author.is_a?(Organization)
available_user_credits = current_user.credits.unspent
end
if available_author_credits.size >= cost
create_listing(@classified_listing.author, cost)
elsif available_user_credits.size >= cost
create_listing(current_user, cost)
else
process_no_credit_left
end
end
def listing_updatable?
at_least_one_param_present? && (bumped_in_last_24_hrs? || !@classified_listing.published)
end
def at_least_one_param_present?
MANDATORY_FIELDS_FOR_UPDATE.any? { |i| listing_params.include? i }
end
def bumped_in_last_24_hrs?
@classified_listing.bumped_at && @classified_listing.bumped_at > 24.hours.ago
end
end

View file

@ -40,6 +40,8 @@ class ClassifiedListing < ApplicationRecord
searchableAttributes %w[title processed_html tag_list slug location]
end
scope :published, -> { where(published: true) }
def self.cost_by_category(category = "education")
categories_available[category][:cost]
end

View file

@ -14,10 +14,10 @@ json.array! @articles do |article|
json.positive_reactions_count article.positive_reactions_count
json.published_timestamp article.published_timestamp
json.partial! "user", user: article.user
json.partial! "api/v0/shared/user", user: article.user
if article.organization
json.partial! "organization", organization: article.organization
json.partial! "api/v0/shared/organization", organization: article.organization
end
flare_tag = FlareTag.new(article).tag

View file

@ -16,10 +16,10 @@ json.array! @articles do |article|
json.published_timestamp article.published_timestamp
json.body_markdown article.body_markdown
json.partial! "user", user: article.user
json.partial! "api/v0/shared/user", user: article.user
if article.organization
json.partial! "organization", organization: article.organization
json.partial! "api/v0/shared/organization", organization: article.organization
end
flare_tag = FlareTag.new(article).tag

View file

@ -23,4 +23,4 @@ json.last_comment_at @article.last_comment_at&.utc&.iso8601
json.body_html @article.processed_html
json.body_markdown @article.body_markdown
json.partial! "user", user: @article.user
json.partial! "api/v0/shared/user", user: @article.user

View file

@ -0,0 +1,11 @@
json.type_of "classified_listing"
json.id listing.id
json.title listing.title
json.slug listing.slug
json.body_markdown listing.body_markdown
json.tag_list listing.cached_tag_list
json.tags listing.tag_list
json.category listing.category
json.processed_html listing.processed_html
json.published listing.published
json.slug listing.slug

View file

@ -0,0 +1,8 @@
json.array! @classified_listings do |listing|
json.partial! "listing", listing: listing
json.partial! "api/v0/shared/user", user: listing.user
if listing.organization
json.partial! "api/v0/shared/organization", organization: listing.organization
end
end

View file

@ -0,0 +1,7 @@
json.partial! "listing", listing: @classified_listing
json.partial! "api/v0/shared/user", user: @classified_listing.user
if @classified_listing.organization
json.partial! "api/v0/shared/organization", organization: @classified_listing.organization
end

View file

@ -4,4 +4,4 @@ json.call(@webhook, :id, :source, :target_url, :events)
json.created_at @webhook.created_at.rfc3339
json.partial! "api/v0/articles/user", user: @webhook.user
json.partial! "api/v0/shared/user", user: @webhook.user

View file

@ -110,6 +110,8 @@ Rails.application.routes.draw do
end
resources :webhooks, only: %i[index create show destroy]
resources :classified_listings, path: :listings, only: %i[index show create update]
get "/listings/category/:category", to: "classified_listings#index", as: :classified_listings_category
get "/analytics/totals", to: "analytics#totals"
get "/analytics/historical", to: "analytics#historical"
get "/analytics/past_day", to: "analytics#past_day"

View file

@ -4,4 +4,11 @@ FactoryBot.define do
description { Faker::Lorem.sentence }
secret { SecureRandom.base58(24) }
end
trait :org_admin do
after(:create) do |api_secret|
org = create(:organization)
create(:organization_membership, user_id: api_secret.user.id, organization_id: org.id, type_of_user: "admin")
end
end
end

View file

@ -0,0 +1,481 @@
require "rails_helper"
RSpec.describe "Api::V0::Listings" do
shared_context "when user is authorized" do
let(:api_secret) { create(:api_secret) }
let(:user) { api_secret.user }
let(:api_secret_org) { create(:api_secret, :org_admin) }
end
shared_context "when param list is valid" do
let(:listing_params) do
{
title: "Title",
body_markdown: "Markdown text",
category: "cfp",
tags: [],
contact_via_connect: true
}
end
let(:draft_params) do
{
title: "Title draft",
body_markdown: "Markdown draft text",
category: "cfp",
tags: [],
contact_via_connect: true,
action: "draft"
}
end
end
shared_context "when user has enough credit" do
before do
create_list(:credit, 25, user: user)
end
end
shared_context "with 7 listings and 2 user" do
let(:user1) { create(:user) }
let(:user2) { create(:user) }
before do
create_list(:classified_listing, 3, user: user1, category: "cfp")
create_list(:classified_listing, 4, user: user2)
end
end
def json_response
JSON.parse(response.body)
end
describe "GET /api/listings" do
include_context "with 7 listings and 2 user"
it "returns json response and ok status" do
get api_classified_listings_path
expect(response.content_type).to eq("application/json")
expect(response).to have_http_status(:ok)
end
it "returns listings created" do
get api_classified_listings_path
expect(json_response.size).to eq(7)
expect(json_response.first["type_of"]).to eq("classified_listing")
expect(json_response.first["slug"]).to eq(ClassifiedListing.last.slug)
expect(json_response.first["user"]).to include("username")
expect(json_response.first["user"]["username"]).not_to be_empty
end
it "supports pagination" do
get api_classified_listings_path, params: { page: 2, per_page: 2 }
expect(json_response.length).to eq(2)
get api_classified_listings_path, params: { page: 4, per_page: 2 }
expect(json_response.length).to eq(1)
end
end
describe "GET /api/listings/category/:category" do
include_context "with 7 listings and 2 user"
it "displays only listings from the cfp category" do
get api_classified_listings_category_path("cfp")
expect(response).to have_http_status(:ok)
expect(json_response.size).to eq(3)
end
end
describe "GET /api/listings/:id" do
include_context "with 7 listings and 2 user"
let(:listing) { ClassifiedListing.where(category: "cfp").last }
it "displays a unique listing" do
get api_classified_listing_path(listing.id)
expect(response).to have_http_status(:ok)
expect(json_response["type_of"]).to eq("classified_listing")
expect(json_response["slug"]).to eq(listing.slug)
expect(json_response["user"]).to include("username")
expect(json_response["user"]["username"]).not_to be_empty
end
end
describe "POST /api/listings" do
def post_classified_listing(key: api_secret.secret, **params)
headers = { "api-key" => key, "content-type" => "application/json" }
post api_classified_listings_path, params: { classified_listing: params }.to_json, headers: headers
end
def user_admin_organization(user)
org = create(:organization)
create(:organization_membership, user_id: user.id, organization_id: org.id, type_of_user: "admin")
org
end
describe "user cannot proceed if not properly unauthorizedœ" do
let(:api_secret) { create(:api_secret) }
it "fails with no api key" do
post api_classified_listings_path, headers: { "content-type" => "application/json" }
expect(response). to have_http_status(:unauthorized)
end
it "fails with the wrong api key" do
post api_classified_listings_path, headers: { "api-key" => "foobar", "content-type" => "application/json" }
expect(response). to have_http_status(:unauthorized)
end
end
describe "user must have enough credit to create a classified listing" do
include_context "when user is authorized"
it "fails to create a classified listing if user does not have enough credit" do
post_classified_listing(category: "cfp")
expect(response).to have_http_status(:payment_required)
end
it "fails to create a classifiedlisting if the org does not have enough credit" do
org = user_admin_organization(user)
post_classified_listing(category: "cfp", organization_id: org.id)
expect(response).to have_http_status(:payment_required)
end
end
describe "user cannot create a classified with a request lacking mandatory parameters" do
let(:invalid_params) { { title: "Title", category: "cfp" } }
include_context "when user is authorized"
include_context "when user has enough credit"
it "fails if no params are given" do
post_classified_listing
expect(response).to have_http_status(:unprocessable_entity)
end
it "fails if a mandatory param is missing" do
post_classified_listing(invalid_params)
expect(response).to have_http_status(:unprocessable_entity)
end
it "does not subtract credits or create a listing if the listing is not valid" do
expect do
post_classified_listing(invalid_params)
end.to change(ClassifiedListing, :count).by(0).and change(user.credits.spent, :size).by(0)
end
end
describe "user creates classified listings" do
include_context "when user is authorized"
include_context "when param list is valid"
include_context "when user has enough credit"
it "supports oauth's access_token" do
access_token = create(:doorkeeper_access_token, resource_owner_id: user.id)
headers = { "authorization" => "Bearer #{access_token.token}", "content-type" => "application/json" }
post api_classified_listings_path, params: { classified_listing: listing_params }.to_json, headers: headers
expect(response).to have_http_status(:created)
end
it "properly deducts the amount of credits" do
post_classified_listing(listing_params)
listing_cost = ClassifiedListing.categories_available[:cfp][:cost]
expect(user.credits.spent.size).to eq(listing_cost)
end
it "creates a listing draft under the org" do
org_admin = api_secret_org.user
org_id = org_admin.organizations.first.id
Credit.create(organization_id: org_id)
post_classified_listing(key: api_secret_org.secret, **draft_params.merge(organization_id: org_id))
expect(ClassifiedListing.first.organization_id).to eq org_id
end
it "creates a listing under the org" do
org = user_admin_organization(user)
Credit.create(organization_id: org.id)
post_classified_listing(listing_params.merge(organization_id: org.id))
expect(ClassifiedListing.first.organization_id).to eq org.id
end
it "does not create a listing draft for an org not belonging to the user" do
org = create(:organization)
expect { post_classified_listing(draft_params.merge(organization_id: org.id)) }.to raise_error(Pundit::NotAuthorizedError)
end
it "does not create a listing for an org not belonging to the user" do
org = create(:organization)
expect { post_classified_listing(listing_params.merge(organization_id: org.id)) }.to raise_error(Pundit::NotAuthorizedError)
end
it "assigns the spent credits to the listing" do
post_classified_listing(listing_params)
spent_credit = user.credits.spent.last
expect(spent_credit.purchase_type).to eq("ClassifiedListing")
expect(spent_credit.spent_at).not_to be_nil
end
it "cannot create a draft due to internal error" do
listing = create(:classified_listing, user_id: user.id)
allow(ClassifiedListing).to receive_messages(cost_by_category: nil, new: listing)
allow(Organization).to receive(:find_by)
allow(listing).to receive(:save) do
listing.errors.add(:base)
false
end
post_classified_listing(draft_params)
expect(json_response["errors"]["base"]).to eq(["is invalid"])
expect(response).to have_http_status(:unprocessable_entity)
end
it "creates listing draft and does not subtract credits" do
allow(Credits::Buyer).to receive(:call).and_raise(ActiveRecord::Rollback)
expect do
post_classified_listing(draft_params)
end.to change(ClassifiedListing, :count).by(1).
and change(user.credits.spent, :size).by(0)
end
it "does not create a listing or subtract credits if the purchase does not go through" do
allow(Credits::Buyer).to receive(:call).and_raise(ActiveRecord::Rollback)
expect do
post_classified_listing(listing_params)
end.to change(ClassifiedListing, :count).by(0).
and change(user.credits.spent, :size).by(0)
end
it "creates a classified listing belonging to the user" do
expect do
post_classified_listing(listing_params)
expect(response).to have_http_status(:created)
end.to change(ClassifiedListing, :count).by(1)
expect(ClassifiedListing.find(json_response["id"]).user).to eq(user)
end
it "creates a classified listing with a title, a body markdown, a category" do
expect do
post_classified_listing(listing_params)
expect(response).to have_http_status(:created)
end.to change(ClassifiedListing, :count).by(1)
expect(ClassifiedListing.find(json_response["id"]).title).to eq("Title")
expect(ClassifiedListing.find(json_response["id"]).body_markdown).to eq("Markdown text")
expect(ClassifiedListing.find(json_response["id"]).category).to eq("cfp")
end
it "creates a classified listing with a location" do
params = listing_params.merge(location: "Frejus")
expect do
post_classified_listing(params)
expect(response).to have_http_status(:created)
end.to change(ClassifiedListing, :count).by(1)
expect(ClassifiedListing.find(json_response["id"]).location).to eq("Frejus")
end
it "creates a classified listing with a list of tags and a contact" do
params = listing_params.merge(tags: %w[discuss javascript], contact_via_connect: true)
expect do
post_classified_listing(params)
expect(response).to have_http_status(:created)
end.to change(ClassifiedListing, :count).by(1)
expect(ClassifiedListing.find(json_response["id"]).cached_tag_list).to eq("discuss, javascript")
expect(ClassifiedListing.find(json_response["id"]).contact_via_connect).to eq(true)
end
end
end
describe "PUT /api/listings/:id" do
def put_classified_listing(id, **params)
headers = { "api-key" => api_secret.secret, "content-type" => "application/json" }
put api_classified_listing_path(id), params: { classified_listing: params }.to_json, headers: headers
end
let(:user) { create(:user) }
let(:another_user) { create(:user) }
let(:listing) { create(:classified_listing, user_id: user.id) }
let(:another_user_listing) { create(:classified_listing, user_id: another_user.id) }
let(:listing_draft) { create(:classified_listing, user: user) }
let(:organization) { create(:organization) }
let(:org_listing) { create(:classified_listing, user: user, organization: organization) }
let(:org_listing_draft) { create(:classified_listing, user: user, organization: organization) }
before do
listing_draft.update_columns(bumped_at: nil, published: false)
org_listing_draft.update_columns(bumped_at: nil, published: false)
end
describe "user cannot proceed if not properly unauthorizedœ" do
let(:api_secret) { create(:api_secret) }
it "fails with no api key" do
put api_classified_listing_path(listing.id), headers: { "content-type" => "application/json" }
expect(response).to have_http_status(:unauthorized)
end
it "fails with the wrong api key" do
put api_classified_listing_path(listing.id), headers: { "api-key" => "foobar", "content-type" => "application/json" }
expect(response).to have_http_status(:unauthorized)
end
end
context "when authorized user has no credit" do
include_context "when user is authorized"
it "fails to bump a classified listing" do
previous_bumped_at = listing.bumped_at
put_classified_listing(listing.id, action: "bump")
expect(response).to have_http_status(:payment_required)
expect(listing.reload.bumped_at.to_i).to eq(previous_bumped_at.to_i)
end
it "does not subtract spent credits if the user has not enough credits" do
expect do
put_classified_listing(listing.id, action: "bump")
end.to change(user.credits.spent, :size).by(0)
end
end
context "when the bump action is called" do
include_context "when user is authorized"
include_context "when user has enough credit"
let(:params) { { classified_listing: { action: "bump" } } }
it "does not bump the listing or subtract credits if the purchase does not go through" do
previous_bumped_at = listing.bumped_at
allow(Credits::Buyer).to receive(:call).and_raise(ActiveRecord::Rollback)
expect do
put_classified_listing(listing.id, action: "bump")
end.to change(user.credits.spent, :size).by(0)
expect(listing.reload.bumped_at.to_i).to eq(previous_bumped_at.to_i)
end
it "bumps the listing and subtract credits" do
cost = ClassifiedListing.cost_by_category(listing.category)
create_list(:credit, cost, user: user)
previous_bumped_at = listing.bumped_at
expect do
put_classified_listing(listing.id, action: "bump")
end.to change(user.credits.spent, :size).by(cost)
expect(listing.reload.bumped_at >= previous_bumped_at).to eq(true)
end
it "bumps the org listing using org credits before user credits" do
cost = ClassifiedListing.cost_by_category(org_listing.category)
create_list(:credit, cost, organization: organization)
create_list(:credit, cost, user: user)
previous_bumped_at = org_listing.bumped_at
expect do
put_classified_listing(org_listing.id, action: "bump")
end.to change(organization.credits.spent, :size).by(cost)
expect(org_listing.reload.bumped_at >= previous_bumped_at).to eq(true)
end
it "bumps the org listing using user credits if org credits insufficient and user credits are" do
cost = ClassifiedListing.cost_by_category(org_listing.category)
create_list(:credit, cost, user: user)
previous_bumped_at = org_listing.bumped_at
expect do
put_classified_listing(org_listing.id, action: "bump")
end.to change(user.credits.spent, :size).by(cost)
expect(org_listing.reload.bumped_at >= previous_bumped_at).to eq(true)
end
end
context "when the publish action is called" do
include_context "when user is authorized"
it "publishes a draft and charges user credits if first publish" do
cost = ClassifiedListing.cost_by_category(listing_draft.category)
create_list(:credit, cost, user: user)
expect do
put_classified_listing(listing_draft.id, action: "publish")
end.to change(user.credits.spent, :size).by(cost)
end
it "publishes a draft and ensures published column is true" do
cost = ClassifiedListing.cost_by_category(listing_draft.category)
create_list(:credit, cost, user: user)
put_classified_listing(listing_draft.id, action: "publish")
expect(listing_draft.reload.published).to eq(true)
end
it "publishes an org draft and charges org credits if first publish" do
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
create_list(:credit, cost, organization: organization)
expect do
put_classified_listing(org_listing_draft.id, action: "publish")
end.to change(organization.credits.spent, :size).by(cost)
end
it "publishes an org draft and ensures published column is true" do
cost = ClassifiedListing.cost_by_category(org_listing_draft.category)
create_list(:credit, cost, organization: organization)
put_classified_listing(org_listing_draft.id, action: "publish")
expect(org_listing_draft.reload.published).to eq(true)
end
it "publishes a draft that was charged and is within 30 days of bump doesn't charge credits" do
listing.update_column(:published, false)
expect do
put_classified_listing(listing.id, action: "publish")
end.to change(user.credits.spent, :size).by(0)
end
it "publishes a draft that was charged and is within 30 days of bump and successfully sets published as true" do
listing.update_column(:published, false)
put_classified_listing(listing.id, action: "publish")
expect(listing.reload.published).to eq(true)
end
end
context "when the publish action is called without credit" do
include_context "when user is authorized"
it "fails to publish draft and doesn't charge credits" do
expect do
put_classified_listing(listing_draft.id, action: "publish")
end.to change(user.credits.spent, :size).by(0)
end
it "fails to publish draft and published remains false" do
put_classified_listing(listing_draft.id, action: "publish")
expect(listing_draft.reload.published).to eq(false)
end
end
context "when user is authorized and has credit to update one of his listing" do
include_context "when user is authorized"
include_context "when user has enough credit"
it "returns HTTP 422 if no params given" do
put_classified_listing(listing.id)
expect(response).to have_http_status(:unprocessable_entity)
end
it "updates the title of his listing" do
put_classified_listing(listing.id, title: "This is a new title")
expect(response).to have_http_status(:ok)
expect(listing.reload.title).to eq "This is a new title"
end
it "updates the tags" do
put_classified_listing(listing.id, tags: %w[golang api])
expect(response).to have_http_status(:ok)
expect(listing.reload.cached_tag_list).to eq "golang, api"
end
it "unpublishes the listing" do
expect do
put_classified_listing(listing.id, action: "unpublish")
listing.reload
end.to change(listing, :published).from(true).to(false)
expect(response).to have_http_status(:ok)
end
it "cannot update another user listing" do
expect do
put_classified_listing(another_user_listing.id, title: "Test for a new title")
end.to raise_error(Pundit::NotAuthorizedError)
end
end
end
end