Skip to content

Commit b2bcc3f

Browse files
committed
Add v2 API test coverage
This test suite was adapted from the v2 API tests in CDLUC3/dmptool. The original request and view specs were copied and then refactored to align with our v2 API. Supporting factories and spec helper files were updated as needed to accommodate the new coverage.
1 parent 10380fa commit b2bcc3f

22 files changed

+1541
-1
lines changed

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+
# Server and source are present and sensible
62+
expect(json[:server]).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+
# Client is included
69+
expect(json[:client]).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+
# Server and source are present and sensible
58+
expect(json[:server]).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+
# Client is included
65+
expect(json[:client]).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)