Skip to content

Commit 78a84f9

Browse files
authored
Merge pull request #3589 from DMPRoadmap/aaron/add-api-v2-tests
Add v2 API Test Coverage and Address Uncovered Bugs
2 parents a04513b + b544079 commit 78a84f9

24 files changed

Lines changed: 1542 additions & 2 deletions

app/views/api/v2/datasets/_show.json.jbuilder

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ if output.is_a?(ResearchOutput)
77

88
json.type output.output_type
99
json.title output.title
10-
json.doi_url output.doi_url
1110
json.description output.description
1211
json.personal_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.personal_data)
1312
json.sensitive_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.sensitive_data)

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
get :me, controller: :base_api
211211

212212
resources :plans, only: %i[index show]
213+
resources :templates, only: :index
213214
end
214215
end
215216

spec/factories/identifier_schemes.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,19 @@
3232
identifier_scheme.update("#{identifier_scheme.all_context[idx]}": true)
3333
end
3434
end
35+
36+
%i[
37+
authentication
38+
orgs
39+
plans
40+
users
41+
contributors
42+
identification
43+
research_outputs
44+
].each do |context|
45+
trait :"for_#{context}" do
46+
add_attribute(:"for_#{context}") { true }
47+
end
48+
end
3549
end
3650
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
# == Schema Information
4+
#
5+
# Table name: oauth_access_grants
6+
#
7+
# id :integer not null, primary key
8+
# resource_owner_id :integer not null
9+
# application_id :integer not null
10+
# token :string not null
11+
# expires_in :integer
12+
# revoked_at :datetime
13+
# created_at :datetime not null
14+
# scopes :string not null
15+
#
16+
# Indexes
17+
#
18+
# index_oauth_access_grants_on_token (token)
19+
#
20+
# Foreign Keys
21+
#
22+
# fk_rails_... (resource_owner_id => users.id)
23+
# fk_rails_... (application_id => oauth_applications.id)
24+
25+
FactoryBot.define do
26+
factory :oauth_access_grant, class: 'doorkeeper/access_grant' do
27+
token { SecureRandom.uuid }
28+
expires_in { Faker::Number.number(digits: 8) }
29+
scopes { Doorkeeper.config.default_scopes + Doorkeeper.config.optional_scopes }
30+
redirect_uri { Faker::Internet.url }
31+
32+
trait :revoked do
33+
revoked_at { 2.hours.ago }
34+
end
35+
end
36+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
# == Schema Information
4+
#
5+
# Table name: oauth_access_tokens
6+
#
7+
# id :integer not null, primary key
8+
# resource_owner_id :integer not null
9+
# application_id :integer not null
10+
# token :string not null
11+
# refresh_token :string
12+
# expires_in :integer
13+
# revoked_at :datetime
14+
# created_at :datetime not null
15+
# scopes :string not null
16+
# previous_refresh_token :string
17+
#
18+
# Indexes
19+
#
20+
# index_oauth_access_tokens_on_token (token)
21+
#
22+
# Foreign Keys
23+
#
24+
# fk_rails_... (resource_owner_id => users.id)
25+
# fk_rails_... (application_id => oauth_applications.id)
26+
27+
FactoryBot.define do
28+
factory :oauth_access_token, class: 'doorkeeper/access_token' do
29+
token { SecureRandom.uuid }
30+
refresh_token { SecureRandom.uuid }
31+
expires_in { Faker::Number.number(digits: 8) }
32+
scopes { Doorkeeper.config.default_scopes + Doorkeeper.config.optional_scopes }
33+
34+
trait :revoked do
35+
revoked_at { 2.hours.ago }
36+
end
37+
end
38+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
# == Schema Information
4+
#
5+
# Table name: oauth_application
6+
#
7+
# id: :integer
8+
# name: :string
9+
# uid: :string
10+
# secret: :string
11+
# redirect_uri: :text
12+
# scopes: :string
13+
# confidential: :boolean
14+
# created_at: :datetime
15+
# updated_at: :datetime
16+
17+
FactoryBot.define do
18+
factory :oauth_application, class: 'doorkeeper/application' do
19+
name { Faker::Lorem.unique.word }
20+
uid { SecureRandom.uuid }
21+
secret { SecureRandom.uuid }
22+
redirect_uri { "https://#{Faker::Internet.unique.domain_name}/callback" }
23+
scopes { 'read' }
24+
end
25+
end

spec/factories/research_domains.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,5 @@
2323
factory :research_domain do
2424
identifier { SecureRandom.uuid }
2525
label { Faker::Lorem.unique.word }
26-
uri { Faker::Internet.url }
2726
end
2827
end
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Api::V2::PlansController do
6+
include ApiHelper
7+
include Mocks::ApiV2JsonSamples
8+
include Webmocks
9+
include IdentifierHelper
10+
11+
context 'OAuth (authorization_code grant type) — on behalf of a user' do
12+
before do
13+
@user = create(:user)
14+
@client = create(:oauth_application)
15+
token = mock_authorization_code_token(oauth_application: @client, user: @user).token
16+
17+
@headers = {
18+
Accept: 'application/json',
19+
'Content-Type': 'application/json',
20+
Authorization: "Bearer #{token}"
21+
}
22+
end
23+
24+
def fetch_plans_json_response
25+
get(api_v2_plans_path, headers: @headers)
26+
expect(response).to render_template('api/v2/_standard_response')
27+
expect(response).to render_template('api/v2/plans/index')
28+
JSON.parse(response.body).with_indifferent_access
29+
end
30+
31+
describe 'GET /api/v2/plans (index)' do
32+
context 'an invalid API token is included' do
33+
it 'returns a 401 and the expected Oauth 2.0 headers' do
34+
# Swap actual token with a random string
35+
@headers['Authorization'] = "Bearer #{SecureRandom.uuid}"
36+
get(api_v2_plans_path, headers: @headers)
37+
38+
expect(response.code).to eql('401')
39+
expect(response.body).to be_empty
40+
41+
# Expect Doorkeeper to return the standard OAuth 2.0 WWW-Authenticate header for invalid tokens
42+
expect(response.headers['WWW-Authenticate']).to match(
43+
/Bearer realm="Doorkeeper", error="invalid_token", error_description="The access token is invalid"/
44+
)
45+
end
46+
end
47+
48+
context 'a valid API token is included' do
49+
let(:json) { fetch_plans_json_response }
50+
it 'returns a 200 and the expected response body' do
51+
# Items array is empty
52+
expect(json[:items]).to eq([])
53+
54+
# total_items reflects that nothing is returned
55+
expect(json[:total_items]).to eq(0)
56+
57+
# Status code and message are correct
58+
expect(json[:code]).to eq(200)
59+
expect(json[:message]).to eq('OK')
60+
61+
# Application and source are present and sensible
62+
expect(json[:application]).to eq(ApplicationService.application_name)
63+
expect(json[:source]).to eq('GET /api/v2/plans')
64+
65+
# Time is present and parseable
66+
expect { Time.iso8601(json[:time]) }.not_to raise_error
67+
68+
# Caller is included
69+
expect(json[:caller]).to eq(@client.name)
70+
end
71+
72+
it 'returns an empty array if no plans are available' do
73+
# Items array is empty
74+
expect(json[:items]).to eq([])
75+
76+
# total_items reflects that nothing is returned
77+
expect(json[:total_items]).to eq(0)
78+
end
79+
80+
it 'returns the expected plans' do
81+
# See `app/policies/api/v2/plans_policy.rb for plans included/excluded via `GET api/v2/plans`
82+
83+
# Create the included plans
84+
included_plans = [create(:plan, org: @user.org), create(:plan)]
85+
included_plans[0].add_user!(@user.id, :creator)
86+
# Add multiple roles for testing (ensure duplicate plans will not returned)
87+
included_plans[1].add_user!(@user.id, :editor)
88+
included_plans[1].add_user!(@user.id, :commenter)
89+
90+
# Created the excluded plans
91+
create(:plan, :creator, org: @user.org)
92+
inactive_plan = create(:plan, :creator)
93+
inactive_plan.add_user!(@user.id, :editor)
94+
Role.where(plan_id: inactive_plan.id, user_id: @user.id).update!(active: false)
95+
96+
expect(json[:items].length).to be(included_plans.length)
97+
98+
# Api::V2::PlanPresenter.identifier uses api_v2_plan_url(@plan) to set the "identifier".
99+
# That url is constructed using `request.host` / "www.example.com"
100+
# api_v2_plan_url(@plan) within this test will construct the url via
101+
# default_url_options[:host] / "example.org"
102+
# Because the urls are misaligned, we will only compare the paths here.
103+
# TODO: Consider aligning default_url_options[:host] (in test.rb) with `request.host`
104+
returned_identifiers = json[:items].map { |item| item[:dmp][:dmp_id][:identifier] }
105+
returned_paths = returned_identifiers.map { |url| URI(url).path }
106+
expected_paths = included_plans.map { |plan| api_v2_plan_path(plan) }
107+
expect(returned_paths).to eq(expected_paths)
108+
end
109+
110+
it 'allows for paging' do
111+
original_page_size = Rails.configuration.x.application.api_max_page_size
112+
Rails.configuration.x.application.api_max_page_size = 10
113+
114+
create_list(:plan, 11, :publicly_visible) do |plan|
115+
plan.add_user!(@user.id, :commenter)
116+
end
117+
json = fetch_plans_json_response
118+
119+
test_paging(json: json, headers: @headers)
120+
121+
Rails.configuration.x.application.api_max_page_size = original_page_size
122+
end
123+
end
124+
end
125+
end
126+
end
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Api::V2::TemplatesController do
6+
include ApiHelper
7+
8+
before do
9+
@user = create(:user)
10+
@client = create(:oauth_application)
11+
token = mock_authorization_code_token(oauth_application: @client, user: @user).token
12+
13+
@headers = {
14+
Accept: 'application/json',
15+
'Content-Type': 'application/json',
16+
Authorization: "Bearer #{token}"
17+
}
18+
end
19+
20+
def fetch_templates_json_response
21+
get(api_v2_templates_path, headers: @headers)
22+
expect(response).to render_template('api/v2/_standard_response')
23+
expect(response).to render_template('api/v2/templates/index')
24+
JSON.parse(response.body).with_indifferent_access
25+
end
26+
27+
describe 'GET /api/v2/templates (index)' do
28+
context 'an invalid API token is included' do
29+
it 'returns 401 if the token is invalid' do
30+
@headers['Authorization'] = "Bearer #{SecureRandom.uuid}"
31+
get(api_v2_templates_path, headers: @headers)
32+
33+
expect(response.code).to eql('401')
34+
expect(response.body).to be_empty
35+
36+
# Expect Doorkeeper to return the standard OAuth 2.0 WWW-Authenticate header for invalid tokens
37+
expect(response.headers['WWW-Authenticate']).to match(
38+
/Bearer realm="Doorkeeper", error="invalid_token", error_description="The access token is invalid"/
39+
)
40+
end
41+
end
42+
43+
context 'a valid API token is included' do
44+
it 'returns a 200 and the expected response body' do
45+
json = fetch_templates_json_response
46+
47+
# Items array is empty
48+
expect(json[:items]).to eq([])
49+
50+
# total_items reflects that nothing is returned
51+
expect(json[:total_items]).to eq(0)
52+
53+
# Status code and message are correct
54+
expect(json[:code]).to eq(200)
55+
expect(json[:message]).to eq('OK')
56+
57+
# Application and source are present and sensible
58+
expect(json[:application]).to eq(ApplicationService.application_name)
59+
expect(json[:source]).to eq('GET /api/v2/templates')
60+
61+
# Time is present and parseable
62+
expect { Time.iso8601(json[:time]) }.not_to raise_error
63+
64+
# Caller is included
65+
expect(json[:caller]).to eq(@client.name)
66+
end
67+
68+
it 'returns an empty array if no templates are available' do
69+
get(api_v2_templates_path, headers: @headers)
70+
71+
expect(response.code).to eql('200')
72+
expect(response).to render_template('api/v2/_standard_response')
73+
expect(response).to render_template('api/v2/templates/index')
74+
75+
json = JSON.parse(response.body).with_indifferent_access
76+
expect(json[:items].empty?).to be(true)
77+
expect(json[:errors].nil?).to be(true)
78+
end
79+
80+
it 'returns the expected templates' do
81+
# See `app/policies/api/v2/templates_policy.rb for templates included/excluded via `GET api/v2/templates`
82+
83+
# All included templates must be published and are either:
84+
# - 1) organisationally_visible and template.org_id == user.org_id
85+
# - 2) publicly_visible and customization of == nil
86+
87+
public_template = create(:template, :publicly_visible, published: true)
88+
89+
included_templates = [
90+
public_template,
91+
create(:template, :organisationally_visible, published: true, org: @user.org)
92+
]
93+
94+
# excluded_templates
95+
# unpublished template
96+
create(:template, :publicly_visible, published: false, org: @user.org)
97+
# organisationally_visible and template.org_id != user.org_id
98+
create(:template, :organisationally_visible, published: true)
99+
# publicly_visible and customization of != nil
100+
create(:template, :publicly_visible, published: true, customization_of: public_template.family_id)
101+
102+
json = fetch_templates_json_response
103+
104+
expect(json[:items].length).to be(2)
105+
template_ids = json[:items].map { |item| item[:dmp_template][:template_id][:identifier] }
106+
expect(template_ids).to match_array(included_templates.map { |t| t.id.to_s })
107+
end
108+
109+
it 'allows for paging' do
110+
original_page_size = Rails.configuration.x.application.api_max_page_size
111+
Rails.configuration.x.application.api_max_page_size = 10
112+
create_list(:template, 11, visibility: 1, published: true)
113+
get(api_v2_templates_path, headers: @headers)
114+
115+
test_paging(json: JSON.parse(response.body), headers: @headers)
116+
Rails.configuration.x.application.api_max_page_size = original_page_size
117+
end
118+
end
119+
end
120+
end

0 commit comments

Comments
 (0)