diff --git a/app/views/api/v2/_standard_response.json.jbuilder b/app/views/api/v2/_standard_response.json.jbuilder index c979fec01e..cc4f8b05a7 100644 --- a/app/views/api/v2/_standard_response.json.jbuilder +++ b/app/views/api/v2/_standard_response.json.jbuilder @@ -15,7 +15,7 @@ json.ignore_nil! json.server @server json.source "#{request.method} #{request.path}" json.time Time.now.to_formatted_s(:iso8601) -json.client @client.name +json.client @client&.name json.code response.status json.message Rack::Utils::HTTP_STATUS_CODES[response.status] diff --git a/app/views/api/v2/datasets/_show.json.jbuilder b/app/views/api/v2/datasets/_show.json.jbuilder index 1581dff785..6e856019cd 100644 --- a/app/views/api/v2/datasets/_show.json.jbuilder +++ b/app/views/api/v2/datasets/_show.json.jbuilder @@ -7,7 +7,6 @@ if output.is_a?(ResearchOutput) json.type output.output_type json.title output.title - json.doi_url output.doi_url json.description output.description json.personal_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.personal_data) json.sensitive_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.sensitive_data) diff --git a/app/views/api/v2/templates/index.json.jbuilder b/app/views/api/v2/templates/index.json.jbuilder index cdb35f54fb..8f5860f2c2 100644 --- a/app/views/api/v2/templates/index.json.jbuilder +++ b/app/views/api/v2/templates/index.json.jbuilder @@ -17,7 +17,7 @@ json.items @items do |template| end json.template_id do - identifier = Api::V2::ConversionService.to_identifier(context: @application, + identifier = Api::V2::ConversionService.to_identifier(context: @server, value: template.id) json.partial! 'api/v2/identifiers/show', identifier: identifier end diff --git a/config/routes.rb b/config/routes.rb index 152c4a327d..274c494d1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -210,6 +210,7 @@ get :me, controller: :base_api resources :plans, only: %i[index show] + resources :templates, only: :index end end diff --git a/spec/factories/identifier_schemes.rb b/spec/factories/identifier_schemes.rb index 3e52d43b66..3e24252c61 100644 --- a/spec/factories/identifier_schemes.rb +++ b/spec/factories/identifier_schemes.rb @@ -32,5 +32,19 @@ identifier_scheme.update("#{identifier_scheme.all_context[idx]}": true) end end + + %i[ + authentication + orgs + plans + users + contributors + identification + research_outputs + ].each do |context| + trait :"for_#{context}" do + add_attribute(:"for_#{context}") { true } + end + end end end diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb new file mode 100644 index 0000000000..2cd8dc8875 --- /dev/null +++ b/spec/factories/oauth_access_grants.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: oauth_access_grants +# +# id :integer not null, primary key +# resource_owner_id :integer not null +# application_id :integer not null +# token :string not null +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string not null +# +# Indexes +# +# index_oauth_access_grants_on_token (token) +# +# Foreign Keys +# +# fk_rails_... (resource_owner_id => users.id) +# fk_rails_... (application_id => oauth_applications.id) + +FactoryBot.define do + factory :oauth_access_grant, class: 'doorkeeper/access_grant' do + token { SecureRandom.uuid } + expires_in { Faker::Number.number(digits: 8) } + scopes { Doorkeeper.config.default_scopes + Doorkeeper.config.optional_scopes } + redirect_uri { Faker::Internet.url } + + trait :revoked do + revoked_at { 2.hours.ago } + end + end +end diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb new file mode 100644 index 0000000000..aa94d6b373 --- /dev/null +++ b/spec/factories/oauth_access_tokens.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer not null +# application_id :integer not null +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string not null +# previous_refresh_token :string +# +# Indexes +# +# index_oauth_access_tokens_on_token (token) +# +# Foreign Keys +# +# fk_rails_... (resource_owner_id => users.id) +# fk_rails_... (application_id => oauth_applications.id) + +FactoryBot.define do + factory :oauth_access_token, class: 'doorkeeper/access_token' do + token { SecureRandom.uuid } + refresh_token { SecureRandom.uuid } + expires_in { Faker::Number.number(digits: 8) } + scopes { Doorkeeper.config.default_scopes + Doorkeeper.config.optional_scopes } + + trait :revoked do + revoked_at { 2.hours.ago } + end + end +end diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb new file mode 100644 index 0000000000..469dd66269 --- /dev/null +++ b/spec/factories/oauth_applications.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: oauth_application +# +# id: :integer +# name: :string +# uid: :string +# secret: :string +# redirect_uri: :text +# scopes: :string +# confidential: :boolean +# created_at: :datetime +# updated_at: :datetime + +FactoryBot.define do + factory :oauth_application, class: 'doorkeeper/application' do + name { Faker::Lorem.unique.word } + uid { SecureRandom.uuid } + secret { SecureRandom.uuid } + redirect_uri { "https://#{Faker::Internet.unique.domain_name}/callback" } + scopes { 'read' } + end +end diff --git a/spec/factories/research_domains.rb b/spec/factories/research_domains.rb index 7d4017f924..23049903be 100644 --- a/spec/factories/research_domains.rb +++ b/spec/factories/research_domains.rb @@ -23,6 +23,5 @@ factory :research_domain do identifier { SecureRandom.uuid } label { Faker::Lorem.unique.word } - uri { Faker::Internet.url } end end diff --git a/spec/requests/api/v2/plans_controller_spec.rb b/spec/requests/api/v2/plans_controller_spec.rb new file mode 100644 index 0000000000..bd09f7783b --- /dev/null +++ b/spec/requests/api/v2/plans_controller_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V2::PlansController do + include ApiHelper + include Mocks::ApiV2JsonSamples + include Webmocks + include IdentifierHelper + + context 'OAuth (authorization_code grant type) — on behalf of a user' do + before do + @user = create(:user) + @client = create(:oauth_application) + token = mock_authorization_code_token(oauth_application: @client, user: @user).token + + @headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: "Bearer #{token}" + } + end + + def fetch_plans_json_response + get(api_v2_plans_path, headers: @headers) + expect(response).to render_template('api/v2/_standard_response') + expect(response).to render_template('api/v2/plans/index') + JSON.parse(response.body).with_indifferent_access + end + + describe 'GET /api/v2/plans (index)' do + context 'an invalid API token is included' do + it 'returns a 401 and the expected Oauth 2.0 headers' do + # Swap actual token with a random string + @headers['Authorization'] = "Bearer #{SecureRandom.uuid}" + get(api_v2_plans_path, headers: @headers) + + expect(response.code).to eql('401') + expect(response.body).to be_empty + + # Expect Doorkeeper to return the standard OAuth 2.0 WWW-Authenticate header for invalid tokens + expect(response.headers['WWW-Authenticate']).to match( + /Bearer realm="Doorkeeper", error="invalid_token", error_description="The access token is invalid"/ + ) + end + end + + context 'a valid API token is included' do + let(:json) { fetch_plans_json_response } + it 'returns a 200 and the expected response body' do + # Items array is empty + expect(json[:items]).to eq([]) + + # total_items reflects that nothing is returned + expect(json[:total_items]).to eq(0) + + # Status code and message are correct + expect(json[:code]).to eq(200) + expect(json[:message]).to eq('OK') + + # Server and source are present and sensible + expect(json[:server]).to eq(ApplicationService.application_name) + expect(json[:source]).to eq('GET /api/v2/plans') + + # Time is present and parseable + expect { Time.iso8601(json[:time]) }.not_to raise_error + + # Client is included + expect(json[:client]).to eq(@client.name) + end + + it 'returns an empty array if no plans are available' do + # Items array is empty + expect(json[:items]).to eq([]) + + # total_items reflects that nothing is returned + expect(json[:total_items]).to eq(0) + end + + it 'returns the expected plans' do + # See `app/policies/api/v2/plans_policy.rb for plans included/excluded via `GET api/v2/plans` + + # Create the included plans + included_plans = [create(:plan, org: @user.org), create(:plan)] + included_plans[0].add_user!(@user.id, :creator) + # Add multiple roles for testing (ensure duplicate plans will not returned) + included_plans[1].add_user!(@user.id, :editor) + included_plans[1].add_user!(@user.id, :commenter) + + # Created the excluded plans + create(:plan, :creator, org: @user.org) + inactive_plan = create(:plan, :creator) + inactive_plan.add_user!(@user.id, :editor) + Role.where(plan_id: inactive_plan.id, user_id: @user.id).update!(active: false) + + expect(json[:items].length).to be(included_plans.length) + + # Api::V2::PlanPresenter.identifier uses api_v2_plan_url(@plan) to set the "identifier". + # That url is constructed using `request.host` / "www.example.com" + # api_v2_plan_url(@plan) within this test will construct the url via + # default_url_options[:host] / "example.org" + # Because the urls are misaligned, we will only compare the paths here. + # TODO: Consider aligning default_url_options[:host] (in test.rb) with `request.host` + returned_identifiers = json[:items].map { |item| item[:dmp][:dmp_id][:identifier] } + returned_paths = returned_identifiers.map { |url| URI(url).path } + expected_paths = included_plans.map { |plan| api_v2_plan_path(plan) } + expect(returned_paths).to eq(expected_paths) + end + + it 'allows for paging' do + original_page_size = Rails.configuration.x.application.api_max_page_size + Rails.configuration.x.application.api_max_page_size = 10 + + create_list(:plan, 11, :publicly_visible) do |plan| + plan.add_user!(@user.id, :commenter) + end + json = fetch_plans_json_response + + test_paging(json: json, headers: @headers) + + Rails.configuration.x.application.api_max_page_size = original_page_size + end + end + end + end +end diff --git a/spec/requests/api/v2/templates_controller_spec.rb b/spec/requests/api/v2/templates_controller_spec.rb new file mode 100644 index 0000000000..f5a25ac110 --- /dev/null +++ b/spec/requests/api/v2/templates_controller_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V2::TemplatesController do + include ApiHelper + + before do + @user = create(:user) + @client = create(:oauth_application) + token = mock_authorization_code_token(oauth_application: @client, user: @user).token + + @headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: "Bearer #{token}" + } + end + + def fetch_templates_json_response + get(api_v2_templates_path, headers: @headers) + expect(response).to render_template('api/v2/_standard_response') + expect(response).to render_template('api/v2/templates/index') + JSON.parse(response.body).with_indifferent_access + end + + describe 'GET /api/v2/templates (index)' do + context 'an invalid API token is included' do + it 'returns 401 if the token is invalid' do + @headers['Authorization'] = "Bearer #{SecureRandom.uuid}" + get(api_v2_templates_path, headers: @headers) + + expect(response.code).to eql('401') + expect(response.body).to be_empty + + # Expect Doorkeeper to return the standard OAuth 2.0 WWW-Authenticate header for invalid tokens + expect(response.headers['WWW-Authenticate']).to match( + /Bearer realm="Doorkeeper", error="invalid_token", error_description="The access token is invalid"/ + ) + end + end + + context 'a valid API token is included' do + it 'returns a 200 and the expected response body' do + json = fetch_templates_json_response + + # Items array is empty + expect(json[:items]).to eq([]) + + # total_items reflects that nothing is returned + expect(json[:total_items]).to eq(0) + + # Status code and message are correct + expect(json[:code]).to eq(200) + expect(json[:message]).to eq('OK') + + # Server and source are present and sensible + expect(json[:server]).to eq(ApplicationService.application_name) + expect(json[:source]).to eq('GET /api/v2/templates') + + # Time is present and parseable + expect { Time.iso8601(json[:time]) }.not_to raise_error + + # Client is included + expect(json[:client]).to eq(@client.name) + end + + it 'returns an empty array if no templates are available' do + get(api_v2_templates_path, headers: @headers) + + expect(response.code).to eql('200') + expect(response).to render_template('api/v2/_standard_response') + expect(response).to render_template('api/v2/templates/index') + + json = JSON.parse(response.body).with_indifferent_access + expect(json[:items].empty?).to be(true) + expect(json[:errors].nil?).to be(true) + end + + it 'returns the expected templates' do + # See `app/policies/api/v2/templates_policy.rb for templates included/excluded via `GET api/v2/templates` + + # All included templates must be published and are either: + # - 1) organisationally_visible and template.org_id == user.org_id + # - 2) publicly_visible and customization of == nil + + public_template = create(:template, :publicly_visible, published: true) + + included_templates = [ + public_template, + create(:template, :organisationally_visible, published: true, org: @user.org) + ] + + # excluded_templates + # unpublished template + create(:template, :publicly_visible, published: false, org: @user.org) + # organisationally_visible and template.org_id != user.org_id + create(:template, :organisationally_visible, published: true) + # publicly_visible and customization of != nil + create(:template, :publicly_visible, published: true, customization_of: public_template.family_id) + + json = fetch_templates_json_response + + expect(json[:items].length).to be(2) + template_ids = json[:items].map { |item| item[:dmp_template][:template_id][:identifier] } + expect(template_ids).to match_array(included_templates.map { |t| t.id.to_s }) + end + + it 'allows for paging' do + original_page_size = Rails.configuration.x.application.api_max_page_size + Rails.configuration.x.application.api_max_page_size = 10 + create_list(:template, 11, visibility: 1, published: true) + get(api_v2_templates_path, headers: @headers) + + test_paging(json: JSON.parse(response.body), headers: @headers) + Rails.configuration.x.application.api_max_page_size = original_page_size + end + end + end +end diff --git a/spec/support/helpers/api.rb b/spec/support/helpers/api.rb index c87931c12e..2a2398dfe7 100644 --- a/spec/support/helpers/api.rb +++ b/spec/support/helpers/api.rb @@ -18,4 +18,44 @@ def mock_authorization_for_user(user: nil) Api::V1::BaseApiController.any_instance.stubs(:authorize_request).returns(true) Api::V1::BaseApiController.any_instance.stubs(:client).returns(user) end + + # API V2+ - Oauth authorization_code grant flow (on behalf of a user) + def mock_authorization_code_token(oauth_application: create(:oauth_application), user: create(:user), + scopes: 'read') + create(:oauth_access_grant, application_id: oauth_application.id, resource_owner_id: user.id, scopes: scopes) + create(:oauth_access_token, application: oauth_application, resource_owner_id: user.id, scopes: scopes) + end + + # Tests the standard pagination functionality + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def test_paging(json: {}, headers: {}) + json = json.with_indifferent_access + original = json[:items].first + if json[:next].present? + # Move to the next page + get(json[:next], headers: headers) + expect(response.code).to eql('200') + next_json = JSON.parse(response.body).with_indifferent_access + expect(next_json[:prev].present?).to be(true) + expect(next_json[:items].first).not_to eql(original) + # Move back to previous page + get(next_json[:prev], headers: headers) + expect(response.code).to eql('200') + prev_json = JSON.parse(response.body).with_indifferent_access + expect(prev_json[:items].first).to eql(original) + elsif json[:prev].present? + get(json[:prev], headers: headers) + expect(response.code).to eql('200') + prev_json = JSON.parse(response.body).with_indifferent_access + expect(prev_json[:next].present?).to be(true) + expect(next_json[:items].first).not_to eql(original) + get(prev_json[:next], headers: headers) + expect(response.code).to eql('200') + next_json = JSON.parse(response.body).with_indifferent_access + expect(next_json[:items].first).to eql(original) + else + raise StandardError, 'Expected to test API pagination but there are not enough items!' + end + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength end diff --git a/spec/support/mocks/api_v2_json_samples.rb b/spec/support/mocks/api_v2_json_samples.rb new file mode 100644 index 0000000000..aaec320811 --- /dev/null +++ b/spec/support/mocks/api_v2_json_samples.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: true + +# Mock JSON submissions +module Mocks + # Disabling rubocop checks here since its basically just large hashes and + # would be difficult to read if broken up into multiple smaller functions. + # One option might be to store them as .json files and then just load them here + # but we would lose the use of Faker + + # rubocop:disable Metrics/ModuleLength, Metrics/MethodLength + module ApiV2JsonSamples + ROLES = %w[Investigation Project_administration Data_curation].freeze + + def mock_identifier_schemes + create(:identifier_scheme, name: 'ror') + create(:identifier_scheme, name: 'fundref') + create(:identifier_scheme, name: 'orcid') + create(:identifier_scheme, name: 'grant') + end + + def minimal_update_json + { + total_items: 1, + items: [ + { + dmp: { + title: Faker::Lorem.sentence, + contact: { + mbox: Faker::Internet.email, + affiliation: { name: Faker::Movies::StarWars.planet } + }, + dataset: [{ + title: Faker::Lorem.sentence + }], + dmp_id: { + type: 'doi', + identifier: SecureRandom.uuid + } + } + } + ] + }.to_json + end + + def minimal_create_json + { + dmp: { + title: Faker::Lorem.sentence, + contact: { + mbox: Faker::Internet.email, + affiliation: { name: Faker::Movies::StarWars.planet } + }, + dataset: [{ + title: Faker::Lorem.sentence + }], + extension: [ + "#{ApplicationService.application_name.split('-').first}": { + template: { + id: Template.last.id, + title: Faker::Lorem.sentence + } + } + ] + } + }.to_json + end + + # rubocop:disable Metrics/AbcSize + def complete_create_json(client: nil) + template = create(:template, :published, :publicly_visible) + lang = Language.all.pluck(:abbreviation).sample || 'en-UK' + ror_scheme = IdentifierScheme.find_or_create_by(name: 'ror') + fundref_scheme = IdentifierScheme.find_or_create_by(name: 'fundref') + ror = create(:identifier, identifiable: create(:org), identifier_scheme: ror_scheme) + fundref = create(:identifier, identifiable: create(:org), identifier_scheme: fundref_scheme) + + contact = { + name: [ + Faker::TvShows::Simpsons.character.split.first, + Faker::TvShows::Simpsons.character.split.last + ].join(' '), + email: Faker::Internet.email, + id: SecureRandom.uuid + } + + { + dmp: { + created: 3.months.ago.to_formatted_s(:iso8601), + title: Faker::Lorem.sentence, + description: Faker::Lorem.paragraph, + language: Api::V1::LanguagePresenter.three_char_code(lang: lang), + ethical_issues_exist: %w[yes no unknown].sample, + ethical_issues_description: Faker::Lorem.paragraph, + ethical_issues_report: Faker::Internet.url, + dmp_id: { + type: client.present? ? client.name.downcase : 'other', + identifier: SecureRandom.uuid + }, + contact: { + name: contact[:name], + mbox: contact[:email], + affiliation: { + name: ror.identifiable.name, + abbreviation: ror.identifiable.abbreviation, + region: Faker::Space.planet, + affiliation_id: { + type: 'ror', + identifier: ror.value + } + }, + contact_id: { + type: 'orcid', + identifier: contact[:id] + } + }, + contributor: [{ + role: [ + 'http://credit.niso.org/contributor-roles/project-administration', + 'http://credit.niso.org/contributor-roles/investigation', + 'other' + ], + name: Faker::Movies::StarWars.character, + mbox: Faker::Internet.email, + affiliation: { + name: Faker::Movies::StarWars.planet, + abbreviation: Faker::Lorem.word.upcase, + affiliation_id: { + type: 'ror', + identifier: SecureRandom.uuid + } + }, + contributor_id: { + type: 'orcid', + identifier: SecureRandom.uuid + } + }, { + role: [ + 'http://credit.niso.org/contributor-roles/investigation' + ], + name: contact[:name], + mbox: contact[:email], + affiliation: { + name: ror.identifiable.name, + abbreviation: ror.identifiable.abbreviation, + affiliation_id: { + type: 'ror', + identifier: ror.value + } + }, + contributor_id: { + type: 'orcid', + identifier: contact[:id] + } + }], + project: [{ + title: Faker::Lorem.sentence, + description: Faker::Lorem.paragraph, + start: 3.months.from_now.to_formatted_s(:iso8601), + end: 2.years.from_now.to_formatted_s(:iso8601), + funding: [{ + name: fundref.identifiable.name, + funder_id: { + type: 'fundref', + identifier: fundref.value + }, + grant_id: { + type: 'other', + identifier: SecureRandom.uuid + }, + dmproadmap_funding_opportunity_id: { + type: 'other', + identifier: SecureRandom.uuid + }, + funding_status: %w[planned rejected granted].sample + }] + }], + dataset: [{ + title: Faker::Lorem.sentence, + description: Faker::Lorem.paragraph, + personal_data: %w[yes no unknown].sample, + sensitive_data: %w[yes no unknown].sample, + issued: 6.months.from_now.to_formatted_s(:iso8601), + dataset_id: { + type: 'url', + identifier: Faker::Internet.url + }, + distribution: [{ + title: Faker::Lorem.sentence, + byte_size: Faker::Number.number(digits: 6), + data_access: %w[open embargoed restricted closed].sample, + host: { + title: Faker::Company.name, + description: Faker::Lorem.paragraph, + url: Faker::Internet.url, + dmproadmap_host_id: { + type: 'url', + identifier: Faker::Internet.url + } + }, + license: [ + { + license_ref: 'http://spdx.org/licenses/CC0-1.0.json', + start_date: 6.months.from_now.to_formatted_s(:iso8601) + } + ] + }] + }], + dmproadmap_template: { id: template.family_id, title: template.title } + } + }.to_json + end + # rubocop:enable Metrics/AbcSize + end + # rubocop:enable Metrics/ModuleLength, Metrics/MethodLength +end diff --git a/spec/views/api/v2/_standard_response.json_jbuilder_spec.rb b/spec/views/api/v2/_standard_response.json_jbuilder_spec.rb new file mode 100644 index 0000000000..2758d51d22 --- /dev/null +++ b/spec/views/api/v2/_standard_response.json_jbuilder_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/_standard_response.json.jbuilder' do + before do + @application = Faker::Lorem.word + # @caller = Faker::Lorem.word + @url = Faker::Internet.url + @code = 200 + + assign :application, @application + # assign :caller, @caller + + @response = OpenStruct.new(status: @code) + @request = Net::HTTPGenericRequest.new('GET', nil, nil, @url) + end + + describe 'standard response items - Also the same as: GET /heartbeat' do + before do + render partial: 'api/v2/standard_response', + locals: { response: @response, request: @request } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes the :code' do + expect(@json[:code]).to eql(@code) + end + + it 'includes the :message' do + expect(@json[:message]).to eql(Rack::Utils::HTTP_STATUS_CODES[@code]) + end + + it 'includes the :time' do + expect(@json[:time].present?).to be(true) + end + + it ':time is in UTC format' do + expect(Date.parse(@json[:time]).is_a?(Date)).to be(true) + end + + # it 'includes the :caller' do + # expect(@json[:caller]).to eql(@caller) + # end + + it 'includes the :source' do + expect(@json[:source].include?(@url)).to be(true) + end + + it 'includes the :total_items' do + expect(@json[:total_items]).to be(0) + end + end + + context 'responses with pagination' do + describe 'On the 1st page and there is only one page' do + before do + assign :page, 1 + assign :per_page, 3 + + render partial: 'api/v2/standard_response', + locals: { response: @response, request: @request, + total_items: 3 } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'shows the correct page number' do + expect(@json[:page]).to be(1) + end + + it 'includes the per_page number' do + expect(@json[:per_page]).to be(3) + end + + it 'includes the :total_items' do + expect(@json[:total_items]).to be(3) + end + + it "does not show a 'prev' page link" do + expect(@json[:prev].present?).to be(false) + end + + it "does not show a 'next' page link" do + expect(@json[:prev].present?).to be(false) + end + end + + describe 'On the 1st page and there multiple pages' do + before do + assign :page, 1 + assign :per_page, 3 + + render partial: 'api/v2/standard_response', + locals: { response: @response, request: @request, + total_items: 4 } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'shows the correct page number' do + expect(@json[:page]).to be(1) + end + + it 'includes the per_page number' do + expect(@json[:per_page]).to be(3) + end + + it 'includes the :total_items' do + expect(@json[:total_items]).to be(4) + end + + it "does not show a 'prev' page link" do + expect(@json[:prev].present?).to be(false) + end + + it "does not show a 'next' page link" do + expect(@json[:next].present?).to be(true) + end + end + + describe 'On the 2nd page and there more than 2 pages' do + before do + assign :page, 2 + assign :per_page, 3 + + render partial: 'api/v2/standard_response', + locals: { response: @response, request: @request, + total_items: 7 } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'shows the correct page number' do + expect(@json[:page]).to be(2) + end + + it 'includes the per_page number' do + expect(@json[:per_page]).to be(3) + end + + it 'includes the :total_items' do + expect(@json[:total_items]).to be(7) + end + + it "does not show a 'prev' page link" do + expect(@json[:prev].present?).to be(true) + end + + it "does not show a 'next' page link" do + expect(@json[:next].present?).to be(true) + end + end + + describe 'On the last page' do + before do + assign :page, 2 + assign :per_page, 3 + + render partial: 'api/v2/standard_response', + locals: { response: @response, request: @request, + total_items: 5 } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'shows the correct page number' do + expect(@json[:page]).to be(2) + end + + it 'includes the per_page number' do + expect(@json[:per_page]).to be(3) + end + + it 'includes the :total_items' do + expect(@json[:total_items]).to be(5) + end + + it "does not show a 'prev' page link" do + expect(@json[:prev].present?).to be(true) + end + + it "does not show a 'next' page link" do + expect(@json[:next].present?).to be(false) + end + end + end +end diff --git a/spec/views/api/v2/contributors/_show.json.jbuilder_spec.rb b/spec/views/api/v2/contributors/_show.json.jbuilder_spec.rb new file mode 100644 index 0000000000..c2652decaf --- /dev/null +++ b/spec/views/api/v2/contributors/_show.json.jbuilder_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/contributors/_show.json.jbuilder' do + before do + @plan = create(:plan) + scheme = create(:identifier_scheme, name: 'orcid') + @contact = create(:contributor, org: create(:org), plan: @plan, roles_count: 0, + data_curation: true) + @ident = create(:identifier, identifiable: @contact, value: Faker::Lorem.word, + identifier_scheme: scheme) + @contact.reload + end + + describe 'includes all of the Contributor attributes' do + before do + render partial: 'api/v2/contributors/show', locals: { contributor: @contact } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes the :name' do + expect(@json[:name]).to eql(@contact.name) + end + + it 'includes the :mbox' do + expect(@json[:mbox]).to eql(@contact.email) + end + + it 'includes the :role' do + expect(@json[:role].first.ends_with?('data-curation')).to be(true) + end + + it 'includes :affiliation' do + expect(@json[:affiliation][:name]).to eql(@contact.org.name) + end + + it 'includes :contributor_id' do + expect(@json[:contributor_id][:type]).to eql(@ident.identifier_format) + expect(@json[:contributor_id][:identifier]).to eql(@ident.value) + end + + it 'ignores non-orcid identifiers :contributor_id' do + scheme = create(:identifier_scheme, name: 'shibboleth') + create(:identifier, value: Faker::Lorem.word, identifiable: @contact, + identifier_scheme: scheme) + @contact.reload + expect(@json[:contributor_id][:type]).to eql(@ident.identifier_format) + expect(@json[:contributor_id][:identifier]).to eql(@ident.value) + end + end + + describe 'includes all of the Contact attributes' do + before do + render partial: 'api/v2/contributors/show', locals: { contributor: @contact, + is_contact: true } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes the :name' do + expect(@json[:name]).to eql(@contact.name) + end + + it 'includes the :mbox' do + expect(@json[:mbox]).to eql(@contact.email) + end + + it 'does NOT include the :role' do + expect(@json[:role]).to be_nil + end + + it 'includes :affiliation' do + expect(@json[:affiliation][:name]).to eql(@contact.org.name) + end + + it 'includes :contact_id' do + expect(@json[:contact_id][:type]).to eql(@ident.identifier_format) + expect(@json[:contact_id][:identifier]).to eql(@ident.value) + end + + it 'ignores non-orcid identifiers :contact_id' do + scheme = create(:identifier_scheme, name: 'shibboleth') + create(:identifier, value: Faker::Lorem.word, identifiable: @contact, + identifier_scheme: scheme) + @contact.reload + expect(@json[:contact_id][:type]).to eql(@ident.identifier_format) + expect(@json[:contact_id][:identifier]).to eql(@ident.value) + end + end +end diff --git a/spec/views/api/v2/datasets/_show.json.jbuilder_spec.rb b/spec/views/api/v2/datasets/_show.json.jbuilder_spec.rb new file mode 100644 index 0000000000..fc1b7d4e7b --- /dev/null +++ b/spec/views/api/v2/datasets/_show.json.jbuilder_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/datasets/_show.json.jbuilder' do + context ':output is a ResearchOutput' do + describe 'includes all of the dataset attributes' do + before do + @research_output = create(:research_output, plan: create(:plan)) + @presenter = Api::V2::ResearchOutputPresenter.new(output: @research_output) + end + + describe 'base :dataset attributes' do + before do + render partial: 'api/v2/datasets/show', locals: { output: @research_output } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes :type' do + expect(@json[:type]).to eql(@research_output.output_type) + end + + it 'includes :title' do + expect(@json[:title]).to eql(@research_output.title) + end + + it 'includes :description' do + expect(@json[:description]).to eql(@research_output.description) + end + + it 'includes :personal_data' do + val = Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: @research_output.personal_data) + expect(@json[:personal_data]).to eql(val) + end + + it 'includes :sensitive_data' do + val = Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: @research_output.sensitive_data) + expect(@json[:sensitive_data]).to eql(val) + end + + it 'includes :issued' do + expect(@json[:issued]).to eql(@research_output.release_date.to_formatted_s(:iso8601)) + end + + it 'includes :preservation_statement' do + expect(@json[:preservation_statement]).to eql(@presenter.preservation_statement) + end + + it 'includes :security_and_privacy' do + expect(@json[:security_and_privacy]).to eql(@presenter.security_and_privacy) + end + + it 'includes :data_quality_assurance' do + expect(@json[:data_quality_assurance]).to eql(@presenter.data_quality_assurance) + end + end + + describe ':distribution' do + before do + @repo = @research_output.repositories.first + @license = create(:license) + @research_output.license_id = @license.id + + render partial: 'api/v2/datasets/show', locals: { output: @research_output } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes :distributions' do + expect(@json[:distribution].any?).to be(true) + end + + it 'includes :title' do + expected = "Anticipated distribution for #{@research_output.title}" + expect(@json[:distribution].first[:title]).to eql(expected) + end + + it 'includes :byte_size' do + expect(@json[:distribution].first[:byte_size]).to eql(@research_output.byte_size) + end + + it 'includes :data_access' do + expect(@json[:distribution].first[:data_access]).to eql(@research_output.access) + end + + it 'includes host[:title]' do + expect(@json[:distribution].first[:host][:title]).to eql(@repo.name) + end + + it 'includes host[:description]' do + expect(@json[:distribution].first[:host][:description]).to eql(@repo.description) + end + + it 'includes host[:url]' do + expect(@json[:distribution].first[:host][:url]).to eql(@repo.homepage) + end + + it 'includes host[:dmproadmap_host_id]' do + result = @json[:distribution].first[:host][:dmproadmap_host_id][:identifier] + expect(result).to eql(@repo.uri) + end + + it 'includes license[:license_ref]' do + expect(@json[:distribution].first[:license].first[:license_ref]).to eql(@license.uri) + end + + it 'includes license[:start_date]' do + expected = @research_output.release_date.to_formatted_s(:iso8601) + expect(@json[:distribution].first[:license].first[:start_date]).to eql(expected) + end + end + + describe ':metadata' do + before do + @standard = create(:metadata_standard) + @research_output.metadata_standards << @standard + + render partial: 'api/v2/datasets/show', locals: { output: @research_output } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes :metadata' do + expect(@json[:metadata].any?).to be(true) + end + + it 'includes :description' do + uri = @standard.uri + metadata = @json[:metadata].select { |ms| ms[:metadata_standard_id][:identifier] == uri } + expected = "#{@standard.title} - #{@standard.description}" + expect(metadata.first[:description].start_with?(expected)).to be(true) + expect(metadata.first[:metadata_standard_id].present?).to be(true) + expect(metadata.first[:metadata_standard_id][:type]).to eql('url') + expect(metadata.first[:metadata_standard_id][:identifier]).to eql(uri) + end + end + + describe ':technical_resources' do + it 'is always an empty array because this has not been implemented' do + render partial: 'api/v2/datasets/show', locals: { output: @research_output } + @json = JSON.parse(rendered).with_indifferent_access + expect(@json[:technical_resource].any?).to be(false) + end + end + + describe ':keyword' do + it 'includes the ResearchDomain' do + research_domain = create(:research_domain) + @research_output.plan.research_domain_id = research_domain.id + render partial: 'api/v2/datasets/show', locals: { output: @research_output } + @json = JSON.parse(rendered).with_indifferent_access + expect(@json[:keyword].any?).to be(true) + expect(@json[:keyword].include?(research_domain.label)) + expect(@json[:keyword].include?("#{research_domain.identifier} - #{research_domain.label}")) + end + + it 'is not included if no ResearchDomain is defined' do + render partial: 'api/v2/datasets/show', locals: { output: @research_output } + @json = JSON.parse(rendered).with_indifferent_access + expect(@json[:keyword].present?).to be(false) + end + end + end + end + + context ':output is a Plan' do + describe 'includes all of the dataset attributes' do + before do + @plan = create(:plan) + @research_domain = create(:research_domain) + @plan.research_domain_id = @research_domain.id + + render partial: 'api/v2/datasets/show', locals: { output: @plan } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes :type' do + expect(@json[:type]).to eql('dataset') + end + + it 'includes :title' do + expect(@json[:title]).to eql('Generic dataset') + end + + it 'includes :description' do + expect(@json[:description]).to eql('No individual datasets have been defined for this DMP.') + end + + describe ':keyword' do + it 'includes the ResearchDomain' do + expect(@json[:keyword].any?).to be(true) + expect(@json[:keyword].include?(@research_domain.label)) + expect(@json[:keyword].include?("#{@research_domain.identifier} - #{@research_domain.label}")) + end + end + end + end +end diff --git a/spec/views/api/v2/error.json.jbuilder_spec.rb b/spec/views/api/v2/error.json.jbuilder_spec.rb new file mode 100644 index 0000000000..ca2337ff6d --- /dev/null +++ b/spec/views/api/v2/error.json.jbuilder_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/error.json.jbuilder' do + before do + @url = Faker::Internet.url + @code = [200, 400, 404, 500].sample + @errors = [Faker::Lorem.sentence, Faker::Lorem.sentence] + + assign :payload, { message: @errors } + + @resp = OpenStruct.new(status: @code) + @req = Net::HTTPGenericRequest.new('GET', nil, nil, @url) + + render template: 'api/v2/error', locals: { response: @resp, request: @req } + @json = JSON.parse(rendered).with_indifferent_access + end + + describe 'error responses from controllers' do + it 'renders the standard_response partial' do + expect(response).to render_template(partial: 'api/v2/_standard_response') + end + + it ':errors contains an array of error messages' do + expect(@json[:message]).to eql(@errors) + end + end +end diff --git a/spec/views/api/v2/heartbeat.json.jbuilder_spec.rb b/spec/views/api/v2/heartbeat.json.jbuilder_spec.rb new file mode 100644 index 0000000000..1808117b19 --- /dev/null +++ b/spec/views/api/v2/heartbeat.json.jbuilder_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/heartbeat.json.jbuilder' do + before do + render template: 'api/v2/heartbeat', locals: { response: @resp, request: @req } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'renders the _standard_response template' do + expect(response).to render_template('api/v2/_standard_response') + end + + it ':items array to be empty' do + expect(@json[:items]).to eql([]) + end +end diff --git a/spec/views/api/v2/identifiers/_show.json.jbuilder_spec.rb b/spec/views/api/v2/identifiers/_show.json.jbuilder_spec.rb new file mode 100644 index 0000000000..602169fd75 --- /dev/null +++ b/spec/views/api/v2/identifiers/_show.json.jbuilder_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/identifiers/_show.json.jbuilder' do + before do + @scheme = create(:identifier_scheme) + @identifier = create(:identifier, value: Faker::Lorem.word, + identifier_scheme: @scheme) + render partial: 'api/v2/identifiers/show', locals: { identifier: @identifier } + @json = JSON.parse(rendered).with_indifferent_access + end + + describe 'includes all of the identifier attributes' do + it 'includes :type' do + expect(@json[:type]).to eql(@identifier.identifier_format) + end + + it 'includes :identifier' do + expect(@json[:identifier]).to eql(@identifier.value) + end + end +end diff --git a/spec/views/api/v2/orgs/_show.json.jbuilder_spec.rb b/spec/views/api/v2/orgs/_show.json.jbuilder_spec.rb new file mode 100644 index 0000000000..5480ae0ee1 --- /dev/null +++ b/spec/views/api/v2/orgs/_show.json.jbuilder_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/orgs/_show.json.jbuilder' do + before do + scheme = create(:identifier_scheme, name: 'ror') + @org = create(:org) + @ident = create(:identifier, value: Faker::Lorem.word, identifiable: @org, + identifier_scheme: scheme) + @org.reload + render partial: 'api/v2/orgs/show', locals: { org: @org } + @json = JSON.parse(rendered).with_indifferent_access + end + + describe 'includes all of the org attributes' do + it 'includes :name' do + expect(@json[:name]).to eql(@org.name) + end + + it 'includes :abbreviation' do + expect(@json[:abbreviation]).to eql(@org.abbreviation) + end + + it 'includes :region' do + expect(@json[:region]).to eql(@org.region.abbreviation) + end + + it 'includes :affiliation_id' do + expect(@json[:affiliation_id][:type]).to eql(@ident.identifier_format) + expect(@json[:affiliation_id][:identifier]).to eql(@ident.value) + end + + it 'uses the ROR over the FundRef :affiliation_id' do + scheme = create(:identifier_scheme, name: 'fundref') + create(:identifier, value: Faker::Lorem.word, identifiable: @org, + identifier_scheme: scheme) + @org.reload + expect(@json[:affiliation_id][:type]).to eql(@ident.identifier_format) + expect(@json[:affiliation_id][:identifier]).to eql(@ident.value) + end + end +end diff --git a/spec/views/api/v2/plans/_cost.json.jbuilder_spec.rb b/spec/views/api/v2/plans/_cost.json.jbuilder_spec.rb new file mode 100644 index 0000000000..712e9300e9 --- /dev/null +++ b/spec/views/api/v2/plans/_cost.json.jbuilder_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/plans/_cost.json.jbuilder' do + before do + # TODO: Implement this once the Currency question and Cost theme are in place + # and the PlanPresenter is extracting the info + @cost = { + title: Faker::Lorem.sentence, + description: Faker::Lorem.paragraph, + currency_code: Faker::Currency.code, + value: Faker::Number.decimal(l_digits: 2) + }.with_indifferent_access + + render partial: 'api/v2/plans/cost', locals: { cost: @cost } + @json = JSON.parse(rendered).with_indifferent_access + end + + describe 'includes all of the cost attributes' do + it 'includes :title' do + expect(@json[:title]).to eql(@cost[:title]) + end + + it 'includes :description' do + expect(@json[:description]).to eql(@cost[:description]) + end + + it 'includes :currency_code' do + expect(@json[:currency_code]).to eql(@cost[:currency_code]) + end + + it 'includes :value' do + expect(@json[:value]).to eql(@cost[:value]) + end + end +end diff --git a/spec/views/api/v2/plans/_funding.json.jbuilder_spec.rb b/spec/views/api/v2/plans/_funding.json.jbuilder_spec.rb new file mode 100644 index 0000000000..e764e142b3 --- /dev/null +++ b/spec/views/api/v2/plans/_funding.json.jbuilder_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/plans/_funding.json.jbuilder' do + before do + @funder = create(:org, :funder) + create(:identifier, identifiable: @funder, + identifier_scheme: create(:identifier_scheme, name: 'fundref')) + @funder.reload + @plan = create(:plan, funder: @funder, org: create(:org), identifier: SecureRandom.uuid) + create(:identifier, identifiable: @plan.org, + identifier_scheme: create(:identifier_scheme, name: 'ror')) + @grant = create(:identifier, identifiable: @plan) + @plan.update(grant_id: @grant.id, funding_status: 'funded') + @plan.reload + + render partial: 'api/v2/plans/funding', locals: { plan: @plan } + @json = JSON.parse(rendered).with_indifferent_access + end + + describe 'includes all of the funding attributes' do + it 'includes :name' do + expect(@json[:name]).to eql(@funder.name) + end + + it 'includes :funding_status' do + expected = Api::V1::FundingPresenter.status(plan: @plan) + expect(@json[:funding_status]).to eql(expected) + end + + it 'includes :funder_ids' do + id = @funder.identifiers.first + expect(@json[:funder_id][:type]).to eql(id.identifier_format) + expect(@json[:funder_id][:identifier]).to eql(id.value) + end + + it 'includes :dmproadmap_funding_opportunity_identifier' do + identifier = @plan.identifier + expect(@json[:dmproadmap_funding_opportunity_id][:type]).to eql('other') + expect(@json[:dmproadmap_funding_opportunity_id][:identifier]).to eql(identifier) + end + + it 'includes :grant_ids' do + expect(@json[:grant_id][:type]).to eql(@grant.identifier_format) + expect(@json[:grant_id][:identifier]).to eql(@grant.value) + end + + it 'includes :dmproadmap_funded_affiliations' do + org = @plan.org + expect(@json[:dmproadmap_funded_affiliations].any?).to be(true) + affil = @json[:dmproadmap_funded_affiliations].last + expect(affil[:name]).to eql(org.name) + expect(affil[:affiliation_id][:type]).to eql(org.identifiers.last.identifier_format) + expect(affil[:affiliation_id][:identifier]).to eql(org.identifiers.last.value) + end + end +end diff --git a/spec/views/api/v2/plans/_project.json.jbuilder_spec.rb b/spec/views/api/v2/plans/_project.json.jbuilder_spec.rb new file mode 100644 index 0000000000..53af088379 --- /dev/null +++ b/spec/views/api/v2/plans/_project.json.jbuilder_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/plans/_project.json.jbuilder' do + before do + @plan = build(:plan, funder: build(:org, :funder)) + render partial: 'api/v2/plans/project', locals: { plan: @plan } + @json = JSON.parse(rendered).with_indifferent_access + end + + describe 'includes all of the project attributes' do + it 'includes :title' do + expect(@json[:title]).to eql(@plan.title) + end + + it 'includes :description' do + expect(@json[:description]).to eql(@plan.description) + end + + it 'includes :start' do + expect(@json[:start]).to eql(@plan.start_date.to_formatted_s(:iso8601)) + end + + it 'includes :end' do + expect(@json[:end]).to eql(@plan.end_date.to_formatted_s(:iso8601)) + end + + it 'includes the :funder' do + expect(@json[:funding].length).to be(1) + end + end +end diff --git a/spec/views/api/v2/plans/_show.json.jbuilder_spec.rb b/spec/views/api/v2/plans/_show.json.jbuilder_spec.rb new file mode 100644 index 0000000000..989a2f638b --- /dev/null +++ b/spec/views/api/v2/plans/_show.json.jbuilder_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/plans/_show.json.jbuilder' do + before do + Rails.configuration.x.madmp.enable_dmp_id_registration = true + + @plan = create(:plan) + @data_contact = create(:contributor, data_curation: true, plan: @plan) + @pi = create(:contributor, investigation: true, plan: @plan) + @plan.contributors = [@data_contact, @pi] + create(:identifier, identifiable: @plan) + + # Create an Api Client and connect it to the Plan + @client = create(:api_client) + scheme = create(:identifier_scheme, :for_plans, name: @client.name.downcase) + @client_identifier = create(:identifier, identifier_scheme: scheme, identifiable: @plan) + + @plan.save + @plan.reload + @presenter = Api::V2::PlanPresenter.new(plan: @plan) + end + + describe 'includes all of the DMP attributes' do + before do + render partial: 'api/v2/plans/show', locals: { client: @client, plan: @plan } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'includes the :title' do + expect(@json[:title]).to eql(@plan.title) + end + + it 'includes the :description' do + expect(@json[:description]).to eql(@plan.description) + end + + it 'includes the :language' do + expected = Api::V1::LanguagePresenter.three_char_code( + lang: LocaleService.default_locale + ) + expect(@json[:language]).to eql(expected) + end + + it 'includes the :created' do + expect(@json[:created]).to eql(@plan.created_at.to_formatted_s(:iso8601)) + end + + it 'includes the :modified' do + expect(@json[:modified]).to eql(@plan.updated_at.to_formatted_s(:iso8601)) + end + + it 'includes :ethical_issues' do + expected = Api::V1::ConversionService.boolean_to_yes_no_unknown(@plan.ethical_issues) + expect(@json[:ethical_issues_exist]).to eql(expected) + end + + it 'includes :ethical_issues_description' do + expect(@json[:ethical_issues_description]).to eql(@plan.ethical_issues_description) + end + + it 'includes :ethical_issues_report' do + expect(@json[:ethical_issues_report]).to eql(@plan.ethical_issues_report) + end + + it 'returns the URL of the plan as the :dmp_id if no DMP ID is defined' do + expected = Rails.application.routes.url_helpers.api_v2_plan_url(@plan) + expect(@json[:dmp_id][:type]).to eql('url') + expect(@json[:dmp_id][:identifier]).to eql(expected) + end + + it 'includes the :contact' do + expect(@json[:contact][:mbox]).to eql(@data_contact.email) + end + + it 'includes the :contributors' do + emails = @json[:contributor].pluck(:mbox) + expect(emails.include?(@pi.email)).to be(true) + end + + # TODO: make sure this is working once the new Cost theme and Currency + # question type have been implemented + it 'includes the :cost' do + expect(@json[:cost]).to be_nil + end + + it 'includes the :project' do + expect(@json[:project].length).to be(1) + end + + it 'includes the :dataset' do + expect(@json[:dataset].length).to be(1) + end + end + + describe 'when the system mints DMP IDs', skip: 'DmpIdService not implemented' do + before do + scheme = create(:identifier_scheme) + DmpIdService.expects(:identifier_scheme).at_least(1).returns(scheme) + @doi = create(:identifier, value: '10.9999/123abc.zy/x23', identifiable: @plan, + identifier_scheme: scheme) + @plan.reload + render partial: 'api/v2/plans/show', locals: { client: @client, plan: @plan } + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'returns the DMP ID for the :dmp_id if one is present' do + expect(@json[:dmp_id][:type]).to eql('doi') + expect(@json[:dmp_id][:identifier]).to eql(@doi.value) + end + end +end diff --git a/spec/views/api/v2/plans/index.json.jbuilder_spec.rb b/spec/views/api/v2/plans/index.json.jbuilder_spec.rb new file mode 100644 index 0000000000..5d47770c52 --- /dev/null +++ b/spec/views/api/v2/plans/index.json.jbuilder_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/plans/index.json.jbuilder' do + before do + @plan = create(:plan) + + @client = create(:api_client) + @items = [@plan] + @total_items = 1 + + render template: 'api/v2/plans/index' + @json = JSON.parse(rendered).with_indifferent_access + end + + it 'renders the _standard_response template' do + expect(response).to render_template('api/v2/_standard_response') + end + + it ':items array to be empty' do + expect(@json[:items].length).to be(1) + expect(@json[:items].first[:dmp][:title]).to eql(@plan.title) + end +end diff --git a/spec/views/api/v2/templates/index.json.jbuilder_spec.rb b/spec/views/api/v2/templates/index.json.jbuilder_spec.rb new file mode 100644 index 0000000000..99b35375ec --- /dev/null +++ b/spec/views/api/v2/templates/index.json.jbuilder_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'api/v2/templates/index.json.jbuilder' do + before do + @application = Faker::Lorem.word + @url = Faker::Internet.url + @code = [200, 400, 404, 500].sample + + @template1 = create(:template, :published, org: create(:org), phases: 1) + @template2 = create(:template, :published) + + assign :server, @application + assign :items, [@template1, @template2] + + @resp = OpenStruct.new(status: @code) + @req = Net::HTTPGenericRequest.new('GET', nil, nil, @url) + end + + describe 'includes all of the Template attributes' do + before do + render template: 'api/v2/templates/index', + locals: { response: @resp, request: @req } + @json = JSON.parse(rendered).with_indifferent_access + + @template = @json[:items].first[:dmp_template] + end + + it 'includes both templates' do + expect(@json[:items].length).to be(2) + end + + it 'includes the :title' do + expect(@template[:title]).to eql(@template1.title) + end + + it 'includes the :description' do + expect(@template[:description]).to eql(@template1.description) + end + + it 'includes the :version' do + expect(@template[:version]).to eql(@template1.version) + end + + it 'includes the :created' do + expect(@template[:created]).to eql(@template1.created_at.to_formatted_s(:iso8601)) + end + + it 'includes the :modified' do + expect(@template[:modified]).to eql(@template1.updated_at.to_formatted_s(:iso8601)) + end + + it 'includes the :affiliation' do + expect(@template[:affiliation][:name]).to eql(@template1.org.name) + end + + it 'includes the :template_ids' do + expect(@template[:template_id][:identifier]).to eql(@template1.id.to_s) + expect(@template[:template_id][:type]).to eql('other') + end + + # The show_url parameter is either false or not included + it 'includes the :phases' do + expect(@json[:items].first[:dmp_template][:phases]).to be_nil + end + end + + # The show_url parameter is true + describe 'when the show_phases url parameter is true' do + before do + @show_phases = true + render template: 'api/v2/templates/index', + locals: { response: @resp, request: @req, show_phases: @show_phases } + @json = JSON.parse(rendered).with_indifferent_access + end + end +end