Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
# so a PAT cannot reach write endpoints via content negotiation.
API_ENDPOINTS = {
"links" => %w[index show],
"shares" => %w[index]
"shares" => %w[index create]
}.freeze

skip_forgery_protection if: :api_endpoint?
Expand Down
20 changes: 15 additions & 5 deletions app/controllers/shares_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ def clone
def create
@share = @link.shares.build(share_params)

if @share.save
@share.update shortened_url: "https://#{@share.shorten}"
redirect_to @link, notice: "Share was successfully created."
else
render :new, status: :unprocessable_entity
respond_to do |format|
if @share.save
assign_shortened_url(@share)
format.html { redirect_to @link, notice: "Share was successfully created." }
format.json { render json: share_json(@share), status: :created }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: { errors: @share.errors.full_messages }, status: :unprocessable_entity }
end
end
end

Expand All @@ -55,6 +59,12 @@ def share_params
params.require(:share).permit(:link_id, :shortened_url, :utm_source, :utm_medium, :utm_campaign, :utm_term, :utm_content, :utm_id, :shared_link_name)
end

def assign_shortened_url(share)
share.update(shortened_url: "https://#{share.shorten}")
rescue StandardError => e
Rails.logger.warn("Share ##{share.id} shorten failed: #{e.class}: #{e.message}")
end

def share_json(share)
{
id: share.id,
Expand Down
71 changes: 68 additions & 3 deletions spec/requests/api_links_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,80 @@
end
end

describe 'Bearer cannot reach write endpoints' do
it 'cannot POST a share' do
describe 'POST /links/:link_id/shares.json' do
before do
allow_any_instance_of(Share).to receive(:shorten).and_return('short.link/abc')
end

let(:valid_params) do
{
share: {
utm_source: 'LinkedIn',
utm_medium: 'community',
utm_campaign: 'campaignOne',
utm_term: 'termOne',
utm_content: 'Photo'
}
}
end

it 'creates a share for a valid token' do
expect {
post "/links/#{fastruby_link.id}/shares.json", params: valid_params, headers: auth_headers
}.to change(Share, :count).by(1)

expect(response).to have_http_status(:created)
body = JSON.parse(response.body)
expect(body['utm_source']).to eq('LinkedIn')
expect(body['link_id']).to eq(fastruby_link.id)
expect(body['shortened_url']).to eq('https://short.link/abc')
end

it 'returns 422 with errors when params are invalid' do
expect {
post "/links/#{fastruby_link.id}/shares.json",
params: { share: { utm_source: 'evil', utm_medium: 'evil', utm_campaign: 'evil' } },
params: { share: { utm_source: '' } },
headers: auth_headers
}.not_to change(Share, :count)

expect(response).to have_http_status(:unprocessable_entity)
body = JSON.parse(response.body)
expect(body['errors']).to be_an(Array).and(be_present)
end

it 'returns 401 without a token' do
expect {
post "/links/#{fastruby_link.id}/shares.json",
params: valid_params,
headers: { 'Accept' => 'application/json' }
}.not_to change(Share, :count)

expect(response).to have_http_status(:unauthorized)
end

it 'returns 401 with an invalid token' do
headers = { 'Authorization' => 'Bearer wrong', 'Accept' => 'application/json' }
expect {
post "/links/#{fastruby_link.id}/shares.json", params: valid_params, headers: headers
}.not_to change(Share, :count)

expect(response).to have_http_status(:unauthorized)
end

it 'persists the share even if shortening fails' do
allow_any_instance_of(Share).to receive(:shorten).and_raise(StandardError, 'rebrandly down')

expect {
post "/links/#{fastruby_link.id}/shares.json", params: valid_params, headers: auth_headers
}.to change(Share, :count).by(1)

expect(response).to have_http_status(:created)
body = JSON.parse(response.body)
expect(body['shortened_url']).to be_nil
end
end

describe 'Bearer cannot reach write endpoints' do
it 'cannot create a personal access token' do
auth_headers # materialize the lazy `token` let so it doesn't pollute the count check
expect {
Expand Down