Skip to content

Commit

Permalink
Adapt api to expected responses, simplify auth
Browse files Browse the repository at this point in the history
  • Loading branch information
mattwr18 committed Oct 13, 2023
1 parent 41c1309 commit c366a56
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 47 deletions.
49 changes: 30 additions & 19 deletions app/controllers/api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,56 @@ class ApiController < ApplicationController
skip_before_action :require_login
before_action :authorize_api_access

def contributor
contributor = Contributor.find_by(external_id: contributor_params[:external_id])
return head :not_found unless contributor
def show
contributor = Contributor.find_by(external_id: external_id)

render json: { first_name: contributor.first_name, external_id: contributor.external_id }
unless contributor
render json: { status: 'error', message: 'Not found' }, status: :not_found
return
end

render json: { status: 'ok', data: { first_name: contributor.first_name, external_id: contributor.external_id } }, status: :ok
end

def onboard
contributor = Contributor.find_by(external_id: onboard_params[:external_id])
def create
contributor = Contributor.find_by(external_id: external_id)
if contributor
render json: { id: contributor.id }
render json: {
status: 'ok',
data: { id: contributor.id,
first_name: contributor.first_name,
external_id: contributor.external_id }
}, status: :created
return
end

contributor = Contributor.new(onboard_params.merge(data_processing_consented_at: Time.current))
contributor = Contributor.new(onboard_params.merge(data_processing_consented_at: Time.current, external_id: external_id))

if contributor.save!
render json: { id: contributor.id }
render json: {
status: 'ok',
data: { id: contributor.id,
first_name: contributor.first_name,
external_id: contributor.external_id }
}, status: :created
else
head :unprocessable_entity
render json: { status: 'error', message: 'Record could not be created' }, status: :unprocessable_entity
end
end

private

def authorize_api_access
headers = request.headers
jwt = headers['Authorization'].split.last if headers['Authorization'].present?

JsonWebToken.decode(jwt)
rescue JWT::DecodeError
head :unauthorized
authenticate_or_request_with_http_token do |token, _options|
ActiveSupport::SecurityUtils.secure_compare(token, Setting.api_token)
end
end

def contributor_params
params.permit(:external_id)
def external_id
request.headers['X-100eyes-External-Id']
end

def onboard_params
params.permit(:external_id, :first_name)
params.permit(:first_name)
end
end
2 changes: 2 additions & 0 deletions app/models/setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def self.onboarding_hero=(blob)
end

field :project_name, default: ENV['HUNDRED_EYES_PROJECT_NAME'] || '100eyes'
field :api_token, readonly: true, default: ENV.fetch('HUNDRED_EYES_API_TOKEN', nil)

field :application_host, readonly: true, default: ENV['APPLICATION_HOSTNAME'] || 'localhost:3000'

field :git_commit_sha, readonly: true, default: ENV.fetch('GIT_COMMIT_SHA', nil)
Expand Down
4 changes: 2 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,6 @@
post '/profile/user', to: 'profile#create_user'
put '/profile/upgrade_business_plan', to: 'profile#upgrade_business_plan'

get '/v1/contributor', to: 'api#contributor'
post '/v1/onboard', to: 'api#onboard'
get '/v1/contributors/me', to: 'api#show'
post '/v1/contributors', to: 'api#create'
end
106 changes: 80 additions & 26 deletions spec/requests/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,116 @@

RSpec.describe 'Api' do
let(:external_id) { 'amzn1.ask.account.valid_uuid' }
let(:attrs) do
let(:token) { SecureRandom.urlsafe_base64(128) }
let(:headers) { {} }
let(:valid_headers) do
{
first_name: 'John',
external_id: external_id
'Authorization' => "Bearer #{token}",
'X-100eyes-External-Id' => external_id
}
end
let(:secret_key) { Rails.application.secrets.secret_key_base.to_s }
let(:algorithm) { 'HS256' }
let(:valid_jwt) { JWT.encode({ data: payload }, secret_key, algorithm) }
let(:payload) { { api_key: SecureRandom.base64(16), action: 'api' } }
let(:auth_headers) { {} }

describe 'GET /contributor' do
subject { -> { get '/v1/contributor', params: { external_id: external_id }, headers: auth_headers } }
describe 'GET /contributors/me' do
subject { -> { get '/v1/contributors/me', headers: headers } }

describe 'not authorized' do
it 'returns not authorized' do
subject.call
context 'missing auth headers' do
it 'returns not authorized' do
subject.call

expect(response).to have_http_status(:unauthorized)
expect(response.code.to_i).to eq(401)
end
end

context 'invalid token' do
let(:headers) { { 'Authorization' => "Bearer #{SecureRandom.urlsafe_base64(128)}" } }

it 'returns not authorized' do
subject.call

expect(response).to have_http_status(:unauthorized)
expect(response).to have_http_status(:unauthorized)
expect(response.code.to_i).to eq(401)
end
end
end

describe 'authorized' do
let(:auth_headers) do
{ 'Authorization' => "Bearer #{valid_jwt}" }
end
before { allow(Setting).to receive(:api_token).and_return(token) }
let(:headers) { valid_headers }

context 'unknown contributor' do
it 'returns not found' do
subject.call

expect(response).to have_http_status(:not_found)
expect(response.code.to_i).to eq(404)
end

it 'returns error status with message Not found' do
subject.call

expect(response.body).to eq({ status: 'error', message: 'Not found' }.to_json)
end
end

context 'known contributor' do
before { create(:contributor, external_id: external_id) }
let(:expected_response) do
{
status: 'ok',
data:
{
first_name: 'John',
external_id: external_id
}
}.to_json
end

it 'returns first name and external id' do
subject.call

expect(response.body).to eq(attrs.to_json)
expect(response.body).to eq(expected_response)
expect(response.code.to_i).to eq(200)
end
end
end
end

describe 'POST /v1/onboard' do
subject { -> { post v1_onboard_path, params: attrs, headers: auth_headers } }
describe 'POST /v1/contributors' do
subject { -> { post v1_contributors_path, params: { first_name: 'John' }, headers: headers } }

describe 'not authorized' do
it 'returns not authorized' do
subject.call
context 'missing auth headers' do
it 'returns not authorized' do
subject.call

expect(response).to have_http_status(:unauthorized)
expect(response.code.to_i).to eq(401)
end
end

context 'invalid token' do
let(:headers) { { 'Authorization' => "Bearer #{SecureRandom.urlsafe_base64(128)}" } }

expect(response).to have_http_status(:unauthorized)
it 'returns not authorized' do
subject.call

expect(response).to have_http_status(:unauthorized)
expect(response.code.to_i).to eq(401)
end
end
end

describe 'authorized' do
let(:auth_headers) do
{ 'Authorization' => "Bearer #{valid_jwt}" }
before { allow(Setting).to receive(:api_token).and_return(token) }

let(:headers) { valid_headers }
let(:expected_response) do
{
first_name: 'John',
external_id: external_id
}
end

context 'unknown contributor' do
Expand All @@ -74,7 +124,9 @@
it 'returns internal id' do
subject.call

expect(response.body).to eq({ id: Contributor.first.id }.to_json)
expect(JSON.parse(response.body)).to eq({ status: 'ok',
data: expected_response.merge(id: Contributor.first.id) }.with_indifferent_access)
expect(response.code.to_i).to eq(201)
end
end

Expand All @@ -88,7 +140,9 @@
it 'returns internal id' do
subject.call

expect(response.body).to eq({ id: contributor.id }.to_json)
expect(JSON.parse(response.body)).to eq({ status: 'ok',
data: expected_response.merge(id: contributor.id) }.with_indifferent_access)
expect(response.code.to_i).to eq(201)
end
end
end
Expand Down

0 comments on commit c366a56

Please sign in to comment.