├── docs-intro.html ├── .ruby-version ├── test ├── fixtures │ ├── hello.txt │ ├── no_services_and_info_data_found_fixture.json │ ├── sub_sector_organisations.json │ └── services_and_info_fixture.json ├── exceptions_test.rb ├── support_test.rb ├── middleware │ └── govuk_header_sniffer_test.rb ├── search_test_helpers.rb ├── govuk_headers_test.rb ├── pacts │ ├── publishing_api │ │ ├── lookup_content_ids_pact_test.rb │ │ ├── graphql_query_pact_test.rb │ │ ├── get_editions_pact_test.rb │ │ ├── get_schemas_pact_test.rb │ │ ├── put_intent_pact_test.rb │ │ ├── get_linkables_pact_test.rb │ │ ├── delete_intent_pact_test.rb │ │ ├── get_links_by_content_id_pact_test.rb │ │ ├── get_schema_by_name_pact_test.rb │ │ ├── lookup_content_id_pact_test.rb │ │ ├── get_links_pact_test.rb │ │ ├── get_expanded_links_pact_test.rb │ │ ├── get_links_changes_pact_test.rb │ │ ├── discard_draft_pact_test.rb │ │ ├── unreserve_path_pact_test.rb │ │ ├── get_paged_content_items_pact_test.rb │ │ ├── put_path_pact_test.rb │ │ ├── get_linked_items_pact_test.rb │ │ ├── republish_pact_test.rb │ │ ├── get_host_content_item_for_content_id_pact_test.rb │ │ ├── get_live_content_pact_test.rb │ │ ├── get_paged_editions_pact_test.rb │ │ └── unpublish_pact_test.rb │ ├── signon_api_pact_test.rb │ ├── support_api_pact_test.rb │ ├── locations_api_pact_test.rb │ ├── calendars_pact_test.rb │ ├── link_checker_api_pact_test.rb │ ├── places_manager_api_pact_test.rb │ └── organisations_api_pact_test.rb ├── test_helper.rb ├── link_checker_api_test.rb ├── organisations_api_test.rb ├── publishing_api_test.rb ├── gds_api_base_test.rb ├── calendars_test.rb ├── test_helpers │ ├── email_alert_api_test.rb │ └── asset_manager_test.rb ├── search_api_v2_test.rb ├── support │ └── pact_helper.rb ├── locations_api_test.rb ├── asset_manager_test.rb ├── publishing_api │ └── special_route_publisher_test.rb └── places_manager_api_test.rb ├── lib ├── gds_api │ ├── test_helpers │ │ ├── json_client_helper.rb │ │ ├── common_responses.rb │ │ ├── content_item_helpers.rb │ │ ├── licence_application.rb │ │ ├── locations_api.rb │ │ ├── calendars.rb │ │ ├── places_manager.rb │ │ ├── worldwide.rb │ │ ├── link_checker_api.rb │ │ ├── content_store.rb │ │ ├── asset_manager.rb │ │ ├── organisations.rb │ │ └── search.rb │ ├── version.rb │ ├── support.rb │ ├── calendars.rb │ ├── organisations.rb │ ├── search_api_v2.rb │ ├── signon_api.rb │ ├── govuk_headers.rb │ ├── middleware │ │ └── govuk_header_sniffer.rb │ ├── licence_application.rb │ ├── local_links_manager.rb │ ├── places_manager.rb │ ├── railtie.rb │ ├── locations_api.rb │ ├── publishing_api │ │ └── special_route_publisher.rb │ ├── base.rb │ ├── list_response.rb │ ├── content_store.rb │ ├── exceptions.rb │ ├── link_checker_api.rb │ ├── account_api.rb │ ├── response.rb │ └── worldwide.rb └── gds-api-adapters.rb ├── .gitignore ├── Gemfile ├── .yardopts ├── .github ├── pull_request_template.md ├── dependabot.yml └── workflows │ ├── autorelease.yml │ ├── actionlint.yml │ ├── publish.yml │ └── ci.yml ├── .govuk_dependabot_merger.yml ├── .rubocop.yml ├── Rakefile ├── LICENCE.txt └── gds-api-adapters.gemspec /docs-intro.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.8 2 | -------------------------------------------------------------------------------- /test/fixtures/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/json_client_helper.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/json_client" 2 | -------------------------------------------------------------------------------- /lib/gds_api/version.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | VERSION = "101.2.0".freeze 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | coverage/* 3 | pkg/ 4 | *.gem 5 | .bundle 6 | /spec/pacts 7 | /log 8 | .yardoc 9 | doc 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in gds-api-adapters.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/gds-api-adapters.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/railtie" if defined?(Rails) 2 | require "gds_api/exceptions" 3 | require "gds_api" 4 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --markup markdown 3 | --output-dir docs 4 | --embed-mixins 5 | --no-stats 6 | --readme docs-intro.html 7 | --api documented 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | This repo is owned by the publishing platform team. Please let us know in #govuk-publishing-platform when you raise any PRs. 2 | -------------------------------------------------------------------------------- /.govuk_dependabot_merger.yml: -------------------------------------------------------------------------------- 1 | api_version: 2 2 | defaults: 3 | allowed_semver_bumps: 4 | - patch 5 | - minor 6 | auto_merge: true 7 | update_external_dependencies: true 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /lib/gds_api/support.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::Support < GdsApi::Base 4 | def feedback_url(slug) 5 | "#{base_url}/anonymous_feedback?path=#{slug}" 6 | end 7 | 8 | private 9 | 10 | def base_url 11 | endpoint 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/exceptions_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class GdsApiBaseTest < Minitest::Test 4 | def test_fingerprints_per_exception_type 5 | exception = GdsApi::HTTPBadGateway.new(200) 6 | 7 | assert_equal ["GdsApi::HTTPBadGateway"], exception.sentry_context[:fingerprint] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/gds_api/calendars.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::Calendars < GdsApi::Base 4 | def bank_holidays(division = nil) 5 | json_url = "#{endpoint}/bank-holidays" 6 | json_url += "/#{division.to_s.tr('_', '-')}" unless division.nil? 7 | json_url += ".json" 8 | get_json json_url 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.github/workflows/autorelease.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: {} 3 | schedule: 4 | - cron: '30 10 * * 1-5' # 10:30am UTC, Mon-Fri. 5 | 6 | jobs: 7 | autorelease: 8 | uses: alphagov/govuk-infrastructure/.github/workflows/autorelease-rubygem.yml@main 9 | secrets: 10 | GH_TOKEN: ${{ secrets.GOVUK_CI_GITHUB_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Actions 2 | on: 3 | push: 4 | paths: ['.github/**'] 5 | jobs: 6 | actionlint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v6 10 | with: 11 | show-progress: false 12 | - uses: alphagov/govuk-infrastructure/.github/actions/actionlint@main 13 | -------------------------------------------------------------------------------- /test/fixtures/no_services_and_info_data_found_fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [], 3 | "total": 712, 4 | "start": 0, 5 | "facets": { 6 | "specialist_sectors": { 7 | "options": [], 8 | "documents_with_no_value": 712, 9 | "total_options": 0, 10 | "missing_options": 0 11 | } 12 | }, 13 | "suggested_queries": [] 14 | } 15 | -------------------------------------------------------------------------------- /lib/gds_api/organisations.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::Organisations < GdsApi::Base 4 | def organisations 5 | get_list "#{base_url}/organisations" 6 | end 7 | 8 | def organisation(organisation_slug) 9 | get_json "#{base_url}/organisations/#{organisation_slug}" 10 | end 11 | 12 | private 13 | 14 | def base_url 15 | "#{endpoint}/api" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/support_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/support" 3 | 4 | describe GdsApi::Support do 5 | before do 6 | @base_api_url = Plek.find("support") 7 | @api = GdsApi::Support.new(@base_api_url) 8 | end 9 | 10 | it "gets the correct feedback URL" do 11 | assert_equal( 12 | "#{@base_api_url}/anonymous_feedback?path=foo", 13 | @api.feedback_url("foo"), 14 | ) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: [CI] 7 | types: [completed] 8 | branches: [main] 9 | 10 | jobs: 11 | publish: 12 | if: ${{ github.ref == 'refs/heads/main' }} 13 | permissions: 14 | contents: write 15 | uses: alphagov/govuk-infrastructure/.github/workflows/publish-rubygem.yml@main 16 | secrets: 17 | GEM_HOST_API_KEY: ${{ secrets.ALPHAGOV_RUBYGEMS_API_KEY }} 18 | -------------------------------------------------------------------------------- /lib/gds_api/search_api_v2.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | class SearchApiV2 < Base 3 | def search(args, additional_headers = {}) 4 | request_url = "#{endpoint}/search.json?#{Rack::Utils.build_nested_query(args)}" 5 | get_json(request_url, additional_headers) 6 | end 7 | 8 | def autocomplete(query) 9 | args = { q: query } 10 | request_url = "#{endpoint}/autocomplete.json?#{Rack::Utils.build_nested_query(args)}" 11 | get_json(request_url) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/gds_api/signon_api.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::SignonApi < GdsApi::Base 4 | # Get users with specific UUIDs 5 | # 6 | # @param uuids [Array] 7 | # 8 | # signon_api.users( 9 | # ["7ac47b33-c09c-4c1d-a9a7-0cfef99081ac"], 10 | # ) 11 | # 12 | # @return [GdsApi::Response] A response containing a list of users with the specified UUIDs 13 | def get_users(uuids:) 14 | query = query_string(uuids:) 15 | get_json("#{endpoint}/api/users#{query}") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/gds_api/govuk_headers.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | class GovukHeaders 3 | class << self 4 | def set_header(header_name, value) 5 | header_data[header_name] = value 6 | end 7 | 8 | def headers 9 | header_data.reject { |_k, v| v.nil? || v.empty? } 10 | end 11 | 12 | def clear_headers 13 | Thread.current[:headers] = {} 14 | end 15 | 16 | private 17 | 18 | def header_data 19 | Thread.current[:headers] ||= {} 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/gds_api/middleware/govuk_header_sniffer.rb: -------------------------------------------------------------------------------- 1 | require_relative "../govuk_headers" 2 | 3 | module GdsApi 4 | class GovukHeaderSniffer 5 | def initialize(app, header_name) 6 | @app = app 7 | @header_name = header_name 8 | end 9 | 10 | def call(env) 11 | GdsApi::GovukHeaders.set_header(readable_name, env[@header_name]) 12 | @app.call(env) 13 | end 14 | 15 | private 16 | 17 | def readable_name 18 | @header_name.sub(/^HTTP_/, "").downcase.to_sym 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/gds_api/licence_application.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::LicenceApplication < GdsApi::Base 4 | def all_licences 5 | get_json("#{@endpoint}/api/licences") 6 | end 7 | 8 | def details_for_licence(id, snac_code = nil) 9 | return nil if id.nil? 10 | 11 | get_json(build_licence_url(id, snac_code)) 12 | end 13 | 14 | private 15 | 16 | def build_licence_url(id, snac_code) 17 | if snac_code 18 | "#{@endpoint}/api/licence/#{id}/#{snac_code}" 19 | else 20 | "#{@endpoint}/api/licence/#{id}" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/middleware/govuk_header_sniffer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/middleware/govuk_header_sniffer" 3 | 4 | describe GdsApi::GovukHeaderSniffer do 5 | include Rack::Test::Methods 6 | 7 | let(:inner_app) do 8 | ->(_env) { [200, { "Content-Type" => "text/plain" }, ["All good!"]] } 9 | end 10 | 11 | let(:app) { GdsApi::GovukHeaderSniffer.new(inner_app, "HTTP_GOVUK_REQUEST_ID") } 12 | 13 | it "sniffs custom request headers and stores them for later use" do 14 | header "Govuk-Request-Id", "12345" 15 | get "/" 16 | assert_equal "12345", GdsApi::GovukHeaders.headers[:govuk_request_id] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/search_test_helpers.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/search" 3 | require "gds_api/test_helpers/search" 4 | 5 | class SearchHelpersTest < Minitest::Test 6 | include GdsApi::TestHelpers::Search 7 | 8 | def test_services_and_info_data_returns_an_adequate_response_object 9 | response = stub_search_has_services_and_info_data_for_organisation 10 | 11 | assert_instance_of GdsApi::Response, response 12 | end 13 | 14 | def test_no_services_and_info_data_found_for_organisation 15 | response = stub_search_has_no_services_and_info_data_for_organisation 16 | 17 | assert_instance_of GdsApi::Response, response 18 | assert_equal 0, response["facets"]["specialist_sectors"]["total_options"] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-govuk: 3 | - config/default.yml 4 | 5 | inherit_mode: 6 | merge: 7 | - Exclude 8 | 9 | # ************************************************************** 10 | # TRY NOT TO ADD OVERRIDES IN THIS FILE 11 | # 12 | # This repo is configured to follow the RuboCop GOV.UK styleguide. 13 | # Any rules you override here will cause this repo to diverge from 14 | # the way we write code in all other GOV.UK repos. 15 | # 16 | # See https://github.com/alphagov/rubocop-govuk/blob/main/CONTRIBUTING.md 17 | # ************************************************************** 18 | 19 | Naming/FileName: 20 | Exclude: 21 | # This file is named with kebab case to match the gem name so that 22 | # require "gds-api-adpaters" works. 23 | - lib/gds-api-adapters.rb 24 | -------------------------------------------------------------------------------- /lib/gds_api/local_links_manager.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::LocalLinksManager < GdsApi::Base 4 | def local_link(authority_slug, lgsl, lgil) 5 | url = "#{endpoint}/api/link?authority_slug=#{authority_slug}&lgsl=#{lgsl}&lgil=#{lgil}" 6 | get_json(url) 7 | end 8 | 9 | def local_link_by_custodian_code(local_custodian_code, lgsl, lgil) 10 | url = "#{endpoint}/api/link?local_custodian_code=#{local_custodian_code}&lgsl=#{lgsl}&lgil=#{lgil}" 11 | get_json(url) 12 | end 13 | 14 | def local_authority(authority_slug) 15 | url = "#{endpoint}/api/local-authority?authority_slug=#{authority_slug}" 16 | get_json(url) 17 | end 18 | 19 | def local_authority_by_custodian_code(local_custodian_code) 20 | url = "#{endpoint}/api/local-authority?local_custodian_code=#{local_custodian_code}" 21 | get_json(url) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/gds_api/places_manager.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::PlacesManager < GdsApi::Base 4 | def api_url(type, params) 5 | vals = %i[limit lat lng postcode local_authority_slug].select { |p| params.include? p } 6 | querystring = URI.encode_www_form(vals.map { |p| [p, params[p]] }) 7 | "#{@endpoint}/places/#{type}.json?#{querystring}" 8 | end 9 | 10 | def places(type, lat, lon, limit = 5) 11 | url = api_url(type, lat:, lng: lon, limit:) 12 | get_json(url) 13 | end 14 | 15 | def places_for_postcode(type, postcode, limit = 5, local_authority_slug = nil) 16 | options = { postcode:, limit: } 17 | options.merge!(local_authority_slug:) if local_authority_slug 18 | url = api_url(type, options) 19 | get_json(url) || [] 20 | end 21 | 22 | def places_kml(type) 23 | get_raw("#{@endpoint}/places/#{type}.kml").body 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/govuk_headers_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | require "gds_api/govuk_headers" 3 | 4 | describe GdsApi::GovukHeaders do 5 | before :each do 6 | Thread.current[:headers] = nil if Thread.current[:headers] 7 | end 8 | 9 | after :each do 10 | GdsApi::GovukHeaders.clear_headers 11 | end 12 | 13 | it "supports read/write of headers" do 14 | GdsApi::GovukHeaders.set_header("GDS-Request-Id", "123-456") 15 | GdsApi::GovukHeaders.set_header("Content-Type", "application/pdf") 16 | 17 | assert_equal( 18 | { 19 | "GDS-Request-Id" => "123-456", 20 | "Content-Type" => "application/pdf", 21 | }, 22 | GdsApi::GovukHeaders.headers, 23 | ) 24 | end 25 | 26 | it "supports clearing of headers" do 27 | GdsApi::GovukHeaders.set_header("GDS-Request-Id", "123-456") 28 | 29 | GdsApi::GovukHeaders.clear_headers 30 | 31 | assert_equal({}, GdsApi::GovukHeaders.headers) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rdoc/task" 2 | require "rake/testtask" 3 | 4 | RDoc::Task.new do |rd| 5 | rd.rdoc_files.include("lib/**/*.rb") 6 | rd.rdoc_dir = "rdoc" 7 | end 8 | 9 | Rake::TestTask.new("test") do |t| 10 | t.libs << "test" 11 | t.test_files = FileList["test/**/*_test.rb"] 12 | t.warning = false 13 | end 14 | 15 | Rake::TestTask.new("pact_test") do |t| 16 | t.libs << "test" 17 | t.test_files = FileList["test/pacts/**/*_test.rb"] 18 | t.warning = false 19 | end 20 | 21 | task default: %i[lint test] 22 | 23 | require "pact_broker/client/tasks" 24 | 25 | PactBroker::Client::PublicationTask.new do |task| 26 | task.consumer_version = ENV.fetch("PACT_CONSUMER_VERSION") 27 | task.pact_broker_base_url = ENV.fetch("PACT_BROKER_BASE_URL") 28 | task.pact_broker_basic_auth = { 29 | username: ENV.fetch("PACT_BROKER_USERNAME"), 30 | password: ENV.fetch("PACT_BROKER_PASSWORD"), 31 | } 32 | task.pattern = ENV["PACT_PATTERN"] if ENV["PACT_PATTERN"] 33 | end 34 | 35 | desc "Run the linter against changed files" 36 | task :lint do 37 | sh "bundle exec rubocop" 38 | end 39 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/lookup_content_ids_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | require "json" 4 | 5 | describe "GdsApi::PublishingApi#lookup_content_ids pact test" do 6 | include PactTest 7 | 8 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 9 | 10 | it "returns the content_id for a base_path" do 11 | reponse_hash = { 12 | "/foo" => "08f86d00-e95f-492f-af1d-470c5ba4752e", 13 | "/bar" => "ca6c58a6-fb9d-479d-b3e6-74908781cb18", 14 | } 15 | 16 | publishing_api 17 | .given("there are live content items with base_paths /foo and /bar") 18 | .upon_receiving("a request for multiple base_paths") 19 | .with( 20 | method: :post, 21 | path: "/lookup-by-base-path", 22 | body: { 23 | base_paths: ["/foo", "/bar"], 24 | }, 25 | headers: { 26 | "Content-Type" => "application/json", 27 | }, 28 | ) 29 | .will_respond_with( 30 | status: 200, 31 | body: reponse_hash, 32 | ) 33 | 34 | api_client.lookup_content_ids(base_paths: ["/foo", "/bar"]) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 Crown Copyright (Government Digital Service) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/common_responses.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | module TestHelpers 3 | module CommonResponses 4 | def titleize_slug(slug, options = {}) 5 | if options[:title_case] 6 | slug.tr("-", " ").gsub(/\b./, &:upcase) 7 | else 8 | slug.tr("-", " ").capitalize 9 | end 10 | end 11 | 12 | # expects a slug like "ministry-of-funk" 13 | # returns an acronym like "MOF" 14 | def acronymize_slug(slug) 15 | initials = slug.gsub(/\b\w+/) { |m| m[0] }.delete("-") 16 | initials.upcase 17 | end 18 | 19 | def response_base 20 | { 21 | "_response_info" => { 22 | "status" => "ok", 23 | }, 24 | } 25 | end 26 | alias_method :singular_response_base, :response_base 27 | 28 | def plural_response_base 29 | response_base.merge( 30 | "description" => "Tags!", 31 | "total" => 100, 32 | "start_index" => 1, 33 | "page_size" => 100, 34 | "current_page" => 1, 35 | "pages" => 1, 36 | "results" => [], 37 | ) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/graphql_query_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#graphql_query pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "returns the response to the query" do 10 | query = <<~QUERY 11 | { 12 | edition(base_path: "/my-document") { 13 | ... on Edition { 14 | title 15 | } 16 | } 17 | } 18 | QUERY 19 | 20 | publishing_api 21 | .given("a published content item exists with base_path /my-document") 22 | .upon_receiving("a GraphQL request") 23 | .with( 24 | method: :post, 25 | path: "/graphql", 26 | body: { query: }, 27 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 28 | ) 29 | .will_respond_with( 30 | status: 200, 31 | body: { 32 | "data": { 33 | "edition": { 34 | "title": "My document", 35 | }, 36 | }, 37 | }, 38 | ) 39 | 40 | api_client.graphql_query(query) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_editions_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_editions pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 9 | 10 | it "responds correctly when there are editions available to paginate over" do 11 | publishing_api 12 | .given("there are live content items with base_paths /foo and /bar") 13 | .upon_receiving("a get editions request") 14 | .with( 15 | method: :get, 16 | path: "/v2/editions", 17 | query: "fields%5B%5D=content_id", 18 | ) 19 | .will_respond_with( 20 | status: 200, 21 | body: { 22 | results: [ 23 | { content_id: "08f86d00-e95f-492f-af1d-470c5ba4752e" }, 24 | { content_id: "ca6c58a6-fb9d-479d-b3e6-74908781cb18" }, 25 | ], 26 | links: [ 27 | { href: "http://example.org/v2/editions?fields%5B%5D=content_id", rel: "self" }, 28 | ], 29 | }, 30 | ) 31 | 32 | api_client.get_editions(fields: %w[content_id]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_schemas_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_schemas pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | let(:schemas) do 10 | { 11 | "email_address": { 12 | type: "object", 13 | required: %w[a], 14 | properties: { 15 | email_address: { "some" => "schema" }, 16 | }, 17 | }, 18 | "tax_license": { 19 | type: "object", 20 | required: %w[a], 21 | properties: { 22 | tax_license: { "another" => "schema" }, 23 | }, 24 | }, 25 | } 26 | end 27 | 28 | before do 29 | publishing_api 30 | .given("there are publisher schemas") 31 | .upon_receiving("a get schemas request") 32 | .with( 33 | method: :get, 34 | path: "/v2/schemas", 35 | ) 36 | .will_respond_with( 37 | status: 200, 38 | body: schemas, 39 | ) 40 | end 41 | 42 | it "returns all the schemas" do 43 | response = api_client.get_schemas 44 | assert_equal(schemas.to_json, response.to_json) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.setup :default, :development, :test 3 | 4 | require "simplecov" 5 | 6 | SimpleCov.start do 7 | add_filter "/test/" 8 | add_group "Test Helpers", "lib/gds_api/test_helpers" 9 | end 10 | 11 | require "minitest/autorun" 12 | require "minitest/around" 13 | require "rack/utils" 14 | require "rack/test" 15 | require "mocha/minitest" 16 | require "timecop" 17 | require "gds-api-adapters" 18 | require "govuk_schemas" 19 | require "climate_control" 20 | 21 | class Minitest::Test 22 | def teardown 23 | Timecop.return 24 | end 25 | end 26 | 27 | require "pact/consumer/minitest" 28 | module PactTest 29 | include Pact::Consumer::Minitest 30 | 31 | def before_setup 32 | # Pact does its own stubbing of network connections, so we want to 33 | # prevent WebMock interfering when pact is being used. 34 | ::WebMock.allow_net_connect! 35 | super 36 | end 37 | 38 | def after_teardown 39 | super 40 | ::WebMock.disable_net_connect! 41 | end 42 | end 43 | 44 | def load_fixture_file(filename) 45 | File.open(File.join(File.dirname(__FILE__), "fixtures", filename), encoding: "utf-8") 46 | end 47 | 48 | require "gds_api/test_helpers/json_client_helper" 49 | require "support/pact_helper" 50 | 51 | require "webmock/minitest" 52 | WebMock.disable_net_connect! 53 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/put_intent_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#put_intent pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "responds with 200 OK if publish intent is valid" do 10 | base_path = "/test-intent" 11 | publish_intent = { publishing_app: "publisher", 12 | rendering_app: "frontend", 13 | publish_time: "2019-11-11t17:56:17+00:00" } 14 | 15 | publishing_api 16 | .given("no content exists") 17 | .upon_receiving("a request to create a publish intent") 18 | .with( 19 | method: :put, 20 | path: "/publish-intent#{base_path}", 21 | body: publish_intent, 22 | headers: { 23 | "Content-Type" => "application/json", 24 | }, 25 | ) 26 | .will_respond_with( 27 | status: 200, 28 | body: { 29 | "publishing_app" => "publisher", 30 | "rendering_app" => "frontend", 31 | "publish_time" => "2019-11-11t17:56:17+00:00", 32 | }, 33 | headers: { 34 | "Content-Type" => "application/json; charset=utf-8", 35 | }, 36 | ) 37 | 38 | api_client.put_intent(base_path, publish_intent) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_linkables_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_linkables pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:linkables) do 9 | [ 10 | { 11 | "title" => "Content Item A", 12 | "internal_name" => "an internal name", 13 | "content_id" => "aaaaaaaa-aaaa-1aaa-aaaa-aaaaaaaaaaaa", 14 | "publication_state" => "draft", 15 | "base_path" => "/a-base-path", 16 | }, 17 | { 18 | "title" => "Content Item B", 19 | "internal_name" => "Content Item B", 20 | "content_id" => "bbbbbbbb-bbbb-2bbb-bbbb-bbbbbbbbbbbb", 21 | "publication_state" => "published", 22 | "base_path" => "/another-base-path", 23 | }, 24 | ] 25 | end 26 | 27 | it "returns the content items of a given document_type" do 28 | publishing_api 29 | .given("there is content with document_type 'taxon'") 30 | .upon_receiving("a get linkables request") 31 | .with( 32 | method: :get, 33 | path: "/v2/linkables", 34 | query: "document_type=taxon", 35 | ) 36 | .will_respond_with( 37 | status: 200, 38 | body: linkables, 39 | ) 40 | 41 | api_client.get_linkables(document_type: "taxon") 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/content_item_helpers.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | module TestHelpers 3 | module ContentItemHelpers 4 | def content_item_for_base_path(base_path) 5 | { 6 | "title" => titleize_base_path(base_path), 7 | "description" => "Description for #{base_path}", 8 | "schema_name" => "guide", 9 | "document_type" => "guide", 10 | "public_updated_at" => "2014-05-06T12:01:00+00:00", 11 | # base_path is added in as necessary (ie for content-store GET responses) 12 | # "base_path" => base_path, 13 | "details" => { 14 | "body" => "Some content for #{base_path}", 15 | }, 16 | } 17 | end 18 | 19 | def gone_content_item_for_base_path(base_path) 20 | { 21 | "title" => nil, 22 | "description" => nil, 23 | "document_type" => "gone", 24 | "schema_name" => "gone", 25 | "public_updated_at" => nil, 26 | "base_path" => base_path, 27 | "withdrawn_notice" => {}, 28 | "details" => {}, 29 | } 30 | end 31 | 32 | def titleize_base_path(base_path, options = {}) 33 | if options[:title_case] 34 | base_path.tr("-", " ").gsub(/\b./, &:upcase) 35 | else 36 | base_path.gsub(%r{[-/]}, " ").strip.capitalize 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/link_checker_api_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/link_checker_api" 3 | require "gds_api/test_helpers/link_checker_api" 4 | 5 | describe GdsApi::LinkCheckerApi do 6 | include GdsApi::TestHelpers::LinkCheckerApi 7 | 8 | let(:base_url) { Plek.find("link-checker-api") } 9 | let(:api_client) { GdsApi::LinkCheckerApi.new(base_url) } 10 | 11 | describe "#check" do 12 | it "returns a useful response" do 13 | stub_link_checker_api_check(uri: "http://example.com", status: :broken) 14 | 15 | link_report = api_client.check("http://example.com") 16 | 17 | assert_equal :broken, link_report.status 18 | end 19 | end 20 | 21 | describe "#create_batch" do 22 | it "returns a useful response" do 23 | stub_link_checker_api_create_batch(uris: ["http://example.com"]) 24 | 25 | batch_report = api_client.create_batch(["http://example.com"]) 26 | 27 | assert_equal :in_progress, batch_report.status 28 | assert_equal "http://example.com", batch_report.links[0].uri 29 | end 30 | end 31 | 32 | describe "#get_batch" do 33 | it "returns a useful response" do 34 | stub_link_checker_api_get_batch(id: 10, links: [{ uri: "http://example.com" }]) 35 | 36 | batch_report = api_client.get_batch(10) 37 | 38 | assert_equal :completed, batch_report.status 39 | assert_equal "http://example.com", batch_report.links[0].uri 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/delete_intent_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#delete_intent pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "returns 200 OK if intent existed and was deleted" do 10 | base_path = "/test-intent" 11 | 12 | publishing_api 13 | .given("a publish intent exists at /test-intent") 14 | .upon_receiving("a request to delete a publish intent") 15 | .with( 16 | method: :delete, 17 | path: "/publish-intent#{base_path}", 18 | ) 19 | .will_respond_with( 20 | status: 200, 21 | body: {}, 22 | headers: { 23 | "Content-Type" => "application/json; charset=utf-8", 24 | }, 25 | ) 26 | 27 | api_client.destroy_intent(base_path) 28 | end 29 | 30 | it "returns 404 Not found if the intent does not exist" do 31 | base_path = "/test-intent" 32 | 33 | publishing_api 34 | .given("no content exists") 35 | .upon_receiving("a request to delete a publish intent") 36 | .with( 37 | method: :delete, 38 | path: "/publish-intent#{base_path}", 39 | ) 40 | .will_respond_with( 41 | status: 404, 42 | body: {}, 43 | headers: { 44 | "Content-Type" => "application/json; charset=utf-8", 45 | }, 46 | ) 47 | 48 | api_client.destroy_intent(base_path) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_links_by_content_id_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_links_by_content_id pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "returns the links for some content_ids" do 10 | content_id_with_links = "bed722e6-db68-43e5-9079-063f623335a7" 11 | content_id_no_links = "f40a63ce-ac0c-4102-84d1-f1835cb7daac" 12 | 13 | response_hash = { 14 | content_id_with_links => { 15 | "links" => { 16 | "taxons" => %w[20583132-1619-4c68-af24-77583172c070], 17 | }, 18 | "version" => 2, 19 | }, 20 | content_id_no_links => { 21 | "links" => {}, 22 | "version" => 0, 23 | }, 24 | } 25 | 26 | publishing_api 27 | .given("taxon links exist for content_id bed722e6-db68-43e5-9079-063f623335a7") 28 | .upon_receiving("a bulk_links request") 29 | .with( 30 | method: :post, 31 | path: "/v2/links/by-content-id", 32 | body: { 33 | content_ids: [content_id_with_links, content_id_no_links], 34 | }, 35 | headers: { 36 | "Content-Type" => "application/json", 37 | }, 38 | ) 39 | .will_respond_with( 40 | status: 200, 41 | body: response_hash, 42 | ) 43 | 44 | api_client.get_links_for_content_ids([content_id_with_links, content_id_no_links]) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/licence_application.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/test_helpers/json_client_helper" 2 | 3 | module GdsApi 4 | module TestHelpers 5 | module LicenceApplication 6 | # Generally true. If you are initializing the client differently, 7 | # you could redefine/override the constant or stub directly. 8 | LICENCE_APPLICATION_ENDPOINT = Plek.find("licensify") 9 | 10 | def stub_licence_exists(identifier, licence) 11 | licence = licence.to_json unless licence.is_a?(String) 12 | stub_request(:get, "#{LICENCE_APPLICATION_ENDPOINT}/api/licence/#{identifier}") 13 | .with(headers: GdsApi::JsonClient.default_request_headers) 14 | .to_return(status: 200, 15 | body: licence) 16 | end 17 | 18 | def stub_licence_does_not_exist(identifier) 19 | stub_request(:get, "#{LICENCE_APPLICATION_ENDPOINT}/api/licence/#{identifier}") 20 | .with(headers: GdsApi::JsonClient.default_request_headers) 21 | .to_return(status: 404, 22 | body: "{\"error\": [\"Unrecognised Licence Id: #{identifier}\"]}") 23 | end 24 | 25 | def stub_licence_times_out(identifier) 26 | stub_request(:get, "#{LICENCE_APPLICATION_ENDPOINT}/api/licence/#{identifier}").to_timeout 27 | end 28 | 29 | def stub_licence_returns_error(identifier) 30 | stub_request(:get, "#{LICENCE_APPLICATION_ENDPOINT}/api/licence/#{identifier}").to_return(status: 500) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/locations_api.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | module TestHelpers 3 | module LocationsApi 4 | LOCATIONS_API_ENDPOINT = Plek.find("locations-api") 5 | 6 | def stub_locations_api_has_location(postcode, locations) 7 | results = [] 8 | locations.each_with_index do |l, i| 9 | results << { 10 | "address" => l["address"] || "Empty Address #{i}", 11 | "latitude" => l["latitude"] || 0, 12 | "longitude" => l["longitude"] || 0, 13 | "local_custodian_code" => l["local_custodian_code"], 14 | } 15 | end 16 | 17 | response = { 18 | "average_latitude" => results.sum { |r| r["latitude"] } / results.size.to_f, 19 | "average_longitude" => results.sum { |r| r["longitude"] } / results.size.to_f, 20 | "results" => results, 21 | } 22 | 23 | stub_request(:get, "#{LOCATIONS_API_ENDPOINT}/v1/locations?postcode=#{postcode}") 24 | .to_return(body: response.to_json, status: 200) 25 | end 26 | 27 | def stub_locations_api_has_no_location(postcode) 28 | stub_request(:get, "#{LOCATIONS_API_ENDPOINT}/v1/locations?postcode=#{postcode}") 29 | .to_return(body: { "results" => nil }.to_json, status: 200) 30 | end 31 | 32 | def stub_locations_api_does_not_have_a_bad_postcode(postcode) 33 | stub_request(:get, "#{LOCATIONS_API_ENDPOINT}/v1/locations?postcode=#{postcode}") 34 | .to_return(body: { "code" => 400, "error" => "Postcode '#{postcode}' is not valid." }.to_json, status: 400) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/gds_api/railtie.rb: -------------------------------------------------------------------------------- 1 | require_relative "middleware/govuk_header_sniffer" 2 | 3 | module GdsApi 4 | class Railtie < Rails::Railtie 5 | initializer "gds_api.initialize_govuk_request_id_sniffer" do |app| 6 | Rails.logger.debug "Using middleware GdsApi::GovukHeaderSniffer to sniff for Govuk-Request-Id header" 7 | app.middleware.use GdsApi::GovukHeaderSniffer, "HTTP_GOVUK_REQUEST_ID" 8 | end 9 | 10 | initializer "gds_api.initialize_govuk_original_url_sniffer" do |app| 11 | Rails.logger.debug "Using middleware GdsApi::GovukHeaderSniffer to sniff for Govuk-Original-Url header" 12 | app.middleware.use GdsApi::GovukHeaderSniffer, "HTTP_GOVUK_ORIGINAL_URL" 13 | end 14 | 15 | initializer "gds_api.initialize_govuk_authenticated_user_sniffer" do |app| 16 | Rails.logger.debug "Using middleware GdsApi::GovukHeaderSniffer to sniff for X-Govuk-Authenticated-User header" 17 | app.middleware.use GdsApi::GovukHeaderSniffer, "HTTP_X_GOVUK_AUTHENTICATED_USER" 18 | end 19 | 20 | initializer "gds_api.initialize_govuk_authenticated_user_organisation_sniffer" do |app| 21 | Rails.logger.debug "Using middleware GdsApi::GovukHeaderSniffer to sniff for X-Govuk-Authenticated-User-Organisation header" 22 | app.middleware.use GdsApi::GovukHeaderSniffer, "HTTP_X_GOVUK_AUTHENTICATED_USER_ORGANISATION" 23 | end 24 | 25 | initializer "gds_api.initialize_govuk_content_id_sniffer" do |app| 26 | Rails.logger.debug "Using middleware GdsApi::GovukHeaderSniffer to sniff for Govuk-Auth-Bypass-Id header" 27 | app.middleware.use GdsApi::GovukHeaderSniffer, "HTTP_GOVUK_AUTH_BYPASS_ID" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/calendars.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | module TestHelpers 3 | module Calendars 4 | def calendars_endpoint(in_division: nil) 5 | endpoint = "#{Plek.new.website_root}/bank-holidays" 6 | endpoint += "/#{in_division}" unless in_division.nil? 7 | "#{endpoint}.json" 8 | end 9 | 10 | def stub_calendars_has_no_bank_holidays(in_division: nil) 11 | stub_calendars_has_bank_holidays_on([], in_division:) 12 | end 13 | 14 | def stub_calendars_has_bank_holidays_on(dates, in_division: nil) 15 | events = dates.map.with_index do |date, idx| 16 | { 17 | title: "Caturday #{idx}!", 18 | date: date.to_date.iso8601, 19 | notes: "Y'know, for cats!", 20 | bunting: true, 21 | } 22 | end 23 | 24 | response = 25 | if in_division.nil? 26 | { 27 | "england-and-wales" => { 28 | division: "england-and-wales", 29 | events:, 30 | }, 31 | "scotland" => { 32 | division: "scotland", 33 | events:, 34 | }, 35 | "northern-ireland" => { 36 | division: "northern-ireland", 37 | events:, 38 | }, 39 | } 40 | else 41 | { 42 | division: in_division, 43 | events:, 44 | } 45 | end 46 | 47 | stub_request(:get, calendars_endpoint(in_division:)) 48 | .to_return(body: response.to_json, status: 200) 49 | end 50 | 51 | def stub_calendars_has_a_bank_holiday_on(date, in_division: nil) 52 | stub_calendars_has_bank_holidays_on([date], in_division:) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_schema_by_name_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi##get_schemas_by_name pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | let(:schema) do 10 | { 11 | "/govuk/publishing-api/content_schemas/dist/formats/email_address/publisher_v2/schema.json": { 12 | type: "object", 13 | required: %w[a], 14 | properties: { 15 | email_address: { "some" => "schema" }, 16 | }, 17 | }, 18 | } 19 | end 20 | 21 | describe "when a schema is found" do 22 | before do 23 | publishing_api 24 | .given("there is a schema for an email_address") 25 | .upon_receiving("a get schema by name request") 26 | .with( 27 | method: :get, 28 | path: "/v2/schemas/email_address", 29 | ) 30 | .will_respond_with( 31 | status: 200, 32 | body: schema, 33 | ) 34 | end 35 | 36 | it "returns the named schema" do 37 | response = api_client.get_schema("email_address") 38 | assert_equal(schema.to_json, response.to_json) 39 | end 40 | end 41 | 42 | describe "when a schema is not found" do 43 | before do 44 | publishing_api 45 | .given("there is not a schema for an email_address") 46 | .upon_receiving("a get schema by name request") 47 | .with( 48 | method: :get, 49 | path: "/v2/schemas/email_address", 50 | ) 51 | .will_respond_with( 52 | status: 404, 53 | ) 54 | end 55 | 56 | it "returns a 404 error" do 57 | assert_raises(GdsApi::HTTPNotFound) do 58 | api_client.get_schema("email_address") 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/lookup_content_id_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#lookup_content_id pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "returns the content_id for a base_path" do 10 | publishing_api 11 | .given("there are live content items with base_paths /foo and /bar") 12 | .upon_receiving("a /lookup-by-base-path-request") 13 | .with( 14 | method: :post, 15 | path: "/lookup-by-base-path", 16 | body: { 17 | base_paths: ["/foo"], 18 | }, 19 | headers: { 20 | "Content-Type" => "application/json", 21 | }, 22 | ) 23 | .will_respond_with( 24 | status: 200, 25 | body: { 26 | "/foo" => "08f86d00-e95f-492f-af1d-470c5ba4752e", 27 | }, 28 | ) 29 | 30 | content_id = api_client.lookup_content_id(base_path: "/foo") 31 | 32 | assert_equal "08f86d00-e95f-492f-af1d-470c5ba4752e", content_id 33 | end 34 | 35 | it "returns the content_id of a draft document for a base_path" do 36 | publishing_api 37 | .given("there is a draft content item with base_path /foo") 38 | .upon_receiving("a /lookup-by-base-path-request") 39 | .with( 40 | method: :post, 41 | path: "/lookup-by-base-path", 42 | body: { 43 | base_paths: ["/foo"], 44 | with_drafts: true, 45 | }, 46 | headers: { 47 | "Content-Type" => "application/json", 48 | }, 49 | ) 50 | .will_respond_with( 51 | status: 200, 52 | body: { 53 | "/foo" => "cbb460a7-60de-4a74-b5be-0b27c6d6af9b", 54 | }, 55 | ) 56 | 57 | api_client.lookup_content_id(base_path: "/foo", with_drafts: true) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/gds_api/locations_api.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "exceptions" 3 | 4 | class GdsApi::LocationsApi < GdsApi::Base 5 | # Get a list of local custodian codes for a postcode 6 | # 7 | # @param [String, nil] postcode The postcode for which the custodian codes are requested 8 | # 9 | # @return [Array] All local custodian codes for a specific postcode 10 | def local_custodian_code_for_postcode(postcode) 11 | response = get_json("#{endpoint}/v1/locations?postcode=#{postcode}") 12 | 13 | return [] if response["results"].nil? 14 | 15 | response["results"].map { |r| r["local_custodian_code"] }.uniq 16 | end 17 | 18 | # Get the average coordinates for a postcode 19 | # 20 | # @param [String, nil] postcode The postcode for which the coordinates are requested 21 | # 22 | # @return [Hash] The average coordinates (two fields, "latitude" and "longitude") for a specific postcode 23 | def coordinates_for_postcode(postcode) 24 | response = get_json("#{endpoint}/v1/locations?postcode=#{postcode}") 25 | 26 | { "latitude" => response["average_latitude"], "longitude" => response["average_longitude"] } unless response["results"].nil? 27 | end 28 | 29 | # Get all results for a postcode 30 | # 31 | # @param [String, nil] postcode The postcode for which results are requested 32 | # 33 | # @return [Hash] The fulls results as returned from Locations API, with the average latitude 34 | # and longitude, and an array of results for individual addresses with lat/long/lcc, eg: 35 | # { 36 | # "average_latitude"=>51.43122412857143, 37 | # "average_longitude"=>-0.37395367142857144, 38 | # "results"=> 39 | # [{"address"=>"29, DEAN ROAD, HAMPTON, TW12 1AQ", 40 | # "latitude"=>51.4303819, 41 | # "longitude"=>-0.3745976, 42 | # "local_custodian_code"=>5810}, ETC... 43 | def results_for_postcode(postcode) 44 | get_json("#{endpoint}/v1/locations?postcode=#{postcode}") 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /gds-api-adapters.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("lib", __dir__) 2 | $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) 3 | 4 | require "gds_api/version" 5 | Gem::Specification.new do |s| 6 | s.name = "gds-api-adapters" 7 | s.version = GdsApi::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["GOV.UK Dev"] 10 | s.email = ["govuk-dev@digital.cabinet-office.gov.uk"] 11 | s.summary = "Adapters to work with GDS APIs" 12 | s.homepage = "http://github.com/alphagov/gds-api-adapters" 13 | s.description = "A set of adapters providing easy access to the GDS GOV.UK APIs" 14 | 15 | s.required_ruby_version = ">= 3.2" 16 | s.files = Dir.glob("lib/**/*") + Dir.glob("test/fixtures/**/*") + %w[README.md Rakefile] 17 | s.require_path = "lib" 18 | s.add_dependency "addressable" 19 | s.add_dependency "link_header" 20 | s.add_dependency "null_logger" 21 | s.add_dependency "plek", ">= 1.9.0" 22 | s.add_dependency "rack", ">= 2.2.0" 23 | s.add_dependency "rest-client", "~> 2.0" 24 | 25 | s.add_development_dependency "byebug" 26 | s.add_development_dependency "climate_control", "~> 1.2" 27 | s.add_development_dependency "govuk_schemas", "~> 6.0" 28 | s.add_development_dependency "minitest", "~> 5.19" 29 | s.add_development_dependency "minitest-around", "~> 0.5" 30 | s.add_development_dependency "mocha", "~> 3.0" 31 | s.add_development_dependency "pact", "~> 1.62" 32 | s.add_development_dependency "pact_broker-client", "~> 1.65" 33 | s.add_development_dependency "pact-consumer-minitest", "~> 1.0" 34 | s.add_development_dependency "pact-mock_service", "~> 3.10" 35 | s.add_development_dependency "rack-test" 36 | s.add_development_dependency "rake" 37 | s.add_development_dependency "rubocop-govuk", "5.1.20" 38 | s.add_development_dependency "simplecov", "~> 0.21" 39 | s.add_development_dependency "timecop", "~> 0.9" 40 | s.add_development_dependency "webmock", "~> 3.17" 41 | end 42 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_links_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | require "json" 4 | 5 | describe "GdsApi::PublishingApi#get_links pact tests" do 6 | include PactTest 7 | 8 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 9 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 10 | 11 | it "responds with links when there's a links entry with links" do 12 | publishing_api 13 | .given("organisation links exist for content_id #{content_id}") 14 | .upon_receiving("a get-links request") 15 | .with( 16 | method: :get, 17 | path: "/v2/links/#{content_id}", 18 | ) 19 | .will_respond_with( 20 | status: 200, 21 | body: { 22 | links: { 23 | organisations: %w[20583132-1619-4c68-af24-77583172c070], 24 | }, 25 | }, 26 | ) 27 | 28 | api_client.get_links(content_id) 29 | end 30 | 31 | it "responds with the empty link set when there's an empty links entry" do 32 | publishing_api 33 | .given("empty links exist for content_id #{content_id}") 34 | .upon_receiving("a get-links request") 35 | .with( 36 | method: :get, 37 | path: "/v2/links/#{content_id}", 38 | ) 39 | .will_respond_with( 40 | status: 200, 41 | body: { 42 | links: {}, 43 | }, 44 | ) 45 | 46 | api_client.get_links(content_id) 47 | end 48 | 49 | it "responds with 404 when there's no links entry" do 50 | publishing_api 51 | .given("no links exist for content_id #{content_id}") 52 | .upon_receiving("a get-links request") 53 | .with( 54 | method: :get, 55 | path: "/v2/links/#{content_id}", 56 | ) 57 | .will_respond_with( 58 | status: 404, 59 | ) 60 | 61 | assert_raises(GdsApi::HTTPNotFound) do 62 | api_client.get_links(content_id) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/fixtures/sub_sector_organisations.json: -------------------------------------------------------------------------------- 1 | { 2 | "suggested_queries": [], 3 | "facets": { 4 | "organisations": { 5 | "missing_options": 0, 6 | "total_options": 4, 7 | "documents_with_no_value": 2, 8 | "options": [ 9 | { 10 | "documents": 3, 11 | "value": { 12 | "organisation_state": "live", 13 | "organisation_type": "Ministerial department", 14 | "acronym": "DECC", 15 | "title": "Department of Energy & Climate Change", 16 | "link": "/government/organisations/department-of-energy-climate-change", 17 | "slug": "department-of-energy-climate-change" 18 | } 19 | }, 20 | { 21 | "documents": 2, 22 | "value": { 23 | "organisation_state": "joining", 24 | "acronym": "AAIB", 25 | "title": "Air Accidents Investigation Branch", 26 | "link": "/government/organisations/air-accidents-investigation-branch", 27 | "slug": "air-accidents-investigation-branch" 28 | } 29 | }, 30 | { 31 | "documents": 1, 32 | "value": { 33 | "organisation_state": "live", 34 | "acronym": "DPMO", 35 | "title": "Deputy Prime Minister's Office", 36 | "link": "/government/organisations/deputy-prime-ministers-office", 37 | "slug": "deputy-prime-ministers-office" 38 | } 39 | }, 40 | { 41 | "documents": 1, 42 | "value": { 43 | "organisation_state": "live", 44 | "organisation_type": "Ministerial department", 45 | "acronym": "FCO", 46 | "title": "Foreign & Commonwealth Office", 47 | "link": "/government/organisations/foreign-commonwealth-office", 48 | "slug": "foreign-commonwealth-office" 49 | } 50 | } 51 | ] 52 | } 53 | }, 54 | "start": 0, 55 | "total": 8, 56 | "results": [] 57 | } 58 | -------------------------------------------------------------------------------- /lib/gds_api/publishing_api/special_route_publisher.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/publishing_api" 2 | require "time" 3 | 4 | module GdsApi 5 | class PublishingApi < GdsApi::Base 6 | class SpecialRoutePublisher 7 | def initialize(options = {}) 8 | @logger = options[:logger] || GdsApi::Base.logger 9 | @publishing_api = options[:publishing_api] || GdsApi::PublishingApi.new(Plek.find("publishing-api")) 10 | end 11 | 12 | def publish(options) 13 | logger.info("Publishing #{options.fetch(:type)} route #{options.fetch(:base_path)}, routing to #{options.fetch(:rendering_app)}") 14 | 15 | update_type = options.fetch(:update_type, "major") 16 | locale = options.fetch(:locale, "en") 17 | 18 | put_content_response = publishing_api.put_content( 19 | options.fetch(:content_id), 20 | base_path: options.fetch(:base_path), 21 | document_type: options.fetch(:document_type, "special_route"), 22 | schema_name: options.fetch(:schema_name, "special_route"), 23 | title: options.fetch(:title), 24 | description: options.fetch(:description, ""), 25 | locale:, 26 | details: {}, 27 | routes: [ 28 | { 29 | path: options.fetch(:base_path), 30 | type: options.fetch(:type), 31 | }, 32 | ], 33 | publishing_app: options.fetch(:publishing_app), 34 | rendering_app: options.fetch(:rendering_app), 35 | public_updated_at: time.now.iso8601, 36 | update_type:, 37 | ) 38 | 39 | publishing_api.patch_links(options.fetch(:content_id), links: options[:links]) if options[:links] 40 | publishing_api.publish(options.fetch(:content_id), update_type, locale:) 41 | put_content_response 42 | end 43 | 44 | private 45 | 46 | attr_reader :logger, :publishing_api 47 | 48 | def time 49 | (Time.respond_to?(:zone) && Time.zone) || Time 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/pacts/signon_api_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/support_api" 3 | 4 | describe "GdsApi::SignonApi pact tests" do 5 | include PactTest 6 | 7 | describe "#get_users" do 8 | let(:bearer_token) { "SOME_BEARER_TOKEN" } 9 | let(:api_client) { GdsApi::SignonApi.new(signon_api_host, { bearer_token: }) } 10 | 11 | it "returns a list of users" do 12 | uuids = %w[9ef9779f-3cba-481a-9a73-00d39e33eb7b b55873b4-bc83-4efe-bdc9-6b7d381a723e 64c7d994-17e0-44d9-97b0-87b43a581eb9] 13 | signon_api 14 | .given("users exist with the UUIDs #{uuids[0]}, #{uuids[1]} and #{uuids[2]}") 15 | .upon_receiving("a raise ticket request") 16 | .with( 17 | method: :get, 18 | path: "/api/users", 19 | headers: GdsApi::JsonClient.default_request_headers.merge("Authorization" => "Bearer #{bearer_token}"), 20 | query: "uuids%5B%5D=#{uuids[0]}&uuids%5B%5D=#{uuids[1]}&uuids%5B%5D=#{uuids[2]}", 21 | ) 22 | .will_respond_with( 23 | status: 200, 24 | body: [ 25 | { 26 | "uid": "9ef9779f-3cba-481a-9a73-00d39e33eb7b", 27 | "name": Pact.like("Some user"), 28 | "email": Pact.like("user@example.com"), 29 | "organisation": nil, 30 | }, 31 | { 32 | "uid": "b55873b4-bc83-4efe-bdc9-6b7d381a723e", 33 | "name": Pact.like("Some user"), 34 | "email": Pact.like("user@example.com"), 35 | "organisation": nil, 36 | }, 37 | { 38 | "uid": "64c7d994-17e0-44d9-97b0-87b43a581eb9", 39 | "name": Pact.like("Some user"), 40 | "email": Pact.like("user@example.com"), 41 | "organisation": nil, 42 | }, 43 | ], 44 | headers: { 45 | "Content-Type" => "application/json; charset=utf-8", 46 | }, 47 | ) 48 | 49 | api_client.get_users(uuids:) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/organisations_api_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/organisations" 3 | require "gds_api/test_helpers/organisations" 4 | 5 | describe GdsApi::Organisations do 6 | include GdsApi::TestHelpers::Organisations 7 | 8 | before do 9 | @base_api_url = Plek.new.website_root 10 | @api = GdsApi::Organisations.new(@base_api_url) 11 | end 12 | 13 | describe "fetching list of organisations" do 14 | it "should get the organisations" do 15 | organisation_slugs = %w[ministry-of-fun tea-agency] 16 | stub_organisations_api_has_organisations(organisation_slugs) 17 | 18 | response = @api.organisations 19 | assert_equal(organisation_slugs, response.map { |r| r["details"]["slug"] }) 20 | assert_equal "Tea Agency", response["results"][1]["title"] 21 | end 22 | 23 | it "should handle the pagination" do 24 | organisation_slugs = (1..50).map { |n| "organisation-#{n}" } 25 | stub_organisations_api_has_organisations(organisation_slugs) 26 | 27 | response = @api.organisations 28 | assert_equal( 29 | organisation_slugs, 30 | response.with_subsequent_pages.map { |r| r["details"]["slug"] }, 31 | ) 32 | end 33 | 34 | it "should raise error if endpoint 404s" do 35 | stub_request(:get, "#{@base_api_url}/api/organisations").to_return(status: 404) 36 | assert_raises GdsApi::HTTPNotFound do 37 | @api.organisations 38 | end 39 | end 40 | end 41 | 42 | describe "fetching an organisation" do 43 | it "should return the details" do 44 | stub_organisations_api_has_organisation("ministry-of-fun") 45 | 46 | response = @api.organisation("ministry-of-fun") 47 | assert_equal "Ministry Of Fun", response["title"] 48 | end 49 | 50 | it "should raise for a non-existent organisation" do 51 | stub_organisations_api_does_not_have_organisation("non-existent") 52 | 53 | assert_raises(GdsApi::HTTPNotFound) do 54 | @api.organisation("non-existent") 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/places_manager.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/test_helpers/json_client_helper" 2 | 3 | module GdsApi 4 | module TestHelpers 5 | module PlacesManager 6 | # Generally true. If you are initializing the client differently, 7 | # you could redefine/override the constant or stub directly. 8 | PLACES_MANAGER_API_ENDPOINT = Plek.find("places-manager") 9 | 10 | def stub_places_manager_has_places(latitude, longitude, details) 11 | query_hash = { "lat" => latitude, "lng" => longitude, "limit" => "5" } 12 | response_data = { 13 | status: "ok", 14 | content: "places", 15 | places: details["details"], 16 | } 17 | stub_places_manager_places_request(details["slug"], query_hash, response_data) 18 | end 19 | 20 | def stub_places_manager_has_multiple_authorities_for_postcode(addresses, slug, postcode, limit) 21 | query_hash = { "postcode" => postcode, "limit" => limit } 22 | response_data = { 23 | status: "address-information-required", 24 | content: "addresses", 25 | addresses:, 26 | } 27 | stub_places_manager_places_request(slug, query_hash, response_data) 28 | end 29 | 30 | def stub_places_manager_has_places_for_postcode(places, slug, postcode, limit, local_authority_slug) 31 | query_hash = { "postcode" => postcode, "limit" => limit } 32 | query_hash.merge!(local_authority_slug:) if local_authority_slug 33 | response_data = { 34 | status: "ok", 35 | content: "places", 36 | places:, 37 | } 38 | stub_places_manager_places_request(slug, query_hash, response_data) 39 | end 40 | 41 | def stub_places_manager_places_request(slug, query_hash, return_data, status_code = 200) 42 | stub_request(:get, "#{PLACES_MANAGER_API_ENDPOINT}/places/#{slug}.json") 43 | .with(query: query_hash) 44 | .to_return(status: status_code, body: return_data.to_json, headers: {}) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_expanded_links_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_expanded_links pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 9 | 10 | it "responds with the links when there's a links entry with links" do 11 | publishing_api 12 | .given("organisation links exist for content_id #{content_id}") 13 | .upon_receiving("a get-expanded-links request") 14 | .with( 15 | method: :get, 16 | path: "/v2/expanded-links/#{content_id}", 17 | ) 18 | .will_respond_with( 19 | status: 200, 20 | body: { 21 | expanded_links: { 22 | organisations: [ 23 | { content_id: "20583132-1619-4c68-af24-77583172c070" }, 24 | ], 25 | }, 26 | }, 27 | ) 28 | 29 | api_client.get_expanded_links(content_id) 30 | end 31 | 32 | it "responds with the empty thing set if there's an empty link set" do 33 | publishing_api 34 | .given("empty links exist for content_id #{content_id}") 35 | .upon_receiving("a get-expanded-links request") 36 | .with( 37 | method: :get, 38 | path: "/v2/expanded-links/#{content_id}", 39 | ) 40 | .will_respond_with( 41 | status: 200, 42 | body: { 43 | expanded_links: {}, 44 | }, 45 | ) 46 | 47 | api_client.get_expanded_links(content_id) 48 | end 49 | 50 | it "responds with 404 if there's no link set entry" do 51 | publishing_api 52 | .given("no links exist for content_id #{content_id}") 53 | .upon_receiving("a get-expanded-links request") 54 | .with( 55 | method: :get, 56 | path: "/v2/expanded-links/#{content_id}", 57 | ) 58 | .will_respond_with( 59 | status: 404, 60 | ) 61 | 62 | assert_raises(GdsApi::HTTPNotFound) do 63 | api_client.get_expanded_links(content_id) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_links_changes_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_link_changes pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:link_changes) do 9 | { "link_changes" => [ 10 | { 11 | "source" => { "title" => "Edition Title A1", 12 | "base_path" => "/base/path/a1", 13 | "content_id" => "aaaaaaaa-aaaa-1aaa-aaaa-aaaaaaaaaaaa" }, 14 | "target" => { "title" => "Edition Title B1", 15 | "base_path" => "/base/path/b1", 16 | "content_id" => "bbbbbbbb-bbbb-1bbb-bbbb-bbbbbbbbbbbb" }, 17 | "link_type" => "taxons", 18 | "change" => "add", 19 | "user_uid" => "11111111-1111-1111-1111-111111111111", 20 | "created_at" => "2017-01-01T09:00:00.100Z", 21 | }, 22 | { 23 | "source" => { "title" => "Edition Title A2", 24 | "base_path" => "/base/path/a2", 25 | "content_id" => "aaaaaaaa-aaaa-2aaa-aaaa-aaaaaaaaaaaa" }, 26 | "target" => { "title" => "Edition Title B2", 27 | "base_path" => "/base/path/b2", 28 | "content_id" => "bbbbbbbb-bbbb-2bbb-bbbb-bbbbbbbbbbbb" }, 29 | "link_type" => "taxons", 30 | "change" => "remove", 31 | "user_uid" => "22222222-2222-2222-2222-222222222222", 32 | "created_at" => "2017-01-01T09:00:00.100Z", 33 | }, 34 | ] } 35 | end 36 | 37 | it "returns the changes for a single link_type" do 38 | publishing_api 39 | .given("there are two link changes with a link_type of 'taxons'") 40 | .upon_receiving("a get links changes request for changes with a link_type of 'taxons'") 41 | .with( 42 | method: :get, 43 | path: "/v2/links/changes", 44 | query: "link_types%5B%5D=taxons", 45 | ) 46 | .will_respond_with( 47 | status: 200, 48 | body: link_changes, 49 | ) 50 | 51 | api_client.get_links_changes(link_types: %w[taxons]) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/discard_draft_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#discard_draft pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 9 | 10 | it "responds with 200 when the content item exists" do 11 | publishing_api 12 | .given("a content item exists with content_id: #{content_id}") 13 | .upon_receiving("a request to discard draft content") 14 | .with( 15 | method: :post, 16 | path: "/v2/content/#{content_id}/discard-draft", 17 | body: {}, 18 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 19 | ) 20 | .will_respond_with( 21 | status: 200, 22 | ) 23 | 24 | api_client.discard_draft(content_id) 25 | end 26 | 27 | it "responds with 200 when the content item exists and is French" do 28 | publishing_api 29 | .given("a French content item exists with content_id: #{content_id}") 30 | .upon_receiving("a request to discard French draft content") 31 | .with( 32 | method: :post, 33 | path: "/v2/content/#{content_id}/discard-draft", 34 | body: { 35 | locale: "fr", 36 | }, 37 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 38 | ) 39 | .will_respond_with( 40 | status: 200, 41 | ) 42 | 43 | api_client.discard_draft(content_id, locale: "fr") 44 | end 45 | 46 | it "responds with a 404 when there is no content with that content_id" do 47 | publishing_api 48 | .given("no content exists") 49 | .upon_receiving("a request to discard draft content") 50 | .with( 51 | method: :post, 52 | path: "/v2/content/#{content_id}/discard-draft", 53 | body: {}, 54 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 55 | ) 56 | .will_respond_with( 57 | status: 404, 58 | ) 59 | 60 | assert_raises(GdsApi::HTTPNotFound) do 61 | api_client.discard_draft(content_id) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/pacts/support_api_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/support_api" 3 | 4 | describe "GdsApi::SupportApi pact tests" do 5 | include PactTest 6 | 7 | describe "#raise_support_ticket" do 8 | let(:api_client) { GdsApi::SupportApi.new(support_api_host) } 9 | 10 | it "responds with a 201 Success if the parameters provided are valid" do 11 | support_api 12 | .given("the parameters are valid") 13 | .upon_receiving("a raise ticket request") 14 | .with( 15 | method: :post, 16 | path: "/support-tickets", 17 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 18 | body: { 19 | subject: "Feedback for app", 20 | tags: %w[app_name], 21 | user_agent: "Safari", 22 | description: "There is something wrong with this page.", 23 | }, 24 | ) 25 | .will_respond_with( 26 | status: 201, 27 | body: { 28 | status: "success", 29 | }, 30 | headers: { 31 | "Content-Type" => "application/json; charset=utf-8", 32 | }, 33 | ) 34 | 35 | api_client.raise_support_ticket( 36 | subject: "Feedback for app", 37 | tags: %w[app_name], 38 | user_agent: "Safari", 39 | description: "There is something wrong with this page.", 40 | ) 41 | end 42 | 43 | it "responds with 422 Error when required parameters are not provided" do 44 | support_api 45 | .given("the required parameters are not provided") 46 | .upon_receiving("a raise ticket request") 47 | .with( 48 | method: :post, 49 | path: "/support-tickets", 50 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 51 | body: { 52 | subject: "Ticket without body", 53 | }, 54 | ) 55 | .will_respond_with( 56 | status: 422, 57 | body: { 58 | status: "error", 59 | }, 60 | ) 61 | 62 | assert_raises GdsApi::HTTPUnprocessableEntity do 63 | api_client.raise_support_ticket(subject: "Ticket without body") 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/pacts/locations_api_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/locations_api" 3 | 4 | describe "GdsApi::LocationsApi pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::LocationsApi.new(locations_api_host) } 8 | 9 | describe "#local_custodian_code_for_postcode" do 10 | it "responds with a list of local custodian codes" do 11 | locations_api 12 | .given("a postcode") 13 | .upon_receiving("the request to get details about a postcode") 14 | .with( 15 | method: :get, 16 | path: "/v1/locations", 17 | headers: GdsApi::JsonClient.default_request_headers, 18 | query: { postcode: "SW1A1AA" }, 19 | ) 20 | .will_respond_with( 21 | status: 200, 22 | body: { 23 | "average_latitude" => 51.50100965, 24 | "average_longitude" => -0.14158705, 25 | "results" => [ 26 | { "local_custodian_code" => 5900 }, 27 | { "local_custodian_code" => 5901 }, 28 | ], 29 | }, 30 | headers: { 31 | "Content-Type" => "application/json; charset=utf-8", 32 | }, 33 | ) 34 | api_client.local_custodian_code_for_postcode("SW1A1AA") 35 | end 36 | end 37 | 38 | describe "#coordinates_for_postcode" do 39 | it "responds with average coordinates for postcode" do 40 | locations_api 41 | .given("a postcode") 42 | .upon_receiving("the request to get details about a postcode") 43 | .with( 44 | method: :get, 45 | path: "/v1/locations", 46 | headers: GdsApi::JsonClient.default_request_headers, 47 | query: { postcode: "SW1A1AA" }, 48 | ) 49 | .will_respond_with( 50 | status: 200, 51 | body: { 52 | "average_latitude" => 51.50100965, 53 | "average_longitude" => -0.14158705, 54 | "results" => [ 55 | { "local_custodian_code" => 5900 }, 56 | { "local_custodian_code" => 5901 }, 57 | ], 58 | }, 59 | headers: { 60 | "Content-Type" => "application/json; charset=utf-8", 61 | }, 62 | ) 63 | api_client.coordinates_for_postcode("SW1A1AA") 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/gds_api/base.rb: -------------------------------------------------------------------------------- 1 | require_relative "json_client" 2 | require "cgi" 3 | require "null_logger" 4 | require "plek" 5 | require_relative "list_response" 6 | 7 | class GdsApi::Base 8 | class InvalidAPIURL < StandardError 9 | end 10 | 11 | class ItemNotFound < GdsApi::HTTPNotFound 12 | def self.build_from(http_error) 13 | new(http_error.code, http_error.message, http_error.error_details) 14 | end 15 | end 16 | 17 | extend Forwardable 18 | 19 | def client 20 | @client ||= create_client 21 | end 22 | 23 | def create_client 24 | GdsApi::JsonClient.new(options) 25 | end 26 | 27 | def_delegators :client, 28 | :get_json, 29 | :post_json, 30 | :put_json, 31 | :patch_json, 32 | :delete_json, 33 | :get_raw, 34 | :get_raw!, 35 | :put_multipart, 36 | :post_multipart 37 | 38 | attr_reader :options 39 | 40 | class << self 41 | attr_writer :logger 42 | attr_accessor :default_options 43 | end 44 | 45 | def self.logger 46 | @logger ||= NullLogger.instance 47 | end 48 | 49 | def initialize(endpoint_url, options = {}) 50 | options[:endpoint_url] = endpoint_url 51 | raise InvalidAPIURL unless endpoint_url =~ URI::RFC3986_Parser::RFC3986_URI 52 | 53 | base_options = { logger: GdsApi::Base.logger } 54 | default_options = base_options.merge(GdsApi::Base.default_options || {}) 55 | @options = default_options.merge(options) 56 | self.endpoint = options[:endpoint_url] 57 | end 58 | 59 | def url_for_slug(slug, options = {}) 60 | "#{base_url}/#{slug}.json#{query_string(options)}" 61 | end 62 | 63 | def get_list(url) 64 | get_json(url) do |r| 65 | GdsApi::ListResponse.new(r, self) 66 | end 67 | end 68 | 69 | private 70 | 71 | attr_accessor :endpoint 72 | 73 | def query_string(params) 74 | return "" if params.empty? 75 | 76 | param_pairs = params.sort.map { |key, value| 77 | case value 78 | when Array 79 | value.map do |v| 80 | "#{CGI.escape("#{key}[]")}=#{CGI.escape(v.to_s)}" 81 | end 82 | else 83 | "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" 84 | end 85 | }.flatten 86 | 87 | "?#{param_pairs.join('&')}" 88 | end 89 | 90 | def uri_encode(param) 91 | Addressable::URI.encode(param.to_s) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/publishing_api_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | require "gds_api/test_helpers/publishing_api" 4 | 5 | describe GdsApi::PublishingApi do 6 | include GdsApi::TestHelpers::PublishingApi 7 | 8 | let(:api_client) { GdsApi::PublishingApi.new(Plek.find("publishing-api")) } 9 | 10 | describe "content ID validation" do 11 | %i[get_content get_links get_linked_items discard_draft].each do |method| 12 | it "happens on #{method}" do 13 | assert_raises ArgumentError do 14 | api_client.send(method, nil) 15 | end 16 | end 17 | end 18 | 19 | it "happens on publish" do 20 | assert_raises ArgumentError do 21 | api_client.publish(nil, "major") 22 | end 23 | end 24 | 25 | it "happens on put_content" do 26 | assert_raises ArgumentError do 27 | api_client.put_content(nil, {}) 28 | end 29 | end 30 | 31 | it "happens on patch_links" do 32 | assert_raises ArgumentError do 33 | api_client.patch_links(nil, links: {}) 34 | end 35 | end 36 | end 37 | 38 | describe "#get_content_by_embedded_document" do 39 | it "sends a warning and calls #get_host_content_for_content_id" do 40 | content_id = SecureRandom.uuid 41 | args = { some: "args" } 42 | api_client.expects(:warn).with("GdsAPI::PublishingApi: #get_content_by_embedded_document deprecated (please use #get_host_content_for_content_id)") 43 | api_client.expects(:get_host_content_for_content_id).with(content_id, args) 44 | 45 | api_client.get_content_by_embedded_document(content_id, args) 46 | end 47 | end 48 | 49 | describe "#graphql_live_content_item" do 50 | it "returns the item" do 51 | base_path = "/test-from-content-store" 52 | stub_publishing_api_graphql_has_item(base_path) 53 | 54 | response = api_client.graphql_live_content_item(base_path) 55 | 56 | assert_equal base_path, response["base_path"] 57 | end 58 | 59 | it "raises if the item doesn't exist" do 60 | stub_publishing_api_graphql_does_not_have_item("/non-existent") 61 | 62 | assert_raises(GdsApi::HTTPNotFound) do 63 | api_client.graphql_live_content_item("/non-existent") 64 | end 65 | end 66 | 67 | it "raises if the item is gone" do 68 | stub_publishing_api_graphql_has_gone_item("/it-is-gone") 69 | 70 | assert_raises(GdsApi::HTTPGone) do 71 | api_client.graphql_live_content_item("/it-is-gone") 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/pacts/calendars_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/calendars" 3 | 4 | describe "GdsApi::Calendars pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::Calendars.new(bank_holidays_api_host) } 8 | 9 | describe "fetching all bank holidays" do 10 | it "responds with 200 OK and a list of bank holidays for each nation" do 11 | bank_holidays_api 12 | .given("there is a list of all bank holidays") 13 | .upon_receiving("the request for the list of all bank holidays") 14 | .with( 15 | method: :get, 16 | path: "/bank-holidays.json", 17 | headers: GdsApi::JsonClient.default_request_headers, 18 | ) 19 | .will_respond_with( 20 | status: 200, 21 | body: { 22 | "england-and-wales": { 23 | division: "england-and-wales", 24 | events: Pact.each_like(event), 25 | }, 26 | "scotland": { 27 | division: "scotland", 28 | events: Pact.each_like(event), 29 | }, 30 | "northern-ireland": { 31 | division: "northern-ireland", 32 | events: Pact.each_like(event), 33 | }, 34 | }, 35 | headers: { 36 | "Content-Type" => "application/json; charset=utf-8", 37 | }, 38 | ) 39 | 40 | api_client.bank_holidays 41 | end 42 | end 43 | 44 | describe "fetching only Scottish bank holidays" do 45 | it "responds with 200 OK and a list of bank holidays" do 46 | bank_holidays_api 47 | .given("there is a list of all bank holidays") 48 | .upon_receiving("the request for the list of Scottish bank holidays") 49 | .with( 50 | method: :get, 51 | path: "/bank-holidays/scotland.json", 52 | headers: GdsApi::JsonClient.default_request_headers, 53 | ) 54 | .will_respond_with( 55 | status: 200, 56 | body: { 57 | division: "scotland", 58 | events: Pact.each_like(event), 59 | }, 60 | headers: { 61 | "Content-Type" => "application/json; charset=utf-8", 62 | }, 63 | ) 64 | 65 | api_client.bank_holidays("scotland") 66 | end 67 | end 68 | 69 | private 70 | 71 | def event 72 | { 73 | "title" => Pact.like("New Year's Day"), 74 | "date" => Pact.like("2016-01-01"), 75 | "notes" => Pact.like("Substitute day"), 76 | "bunting" => Pact.like(true), 77 | } 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/gds_api_base_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/base" 3 | require "uri" 4 | 5 | class GdsApiBaseTest < Minitest::Test 6 | class ConcreteApi < GdsApi::Base 7 | def base_url 8 | endpoint 9 | end 10 | end 11 | 12 | def teardown 13 | GdsApi::Base.default_options = nil 14 | end 15 | 16 | def test_should_construct_escaped_query_string 17 | api = ConcreteApi.new("http://foo") 18 | url = api.url_for_slug("slug", "a" => " ", "b" => "/") 19 | u = URI.parse(url) 20 | assert_equal "a=+&b=%2F", u.query 21 | end 22 | 23 | def test_should_construct_escaped_query_string_for_rails 24 | api = ConcreteApi.new("http://foo") 25 | 26 | url = api.url_for_slug("slug", "b" => %w[123]) 27 | u = URI.parse(url) 28 | assert_equal "b%5B%5D=123", u.query 29 | 30 | url = api.url_for_slug("slug", "b" => %w[123 456]) 31 | u = URI.parse(url) 32 | assert_equal "b%5B%5D=123&b%5B%5D=456", u.query 33 | end 34 | 35 | def test_should_not_add_a_question_mark_if_there_are_no_parameters 36 | api = ConcreteApi.new("http://foo") 37 | url = api.url_for_slug("slug") 38 | refute_match(/\?/, url) 39 | end 40 | 41 | def test_should_use_endpoint_in_url 42 | api = ConcreteApi.new("http://foobarbaz") 43 | url = api.url_for_slug("slug") 44 | u = URI.parse(url) 45 | assert_match(/foobarbaz$/, u.host) 46 | end 47 | 48 | def test_should_accept_options_as_second_arg 49 | api = ConcreteApi.new("http://foo", foo: "bar") 50 | assert_equal "bar", api.options[:foo] 51 | end 52 | 53 | def test_should_barf_if_not_given_valid_url 54 | assert_raises GdsApi::Base::InvalidAPIURL do 55 | ConcreteApi.new("invalid-url") 56 | end 57 | end 58 | 59 | def test_should_set_json_client_logger_to_own_logger_by_default 60 | api = ConcreteApi.new("http://bar") 61 | assert_same GdsApi::Base.logger, api.client.logger 62 | end 63 | 64 | def test_should_set_json_client_logger_to_logger_in_default_options 65 | custom_logger = stub("custom-logger") 66 | GdsApi::Base.default_options = { logger: custom_logger } 67 | api = ConcreteApi.new("http://bar") 68 | assert_same custom_logger, api.client.logger 69 | end 70 | 71 | def test_should_set_json_client_logger_to_logger_in_options 72 | custom_logger = stub("custom-logger") 73 | GdsApi::Base.default_options = { logger: custom_logger } 74 | another_logger = stub("another-logger") 75 | api = ConcreteApi.new("http://bar", logger: another_logger) 76 | assert_same another_logger, api.client.logger 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/fixtures/services_and_info_fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ ], 3 | "total": 3138, 4 | "start": 0, 5 | "facets": { 6 | "specialist_sectors": { 7 | "options": [ 8 | { 9 | "value": { 10 | "link": "/environmental-management/environmental-permits", 11 | "title": "Environmental permits", 12 | "slug": "environmental-management/environmental-permits", 13 | "example_info": { 14 | "total": 47, 15 | "examples": [ 16 | { 17 | "title": "Check if you need an environmental permit", 18 | "link": "/environmental-permit-check-if-you-need-one" 19 | }, 20 | { 21 | "title": "Environmental permit: how to apply", 22 | "link": "/environmental-permit-how-to-apply" 23 | }, 24 | { 25 | "title": "Standard rules: environmental permitting", 26 | "link": "/government/collections/standard-rules-environmental-permitting" 27 | }, 28 | { 29 | "title": "Environmental permitting (EP) charges scheme: April 2014 to March 2015", 30 | "link": "/government/publications/environmental-permitting-ep-charges-scheme-april-2014-to-march-2015" 31 | } 32 | ] 33 | } 34 | }, 35 | "documents": 47 36 | }, 37 | { 38 | "value": { 39 | "link": "/environmental-management/waste", 40 | "title": "Waste", 41 | "slug": "environmental-management/waste", 42 | "example_info": { 43 | "total": 49, 44 | "examples": [ 45 | { 46 | "title": "Register as a waste carrier, broker or dealer (England)", 47 | "link": "/waste-carrier-or-broker-registration" 48 | }, 49 | { 50 | "title": "Hazardous waste producer registration (England and Wales)", 51 | "link": "/hazardous-waste-producer-registration" 52 | }, 53 | { 54 | "title": "Check if you need an environmental permit", 55 | "link": "/environmental-permit-check-if-you-need-one" 56 | }, 57 | { 58 | "title": "Classify different types of waste", 59 | "link": "/how-to-classify-different-types-of-waste" 60 | } 61 | ] 62 | } 63 | }, 64 | "documents": 47 65 | } 66 | ], 67 | "documents_with_no_value": 2900, 68 | "total_options": 17, 69 | "missing_options": 0 70 | } 71 | }, 72 | "suggested_queries": [ ] 73 | } 74 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/unreserve_path_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#unreserve_path pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "responds with 200 OK if reservation is owned by the app" do 10 | publishing_app = "publisher" 11 | base_path = "/test-item" 12 | 13 | publishing_api 14 | .given("/test-item has been reserved by the Publisher application") 15 | .upon_receiving("a request to unreserve a path") 16 | .with( 17 | method: :delete, 18 | path: "/paths#{base_path}", 19 | body: { publishing_app: }, 20 | headers: { 21 | "Content-Type" => "application/json", 22 | }, 23 | ) 24 | .will_respond_with( 25 | status: 200, 26 | body: {}, 27 | headers: { 28 | "Content-Type" => "application/json; charset=utf-8", 29 | }, 30 | ) 31 | 32 | api_client.unreserve_path(base_path, publishing_app) 33 | end 34 | 35 | it "raises an error if the reservation does not exist" do 36 | publishing_app = "publisher" 37 | base_path = "/test-item" 38 | 39 | publishing_api 40 | .given("no content exists") 41 | .upon_receiving("a request to unreserve a non-existant path") 42 | .with( 43 | method: :delete, 44 | path: "/paths#{base_path}", 45 | body: { publishing_app: }, 46 | headers: { 47 | "Content-Type" => "application/json", 48 | }, 49 | ) 50 | .will_respond_with( 51 | status: 404, 52 | body: {}, 53 | headers: { 54 | "Content-Type" => "application/json; charset=utf-8", 55 | }, 56 | ) 57 | 58 | assert_raises(GdsApi::HTTPNotFound) do 59 | api_client.unreserve_path(base_path, publishing_app) 60 | end 61 | end 62 | 63 | it "raises an error if the reservation is with another app" do 64 | publishing_app = "whitehall" 65 | base_path = "/test-item" 66 | 67 | publishing_api 68 | .given("/test-item has been reserved by the Publisher application") 69 | .upon_receiving("a request to unreserve a path owned by another app") 70 | .with( 71 | method: :delete, 72 | path: "/paths#{base_path}", 73 | body: { publishing_app: "whitehall" }, 74 | headers: { 75 | "Content-Type" => "application/json", 76 | }, 77 | ) 78 | .will_respond_with( 79 | status: 422, 80 | body: {}, 81 | headers: { 82 | "Content-Type" => "application/json; charset=utf-8", 83 | }, 84 | ) 85 | 86 | assert_raises(GdsApi::HTTPUnprocessableEntity) do 87 | api_client.unreserve_path(base_path, publishing_app) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/calendars_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/calendars" 3 | require "gds_api/test_helpers/calendars" 4 | 5 | describe GdsApi::Calendars do 6 | include GdsApi::TestHelpers::Calendars 7 | 8 | before do 9 | @host = Plek.new.website_root 10 | @api = GdsApi::Calendars.new(@host) 11 | end 12 | 13 | describe "#bank_holidays" do 14 | it "fetches all bank holidays when called with no argument" do 15 | holidays_request = stub_request(:get, "#{@host}/bank-holidays.json").to_return(status: 200, body: "{}") 16 | 17 | @api.bank_holidays 18 | 19 | assert_requested(holidays_request) 20 | end 21 | 22 | it "fetches just the requested bank holidays when called with an argument" do 23 | all_holidays_request = stub_request(:get, "#{@host}/bank-holidays.json") 24 | scotland_holidays_request = stub_request(:get, "#{@host}/bank-holidays/scotland.json").to_return(status: 200, body: "{}") 25 | 26 | @api.bank_holidays(:scotland) 27 | 28 | assert_not_requested(all_holidays_request) 29 | assert_requested(scotland_holidays_request) 30 | end 31 | 32 | it "normalises the argument from underscores to dashes" do 33 | underscored_england_and_wales_holidays_request = stub_request(:get, "#{@host}/bank-holidays/england_and_wales.json").to_return(status: 200, body: "{}") 34 | dashed_england_and_wales_holidays_request = stub_request(:get, "#{@host}/bank-holidays/england-and-wales.json").to_return(status: 200, body: "{}") 35 | 36 | @api.bank_holidays(:england_and_wales) 37 | 38 | assert_not_requested(underscored_england_and_wales_holidays_request) 39 | assert_requested(dashed_england_and_wales_holidays_request) 40 | end 41 | 42 | it "should raise error if argument is for an area we don't have holidays for" do 43 | stub_request(:get, "#{@host}/bank-holidays/lyonesse.json").to_return(status: 404) 44 | assert_raises GdsApi::HTTPNotFound do 45 | @api.bank_holidays(:lyonesse) 46 | end 47 | end 48 | 49 | it "fetches the bank holidays requested for all divisions" do 50 | stub_calendars_has_a_bank_holiday_on(Date.parse("2012-12-12")) 51 | holidays = @api.bank_holidays 52 | 53 | assert_equal "2012-12-12", holidays["england-and-wales"]["events"][0]["date"] 54 | assert_equal "2012-12-12", holidays["scotland"]["events"][0]["date"] 55 | assert_equal "2012-12-12", holidays["northern-ireland"]["events"][0]["date"] 56 | end 57 | 58 | it "fetches the bank holidays requested for just one divisions" do 59 | stub_calendars_has_a_bank_holiday_on(Date.parse("2012-12-12"), in_division: "scotland") 60 | holidays = @api.bank_holidays(:scotland) 61 | 62 | assert_equal "2012-12-12", holidays["events"][0]["date"] 63 | assert_equal "scotland", holidays["division"] 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_paged_content_items_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_paged_content_items pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "returns two content items" do 10 | publishing_api 11 | .given("there are four content items with document_type 'taxon'") 12 | .upon_receiving("get the first page request") 13 | .with( 14 | method: :get, 15 | path: "/v2/content", 16 | query: "document_type=taxon&fields%5B%5D=title&fields%5B%5D=base_path&page=1&per_page=2", 17 | ) 18 | .will_respond_with( 19 | status: 200, 20 | body: { 21 | total: 4, 22 | pages: 2, 23 | current_page: 1, 24 | links: [{ href: "http://example.org/v2/content?document_type=taxon&fields%5B%5D=title&fields%5B%5D=base_path&per_page=2&page=2", 25 | rel: "next" }, 26 | { href: "http://example.org/v2/content?document_type=taxon&fields%5B%5D=title&fields%5B%5D=base_path&per_page=2&page=1", 27 | rel: "self" }], 28 | results: [ 29 | { title: "title_1", base_path: "/path_1" }, 30 | { title: "title_2", base_path: "/path_2" }, 31 | ], 32 | }, 33 | ) 34 | publishing_api 35 | .given("there are four content items with document_type 'taxon'") 36 | .upon_receiving("get the second page request") 37 | .with( 38 | method: :get, 39 | path: "/v2/content", 40 | query: "document_type=taxon&fields%5B%5D=title&fields%5B%5D=base_path&page=2&per_page=2", 41 | ) 42 | .will_respond_with( 43 | status: 200, 44 | body: { 45 | total: 4, 46 | pages: 2, 47 | current_page: 2, 48 | links: [{ href: "http://example.org/v2/content?document_type=taxon&fields%5B%5D=title&fields%5B%5D=base_path&per_page=2&page=1", 49 | rel: "previous" }, 50 | { href: "http://example.org/v2/content?document_type=taxon&fields%5B%5D=title&fields%5B%5D=base_path&per_page=2&page=2", 51 | rel: "self" }], 52 | results: [ 53 | { title: "title_3", base_path: "/path_3" }, 54 | { title: "title_4", base_path: "/path_4" }, 55 | ], 56 | }, 57 | ) 58 | assert_equal( 59 | api_client.get_content_items_enum(document_type: "taxon", fields: %i[title base_path], per_page: 2).to_a, 60 | [ 61 | { "title" => "title_1", "base_path" => "/path_1" }, 62 | { "title" => "title_2", "base_path" => "/path_2" }, 63 | { "title" => "title_3", "base_path" => "/path_3" }, 64 | { "title" => "title_4", "base_path" => "/path_4" }, 65 | ], 66 | ) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/test_helpers/email_alert_api_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/email_alert_api" 3 | require "gds_api/test_helpers/email_alert_api" 4 | 5 | describe GdsApi::TestHelpers::EmailAlertApi do 6 | include GdsApi::TestHelpers::EmailAlertApi 7 | 8 | let(:base_api_url) { Plek.find("email-alert-api") } 9 | let(:email_alert_api) { GdsApi::EmailAlertApi.new(base_api_url) } 10 | 11 | describe "#assert_email_alert_api_content_change_created" do 12 | before { stub_any_email_alert_api_call } 13 | 14 | it "matches a post request with any empty attributes by default" do 15 | email_alert_api.create_content_change("foo" => "bar") 16 | assert_email_alert_api_content_change_created 17 | end 18 | 19 | it "matches a post request subset of attributes" do 20 | email_alert_api.create_content_change("foo" => "bar", "baz" => "qux") 21 | assert_email_alert_api_content_change_created("foo" => "bar") 22 | end 23 | end 24 | 25 | describe "#stub_email_alert_api_has_subscriber_subscriptions" do 26 | let(:id) { SecureRandom.uuid } 27 | let(:address) { "test@example.com" } 28 | 29 | it "stubs with a single subscription by default" do 30 | stub_email_alert_api_has_subscriber_subscriptions(id, address) 31 | result = email_alert_api.get_subscriptions(id:) 32 | assert_equal(1, result["subscriptions"].count) 33 | end 34 | 35 | it "stubs subscriptions with an optional ordering" do 36 | stub_email_alert_api_has_subscriber_subscriptions(id, address, "-title") 37 | result = email_alert_api.get_subscriptions(id:, order: "-title") 38 | assert_equal(1, result["subscriptions"].count) 39 | end 40 | 41 | it "stubs subscriptions with specific ones" do 42 | stub_email_alert_api_has_subscriber_subscriptions( 43 | id, 44 | address, 45 | subscriptions: %w[one two], 46 | ) 47 | 48 | result = email_alert_api.get_subscriptions(id:) 49 | assert_equal(2, result["subscriptions"].count) 50 | end 51 | end 52 | 53 | describe "#stub_get_subscriber_list_metrics_not_found" do 54 | it "raises 404" do 55 | stub_get_subscriber_list_metrics_not_found(path: "/some/path") 56 | assert_raises(GdsApi::HTTPNotFound) do 57 | email_alert_api.get_subscriber_list_metrics(path: "/some/path") 58 | end 59 | end 60 | end 61 | 62 | describe "#stub_get_subscriber_list_metrics" do 63 | it "returns the stubbed data" do 64 | json = { subscriber_list_count: 3, all_notify_count: 10 }.to_json 65 | stub_get_subscriber_list_metrics(path: "/some/path", response: json) 66 | response = email_alert_api.get_subscriber_list_metrics(path: "/some/path") 67 | expected = { "subscriber_list_count" => 3, "all_notify_count" => 10 } 68 | assert_equal 200, response.code 69 | assert_equal expected, response.to_h 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/search_api_v2_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/search_api_v2" 3 | 4 | describe GdsApi::SearchApiV2 do 5 | describe "#search" do 6 | before(:each) do 7 | stub_request(:get, /example.com\/search/).to_return(body: "[]") 8 | end 9 | 10 | it "should raise an exception if the request is unsuccessful" do 11 | stub_request(:get, /example.com\/search.json/).to_return(status: [500, "Internal Server Error"]) 12 | assert_raises(GdsApi::HTTPServerError) do 13 | GdsApi::SearchApiV2.new("http://example.com").search(q: "query") 14 | end 15 | end 16 | 17 | it "should return the search deserialized from json" do 18 | search_results = [{ "title" => "document-title" }] 19 | stub_request(:get, /example.com\/search/).to_return(body: search_results.to_json) 20 | results = GdsApi::SearchApiV2.new("http://example.com").search(q: "query") 21 | assert_equal search_results, results.to_hash 22 | end 23 | 24 | it "should request the search results in JSON format" do 25 | GdsApi::SearchApiV2.new("http://example.com").search(q: "query") 26 | 27 | assert_requested :get, /.*/, headers: { "Accept" => "application/json" } 28 | end 29 | 30 | it "should issue a request for all the params supplied" do 31 | GdsApi::SearchApiV2.new("http://example.com").search( 32 | q: "query & stuff", 33 | filter_topics: %w[1 2], 34 | order: "-public_timestamp", 35 | ) 36 | 37 | assert_requested :get, /q=query%20%26%20stuff/ 38 | assert_requested :get, /filter_topics\[\]=1&filter_topics\[\]=2/ 39 | assert_requested :get, /order=-public_timestamp/ 40 | end 41 | 42 | it "can pass additional headers" do 43 | GdsApi::SearchApiV2.new("http://example.com").search({ q: "query" }, "authorization" => "token") 44 | 45 | assert_requested :get, /.*/, headers: { "authorization" => "token" } 46 | end 47 | end 48 | 49 | describe "#autocomplete" do 50 | before(:each) do 51 | stub_request(:get, /example.com\/autocomplete/).to_return(body: "[]") 52 | end 53 | 54 | it "should raise an exception if the request is unsuccessful" do 55 | stub_request(:get, /example.com\/autocomplete.json/).to_return( 56 | status: [500, "Internal Server Error"], 57 | ) 58 | assert_raises(GdsApi::HTTPServerError) do 59 | GdsApi::SearchApiV2.new("http://example.com").autocomplete("prime minis") 60 | end 61 | end 62 | 63 | it "should request the autocomplete results in JSON format" do 64 | GdsApi::SearchApiV2.new("http://example.com").autocomplete("prime minis") 65 | 66 | assert_requested :get, /.*/, headers: { "Accept" => "application/json" } 67 | end 68 | 69 | it "should issue a request for the correct query" do 70 | GdsApi::SearchApiV2.new("http://example.com").autocomplete("prime minis") 71 | 72 | assert_requested :get, /q=prime%20minis/ 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/worldwide.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/test_helpers/json_client_helper" 2 | require "gds_api/test_helpers/common_responses" 3 | 4 | module GdsApi 5 | module TestHelpers 6 | module Worldwide 7 | include GdsApi::TestHelpers::CommonResponses 8 | 9 | WORLDWIDE_API_ENDPOINT = Plek.new.website_root 10 | 11 | def stub_worldwide_api_has_locations(location_slugs) 12 | international_delegation_slugs = location_slugs.select do |slug| 13 | slug =~ /(delegation|mission)/ 14 | end 15 | 16 | international_delegations = international_delegation_slugs.map do |slug| 17 | { 18 | "active": true, 19 | "analytics_identifier": "WL1", 20 | "content_id": "content_id_for_#{slug}", 21 | "iso2": slug[0..1].upcase, 22 | "name": titleize_slug(slug, title_case: true), 23 | "slug": slug, 24 | "updated_at": "2013-03-25T13:06:42+00:00", 25 | } 26 | end 27 | 28 | world_locations = (location_slugs - international_delegation_slugs).map do |slug| 29 | { 30 | "active": true, 31 | "analytics_identifier": "WL1", 32 | "content_id": "content_id_for_#{slug}", 33 | "iso2": slug[0..1].upcase, 34 | "name": titleize_slug(slug, title_case: true), 35 | "slug": slug, 36 | "updated_at": "2013-03-25T13:06:42+00:00", 37 | } 38 | end 39 | 40 | content_item = { 41 | "details": { 42 | "international_delegation": international_delegations, 43 | "world_locations": world_locations, 44 | }, 45 | } 46 | 47 | stub_request(:get, "#{WORLDWIDE_API_ENDPOINT}/api/content/world") 48 | .to_return(status: 200, body: content_item.to_json) 49 | end 50 | 51 | def stub_worldwide_api_has_location(location_slug) 52 | stub_worldwide_api_has_locations([location_slug]) 53 | end 54 | 55 | def stub_search_api_has_organisations_for_location(location_slug, organisation_content_items) 56 | response = { 57 | "results": organisation_content_items.map do |content_item| 58 | { 59 | "link": content_item["base_path"], 60 | } 61 | end, 62 | } 63 | 64 | stub_request(:get, "#{WORLDWIDE_API_ENDPOINT}/api/search.json?filter_format=worldwide_organisation&filter_world_locations=#{location_slug}") 65 | .to_return(status: 200, body: response.to_json) 66 | 67 | organisation_content_items.each do |content_item| 68 | stub_content_store_has_worldwide_organisation(content_item) 69 | end 70 | end 71 | 72 | def stub_content_store_has_worldwide_organisation(content_item) 73 | base_path = content_item["base_path"] 74 | 75 | stub_request(:get, "#{WORLDWIDE_API_ENDPOINT}/api/content#{base_path}") 76 | .to_return(status: 200, body: content_item.to_json) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/link_checker_api.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/test_helpers/json_client_helper" 2 | 3 | module GdsApi 4 | module TestHelpers 5 | module LinkCheckerApi 6 | LINK_CHECKER_API_ENDPOINT = Plek.find("link-checker-api") 7 | 8 | def link_checker_api_link_report_hash(uri:, status: :ok, checked: nil, errors: [], warnings: [], danger: [], problem_summary: nil, suggested_fix: nil) 9 | { 10 | uri:, 11 | status:, 12 | checked: checked || Time.now.iso8601, 13 | errors:, 14 | warnings:, 15 | danger:, 16 | problem_summary:, 17 | suggested_fix:, 18 | } 19 | end 20 | 21 | def link_checker_api_batch_report_hash(id:, status: :completed, links: [], totals: {}, completed_at: nil) 22 | { 23 | id:, 24 | status:, 25 | links: links.map { |hash| link_checker_api_link_report_hash(**hash) }, 26 | totals:, 27 | completed_at: completed_at || Time.now.iso8601, 28 | } 29 | end 30 | 31 | def stub_link_checker_api_check(uri:, status: :ok, checked: nil, errors: [], warnings: [], danger: [], problem_summary: nil, suggested_fix: nil) 32 | body = link_checker_api_link_report_hash( 33 | uri:, status:, checked:, errors:, warnings:, danger:, problem_summary:, suggested_fix:, 34 | ).to_json 35 | 36 | stub_request(:get, "#{LINK_CHECKER_API_ENDPOINT}/check") 37 | .with(query: { uri: }) 38 | .to_return(body:, status: 200, headers: { "Content-Type" => "application/json" }) 39 | end 40 | 41 | def stub_link_checker_api_get_batch(id:, status: :completed, links: [], totals: {}, completed_at: nil) 42 | body = link_checker_api_batch_report_hash( 43 | id:, status:, links:, totals:, completed_at:, 44 | ).to_json 45 | 46 | stub_request(:get, "#{LINK_CHECKER_API_ENDPOINT}/batch/#{id}") 47 | .to_return(body:, status: 200, headers: { "Content-Type" => "application/json" }) 48 | end 49 | 50 | def stub_link_checker_api_create_batch(uris:, checked_within: nil, webhook_uri: nil, webhook_secret_token: nil, id: 0, status: :in_progress, links: nil, totals: {}, completed_at: nil) 51 | links = uris.map { |uri| { uri: } } if links.nil? 52 | 53 | response_body = link_checker_api_batch_report_hash( 54 | id:, 55 | status:, 56 | links:, 57 | totals:, 58 | completed_at:, 59 | ).to_json 60 | 61 | request_body = { 62 | uris:, 63 | checked_within:, 64 | webhook_uri:, 65 | webhook_secret_token:, 66 | }.delete_if { |_, v| v.nil? }.to_json 67 | 68 | stub_request(:post, "#{LINK_CHECKER_API_ENDPOINT}/batch") 69 | .with(body: request_body) 70 | .to_return( 71 | body: response_body, 72 | status: status == :in_progress ? 202 : 201, 73 | headers: { "Content-Type" => "application/json" }, 74 | ) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/put_path_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#put_path pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | it "returns 200 if the path was successfully reserved" do 10 | base_path = "/test-intent" 11 | payload = { 12 | publishing_app: "publisher", 13 | } 14 | 15 | publishing_api 16 | .given("no content exists") 17 | .upon_receiving("a request to put a path") 18 | .with( 19 | method: :put, 20 | path: "/paths#{base_path}", 21 | body: payload, 22 | headers: { 23 | "Content-Type" => "application/json", 24 | }, 25 | ) 26 | .will_respond_with( 27 | status: 200, 28 | body: {}, 29 | headers: { 30 | "Content-Type" => "application/json; charset=utf-8", 31 | }, 32 | ) 33 | 34 | api_client.put_path(base_path, payload) 35 | end 36 | 37 | it "returns 422 if the request is invalid" do 38 | base_path = "/test-item" 39 | payload = { 40 | publishing_app: "whitehall", 41 | } 42 | 43 | publishing_api 44 | .given("/test-item has been reserved by the Publisher application") 45 | .upon_receiving("a request to change publishing app") 46 | .with( 47 | method: :put, 48 | path: "/paths#{base_path}", 49 | body: payload, 50 | headers: { 51 | "Content-Type" => "application/json", 52 | }, 53 | ) 54 | .will_respond_with( 55 | status: 422, 56 | body: { 57 | "error" => { 58 | "code" => 422, 59 | "message" => Pact.term(generate: "Unprocessable", matcher: /\S+/), 60 | "fields" => { 61 | "base_path" => Pact.each_like("has been reserved", min: 1), 62 | }, 63 | }, 64 | }, 65 | ) 66 | 67 | assert_raises(GdsApi::HTTPUnprocessableEntity) do 68 | api_client.put_path(base_path, payload) 69 | end 70 | end 71 | 72 | it "returns 200 if an existing path was overridden" do 73 | base_path = "/test-item" 74 | payload = { 75 | publishing_app: "whitehall", 76 | override_existing: "true", 77 | } 78 | 79 | publishing_api 80 | .given("/test-item has been reserved by the Publisher application") 81 | .upon_receiving("a request to change publishing app with override_existing set") 82 | .with( 83 | method: :put, 84 | path: "/paths#{base_path}", 85 | body: payload, 86 | headers: { 87 | "Content-Type" => "application/json", 88 | }, 89 | ) 90 | .will_respond_with( 91 | status: 200, 92 | body: {}, 93 | headers: { 94 | "Content-Type" => "application/json; charset=utf-8", 95 | }, 96 | ) 97 | 98 | api_client.put_path(base_path, payload) 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/gds_api/list_response.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "gds_api/response" 3 | require "link_header" 4 | 5 | module GdsApi 6 | # Response class for lists of multiple items. 7 | # 8 | # This expects responses to be in a common format, with the list of results 9 | # contained under the `results` key. The response may also have previous and 10 | # subsequent pages, indicated by entries in the response's `Link` header. 11 | class ListResponse < Response 12 | # The ListResponse is instantiated with a reference back to the API client, 13 | # so it can make requests for the subsequent pages 14 | def initialize(response, api_client, options = {}) 15 | super(response, options) 16 | @api_client = api_client 17 | end 18 | 19 | # Pass calls to `self.each` to the `results` sub-object, so we can iterate 20 | # over the response directly 21 | def_delegators :results, :each, :to_ary 22 | 23 | def results 24 | to_hash["results"] 25 | end 26 | 27 | def has_next_page? 28 | !page_link("next").nil? 29 | end 30 | 31 | def next_page 32 | # This shouldn't be a performance problem, since the cache will generally 33 | # avoid us making multiple requests for the same page, but we shouldn't 34 | # allow the data to change once it's already been loaded, so long as we 35 | # retain a reference to any one page in the sequence 36 | @next_page ||= if has_next_page? 37 | @api_client.get_list page_link("next").href 38 | end 39 | end 40 | 41 | def has_previous_page? 42 | !page_link("previous").nil? 43 | end 44 | 45 | def previous_page 46 | # See the note in `next_page` for why this is memoised 47 | @previous_page ||= if has_previous_page? 48 | @api_client.get_list(page_link("previous").href) 49 | end 50 | end 51 | 52 | # Transparently get all results across all pages. Compare this with #each 53 | # or #results which only iterate over the current page. 54 | # 55 | # Example: 56 | # 57 | # list_response.with_subsequent_pages.each do |result| 58 | # ... 59 | # end 60 | # 61 | # or: 62 | # 63 | # list_response.with_subsequent_pages.count 64 | # 65 | # Pages of results are fetched on demand. When iterating, that means 66 | # fetching pages as results from the current page are exhausted. If you 67 | # invoke a method such as #count, this method will fetch all pages at that 68 | # point. Note that the responses are stored so subsequent pages will not be 69 | # loaded multiple times. 70 | def with_subsequent_pages 71 | Enumerator.new do |yielder| 72 | each { |i| yielder << i } 73 | if has_next_page? 74 | next_page.with_subsequent_pages.each { |i| yielder << i } 75 | end 76 | end 77 | end 78 | 79 | private 80 | 81 | def link_header 82 | @link_header ||= LinkHeader.parse @http_response.headers[:link] 83 | end 84 | 85 | def page_link(rel) 86 | link_header.find_link(["rel", rel]) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/support/pact_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["PACT_DO_NOT_TRACK"] = "true" 2 | 3 | PUBLISHING_API_PORT = 3001 4 | ORGANISATION_API_PORT = 3002 5 | BANK_HOLIDAYS_API_PORT = 3003 6 | ACCOUNT_API_PORT = 3004 7 | LINK_CHECKER_API_PORT = 3005 8 | PLACES_MANAGER_API_PORT = 3006 9 | LOCATIONS_API_PORT = 3008 10 | ASSET_MANAGER_API_PORT = 3009 11 | EMAIL_ALERT_API_PORT = 3010 12 | SUPPORT_API_PORT = 3011 13 | SIGNON_API_PORT = 3012 14 | 15 | def publishing_api_host 16 | "http://localhost:#{PUBLISHING_API_PORT}" 17 | end 18 | 19 | def organisation_api_host 20 | "http://localhost:#{ORGANISATION_API_PORT}" 21 | end 22 | 23 | def bank_holidays_api_host 24 | "http://localhost:#{BANK_HOLIDAYS_API_PORT}" 25 | end 26 | 27 | def account_api_host 28 | "http://localhost:#{ACCOUNT_API_PORT}" 29 | end 30 | 31 | def link_checker_api_host 32 | "http://localhost:#{LINK_CHECKER_API_PORT}" 33 | end 34 | 35 | def places_manager_api_host 36 | "http://localhost:#{PLACES_MANAGER_API_PORT}" 37 | end 38 | 39 | def locations_api_host 40 | "http://localhost:#{LOCATIONS_API_PORT}" 41 | end 42 | 43 | def asset_manager_api_host 44 | "http://localhost:#{ASSET_MANAGER_API_PORT}" 45 | end 46 | 47 | def email_alert_api_host 48 | "http://localhost:#{EMAIL_ALERT_API_PORT}" 49 | end 50 | 51 | def support_api_host 52 | "http://localhost:#{SUPPORT_API_PORT}" 53 | end 54 | 55 | def signon_api_host 56 | "http://localhost:#{SIGNON_API_PORT}" 57 | end 58 | 59 | Pact.service_consumer "GDS API Adapters" do 60 | has_pact_with "Publishing API" do 61 | mock_service :publishing_api do 62 | port PUBLISHING_API_PORT 63 | end 64 | end 65 | 66 | has_pact_with "Collections Organisation API" do 67 | mock_service :organisation_api do 68 | port ORGANISATION_API_PORT 69 | end 70 | end 71 | 72 | has_pact_with "Bank Holidays API" do 73 | mock_service :bank_holidays_api do 74 | port BANK_HOLIDAYS_API_PORT 75 | end 76 | end 77 | 78 | has_pact_with "Account API" do 79 | mock_service :account_api do 80 | port ACCOUNT_API_PORT 81 | end 82 | end 83 | 84 | has_pact_with "Link Checker API" do 85 | mock_service :link_checker_api do 86 | port LINK_CHECKER_API_PORT 87 | end 88 | end 89 | 90 | has_pact_with "Places Manager API" do 91 | mock_service :places_manager_api do 92 | port PLACES_MANAGER_API_PORT 93 | end 94 | end 95 | 96 | has_pact_with "Locations API" do 97 | mock_service :locations_api do 98 | port LOCATIONS_API_PORT 99 | end 100 | end 101 | 102 | has_pact_with "Asset Manager" do 103 | mock_service :asset_manager do 104 | port ASSET_MANAGER_API_PORT 105 | end 106 | end 107 | 108 | has_pact_with "Email Alert API" do 109 | mock_service :email_alert_api do 110 | port EMAIL_ALERT_API_PORT 111 | end 112 | end 113 | 114 | has_pact_with "Support API" do 115 | mock_service :support_api do 116 | port SUPPORT_API_PORT 117 | end 118 | end 119 | 120 | has_pact_with "Signon API" do 121 | mock_service :signon_api do 122 | port SIGNON_API_PORT 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_linked_items_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_linked_items pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 9 | 10 | it "404s if the content item does not exist" do 11 | publishing_api 12 | .given("no content exists") 13 | .upon_receiving("a request to return the items linked to it") 14 | .with( 15 | method: :get, 16 | path: "/v2/linked/#{content_id}", 17 | query: "fields%5B%5D=content_id&fields%5B%5D=base_path&link_type=taxon", 18 | ) 19 | .will_respond_with( 20 | status: 404, 21 | body: { 22 | "error" => { 23 | "code" => 404, 24 | "message" => Pact.term(generate: "not found", matcher: /\S+/), 25 | }, 26 | }, 27 | headers: { 28 | "Content-Type" => "application/json; charset=utf-8", 29 | }, 30 | ) 31 | 32 | assert_raises(GdsApi::HTTPNotFound) do 33 | api_client.get_linked_items( 34 | content_id, 35 | link_type: "taxon", 36 | fields: %w[content_id base_path], 37 | ) 38 | end 39 | end 40 | 41 | describe "there are two documents that link to the wanted document" do 42 | let(:linked_content_id) { "6cb2cf8c-670f-4de3-97d5-6ad9114581c7" } 43 | 44 | let(:linking_content_item1) do 45 | { 46 | "content_id" => "e2961462-bc37-48e9-bb98-c981ef1a2d59", 47 | "base_path" => "/item-b", 48 | } 49 | end 50 | 51 | let(:linking_content_item2) do 52 | { 53 | "content_id" => "08dfd5c3-d935-4e81-88fd-cfe65b78893d", 54 | "base_path" => "/item-a", 55 | } 56 | end 57 | 58 | before do 59 | publishing_api 60 | .given("there are two documents with a 'taxon' link to another document") 61 | .upon_receiving("a get linked request") 62 | .with( 63 | method: :get, 64 | path: "/v2/linked/#{linked_content_id}", 65 | query: "fields%5B%5D=content_id&fields%5B%5D=base_path&link_type=taxon", 66 | ) 67 | .will_respond_with( 68 | status: 200, 69 | body: [ 70 | { 71 | content_id: linking_content_item1["content_id"], 72 | base_path: linking_content_item1["base_path"], 73 | }, 74 | { 75 | content_id: linking_content_item2["content_id"], 76 | base_path: linking_content_item2["base_path"], 77 | }, 78 | ], 79 | ) 80 | end 81 | 82 | it "returns the requested fields of linking items" do 83 | response = api_client.get_linked_items( 84 | linked_content_id, 85 | link_type: "taxon", 86 | fields: %w[content_id base_path], 87 | ) 88 | assert_equal 200, response.code 89 | 90 | expected_documents = [linking_content_item2, linking_content_item1] 91 | 92 | expected_documents.each do |document| 93 | assert_includes response.to_a, document 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/pacts/link_checker_api_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/link_checker_api" 3 | 4 | describe "GdsApi::LinkCheckerApi pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::LinkCheckerApi.new(link_checker_api_host) } 8 | 9 | describe "#check" do 10 | it "responds with the details of the checked link" do 11 | link_checker_api 12 | .upon_receiving("the request to check a URI") 13 | .with( 14 | method: :get, 15 | path: "/check", 16 | headers: GdsApi::JsonClient.default_request_headers, 17 | query: { uri: "https://www.gov.uk" }, 18 | ) 19 | .will_respond_with( 20 | status: 200, 21 | body: { 22 | uri: "https://www.gov.uk", 23 | status: "pending", 24 | checked: nil, 25 | errors: [], 26 | warnings: [], 27 | danger: [], 28 | problem_summary: nil, 29 | suggested_fix: nil, 30 | }, 31 | headers: { 32 | "Content-Type" => "application/json; charset=utf-8", 33 | }, 34 | ) 35 | 36 | api_client.check("https://www.gov.uk") 37 | end 38 | end 39 | 40 | describe "#create_batch" do 41 | it "responds with details of the created batch" do 42 | link_checker_api 43 | .upon_receiving("the request to create a batch") 44 | .with( 45 | method: :post, 46 | path: "/batch", 47 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 48 | body: { uris: ["https://www.gov.uk"] }, 49 | ) 50 | .will_respond_with( 51 | status: 202, 52 | body: { 53 | id: Pact.like(1), 54 | status: "in_progress", 55 | links: [ 56 | { 57 | uri: "https://www.gov.uk", 58 | status: "pending", 59 | }, 60 | ], 61 | }, 62 | headers: { 63 | "Content-Type" => "application/json; charset=utf-8", 64 | }, 65 | ) 66 | 67 | api_client.create_batch(["https://www.gov.uk"]) 68 | end 69 | end 70 | 71 | describe "#get_batch" do 72 | it "responds with the details of the batch" do 73 | link_checker_api 74 | .given("a batch exists with id 99 and uris https://www.gov.uk") 75 | .upon_receiving("the request to get a batch") 76 | .with( 77 | method: :get, 78 | path: "/batch/99", 79 | headers: GdsApi::JsonClient.default_request_headers, 80 | ) 81 | .will_respond_with( 82 | status: 200, 83 | body: { 84 | id: 99, 85 | status: "in_progress", 86 | links: [ 87 | { 88 | uri: "https://www.gov.uk", 89 | status: "pending", 90 | }, 91 | ], 92 | }, 93 | headers: { 94 | "Content-Type" => "application/json; charset=utf-8", 95 | }, 96 | ) 97 | 98 | api_client.get_batch(99) 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/locations_api_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/locations_api" 3 | require "gds_api/test_helpers/locations_api" 4 | 5 | describe GdsApi::LocationsApi do 6 | include GdsApi::TestHelpers::LocationsApi 7 | 8 | describe "Locations API" do 9 | let(:base_api_url) { Plek.find("locations-api") } 10 | let(:api) { GdsApi::LocationsApi.new(base_api_url) } 11 | let(:locations) do 12 | [ 13 | { 14 | "latitude" => 51.5010096, 15 | "longitude" => -0.1415870, 16 | "local_custodian_code" => 5900, 17 | }, 18 | { 19 | "latitude" => 51.5010097, 20 | "longitude" => -0.1415871, 21 | "local_custodian_code" => 5901, 22 | }, 23 | ] 24 | end 25 | 26 | it "should return the local custodian codes" do 27 | stub_locations_api_has_location("SW1A 1AA", locations) 28 | 29 | response = api.local_custodian_code_for_postcode("SW1A 1AA") 30 | assert_equal [5900, 5901], response 31 | end 32 | 33 | it "should return only unique local custodian codes " do 34 | stub_locations_api_has_location( 35 | "SW1A 1AA", 36 | [ 37 | { 38 | "latitude" => 51.5010096, 39 | "longitude" => -0.1415870, 40 | "local_custodian_code" => 5900, 41 | }, 42 | { 43 | "latitude" => 51.5010097, 44 | "longitude" => -0.1415871, 45 | "local_custodian_code" => 5901, 46 | }, 47 | { 48 | "latitude" => 51.5010097, 49 | "longitude" => -0.1415871, 50 | "local_custodian_code" => 5901, 51 | }, 52 | ], 53 | ) 54 | 55 | response = api.local_custodian_code_for_postcode("SW1A 1AA") 56 | assert_equal [5900, 5901], response 57 | end 58 | 59 | it "should return empty list for postcode with no local custodian codes" do 60 | stub_locations_api_has_no_location("SW1A 1AA") 61 | 62 | response = api.local_custodian_code_for_postcode("SW1A 1AA") 63 | assert_equal response, [] 64 | end 65 | 66 | it "should return the coordinates" do 67 | stub_locations_api_has_location("SW1A 1AA", locations) 68 | 69 | response = api.coordinates_for_postcode("SW1A 1AA") 70 | assert_equal response, { "latitude" => 51.50100965, "longitude" => -0.14158705 } 71 | end 72 | 73 | it "should return zero for postcode with no coordinates specified" do 74 | stub_locations_api_has_location("SW1A 1AA", [{ "local_custodian_code" => 5900 }]) 75 | 76 | response = api.coordinates_for_postcode("SW1A 1AA") 77 | assert_equal response, { "latitude" => 0, "longitude" => 0 } 78 | end 79 | 80 | it "should return nil for postcode with no coordinates" do 81 | stub_locations_api_has_no_location("SW1A 1AA") 82 | 83 | response = api.coordinates_for_postcode("SW1A 1AA") 84 | assert_nil response 85 | end 86 | 87 | it "should return 400 for an invalid postcode" do 88 | stub_locations_api_does_not_have_a_bad_postcode("B4DP05TC0D3") 89 | 90 | assert_raises GdsApi::HTTPClientError do 91 | api.coordinates_for_postcode("B4DP05TC0D3") 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/gds_api/content_store.rb: -------------------------------------------------------------------------------- 1 | require "plek" 2 | 3 | require_relative "base" 4 | require_relative "exceptions" 5 | 6 | class GdsApi::ContentStore < GdsApi::Base 7 | def content_item(base_path) 8 | get_json(content_item_url(base_path)) 9 | rescue GdsApi::HTTPNotFound => e 10 | raise ItemNotFound.build_from(e) 11 | end 12 | 13 | # Returns an array tuple of destination url with status code e.g 14 | # ["https://www.gov.uk/destination", 301] 15 | def self.redirect_for_path(content_item, request_path, request_query = "") 16 | RedirectResolver.call(content_item, request_path, request_query) 17 | end 18 | 19 | private 20 | 21 | def content_item_url(base_path) 22 | "#{endpoint}/content#{base_path}" 23 | end 24 | 25 | class RedirectResolver 26 | def initialize(content_item, request_path, request_query = "") 27 | @content_item = content_item 28 | @request_path = request_path 29 | @request_query = request_query.to_s 30 | end 31 | 32 | def self.call(*args) 33 | new(*args).call 34 | end 35 | 36 | def call 37 | redirect = redirect_for_path(request_path) 38 | raise UnresolvedRedirect, "Could not find a matching redirect" unless redirect 39 | 40 | destination_uri = URI.parse( 41 | resolve_destination(redirect, request_path, request_query), 42 | ) 43 | 44 | url = if destination_uri.absolute? 45 | destination_uri.to_s 46 | else 47 | "#{Plek.new.website_root}#{destination_uri}" 48 | end 49 | 50 | [url, 301] 51 | end 52 | 53 | private_class_method :new 54 | 55 | private 56 | 57 | attr_reader :content_item, :request_path, :request_query 58 | 59 | def redirect_for_path(path) 60 | redirects_by_segments.find do |r| 61 | next true if r["path"] == path 62 | 63 | route_prefix_match?(r["path"], path) if r["type"] == "prefix" 64 | end 65 | end 66 | 67 | def redirects_by_segments 68 | redirects = content_item["redirects"] || [] 69 | redirects.sort_by { |r| r["path"].split("/").count * -1 } 70 | end 71 | 72 | def route_prefix_match?(prefix_path, path_to_match) 73 | prefix_regex = %r{^#{Regexp.escape(prefix_path)}/} 74 | path_to_match.match prefix_regex 75 | end 76 | 77 | def resolve_destination(redirect, path, query) 78 | return redirect["destination"] unless redirect["segments_mode"] == "preserve" 79 | 80 | if redirect["type"] == "prefix" 81 | prefix_destination(redirect, path, query) 82 | else 83 | redirect["destination"] + (query.empty? ? "" : "?#{query}") 84 | end 85 | end 86 | 87 | def prefix_destination(redirect, path, query) 88 | uri = URI.parse(redirect["destination"]) 89 | start_char = redirect["path"].length 90 | suffix = path[start_char..] 91 | 92 | if uri.path == "" && suffix[0] != "/" 93 | uri.path = "/#{suffix}" 94 | else 95 | uri.path += suffix 96 | end 97 | 98 | uri.query = query if uri.query.nil? && !query.empty? 99 | 100 | uri.to_s 101 | end 102 | end 103 | 104 | class UnresolvedRedirect < GdsApi::BaseError; end 105 | end 106 | -------------------------------------------------------------------------------- /test/pacts/places_manager_api_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/places_manager" 3 | 4 | describe "GdsApi::PlacesManager pact tests" do 5 | include PactTest 6 | 7 | describe "#places" do 8 | let(:api_client) { GdsApi::PlacesManager.new(places_manager_api_host) } 9 | 10 | it "responds with all responses for the given dataset" do 11 | places_manager_api 12 | .given("a service exists called number-plate-supplier with places") 13 | .upon_receiving("the request to retrieve relevant places for the current dataset for a lat/lon") 14 | .with( 15 | method: :get, 16 | path: "/places/number-plate-supplier.json", 17 | headers: GdsApi::JsonClient.default_request_headers, 18 | query: { lat: "-2.01", lng: "53.1", limit: "5" }, 19 | ) 20 | .will_respond_with( 21 | status: 200, 22 | body: { 23 | status: "ok", 24 | contents: "places", 25 | places: Pact.each_like( 26 | { 27 | access_notes: nil, 28 | address1: "Yarrow Road Tower Park", 29 | address2: nil, 30 | data_set_version: 473, 31 | email: nil, 32 | fax: nil, 33 | general_notes: nil, 34 | geocode_error: nil, 35 | location: { longitude: -1.9552618901330387, latitude: 50.742754933617285 }, 36 | name: "Breeze Motor Co Ltd", 37 | override_lat: nil, 38 | override_lng: nil, 39 | phone: "01202 713000", 40 | postcode: "BH12 4QY", 41 | service_slug: "number-plate-supplier", 42 | gss: nil, 43 | source_address: "Yarrow Road Tower Park Poole BH12 4QY", 44 | text_phone: nil, 45 | town: "Yarrow", 46 | url: nil, 47 | }, 48 | ), 49 | }, 50 | headers: { 51 | "Content-Type" => "application/json; charset=utf-8", 52 | }, 53 | ) 54 | 55 | api_client.places("number-plate-supplier", "-2.01", "53.1", "5") 56 | end 57 | 58 | it "responds with a choice of addresses for disambiguation of split postcodes" do 59 | places_manager_api 60 | .given("a service exists called register office exists with places, and CH25 9BJ is a split postcode") 61 | .upon_receiving("the request to retrieve relevant places for the current dataset for CH25 9BJ") 62 | .with( 63 | method: :get, 64 | path: "/places/register-office.json", 65 | headers: GdsApi::JsonClient.default_request_headers, 66 | query: { postcode: "CH25 9BJ", limit: "5" }, 67 | ) 68 | .will_respond_with( 69 | status: 200, 70 | body: { 71 | status: "address-information-required", 72 | contents: "addresses", 73 | addresses: Pact.each_like( 74 | { 75 | address: "HOUSE 1", 76 | local_authority_slug: "achester", 77 | }, 78 | ), 79 | }, 80 | headers: { 81 | "Content-Type" => "application/json; charset=utf-8", 82 | }, 83 | ) 84 | 85 | api_client.places_for_postcode("register-office", "CH25 9BJ") 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/republish_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#republish pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 9 | 10 | it "responds with 200 if the republish command succeeds" do 11 | publishing_api 12 | .given("an unpublished content item exists with content_id: #{content_id}") 13 | .upon_receiving("a republish request") 14 | .with( 15 | method: :post, 16 | path: "/v2/content/#{content_id}/republish", 17 | body: {}, 18 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 19 | ) 20 | .will_respond_with( 21 | status: 200, 22 | ) 23 | 24 | api_client.republish(content_id) 25 | end 26 | 27 | it "responds with 404 if the content item does not exist" do 28 | publishing_api 29 | .given("no content exists") 30 | .upon_receiving("a republish request") 31 | .with( 32 | method: :post, 33 | path: "/v2/content/#{content_id}/republish", 34 | body: {}, 35 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 36 | ) 37 | .will_respond_with( 38 | status: 404, 39 | ) 40 | 41 | assert_raises(GdsApi::HTTPNotFound) do 42 | api_client.republish(content_id) 43 | end 44 | end 45 | 46 | describe "optimistic locking" do 47 | it "responds with 200 OK if the content item has not changed since it was requested" do 48 | publishing_api 49 | .given("the published content item #{content_id} is at version 3") 50 | .upon_receiving("a republish request for version 3") 51 | .with( 52 | method: :post, 53 | path: "/v2/content/#{content_id}/republish", 54 | body: { 55 | previous_version: 3, 56 | }, 57 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 58 | ) 59 | .will_respond_with( 60 | status: 200, 61 | ) 62 | 63 | api_client.republish(content_id, previous_version: 3) 64 | end 65 | 66 | it "responds with 409 Conflict if the content item has changed in the meantime" do 67 | publishing_api 68 | .given("the published content item #{content_id} is at version 3") 69 | .upon_receiving("a republish request for version 2") 70 | .with( 71 | method: :post, 72 | path: "/v2/content/#{content_id}/republish", 73 | body: { 74 | previous_version: 2, 75 | }, 76 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 77 | ) 78 | .will_respond_with( 79 | status: 409, 80 | body: { 81 | "error" => { 82 | "code" => 409, 83 | "message" => Pact.term(generate: "Conflict", matcher: /\S+/), 84 | "fields" => { 85 | "previous_version" => Pact.each_like("does not match", min: 1), 86 | }, 87 | }, 88 | }, 89 | headers: { 90 | "Content-Type" => "application/json; charset=utf-8", 91 | }, 92 | ) 93 | 94 | assert_raises(GdsApi::HTTPConflict) do 95 | api_client.republish(content_id, previous_version: 2) 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/gds_api/exceptions.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | # Abstract error class 3 | class BaseError < StandardError 4 | # Give Sentry extra context about this event 5 | # https://docs.sentry.io/clients/ruby/context/ 6 | def sentry_context 7 | { 8 | # Make Sentry group exceptions by type instead of message, so all 9 | # exceptions like `GdsApi::TimedOutException` will get grouped as one 10 | # error and not an error per URL. 11 | fingerprint: [self.class.name], 12 | } 13 | end 14 | end 15 | 16 | class EndpointNotFound < BaseError; end 17 | 18 | class TimedOutException < BaseError; end 19 | 20 | class InvalidUrl < BaseError; end 21 | 22 | class SocketErrorException < BaseError; end 23 | 24 | # Superclass for all 4XX and 5XX errors 25 | class HTTPErrorResponse < BaseError 26 | attr_accessor :code, :error_details, :http_body 27 | 28 | def initialize(code, message = nil, error_details = nil, http_body = nil) 29 | super(message) 30 | @code = code 31 | @error_details = error_details 32 | @http_body = http_body 33 | end 34 | end 35 | 36 | # Superclass & fallback for all 4XX errors 37 | class HTTPClientError < HTTPErrorResponse; end 38 | 39 | class HTTPIntermittentClientError < HTTPClientError; end 40 | 41 | class HTTPNotFound < HTTPClientError; end 42 | 43 | class HTTPGone < HTTPClientError; end 44 | 45 | class HTTPPayloadTooLarge < HTTPClientError; end 46 | 47 | class HTTPUnauthorized < HTTPClientError; end 48 | 49 | class HTTPForbidden < HTTPClientError; end 50 | 51 | class HTTPConflict < HTTPClientError; end 52 | 53 | class HTTPUnprocessableEntity < HTTPClientError; end 54 | 55 | class HTTPBadRequest < HTTPClientError; end 56 | 57 | class HTTPTooManyRequests < HTTPIntermittentClientError; end 58 | 59 | # Superclass & fallback for all 5XX errors 60 | class HTTPServerError < HTTPErrorResponse; end 61 | 62 | class HTTPIntermittentServerError < HTTPServerError; end 63 | 64 | class HTTPInternalServerError < HTTPServerError; end 65 | 66 | class HTTPBadGateway < HTTPIntermittentServerError; end 67 | 68 | class HTTPUnavailable < HTTPIntermittentServerError; end 69 | 70 | class HTTPGatewayTimeout < HTTPIntermittentServerError; end 71 | 72 | module ExceptionHandling 73 | def build_specific_http_error(error, url, details = nil) 74 | message = "URL: #{url}\nResponse body:\n#{error.http_body}" 75 | code = error.http_code 76 | error_class_for_code(code).new(code, message, details, error.http_body) 77 | end 78 | 79 | def error_class_for_code(code) 80 | case code 81 | when 400 82 | GdsApi::HTTPBadRequest 83 | when 401 84 | GdsApi::HTTPUnauthorized 85 | when 403 86 | GdsApi::HTTPForbidden 87 | when 404 88 | GdsApi::HTTPNotFound 89 | when 409 90 | GdsApi::HTTPConflict 91 | when 410 92 | GdsApi::HTTPGone 93 | when 413 94 | GdsApi::HTTPPayloadTooLarge 95 | when 422 96 | GdsApi::HTTPUnprocessableEntity 97 | when 429 98 | GdsApi::HTTPTooManyRequests 99 | when (400..499) 100 | GdsApi::HTTPClientError 101 | when 500 102 | GdsApi::HTTPInternalServerError 103 | when 502 104 | GdsApi::HTTPBadGateway 105 | when 503 106 | GdsApi::HTTPUnavailable 107 | when 504 108 | GdsApi::HTTPGatewayTimeout 109 | when (500..599) 110 | GdsApi::HTTPServerError 111 | else 112 | GdsApi::HTTPErrorResponse 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_host_content_item_for_content_id_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_host_content_item_for_content_id pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | let(:reusable_content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 10 | let(:content_id) { "d66d6552-2627-4451-9dbc-cadbbd2005a1" } 11 | let(:publishing_organisation_content_id) { "d1e7d343-9844-4246-a469-1fa4640e12ad" } 12 | let(:expected_body) do 13 | { 14 | "title" => "foo", 15 | "base_path" => "/foo", 16 | "document_type" => "publication", 17 | "publishing_app" => "publisher", 18 | "primary_publishing_organisation" => { 19 | "content_id" => publishing_organisation_content_id, 20 | "title" => "bar", 21 | "base_path" => "/bar", 22 | }, 23 | } 24 | end 25 | 26 | it "responds with 200 if the target content item exists" do 27 | publishing_api 28 | .given("a content item exists (content_id: #{content_id}) that embeds the reusable content (content_id: #{reusable_content_id})") 29 | .upon_receiving("a get_host_content_item_for_content_id request") 30 | .with( 31 | method: :get, 32 | path: "/v2/content/#{reusable_content_id}/host-content/#{content_id}", 33 | ) 34 | .will_respond_with( 35 | status: 200, 36 | body: expected_body, 37 | ) 38 | 39 | response = api_client.get_host_content_item_for_content_id(reusable_content_id, content_id) 40 | 41 | assert_equal(expected_body, response.parsed_content) 42 | end 43 | 44 | it "allows a locale to be specified" do 45 | publishing_api 46 | .given("a content item exists (content_id: #{content_id}) that embeds the reusable content (content_id: #{reusable_content_id})") 47 | .upon_receiving("a get_host_content_item_for_content_id request with a locale") 48 | .with( 49 | method: :get, 50 | path: "/v2/content/#{reusable_content_id}/host-content/#{content_id}", 51 | query: "locale=en", 52 | ) 53 | .will_respond_with( 54 | status: 200, 55 | body: expected_body, 56 | ) 57 | 58 | response = api_client.get_host_content_item_for_content_id(reusable_content_id, content_id, locale: "en") 59 | 60 | assert_equal(expected_body, response.parsed_content) 61 | end 62 | 63 | it "responds with 404 if the content item does not exist" do 64 | missing_content_id = "missing-content-id" 65 | publishing_api 66 | .given("no content exists") 67 | .upon_receiving("a get_host_content_item_for_content_id request with a missing content ID") 68 | .with( 69 | method: :get, 70 | path: "/v2/content/#{reusable_content_id}/host-content/#{missing_content_id}", 71 | ) 72 | .will_respond_with( 73 | status: 404, 74 | ) 75 | 76 | assert_raises(GdsApi::HTTPNotFound) do 77 | api_client.get_host_content_item_for_content_id(reusable_content_id, missing_content_id) 78 | end 79 | end 80 | 81 | it "responds with 404 if the host content item does not exist" do 82 | missing_content_id = "missing-content-id" 83 | publishing_api 84 | .given("no content exists") 85 | .upon_receiving("a get_host_content_item_for_content_id request with a missing host content ID") 86 | .with( 87 | method: :get, 88 | path: "/v2/content/#{missing_content_id}/host-content/#{content_id}", 89 | ) 90 | .will_respond_with( 91 | status: 404, 92 | ) 93 | 94 | assert_raises(GdsApi::HTTPNotFound) do 95 | api_client.get_host_content_item_for_content_id(missing_content_id, content_id) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/test_helpers/asset_manager_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/asset_manager" 3 | require "gds_api/test_helpers/asset_manager" 4 | 5 | describe GdsApi::TestHelpers::AssetManager do 6 | include GdsApi::TestHelpers::AssetManager 7 | 8 | let(:stub_asset_manager) do 9 | GdsApi::AssetManager.new(Plek.find("asset-manager")) 10 | end 11 | 12 | describe "#stub_asset_manager_receives_an_asset" do 13 | describe "when passed a string" do 14 | it "returns the string as the file url" do 15 | url = "https://assets.example.com/path/to/asset" 16 | stub_asset_manager_receives_an_asset(url) 17 | response = stub_asset_manager.create_asset({}) 18 | 19 | assert_equal url, response["file_url"] 20 | end 21 | end 22 | 23 | describe "when passed no arguments" do 24 | it "returns a random, yet valid asset manager url" do 25 | stub_asset_manager_receives_an_asset 26 | response = stub_asset_manager.create_asset({}) 27 | 28 | url_format = %r{\Ahttp://asset-manager.dev.gov.uk/media/[^/]*/[^/]*\Z} 29 | assert_match url_format, response["file_url"] 30 | end 31 | 32 | it "returns a different URL each call" do 33 | stub_asset_manager_receives_an_asset 34 | response1 = stub_asset_manager.create_asset({}) 35 | response2 = stub_asset_manager.create_asset({}) 36 | 37 | refute_match response1["file_url"], response2["file_url"] 38 | end 39 | end 40 | 41 | describe "when passed a hash" do 42 | it "can specify the id of an asset" do 43 | stub_asset_manager_receives_an_asset(id: "123") 44 | response = stub_asset_manager.create_asset({}) 45 | 46 | url_format = %r{\Ahttp://asset-manager.dev.gov.uk/media/123/[^/]*\Z} 47 | assert_match url_format, response["file_url"] 48 | end 49 | 50 | it "can specify the filename of an asset" do 51 | stub_asset_manager_receives_an_asset(filename: "file.ext") 52 | response = stub_asset_manager.create_asset({}) 53 | 54 | url_format = %r{\Ahttp://asset-manager.dev.gov.uk/media/[^/]*/file.ext\Z} 55 | assert_match url_format, response["file_url"] 56 | end 57 | 58 | it "can specify both filename and id" do 59 | stub_asset_manager_receives_an_asset(id: "123", filename: "file.ext") 60 | response = stub_asset_manager.create_asset({}) 61 | 62 | url_format = %r{\Ahttp://asset-manager.dev.gov.uk/media/123/file.ext\Z} 63 | assert_match url_format, response["file_url"] 64 | end 65 | end 66 | end 67 | 68 | describe "#stub_asset_manager_delete_asset" do 69 | it "returns an ok response and the provided body" do 70 | asset_id = "some-asset-id" 71 | body = { key: "value" } 72 | stub_asset_manager_delete_asset(asset_id, body) 73 | 74 | response = stub_asset_manager.delete_asset(asset_id) 75 | 76 | assert_equal 200, response.code 77 | assert_equal body[:key], response["key"] 78 | end 79 | end 80 | 81 | describe "#stub_asset_manager_delete_asset_missing" do 82 | it "raises a not found error" do 83 | asset_id = "some-asset-id" 84 | stub_asset_manager_delete_asset_missing(asset_id) 85 | 86 | assert_raises GdsApi::HTTPNotFound do 87 | stub_asset_manager.delete_asset(asset_id) 88 | end 89 | end 90 | end 91 | 92 | describe "#stub_asset_manager_delete_asset_failure" do 93 | it "raises an internal server error" do 94 | asset_id = "some-asset-id" 95 | stub_asset_manager_delete_asset_failure(asset_id) 96 | 97 | assert_raises GdsApi::HTTPInternalServerError do 98 | stub_asset_manager.delete_asset(asset_id) 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/content_store.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/test_helpers/json_client_helper" 2 | require "gds_api/test_helpers/content_item_helpers" 3 | require "json" 4 | 5 | module GdsApi 6 | module TestHelpers 7 | module ContentStore 8 | include ContentItemHelpers 9 | 10 | def content_store_endpoint(draft: false) 11 | draft ? Plek.find("draft-content-store") : Plek.find("content-store") 12 | end 13 | 14 | # Stubs a content item in the content store. 15 | # The following options can be passed in: 16 | # 17 | # :max_age will set the max-age of the Cache-Control header in the response. Defaults to 900 18 | # :private if true, the Cache-Control header will include the "private" directive. By default it 19 | # will include "public" 20 | # :draft will point to the draft content store if set to true 21 | def stub_content_store_has_item(base_path, body = content_item_for_base_path(base_path), options = {}) 22 | max_age = options.fetch(:max_age, 900) 23 | visibility = options[:private] ? "private" : "public" 24 | body = body.to_json unless body.is_a?(String) 25 | 26 | endpoint = content_store_endpoint(draft: options[:draft]) 27 | stub_request(:get, "#{endpoint}/content#{base_path}").to_return( 28 | status: 200, 29 | body:, 30 | headers: { 31 | cache_control: "#{visibility}, max-age=#{max_age}", 32 | date: Time.now.httpdate, 33 | }, 34 | ) 35 | end 36 | 37 | def stub_content_store_does_not_have_item(base_path, options = {}) 38 | endpoint = content_store_endpoint(draft: options[:draft]) 39 | stub_request(:get, "#{endpoint}/content#{base_path}").to_return(status: 404, headers: {}) 40 | stub_request(:get, "#{endpoint}/incoming-links#{base_path}").to_return(status: 404, headers: {}) 41 | end 42 | 43 | # Content store has gone item 44 | # 45 | # Stubs a content item in the content store to respond with 410 HTTP Status Code and response body with 'format' set to 'gone'. 46 | # 47 | # @param base_path [String] 48 | # @param body [Hash] 49 | # @param options [Hash] 50 | # @option options [String] draft Will point to the draft content store if set to true 51 | # 52 | # @example 53 | # 54 | # stub_content_store.stub_content_store_has_gone_item('/sample-slug') 55 | # 56 | # # Will return HTTP Status Code 410 and the following response body: 57 | # { 58 | # "title" => nil, 59 | # "description" => nil, 60 | # "format" => "gone", 61 | # "schema_name" => "gone", 62 | # "public_updated_at" => nil, 63 | # "base_path" => "/sample-slug", 64 | # "withdrawn_notice" => {}, 65 | # "details" => {} 66 | # } 67 | def stub_content_store_has_gone_item(base_path, body = gone_content_item_for_base_path(base_path), options = {}) 68 | body = body.to_json unless body.is_a?(String) 69 | endpoint = content_store_endpoint(draft: options[:draft]) 70 | stub_request(:get, "#{endpoint}/content#{base_path}").to_return( 71 | status: 410, 72 | body:, 73 | headers: {}, 74 | ) 75 | end 76 | 77 | def stub_content_store_isnt_available 78 | stub_request(:any, /#{content_store_endpoint}\/.*/).to_return(status: 503) 79 | end 80 | 81 | def content_item_for_base_path(base_path) 82 | super.merge("base_path" => base_path) 83 | end 84 | 85 | def stub_content_store_has_incoming_links(base_path, links) 86 | url = "#{content_store_endpoint}/incoming-links#{base_path}" 87 | body = links.to_json 88 | 89 | stub_request(:get, url).to_return(body:) 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_live_content_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_live_content pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 9 | 10 | it "returns the published content item when the latest version of the content item is published" do 11 | publishing_api 12 | .given("a published content item exists with content_id: #{content_id}") 13 | .upon_receiving("a request to return the live content item") 14 | .with( 15 | method: :get, 16 | path: "/v2/content/#{content_id}", 17 | ) 18 | .will_respond_with( 19 | status: 200, 20 | body: { 21 | "content_id" => content_id, 22 | "document_type" => Pact.like("special_route"), 23 | "schema_name" => Pact.like("special_route"), 24 | "publishing_app" => Pact.like("publisher"), 25 | "rendering_app" => Pact.like("frontend"), 26 | "locale" => Pact.like("en"), 27 | "routes" => Pact.like([{}]), 28 | "public_updated_at" => Pact.like("2015-07-30T13:58:11.000Z"), 29 | "details" => Pact.like({}), 30 | "state_history" => { "1" => "published" }, 31 | "publication_state" => "published", 32 | }, 33 | headers: { 34 | "Content-Type" => "application/json; charset=utf-8", 35 | }, 36 | ) 37 | 38 | api_client.get_live_content(content_id) 39 | end 40 | 41 | it "returns the live content item when there is a draft version of live content" do 42 | publishing_api 43 | .given("a published content item exists with a draft edition for content_id: #{content_id}") 44 | .upon_receiving("a request to return the live content item") 45 | .with( 46 | method: :get, 47 | path: "/v2/content/#{content_id}", 48 | query: "locale=en&content_store=live", 49 | ) 50 | .will_respond_with( 51 | status: 200, 52 | body: { 53 | "content_id" => content_id, 54 | "document_type" => Pact.like("special_route"), 55 | "schema_name" => Pact.like("special_route"), 56 | "publishing_app" => Pact.like("publisher"), 57 | "rendering_app" => Pact.like("frontend"), 58 | "locale" => Pact.like("en"), 59 | "routes" => Pact.like([{}]), 60 | "public_updated_at" => Pact.like("2015-07-30T13:58:11.000Z"), 61 | "details" => Pact.like({}), 62 | "state_history" => { "1" => "published", "2" => "draft" }, 63 | "publication_state" => "published", 64 | }, 65 | headers: { 66 | "Content-Type" => "application/json; charset=utf-8", 67 | }, 68 | ) 69 | 70 | api_client.get_live_content(content_id) 71 | end 72 | 73 | it "returns the unpublished content item when the latest version of the content item is unpublished" do 74 | publishing_api 75 | .given("an unpublished content item exists with content_id: #{content_id}") 76 | .upon_receiving("a request to return the content item") 77 | .with( 78 | method: :get, 79 | path: "/v2/content/#{content_id}", 80 | ) 81 | .will_respond_with( 82 | status: 200, 83 | body: { 84 | "content_id" => content_id, 85 | "document_type" => Pact.like("special_route"), 86 | "schema_name" => Pact.like("special_route"), 87 | "publishing_app" => Pact.like("publisher"), 88 | "rendering_app" => Pact.like("frontend"), 89 | "locale" => Pact.like("en"), 90 | "routes" => Pact.like([{}]), 91 | "public_updated_at" => Pact.like("2015-07-30T13:58:11.000Z"), 92 | "details" => Pact.like({}), 93 | "state_history" => { "1" => "unpublished" }, 94 | "publication_state" => "unpublished", 95 | }, 96 | headers: { 97 | "Content-Type" => "application/json; charset=utf-8", 98 | }, 99 | ) 100 | 101 | api_client.get_live_content(content_id) 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/get_paged_editions_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#get_paged_editions pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | 9 | describe "there are multiple pages of editions" do 10 | let(:content_id_1) { "bd50a6d9-f03d-4ccf-94aa-ad79579990a9" } 11 | let(:content_id_2) { "989033fe-252a-4e69-976d-5c0059bca949" } 12 | let(:content_id_3) { "271d4270-9186-4d60-b2ca-1d7dae7e0f73" } 13 | let(:content_id_4) { "638af19c-27fc-4cc9-a914-4cca49028688" } 14 | 15 | let(:first_page) do 16 | { 17 | request: { 18 | method: :get, 19 | path: "/v2/editions", 20 | query: "fields%5B%5D=content_id&per_page=2", 21 | headers: GdsApi::JsonClient.default_request_headers, 22 | }, 23 | response: { 24 | status: 200, 25 | body: { 26 | results: [ 27 | { content_id: content_id_1 }, 28 | { content_id: content_id_2 }, 29 | ], 30 | links: [ 31 | { href: "http://example.org#{second_page[:request][:path]}?#{second_page[:request][:query]}", rel: "next" }, 32 | { href: "http://example.org/v2/editions?fields%5B%5D=content_id&per_page=2", rel: "self" }, 33 | ], 34 | }, 35 | }, 36 | } 37 | end 38 | 39 | let(:second_page) do 40 | { 41 | request: { 42 | method: :get, 43 | path: "/v2/editions", 44 | query: "fields%5B%5D=content_id&per_page=2&after=2017-02-01T00%3A00%3A00.000000Z%2C2", 45 | headers: GdsApi::JsonClient.default_request_headers, 46 | }, 47 | response: { 48 | status: 200, 49 | body: { 50 | results: [ 51 | { content_id: content_id_3 }, 52 | { content_id: content_id_4 }, 53 | ], 54 | links: [ 55 | { href: "http://example.org/v2/editions?fields%5B%5D=content_id&per_page=2&after=2017-02-01T00%3A00%3A00.000000Z%2C2", rel: "self" }, 56 | { href: "http://example.org/v2/editions?fields%5B%5D=content_id&per_page=2&before=2017-03-01T00%3A00%3A00.000000Z%2C3", rel: "previous" }, 57 | ], 58 | }, 59 | }, 60 | } 61 | end 62 | 63 | before do 64 | publishing_api 65 | .given("there are 4 live content items with fixed updated timestamps") 66 | .upon_receiving("a get editions request for 2 per page") 67 | .with(first_page[:request]) 68 | .will_respond_with(first_page[:response]) 69 | 70 | publishing_api 71 | .given("there are 4 live content items with fixed updated timestamps") 72 | .upon_receiving("a next page editions request") 73 | .with(second_page[:request]) 74 | .will_respond_with(second_page[:response]) 75 | end 76 | 77 | it "receives two pages of results" do 78 | first_page_url = "#{publishing_api_host}#{first_page[:request][:path]}?#{first_page[:request][:query]}" 79 | second_page_path = "#{second_page[:request][:path]}?#{second_page[:request][:query]}" 80 | 81 | # Manually override JsonClient#get_json, because the Pact tests mean we return an invalid pagination 82 | # URL, which we have to replace with our mocked publishing_api_host 83 | api_client 84 | .expects(:get_json) 85 | .with(first_page_url) 86 | .returns(GdsApi::JsonClient.new.get_json(first_page_url, first_page[:request][:headers])) 87 | 88 | api_client 89 | .expects(:get_json) 90 | .with("http://example.org#{second_page_path}") 91 | .returns(GdsApi::JsonClient.new.get_json("#{publishing_api_host}#{second_page_path}", second_page[:request][:headers])) 92 | 93 | response = api_client.get_paged_editions(fields: %w[content_id], per_page: 2).to_a 94 | 95 | assert_equal 2, response.count 96 | first_page_content_ids = response[0]["results"].map { |content_item| content_item["content_id"] } 97 | second_page_content_ids = response[1]["results"].map { |content_item| content_item["content_id"] } 98 | 99 | assert_equal [content_id_1, content_id_2], first_page_content_ids 100 | assert_equal [content_id_3, content_id_4], second_page_content_ids 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/asset_manager.rb: -------------------------------------------------------------------------------- 1 | module GdsApi 2 | module TestHelpers 3 | module AssetManager 4 | ASSET_MANAGER_ENDPOINT = Plek.find("asset-manager") 5 | 6 | def stub_any_asset_manager_call 7 | stub_request(:any, %r{\A#{ASSET_MANAGER_ENDPOINT}}).to_return(status: 200) 8 | end 9 | 10 | def stub_asset_manager_isnt_available 11 | stub_request(:any, %r{\A#{ASSET_MANAGER_ENDPOINT}}).to_return(status: 503) 12 | end 13 | 14 | def stub_asset_manager_updates_any_asset(body = {}) 15 | stub_request(:put, %r{\A#{ASSET_MANAGER_ENDPOINT}/assets}) 16 | .to_return(body: body.to_json, status: 200) 17 | end 18 | 19 | def stub_asset_manager_deletes_any_asset(body = {}) 20 | stub_request(:delete, %r{\A#{ASSET_MANAGER_ENDPOINT}/assets}) 21 | .to_return(body: body.to_json, status: 200) 22 | end 23 | 24 | def stub_asset_manager_has_an_asset(id, atts, filename = "") 25 | response = atts.merge("_response_info" => { "status" => "ok" }) 26 | 27 | stub_request(:get, "#{ASSET_MANAGER_ENDPOINT}/assets/#{id}") 28 | .to_return(body: response.to_json, status: 200) 29 | 30 | stub_request(:get, "#{ASSET_MANAGER_ENDPOINT}/media/#{id}/#{filename}") 31 | .to_return(body: "Some file content", status: 200) 32 | end 33 | 34 | def stub_asset_manager_has_a_whitehall_media_asset(legacy_url_path, content) 35 | stub_request(:get, "#{ASSET_MANAGER_ENDPOINT}/#{legacy_url_path}") 36 | .to_return(body: content, status: 200) 37 | end 38 | 39 | def stub_asset_manager_does_not_have_an_asset(id) 40 | response = { 41 | "_response_info" => { "status" => "not found" }, 42 | } 43 | 44 | stub_request(:any, "#{ASSET_MANAGER_ENDPOINT}/assets/#{id}") 45 | .to_return(body: response.to_json, status: 404) 46 | end 47 | 48 | # This can take a string of an exact url or a hash of options 49 | # 50 | # with a string: 51 | # `stub_asset_manager_receives_an_asset("https://asset-manager/media/619ce797-b415-42e5-b2b1-2ffa0df52302/file.jpg")` 52 | # 53 | # with a hash: 54 | # `stub_asset_manager_receives_an_asset(id: "20d04259-e3ae-4f71-8157-e6c843096e96", filename: "file.jpg")` 55 | # which would return a file url of "https://asset-manager/media/20d04259-e3ae-4f71-8157-e6c843096e96/file.jpg" 56 | # 57 | # with no argument 58 | # 59 | # `stub_asset_manager_receives_an_asset` 60 | # which would return a file url of "https://asset-manager/media/0053adbf-0737-4923-9d8a-8180f2c723af/0d19136c4a94f07" 61 | def stub_asset_manager_receives_an_asset(response_url = {}) 62 | stub_request(:post, "#{ASSET_MANAGER_ENDPOINT}/assets").to_return do 63 | if response_url.is_a?(String) 64 | file_url = response_url 65 | else 66 | options = { 67 | id: SecureRandom.uuid, 68 | filename: SecureRandom.hex(8), 69 | }.merge(response_url) 70 | 71 | file_url = "#{ASSET_MANAGER_ENDPOINT}/media/#{options[:id]}/#{options[:filename]}" 72 | end 73 | { body: { file_url: }.to_json, status: 200 } 74 | end 75 | end 76 | 77 | def stub_asset_manager_upload_failure 78 | stub_request(:post, "#{ASSET_MANAGER_ENDPOINT}/assets").to_return(status: 500) 79 | end 80 | 81 | def stub_asset_manager_update_asset(asset_id, body = {}) 82 | stub_request(:put, "#{ASSET_MANAGER_ENDPOINT}/assets/#{asset_id}") 83 | .to_return(body: body.to_json, status: 200) 84 | end 85 | 86 | def stub_asset_manager_update_asset_failure(asset_id) 87 | stub_request(:put, "#{ASSET_MANAGER_ENDPOINT}/assets/#{asset_id}").to_return(status: 500) 88 | end 89 | 90 | def stub_asset_manager_delete_asset(asset_id, body = {}) 91 | stub_request(:delete, "#{ASSET_MANAGER_ENDPOINT}/assets/#{asset_id}") 92 | .to_return(body: body.to_json, status: 200) 93 | end 94 | 95 | def stub_asset_manager_delete_asset_missing(asset_id) 96 | stub_request(:delete, "#{ASSET_MANAGER_ENDPOINT}/assets/#{asset_id}") 97 | .to_return(status: 404) 98 | end 99 | 100 | def stub_asset_manager_delete_asset_failure(asset_id) 101 | stub_request(:delete, "#{ASSET_MANAGER_ENDPOINT}/assets/#{asset_id}").to_return(status: 500) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /test/asset_manager_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/asset_manager" 3 | require "gds_api/test_helpers/asset_manager" 4 | require "json" 5 | 6 | describe GdsApi::AssetManager do 7 | include GdsApi::TestHelpers::AssetManager 8 | 9 | let(:base_api_url) { Plek.find("asset-manager") } 10 | let(:api) { GdsApi::AssetManager.new(base_api_url) } 11 | 12 | let(:file_fixture) { load_fixture_file("hello.txt") } 13 | 14 | let(:asset_url) { [base_api_url, "assets", asset_id].join("/") } 15 | let(:asset_id) { "new-asset-id" } 16 | 17 | let(:stub_asset_manager_response) do 18 | { 19 | asset: { 20 | id: asset_url, 21 | }, 22 | } 23 | end 24 | 25 | it "creates the asset with a file" do 26 | req = stub_request(:post, "#{base_api_url}/assets") 27 | .with { |request| 28 | request.body =~ %r{Content-Disposition: form-data; name="asset\[file\]"; filename="hello\.txt"\r\nContent-Type: text/plain} 29 | }.to_return(body: JSON.dump(stub_asset_manager_response), status: 201) 30 | 31 | response = api.create_asset(file: file_fixture) 32 | 33 | assert_equal asset_url, response["asset"]["id"] 34 | assert_requested(req) 35 | end 36 | 37 | it "returns not found when the asset does not exist" do 38 | stub_asset_manager_does_not_have_an_asset("not-really-here") 39 | 40 | assert_raises GdsApi::HTTPNotFound do 41 | api.asset("not-really-here") 42 | end 43 | 44 | assert_raises GdsApi::HTTPNotFound do 45 | api.delete_asset("not-really-here") 46 | end 47 | end 48 | 49 | describe "the asset exists" do 50 | before do 51 | stub_asset_manager_has_an_asset( 52 | asset_id, 53 | { 54 | "name" => "photo.jpg", 55 | "content_type" => "image/jpeg", 56 | "file_url" => "http://fooey.gov.uk/media/photo.jpg", 57 | }, 58 | "photo.jpg", 59 | ) 60 | end 61 | 62 | let(:asset_id) { "test-id" } 63 | 64 | it "updates the asset with a file" do 65 | req = stub_request(:put, "#{base_api_url}/assets/test-id") 66 | .to_return(body: JSON.dump(stub_asset_manager_response), status: 200) 67 | 68 | response = api.update_asset(asset_id, file: file_fixture) 69 | 70 | assert_equal "#{base_api_url}/assets/#{asset_id}", response["asset"]["id"] 71 | assert_requested(req) 72 | end 73 | 74 | it "retrieves the asset's metadata" do 75 | asset = api.asset(asset_id) 76 | 77 | assert_equal "photo.jpg", asset["name"] 78 | assert_equal "image/jpeg", asset["content_type"] 79 | assert_equal "http://fooey.gov.uk/media/photo.jpg", asset["file_url"] 80 | end 81 | 82 | it "retrieves the asset from media" do 83 | asset = api.media(asset_id, "photo.jpg") 84 | 85 | assert_equal "Some file content", asset.body 86 | end 87 | end 88 | 89 | describe "a Whitehall asset exists" do 90 | before do 91 | stub_asset_manager_has_a_whitehall_media_asset( 92 | "/government/uploads/photo.jpg", 93 | "Some file content", 94 | ) 95 | end 96 | 97 | it "retrieves the asset" do 98 | asset = api.whitehall_media("/government/uploads/photo.jpg") 99 | 100 | assert_equal "Some file content", asset.body 101 | end 102 | end 103 | 104 | describe "a Whitehall asset with a legacy_url_path containing non-ascii characters exists" do 105 | before do 106 | stub_asset_manager_has_a_whitehall_media_asset( 107 | "/government/uploads/phot%C3%B8.jpg", 108 | "Some file content", 109 | ) 110 | end 111 | 112 | it "retrieves the asset's metadata" do 113 | asset = api.whitehall_media("/government/uploads/photø.jpg") 114 | 115 | assert_equal "Some file content", asset.body 116 | end 117 | end 118 | 119 | it "deletes the asset for the given id" do 120 | req = stub_request(:delete, "#{base_api_url}/assets/#{asset_id}") 121 | .to_return(body: JSON.dump(stub_asset_manager_response), status: 200) 122 | 123 | response = api.delete_asset(asset_id) 124 | 125 | assert_equal "#{base_api_url}/assets/#{asset_id}", response["asset"]["id"] 126 | assert_requested(req) 127 | end 128 | 129 | it "restores the asset for the given id" do 130 | req = stub_request(:post, "#{base_api_url}/assets/#{asset_id}/restore") 131 | .to_return(body: JSON.dump(stub_asset_manager_response), status: 200) 132 | 133 | response = api.restore_asset(asset_id) 134 | 135 | assert_equal "#{base_api_url}/assets/#{asset_id}", response["asset"]["id"] 136 | assert_requested(req) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/publishing_api/special_route_publisher_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api/special_route_publisher" 3 | require "govuk_schemas/assert_matchers" 4 | require "gds_api/test_helpers/publishing_api" 5 | 6 | describe GdsApi::PublishingApi::SpecialRoutePublisher do 7 | include GdsApi::TestHelpers::PublishingApi 8 | include GovukSchemas::AssertMatchers 9 | 10 | let(:content_id) { "a-content-id-of-sorts" } 11 | let(:special_route) do 12 | { 13 | content_id:, 14 | title: "A title", 15 | description: "A description", 16 | base_path: "/favicon.ico", 17 | type: "exact", 18 | publishing_app: "static", 19 | rendering_app: "static", 20 | } 21 | end 22 | 23 | let(:publisher) { GdsApi::PublishingApi::SpecialRoutePublisher.new } 24 | let(:endpoint) { Plek.find("publishing-api") } 25 | 26 | describe ".publish" do 27 | before do 28 | stub_any_publishing_api_call 29 | end 30 | 31 | it "publishes valid special routes" do 32 | Timecop.freeze(Time.now) do 33 | publisher.publish(special_route) 34 | 35 | expected_payload = { 36 | base_path: special_route[:base_path], 37 | document_type: "special_route", 38 | schema_name: "special_route", 39 | title: special_route[:title], 40 | description: special_route[:description], 41 | routes: [ 42 | { 43 | path: special_route[:base_path], 44 | type: special_route[:type], 45 | }, 46 | ], 47 | locale: "en", 48 | details: {}, 49 | publishing_app: special_route[:publishing_app], 50 | rendering_app: special_route[:rendering_app], 51 | public_updated_at: Time.now.iso8601, 52 | update_type: "major", 53 | } 54 | 55 | assert_requested(:put, "#{endpoint}/v2/content/#{content_id}", body: expected_payload) 56 | assert_valid_against_publisher_schema(expected_payload, "special_route") 57 | assert_publishing_api_publish(content_id) 58 | end 59 | end 60 | 61 | it "publishes non-English locales" do 62 | publisher.publish(special_route.merge(locale: "cy")) 63 | 64 | assert_requested(:put, "#{endpoint}/v2/content/#{content_id}") do |req| 65 | JSON.parse(req.body)["locale"] == "cy" 66 | end 67 | assert_publishing_api_publish(content_id, { update_type: "major", locale: "cy" }) 68 | end 69 | 70 | it "publishes customized document type" do 71 | publisher.publish(special_route.merge(document_type: "other_document_type")) 72 | 73 | assert_requested(:put, "#{endpoint}/v2/content/#{content_id}") do |req| 74 | JSON.parse(req.body)["document_type"] == "other_document_type" 75 | end 76 | assert_publishing_api_publish(content_id) 77 | end 78 | 79 | it "publishes customized schema_name" do 80 | publisher.publish(special_route.merge(schema_name: "dummy_schema")) 81 | 82 | assert_requested(:put, "#{endpoint}/v2/content/#{content_id}") do |req| 83 | JSON.parse(req.body)["schema_name"] == "dummy_schema" 84 | end 85 | end 86 | 87 | it "publishes links" do 88 | links = { 89 | links: { 90 | organisations: %w[org-content-id], 91 | }, 92 | } 93 | 94 | publisher.publish(special_route.merge(links)) 95 | 96 | assert_requested(:patch, "#{endpoint}/v2/links/#{content_id}", body: links) 97 | end 98 | 99 | describe "Timezone handling" do 100 | let(:publishing_api) do 101 | stub(:publishing_api, put_content_item: nil) 102 | end 103 | let(:publisher) do 104 | GdsApi::PublishingApi::SpecialRoutePublisher.new(publishing_api:) 105 | end 106 | 107 | it "is robust to Time.zone returning nil" do 108 | Timecop.freeze(Time.now) do 109 | Time.stubs(:zone).returns(nil) 110 | publishing_api.expects(:put_content).with( 111 | anything, 112 | has_entries(public_updated_at: Time.now.iso8601), 113 | ) 114 | publishing_api.expects(:publish) 115 | 116 | publisher.publish(special_route) 117 | end 118 | end 119 | 120 | it "uses Time.zone if available" do 121 | Timecop.freeze(Time.now) do 122 | time_in_zone = stub("Time in zone", now: Time.parse("2010-01-01 10:10:10 +04:00")) 123 | Time.stubs(:zone).returns(time_in_zone) 124 | 125 | publishing_api.expects(:put_content).with( 126 | anything, 127 | has_entries(public_updated_at: time_in_zone.now.iso8601), 128 | ) 129 | publishing_api.expects(:publish) 130 | 131 | publisher.publish(special_route) 132 | end 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/gds_api/link_checker_api.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::LinkCheckerApi < GdsApi::Base 4 | # Checks whether a link is broken. 5 | # 6 | # Makes a +GET+ request to the link checker api to check a link. 7 | # 8 | # @param uri [String] The URI to check. 9 | # @param synchronous [Boolean] Whether the check should happen immediately. (optional) 10 | # @param checked_within [Fixnum] The number of seconds the last check should 11 | # be within before doing another check. (optional) 12 | # @return [LinkReport] A +SimpleDelegator+ of the +GdsApi::Response+ which 13 | # responds to: 14 | # :uri the URI of the link 15 | # :status the status of the link, one of: ok, pending, broken, caution 16 | # :checked the date the link was checked 17 | # :errors a list of error descriptions 18 | # :warnings a list of warning descriptions 19 | # :problem_summary a short description of the most critical problem with the link 20 | # :suggested_fix where possible, this provides a potential fix for the problem 21 | # 22 | # @raise [HTTPErrorResponse] if the request returns an error 23 | def check(uri, synchronous: nil, checked_within: nil) 24 | params = { 25 | uri:, 26 | synchronous:, 27 | checked_within:, 28 | } 29 | 30 | response = get_json( 31 | "#{endpoint}/check" + query_string(params.delete_if { |_, v| v.nil? }), 32 | ) 33 | 34 | LinkReport.new(response.to_hash) 35 | end 36 | 37 | # Create a batch of links to check. 38 | # 39 | # Makes a +POST+ request to the link checker api to create a batch. 40 | # 41 | # @param uris [Array] A list of URIs to check. 42 | # @param checked_within [Fixnum] The number of seconds the last check should 43 | # be within before doing another check. (optional) 44 | # @param webhook_uri [String] The URI to be called when the batch finishes. (optional) 45 | # @param webhook_secret_token [String] A secret token that the API will use to generate a signature of the request. (optional) 46 | # @return [BatchReport] A +SimpleDelegator+ of the +GdsApi::Response+ which 47 | # responds to: 48 | # :id the ID of the batch 49 | # :status the status of the check, one of: complete or in_progress 50 | # :links an array of link reports 51 | # :totals an +OpenStruct+ of total information, fields: links, ok, caution, broken, pending 52 | # :completed_at a date when the batch was completed 53 | # 54 | # @raise [HTTPErrorResponse] if the request returns an error 55 | def create_batch(uris, checked_within: nil, webhook_uri: nil, webhook_secret_token: nil) 56 | payload = { 57 | uris:, 58 | checked_within:, 59 | webhook_uri:, 60 | webhook_secret_token:, 61 | } 62 | 63 | response = post_json( 64 | "#{endpoint}/batch", payload.delete_if { |_, v| v.nil? } 65 | ) 66 | 67 | BatchReport.new(response.to_hash) 68 | end 69 | 70 | # Get information about a batch. 71 | # 72 | # Makes a +GET+ request to the link checker api to get a batch. 73 | # 74 | # @param id [Fixnum] The batch ID to get information about. 75 | # @return [BatchReport] A +SimpleDelegator+ of the +GdsApi::Response+ which 76 | # responds to: 77 | # :id the ID of the batch 78 | # :status the status of the check, one of: completed or in_progress 79 | # :links an array of link reports 80 | # :totals an +OpenStruct+ of total information, fields: links, ok, caution, broken, pending 81 | # :completed_at a date when the batch was completed 82 | # 83 | # @raise [HTTPErrorResponse] if the request returns an error 84 | def get_batch(id) 85 | BatchReport.new( 86 | get_json( 87 | "#{endpoint}/batch/#{id}", 88 | ).to_hash, 89 | ) 90 | end 91 | 92 | class LinkReport < SimpleDelegator 93 | def uri 94 | self["uri"] 95 | end 96 | 97 | def status 98 | self["status"].to_sym 99 | end 100 | 101 | def checked 102 | Time.iso8601(self["checked"]) 103 | end 104 | 105 | def problem_summary 106 | self["problem_summary"] 107 | end 108 | 109 | def suggested_fix 110 | self["suggested_fix"] 111 | end 112 | 113 | def errors 114 | self["errors"] 115 | end 116 | 117 | def warnings 118 | self["warnings"] 119 | end 120 | end 121 | 122 | class BatchReport < SimpleDelegator 123 | def id 124 | self["id"] 125 | end 126 | 127 | def status 128 | self["status"].to_sym 129 | end 130 | 131 | def links 132 | self["links"].map { |link_report| LinkReport.new(link_report) } 133 | end 134 | 135 | def totals 136 | OpenStruct.new(self["totals"]) 137 | end 138 | 139 | def completed_at 140 | Time.iso8601(self["completed_at"]) 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/gds_api/account_api.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "exceptions" 3 | 4 | # Adapter for the Account API 5 | # 6 | # @see https://github.com/alphagov/account-api 7 | # @api documented 8 | class GdsApi::AccountApi < GdsApi::Base 9 | AUTH_HEADER_NAME = "GOVUK-Account-Session".freeze 10 | 11 | # Get an OAuth sign-in URL to redirect the user to 12 | # 13 | # @param [String, nil] redirect_path path on GOV.UK to send the user to after authentication 14 | # @param [Boolean, nil] mfa whether to authenticate the user with MFA or not 15 | # 16 | # @return [Hash] An authentication URL and the OAuth state parameter (for CSRF protection) 17 | def get_sign_in_url(redirect_path: nil, mfa: false) 18 | querystring = nested_query_string( 19 | { 20 | redirect_path:, 21 | mfa:, 22 | }.compact, 23 | ) 24 | get_json("#{endpoint}/api/oauth2/sign-in?#{querystring}") 25 | end 26 | 27 | # Validate an OAuth authentication response 28 | # 29 | # @param [String] code The OAuth code parameter, from the auth server. 30 | # @param [String] state The OAuth state parameter, from the auth server. 31 | # 32 | # @return [Hash] The value for the govuk_account_session header, the path to redirect the user to, and the GA client ID (if there is one) 33 | def validate_auth_response(code:, state:) 34 | post_json("#{endpoint}/api/oauth2/callback", code:, state:) 35 | end 36 | 37 | # Get an OIDC end-session URL to redirect the user to 38 | # 39 | # @param [String, nil] govuk_account_session Value of the session header 40 | # 41 | # @return [Hash] An end-session URL 42 | def get_end_session_url(govuk_account_session: nil) 43 | get_json("#{endpoint}/api/oauth2/end-session", auth_headers(govuk_account_session)) 44 | end 45 | 46 | # Get all the information about a user needed to render the account home page 47 | # 48 | # @param [String] govuk_account_session Value of the session header 49 | # 50 | # @return [Hash] Information about the user and the services they've used, and a new session header 51 | def get_user(govuk_account_session:) 52 | get_json("#{endpoint}/api/user", auth_headers(govuk_account_session)) 53 | end 54 | 55 | # Find a user by email address, returning whether they match the given session (if any) 56 | # 57 | # @param [String] email The email address to search for 58 | # @param [String, nil] govuk_account_session Value of the session header, if not given just checks if the given email address exists. 59 | # 60 | # @return [Hash] One field, "match", indicating whether the session matches the given email address 61 | def match_user_by_email(email:, govuk_account_session: nil) 62 | querystring = nested_query_string({ email: }) 63 | get_json("#{endpoint}/api/user/match-by-email?#{querystring}", auth_headers(govuk_account_session)) 64 | end 65 | 66 | # Delete a users account 67 | # 68 | # @param [String] subject_identifier The identifier of the user, shared between the auth service and GOV.UK. 69 | def delete_user_by_subject_identifier(subject_identifier:) 70 | delete_json("#{endpoint}/api/oidc-users/#{subject_identifier}") 71 | end 72 | 73 | # Update the user record with privileged information from the auth service. Only the auth service will call this. 74 | # 75 | # @param [String] subject_identifier The identifier of the user, shared between the auth service and GOV.UK. 76 | # @param [String, nil] email The user's current email address 77 | # @param [Boolean, nil] email_verified Whether the user's current email address is verified 78 | # 79 | # @return [Hash] The user's subject identifier and email attributes 80 | def update_user_by_subject_identifier(subject_identifier:, email: nil, email_verified: nil) 81 | params = { 82 | email:, 83 | email_verified:, 84 | }.compact 85 | 86 | patch_json("#{endpoint}/api/oidc-users/#{subject_identifier}", params) 87 | end 88 | 89 | # Look up the values of a user's attributes 90 | # 91 | # @param [String] attributes Names of the attributes to check 92 | # @param [String] govuk_account_session Value of the session header 93 | # 94 | # @return [Hash] The attribute values (if present), and a new session header 95 | def get_attributes(attributes:, govuk_account_session:) 96 | querystring = nested_query_string({ attributes: }.compact) 97 | get_json("#{endpoint}/api/attributes?#{querystring}", auth_headers(govuk_account_session)) 98 | end 99 | 100 | # Create or update attributes for a user 101 | # 102 | # @param [String] attributes Hash of new attribute values 103 | # @param [String] govuk_account_session Value of the session header 104 | # 105 | # @return [Hash] A new session header 106 | def set_attributes(attributes:, govuk_account_session:) 107 | patch_json("#{endpoint}/api/attributes", { attributes: }, auth_headers(govuk_account_session)) 108 | end 109 | 110 | private 111 | 112 | def nested_query_string(params) 113 | Rack::Utils.build_nested_query(params) 114 | end 115 | 116 | def auth_headers(govuk_account_session) 117 | { AUTH_HEADER_NAME => govuk_account_session }.compact 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/pacts/organisations_api_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/organisations" 3 | 4 | describe "GdsApi::Organisations pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::Organisations.new(organisation_api_host) } 8 | let(:host_agnostic_endpoint_regex) { %r{https?://(?:[^/]+)/api/organisations} } 9 | let(:api_client_endpoint) { "#{organisation_api_host}/api/organisations" } 10 | 11 | it "fetches a list of organisations" do 12 | organisation_api 13 | .given("there is a list of organisations") 14 | .upon_receiving("a request for the organisation list") 15 | .with( 16 | method: :get, 17 | path: "/api/organisations", 18 | headers: GdsApi::JsonClient.default_request_headers, 19 | ) 20 | .will_respond_with( 21 | status: 200, 22 | body: { 23 | results: [ 24 | organisation, 25 | organisation, 26 | ], 27 | }, 28 | headers: { 29 | "Content-Type" => "application/json; charset=utf-8", 30 | }, 31 | ) 32 | 33 | api_client.organisations 34 | end 35 | 36 | describe "fetching a paginated list of organisations" do 37 | let(:page_one_links) do 38 | Pact.term( 39 | generate: %(<#{api_client_endpoint}?page=2>; rel="next", <#{api_client_endpoint}?page=1>; rel="self"), 40 | matcher: /^<#{host_agnostic_endpoint_regex}\?page=2>; rel="next", <#{host_agnostic_endpoint_regex}\?page=1>; rel="self"$/, 41 | ) 42 | end 43 | let(:page_two_links) do 44 | Pact.term( 45 | generate: %(<#{api_client_endpoint}?page=1>; rel="previous", <#{api_client_endpoint}?page=2>; rel="self"), 46 | matcher: /^<#{host_agnostic_endpoint_regex}\?page=1>; rel="previous", <#{host_agnostic_endpoint_regex}\?page=2>; rel="self"$/, 47 | ) 48 | end 49 | 50 | let(:request) do 51 | { 52 | method: :get, 53 | path: "/api/organisations", 54 | headers: GdsApi::JsonClient.default_request_headers, 55 | } 56 | end 57 | let(:body) do 58 | { 59 | results: Pact.each_like({}, min: 20), 60 | page_size: 20, 61 | pages: 2, 62 | } 63 | end 64 | let(:response) do 65 | { 66 | status: 200, 67 | body:, 68 | } 69 | end 70 | 71 | it "handles pagination" do 72 | organisation_api 73 | .given("the organisation list is paginated, beginning at page 1") 74 | .upon_receiving("a request without a query param") 75 | .with(request.merge(query: "")) 76 | .will_respond_with(response.merge(headers: { "link" => page_one_links })) 77 | 78 | organisation_api 79 | .given("the organisation list is paginated, beginning at page 2") 80 | .upon_receiving("a request with page 2 params") 81 | .with(request.merge(query: "page=2")) 82 | .will_respond_with(response.merge(headers: { "link" => page_two_links })) 83 | 84 | api_client.organisations.with_subsequent_pages.count 85 | end 86 | end 87 | 88 | it "fetches an organisation by slug" do 89 | hmrc = "hm-revenue-customs" 90 | api_response = organisation(slug: hmrc) 91 | api_response["id"] = Pact.term( 92 | generate: %(#{api_client_endpoint}/#{hmrc}), 93 | matcher: /^#{host_agnostic_endpoint_regex}\/#{hmrc}$/, 94 | ) 95 | 96 | organisation_api 97 | .given("the organisation hmrc exists") 98 | .upon_receiving("a request for hm-revenue-customs") 99 | .with( 100 | method: :get, 101 | path: "/api/organisations/#{hmrc}", 102 | headers: GdsApi::JsonClient.default_request_headers, 103 | ) 104 | .will_respond_with( 105 | status: 200, 106 | body: api_response, 107 | ) 108 | 109 | api_client.organisation(hmrc) 110 | end 111 | 112 | it "returns a 404 if no organisation exists for a given slug" do 113 | organisation_api 114 | .given("no organisation exists") 115 | .upon_receiving("a request for a non-existant organisation") 116 | .with( 117 | method: :get, 118 | path: "/api/organisations/department-for-making-life-better", 119 | headers: GdsApi::JsonClient.default_request_headers, 120 | ) 121 | .will_respond_with( 122 | status: 404, 123 | body: "404 error", 124 | ) 125 | 126 | assert_raises(GdsApi::HTTPNotFound) do 127 | api_client.organisation("department-for-making-life-better") 128 | end 129 | end 130 | 131 | private 132 | 133 | def organisation(slug: "test-department") 134 | { 135 | "id" => Pact.like("www.gov.uk/api/organisations/#{slug}"), 136 | "title" => Pact.like("Test Department"), 137 | "updated_at" => Pact.like("2019-05-15T12:12:17.000+01:00"), 138 | "web_url" => Pact.like("www.gov.uk/government/organisations/#{slug}"), 139 | "details" => { 140 | "slug" => Pact.like(slug), 141 | "content_id" => Pact.like("b854f170-53c8-4098-bf77-e8ef42f93107"), 142 | }, 143 | "analytics_identifier" => Pact.like("OT1276"), 144 | "child_organisations" => [], 145 | "superseded_organisations" => [], 146 | } 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | codeql-sast: 7 | name: CodeQL SAST scan 8 | uses: alphagov/govuk-infrastructure/.github/workflows/codeql-analysis.yml@main 9 | permissions: 10 | security-events: write 11 | 12 | dependency-review: 13 | name: Dependency Review scan 14 | uses: alphagov/govuk-infrastructure/.github/workflows/dependency-review.yml@main 15 | 16 | # This matrix job runs the test suite against multiple Ruby versions 17 | test_matrix: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | ruby: [3.2, 3.3, 3.4] 22 | runs-on: ubuntu-latest 23 | env: 24 | GOVUK_CONTENT_SCHEMAS_PATH: vendor/publishing-api/content_schemas 25 | steps: 26 | - uses: actions/checkout@v6 27 | - name: Checkout Publishing API for content schemas 28 | uses: actions/checkout@v6 29 | with: 30 | repository: alphagov/publishing-api 31 | ref: main 32 | path: vendor/publishing-api 33 | - uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: ${{ matrix.ruby }} 36 | bundler-cache: true 37 | - run: bundle exec rake 38 | 39 | # This job is needed to work around the fact that matrix jobs spawn multiple status checks – i.e. one job per variant. 40 | # The branch protection rules depend on this as a composite job to ensure that all preceding test_matrix checks passed. 41 | # Solution taken from: https://github.community/t/status-check-for-a-matrix-jobs/127354/3 42 | test: 43 | needs: test_matrix 44 | runs-on: ubuntu-latest 45 | steps: 46 | - run: echo "All matrix tests have passed 🚀" 47 | 48 | generate_pacts: 49 | needs: test 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v6 53 | - uses: ruby/setup-ruby@v1 54 | with: 55 | bundler-cache: true 56 | - run: bundle exec rake pact_test 57 | - uses: actions/upload-artifact@v6 58 | with: 59 | name: pacts 60 | path: spec/pacts/*.json 61 | 62 | account_api_pact: 63 | needs: generate_pacts 64 | uses: alphagov/account-api/.github/workflows/pact-verify.yml@main 65 | with: 66 | pact_artifact: pacts 67 | 68 | asset_manager_pact: 69 | needs: generate_pacts 70 | uses: alphagov/asset-manager/.github/workflows/pact-verify.yml@main 71 | with: 72 | pact_artifact: pacts 73 | 74 | collections_pact: 75 | needs: generate_pacts 76 | uses: alphagov/collections/.github/workflows/pact-verify.yml@main 77 | with: 78 | pact_artifact: pacts 79 | 80 | email_alert_api_pact: 81 | needs: generate_pacts 82 | uses: alphagov/email-alert-api/.github/workflows/pact-verify.yml@main 83 | with: 84 | pact_artifact: pacts 85 | 86 | frontend_pact: 87 | needs: generate_pacts 88 | uses: alphagov/frontend/.github/workflows/pact-verify.yml@main 89 | with: 90 | pact_artifact: pacts 91 | 92 | places_manager_pact: 93 | needs: generate_pacts 94 | uses: alphagov/places-manager/.github/workflows/pact-verify.yml@main 95 | with: 96 | pact_artifact: pacts 97 | 98 | link_checker_api_pact: 99 | needs: generate_pacts 100 | uses: alphagov/link-checker-api/.github/workflows/pact-verify.yml@main 101 | with: 102 | pact_artifact: pacts 103 | 104 | locations_api_pact: 105 | needs: generate_pacts 106 | uses: alphagov/locations-api/.github/workflows/pact-verify.yml@main 107 | with: 108 | pact_artifact: pacts 109 | 110 | publishing_api_pact: 111 | needs: generate_pacts 112 | uses: alphagov/publishing-api/.github/workflows/pact-verify.yml@main 113 | with: 114 | pact_artifact: pacts 115 | 116 | signon_api_pact: 117 | needs: generate_pacts 118 | uses: alphagov/signon/.github/workflows/pact-verify.yml@main 119 | with: 120 | pact_artifact: pacts 121 | 122 | support_api_pact: 123 | needs: generate_pacts 124 | uses: alphagov/support-api/.github/workflows/pact-verify.yml@main 125 | with: 126 | pact_artifact: pacts 127 | 128 | publish_pacts: 129 | if: ${{ github.actor != 'dependabot[bot]' }} 130 | needs: 131 | - account_api_pact 132 | - asset_manager_pact 133 | - collections_pact 134 | - email_alert_api_pact 135 | - frontend_pact 136 | - places_manager_pact 137 | - link_checker_api_pact 138 | - locations_api_pact 139 | - publishing_api_pact 140 | - support_api_pact 141 | - signon_api_pact 142 | runs-on: ubuntu-latest 143 | steps: 144 | - uses: actions/checkout@v6 145 | - uses: ruby/setup-ruby@v1 146 | with: 147 | bundler-cache: true 148 | - uses: actions/download-artifact@v4 149 | with: 150 | name: pacts 151 | path: tmp/pacts 152 | - run: bundle exec rake pact:publish 153 | env: 154 | PACT_CONSUMER_VERSION: branch-${{ github.ref_name }} 155 | PACT_BROKER_BASE_URL: https://govuk-pact-broker-6991351eca05.herokuapp.com 156 | PACT_BROKER_USERNAME: ${{ secrets.GOVUK_PACT_BROKER_USERNAME }} 157 | PACT_BROKER_PASSWORD: ${{ secrets.GOVUK_PACT_BROKER_PASSWORD }} 158 | PACT_PATTERN: tmp/pacts/*.json 159 | -------------------------------------------------------------------------------- /test/pacts/publishing_api/unpublish_pact_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/publishing_api" 3 | 4 | describe "GdsApi::PublishingApi#unpublish pact tests" do 5 | include PactTest 6 | 7 | let(:api_client) { GdsApi::PublishingApi.new(publishing_api_host) } 8 | let(:content_id) { "bed722e6-db68-43e5-9079-063f623335a7" } 9 | 10 | it "responds with 200 if the unpublish command succeeds" do 11 | publishing_api 12 | .given("a published content item exists with content_id: #{content_id}") 13 | .upon_receiving("an unpublish request") 14 | .with( 15 | method: :post, 16 | path: "/v2/content/#{content_id}/unpublish", 17 | body: { 18 | type: "gone", 19 | }, 20 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 21 | ) 22 | .will_respond_with( 23 | status: 200, 24 | ) 25 | 26 | api_client.unpublish(content_id, type: "gone") 27 | end 28 | 29 | it "responds with 404 if the content item does not exist" do 30 | publishing_api 31 | .given("no content exists") 32 | .upon_receiving("an unpublish request") 33 | .with( 34 | method: :post, 35 | path: "/v2/content/#{content_id}/unpublish", 36 | body: { 37 | type: "gone", 38 | }, 39 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 40 | ) 41 | .will_respond_with( 42 | status: 404, 43 | ) 44 | 45 | assert_raises(GdsApi::HTTPNotFound) do 46 | api_client.unpublish(content_id, type: "gone") 47 | end 48 | end 49 | 50 | it "responds with 422 if the type is incorrect" do 51 | publishing_api 52 | .given("a published content item exists with content_id: #{content_id}") 53 | .upon_receiving("an invalid unpublish request") 54 | .with( 55 | method: :post, 56 | path: "/v2/content/#{content_id}/unpublish", 57 | body: { 58 | type: "not-a-valid-type", 59 | }, 60 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 61 | ) 62 | .will_respond_with( 63 | status: 422, 64 | body: { 65 | "error" => { 66 | "code" => 422, 67 | "message" => Pact.term(generate: "not-a-valid-type is not a valid unpublishing type", matcher: /\S+/), 68 | "fields" => {}, 69 | }, 70 | }, 71 | ) 72 | 73 | assert_raises(GdsApi::HTTPUnprocessableEntity) do 74 | api_client.unpublish(content_id, type: "not-a-valid-type") 75 | end 76 | end 77 | 78 | it "responds with 200 and updates the unpublishing if the content item is already unpublished" do 79 | publishing_api 80 | .given("an unpublished content item exists with content_id: #{content_id}") 81 | .upon_receiving("an unpublish request") 82 | .with( 83 | method: :post, 84 | path: "/v2/content/#{content_id}/unpublish", 85 | body: { 86 | type: "gone", 87 | }, 88 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 89 | ) 90 | .will_respond_with( 91 | status: 200, 92 | ) 93 | 94 | api_client.unpublish(content_id, type: "gone") 95 | end 96 | 97 | describe "optimistic locking" do 98 | it "responds with 200 OK if the content item has not changed since it was requested" do 99 | publishing_api 100 | .given("the published content item #{content_id} is at version 3") 101 | .upon_receiving("an unpublish request for version 3") 102 | .with( 103 | method: :post, 104 | path: "/v2/content/#{content_id}/unpublish", 105 | body: { 106 | type: "gone", 107 | previous_version: 3, 108 | }, 109 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 110 | ) 111 | .will_respond_with( 112 | status: 200, 113 | ) 114 | 115 | api_client.unpublish(content_id, type: "gone", previous_version: 3) 116 | end 117 | 118 | it "responds with 409 Conflict if the content item has changed in the meantime" do 119 | publishing_api 120 | .given("the published content item #{content_id} is at version 3") 121 | .upon_receiving("an unpublish request for version 2") 122 | .with( 123 | method: :post, 124 | path: "/v2/content/#{content_id}/unpublish", 125 | body: { 126 | type: "gone", 127 | previous_version: 2, 128 | }, 129 | headers: GdsApi::JsonClient.default_request_with_json_body_headers, 130 | ) 131 | .will_respond_with( 132 | status: 409, 133 | body: { 134 | "error" => { 135 | "code" => 409, 136 | "message" => Pact.term(generate: "Conflict", matcher: /\S+/), 137 | "fields" => { 138 | "previous_version" => Pact.each_like("does not match", min: 1), 139 | }, 140 | }, 141 | }, 142 | headers: { 143 | "Content-Type" => "application/json; charset=utf-8", 144 | }, 145 | ) 146 | 147 | assert_raises(GdsApi::HTTPConflict) do 148 | api_client.unpublish(content_id, type: "gone", previous_version: 2) 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/gds_api/response.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "forwardable" 3 | 4 | module GdsApi 5 | # This wraps an HTTP response with a JSON body. 6 | # 7 | # Responses can be configured to use relative URLs for `web_url` properties. 8 | # API endpoints should return absolute URLs so that they make sense outside of the 9 | # GOV.UK context. However on internal systems we want to present relative URLs. 10 | # By specifying a base URI, this will convert all matching web_urls into relative URLs 11 | # This is useful on non-canonical frontends, such as those in staging environments. 12 | # See: https://github.com/alphagov/wiki/wiki/API-conventions for details on the API conventions 13 | # 14 | # Example: 15 | # 16 | # r = Response.new(response, web_urls_relative_to: "https://www.gov.uk") 17 | # r['results'][0]['web_url'] 18 | # => "/bank-holidays" 19 | class Response 20 | extend Forwardable 21 | include Enumerable 22 | 23 | class CacheControl < Hash 24 | PATTERN = /([-a-z]+)(?:\s*=\s*([^,\s]+))?,?+/i 25 | 26 | def initialize(value = nil) 27 | super() 28 | parse(value) 29 | end 30 | 31 | def public? 32 | self["public"] 33 | end 34 | 35 | def private? 36 | self["private"] 37 | end 38 | 39 | def no_cache? 40 | self["no-cache"] 41 | end 42 | 43 | def no_store? 44 | self["no-store"] 45 | end 46 | 47 | def must_revalidate? 48 | self["must-revalidate"] 49 | end 50 | 51 | def proxy_revalidate? 52 | self["proxy-revalidate"] 53 | end 54 | 55 | def max_age 56 | self["max-age"].to_i if key?("max-age") 57 | end 58 | 59 | def reverse_max_age 60 | self["r-maxage"].to_i if key?("r-maxage") 61 | end 62 | alias_method :r_maxage, :reverse_max_age 63 | 64 | def shared_max_age 65 | self["s-maxage"].to_i if key?("r-maxage") 66 | end 67 | alias_method :s_maxage, :shared_max_age 68 | 69 | def to_s 70 | directives = [] 71 | values = [] 72 | 73 | each do |key, value| 74 | if value == true 75 | directives << key 76 | elsif value 77 | values << "#{key}=#{value}" 78 | end 79 | end 80 | 81 | (directives.sort + values.sort).join(", ") 82 | end 83 | 84 | private 85 | 86 | def parse(header) 87 | return if header.nil? || header.empty? 88 | 89 | header.scan(PATTERN).each do |name, value| 90 | self[name.downcase] = value || true 91 | end 92 | end 93 | end 94 | 95 | def_delegators :to_hash, :[], :"<=>", :each, :dig 96 | 97 | def initialize(http_response, options = {}) 98 | @http_response = http_response 99 | @web_urls_relative_to = options[:web_urls_relative_to] ? URI.parse(options[:web_urls_relative_to]) : nil 100 | end 101 | 102 | def raw_response_body 103 | @http_response.body 104 | end 105 | 106 | def code 107 | # Return an integer code for consistency with HTTPErrorResponse 108 | @http_response.code 109 | end 110 | 111 | def headers 112 | @http_response.headers 113 | end 114 | 115 | def expires_at 116 | if headers[:date] && cache_control.max_age 117 | response_date = Time.parse(headers[:date]) 118 | response_date + cache_control.max_age 119 | elsif headers[:expires] 120 | Time.parse(headers[:expires]) 121 | end 122 | end 123 | 124 | def expires_in 125 | return unless headers[:date] 126 | 127 | age = Time.now.utc - Time.parse(headers[:date]) 128 | 129 | if cache_control.max_age 130 | cache_control.max_age - age.to_i 131 | elsif headers[:expires] 132 | Time.parse(headers[:expires]).to_i - Time.now.utc.to_i 133 | end 134 | end 135 | 136 | def cache_control 137 | @cache_control ||= CacheControl.new(headers[:cache_control]) 138 | end 139 | 140 | def to_hash 141 | parsed_content 142 | end 143 | 144 | def parsed_content 145 | @parsed_content ||= transform_parsed(JSON.parse(@http_response.body)) 146 | end 147 | 148 | def present? 149 | true 150 | end 151 | 152 | def blank? 153 | false 154 | end 155 | 156 | private 157 | 158 | def transform_parsed(value) 159 | return value if @web_urls_relative_to.nil? 160 | 161 | case value 162 | when Hash 163 | Hash[value.map do |k, v| 164 | # NOTE: Don't bother transforming if the value is nil 165 | if k == "web_url" && v 166 | # Use relative URLs to route when the web_url value is on the 167 | # same domain as the site root. Note that we can't just use the 168 | # `route_to` method, as this would give us technically correct 169 | # but potentially confusing `//host/path` URLs for URLs with the 170 | # same scheme but different hosts. 171 | relative_url = @web_urls_relative_to.route_to(v) 172 | [k, relative_url.host ? v : relative_url.to_s] 173 | else 174 | [k, transform_parsed(v)] 175 | end 176 | end] 177 | when Array 178 | value.map { |v| transform_parsed(v) } 179 | else 180 | value 181 | end 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /test/places_manager_api_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "gds_api/places_manager" 3 | 4 | class PlacesManagerApiTest < Minitest::Test 5 | ROOT = Plek.find("places-manager") 6 | LATITUDE = 52.1327584352089 7 | LONGITUDE = -0.4702813074674147 8 | 9 | def api_client 10 | GdsApi::PlacesManager.new(ROOT) 11 | end 12 | 13 | def dummy_place 14 | { 15 | "access_notes" => nil, 16 | "address1" => "Cauldwell Street", 17 | "address2" => "Bedford", 18 | "fax" => nil, 19 | "general_notes" => nil, 20 | "geocode_error" => nil, 21 | "location" => { "latitude" => LATITUDE, "longitude" => LONGITUDE }, 22 | "name" => "Town Hall", 23 | "phone" => nil, 24 | "postcode" => "MK42 9AP", 25 | "source_address" => "Town Hall, Cauldwell Street, Bedford", 26 | "text_phone" => nil, 27 | "town" => nil, 28 | "url" => "http://www.bedford.gov.uk/advice_and_benefits/registration_service.aspx", 29 | } 30 | end 31 | 32 | def dummy_place_response(place_array) 33 | { 34 | "status" => "ok", 35 | "contents" => "places", 36 | "places" => place_array, 37 | } 38 | end 39 | 40 | def test_search_for_places 41 | c = api_client 42 | url = "#{ROOT}/places/wibble.json?limit=5&lat=52&lng=0" 43 | c.expects(:get_json).with(url).returns(dummy_place_response([dummy_place])) 44 | output = c.places("wibble", 52, 0) 45 | 46 | places = output["places"] 47 | assert_equal 1, places.size 48 | place = places[0] 49 | assert_equal LATITUDE, place["location"]["latitude"] 50 | assert_equal LONGITUDE, place["location"]["longitude"] 51 | end 52 | 53 | def test_nil_location 54 | # Test behaviour when the location field is nil 55 | c = api_client 56 | url = "#{ROOT}/places/wibble.json?limit=5&lat=52&lng=0" 57 | place_info = dummy_place.merge("location" => nil) 58 | c.expects(:get_json).with(url).returns(dummy_place_response([place_info])) 59 | output = c.places("wibble", 52, 0) 60 | places = output["places"] 61 | 62 | assert_equal 1, places.size 63 | place = places[0] 64 | assert_nil place["latitude"] 65 | assert_nil place["longitude"] 66 | end 67 | 68 | def test_hash_location 69 | # Test behaviour when the location field is a longitude/latitude hash 70 | c = api_client 71 | url = "#{ROOT}/places/wibble.json?limit=5&lat=52&lng=0" 72 | place_info = dummy_place.merge( 73 | "location" => { "longitude" => LONGITUDE, "latitude" => LATITUDE }, 74 | ) 75 | c.expects(:get_json).with(url).returns(dummy_place_response([place_info])) 76 | output = c.places("wibble", 52, 0) 77 | places = output["places"] 78 | 79 | assert_equal 1, places.size 80 | place = places[0] 81 | assert_equal LATITUDE, place["location"]["latitude"] 82 | assert_equal LONGITUDE, place["location"]["longitude"] 83 | end 84 | 85 | def test_postcode_search 86 | # Test behaviour when searching by postcode 87 | c = api_client 88 | url = "#{ROOT}/places/wibble.json?limit=5&postcode=MK42+9AA" 89 | c.expects(:get_json).with(url).returns(dummy_place_response([dummy_place])) 90 | output = c.places_for_postcode("wibble", "MK42 9AA") 91 | places = output["places"] 92 | 93 | assert_equal 1, places.size 94 | end 95 | 96 | def test_postcode_with_local_authority_search 97 | # Test behaviour when searching by postcode 98 | c = api_client 99 | url = "#{ROOT}/places/wibble.json?limit=10&postcode=MK42+9AA&local_authority_slug=broadlands" 100 | c.expects(:get_json).with(url).returns(dummy_place_response([dummy_place])) 101 | output = c.places_for_postcode("wibble", "MK42 9AA", 10, "broadlands") 102 | places = output["places"] 103 | 104 | assert_equal 1, places.size 105 | end 106 | 107 | def test_invalid_postcode_search 108 | # Test behaviour when searching by invalid postcode 109 | c = api_client 110 | url = "#{ROOT}/places/wibble.json?limit=5&postcode=MK99+9AA" 111 | c.expects(:get_json).with(url).raises(GdsApi::HTTPErrorResponse.new(400)) 112 | assert_raises GdsApi::HTTPErrorResponse do 113 | c.places_for_postcode("wibble", "MK99 9AA") 114 | end 115 | end 116 | 117 | def test_places_kml 118 | kml_body = <<~KML 119 | 120 | 121 | 122 | DVLA Offices 123 | 124 | 125 | DVLA Aberdeen local office 126 | For enquiries about vehicles: 0300 790 6802 (Textphone minicom users 0300 123 1279).For enquiries about driving licences: 0300 790 6801 (Textphone minicom users 0300 123 1278).Please note, all calls are handled initially by our call centre based in Swansea 127 |
Greyfriars House, Gallowgate, Aberdeen, AB10 1WG, UK
128 | 129 | -2.0971999005177566,57.150739708305785,0 130 | 131 |
132 |
133 |
134 | KML 135 | 136 | stub_request(:get, "#{ROOT}/places/test.kml") 137 | .with(headers: GdsApi::JsonClient.default_request_headers) 138 | .to_return(status: 200, body: kml_body) 139 | 140 | response_body = api_client.places_kml("test") 141 | assert_equal kml_body, response_body 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/gds_api/worldwide.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | class GdsApi::Worldwide < GdsApi::Base 4 | def world_locations 5 | all_world_locations 6 | end 7 | 8 | def world_location(location_slug) 9 | world_location = all_world_locations.find do |location| 10 | location.dig("details", "slug") == location_slug 11 | end 12 | 13 | raise GdsApi::HTTPNotFound, 404 unless world_location 14 | 15 | world_location 16 | end 17 | 18 | def organisations_for_world_location(location_slug) 19 | worldwide_organisations = worldwide_organisations_for_location(location_slug) 20 | 21 | worldwide_organisations.map do |organisation| 22 | worldwide_organisation(organisation["link"]) 23 | end 24 | end 25 | 26 | private 27 | 28 | def base_url 29 | "#{endpoint}/api" 30 | end 31 | 32 | def all_world_locations 33 | content_item = JSON.parse(get_raw("#{base_url}/content/world")) 34 | 35 | world_locations = format_locations(content_item.dig("details", "world_locations"), "World location") 36 | international_delegations = format_locations(content_item.dig("details", "international_delegations"), "International delegation") 37 | 38 | Array(world_locations) + Array(international_delegations) 39 | end 40 | 41 | def format_locations(locations, type) 42 | locations&.map do |location| 43 | { 44 | "id" => "#{Plek.new.website_root}/world/#{location['slug']}", 45 | "title" => location["name"], 46 | "format" => type, 47 | "updated_at" => location["updated_at"], 48 | "web_url" => "#{Plek.new.website_root}/world/#{location['slug']}", 49 | "analytics_identifier" => location["analytics_identifier"], 50 | "details" => { 51 | "slug" => location["slug"], 52 | "iso2" => location["iso2"], 53 | }, 54 | "organisations" => { 55 | "id" => "#{Plek.new.website_root}/world/#{location['slug']}#organisations", 56 | "web_url" => "#{Plek.new.website_root}/world/#{location['slug']}#organisations", 57 | }, 58 | "content_id" => location["content_id"], 59 | } 60 | end 61 | end 62 | 63 | def worldwide_organisations_for_location(world_location) 64 | search_results = JSON.parse(get_raw("#{base_url}/search.json?filter_format=worldwide_organisation&filter_world_locations=#{world_location}")) 65 | 66 | search_results["results"] 67 | end 68 | 69 | def worldwide_organisation(path) 70 | content_item = JSON.parse(get_raw("#{base_url}/content#{path}")) 71 | 72 | { 73 | "id" => "#{Plek.new.website_root}#{path}", 74 | "title" => content_item["title"], 75 | "format" => "Worldwide Organisation", 76 | "updated_at" => content_item["updated_at"], 77 | "web_url" => "#{Plek.new.website_root}#{path}", 78 | "details" => { 79 | "slug" => path.gsub("/world/organisations/", ""), 80 | }, 81 | "analytics_identifier" => content_item["analytics_identifier"], 82 | "offices" => { 83 | "main" => format_office(content_item.dig("links", "main_office", 0)), 84 | "other" => content_item.dig("links", "home_page_offices")&.map do |office| 85 | format_office(office) 86 | end || [], 87 | }, 88 | "sponsors" => content_item.dig("links", "sponsoring_organisations")&.map do |sponsor| 89 | format_sponsor(sponsor) 90 | end || [], 91 | } 92 | end 93 | 94 | def format_office(office) 95 | return {} unless office 96 | 97 | contact = office.dig("links", "contact", 0) 98 | 99 | { 100 | "title" => office["title"], 101 | "format" => "World Office", 102 | "updated_at" => office["public_updated_at"], 103 | "web_url" => office["web_url"], 104 | "details" => { 105 | "email" => contact&.dig("details", "email_addresses"), 106 | "description" => contact&.dig("details", "description"), 107 | "contact_form_url" => contact&.dig("details", "contact_form_links"), 108 | "access_and_opening_times" => office.dig("details", "access_and_opening_times"), 109 | "type" => office.dig("details", "type"), 110 | }, 111 | "address" => { 112 | "adr" => { 113 | "fn" => contact&.dig("details", "post_addresses", 0, "title"), 114 | "street-address" => contact&.dig("details", "post_addresses", 0, "street_address"), 115 | "postal-code" => contact&.dig("details", "post_addresses", 0, "postal_code"), 116 | "locality" => contact&.dig("details", "post_addresses", 0, "locality"), 117 | "region" => contact&.dig("details", "post_addresses", 0, "region"), 118 | "country-name" => contact&.dig("details", "post_addresses", 0, "world_location"), 119 | }, 120 | }, 121 | "contact_numbers" => contact&.dig("details", "phone_numbers")&.map do |phone_number| 122 | { 123 | "label" => phone_number["title"], 124 | "number" => phone_number["number"], 125 | } 126 | end, 127 | "services" => contact&.dig("details", "services")&.map do |service| 128 | { 129 | title: service["title"], 130 | type: service["type"], 131 | } 132 | end, 133 | } 134 | end 135 | 136 | def format_sponsor(sponsor) 137 | { 138 | "title" => sponsor["title"], 139 | "web_url" => sponsor["web_url"], 140 | "details" => { 141 | "acronym" => sponsor.dig("details", "acronym"), 142 | }, 143 | } 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/organisations.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/test_helpers/json_client_helper" 2 | require "gds_api/test_helpers/common_responses" 3 | require "plek" 4 | require "securerandom" 5 | 6 | module GdsApi 7 | module TestHelpers 8 | module Organisations 9 | include GdsApi::TestHelpers::CommonResponses 10 | 11 | WEBSITE_ROOT = Plek.new.website_root 12 | 13 | def stub_organisations_api_has_organisations(organisation_slugs) 14 | bodies = organisation_slugs.map { |slug| organisation_for_slug(slug) } 15 | stub_organisations_api_has_organisations_with_bodies(bodies) 16 | end 17 | 18 | # Sets up the index endpoints for the given organisation slugs 19 | # The stubs are setup to paginate in chunks of 20 20 | # 21 | # This also sets up the individual endpoints for each slug 22 | # by calling organisations_api_has_organisation below 23 | def stub_organisations_api_has_organisations_with_bodies(organisation_bodies) 24 | # Stub API call to the endpoint for an individual organisation 25 | organisation_bodies.each do |body| 26 | slug = body["details"]["slug"] 27 | stub_organisations_api_has_organisation(slug, body) 28 | end 29 | 30 | pages = [] 31 | organisation_bodies.each_slice(20) do |bodies| 32 | pages << bodies 33 | end 34 | 35 | pages.each_with_index do |page, i| 36 | page_details = plural_response_base.merge( 37 | "results" => page, 38 | "total" => organisation_bodies.size, 39 | "pages" => pages.size, 40 | "current_page" => i + 1, 41 | "page_size" => 20, 42 | "start_index" => i * 20 + 1, 43 | ) 44 | 45 | links = { self: "#{WEBSITE_ROOT}/api/organisations?page=#{i + 1}" } 46 | links[:next] = "#{WEBSITE_ROOT}/api/organisations?page=#{i + 2}" if pages[i + 1] 47 | links[:previous] = "#{WEBSITE_ROOT}/api/organisations?page=#{i}" unless i.zero? 48 | page_details["_response_info"]["links"] = [] 49 | link_headers = [] 50 | links.each do |rel, href| 51 | page_details["_response_info"]["links"] << { "rel" => rel, "href" => href } 52 | link_headers << "<#{href}>; rel=\"#{rel}\"" 53 | end 54 | 55 | stub_request(:get, links[:self]) 56 | .to_return(status: 200, body: page_details.to_json, headers: { "Link" => link_headers.join(", ") }) 57 | 58 | next unless i.zero? 59 | 60 | # First page exists at URL with and without page param 61 | stub_request(:get, links[:self].sub(/\?page=1/, "")) 62 | .to_return(status: 200, body: page_details.to_json, headers: { "Link" => link_headers.join(", ") }) 63 | end 64 | 65 | if pages.empty? 66 | # If there are no pages - and so no organisations specified - then stub /api/organisations. 67 | stub_request(:get, "#{WEBSITE_ROOT}/api/organisations").to_return(status: 200, body: plural_response_base.to_json, headers: {}) 68 | end 69 | end 70 | 71 | def stub_organisations_api_has_organisation(organisation_slug, details = nil) 72 | details ||= organisation_for_slug(organisation_slug) 73 | stub_request(:get, "#{WEBSITE_ROOT}/api/organisations/#{organisation_slug}") 74 | .to_return(status: 200, body: details.to_json) 75 | end 76 | 77 | def stub_organisations_api_does_not_have_organisation(organisation_slug) 78 | stub_request(:get, "#{WEBSITE_ROOT}/api/organisations/#{organisation_slug}").to_return(status: 404) 79 | end 80 | 81 | def organisation_for_slug(slug) 82 | singular_response_base.merge(organisation_details_for_slug(slug)) 83 | end 84 | 85 | # Constructs a sample organisation 86 | # 87 | # if the slug contains 'ministry' the format will be set to 'Ministerial department' 88 | # otherwise it will be set to 'Executive agency' 89 | def organisation_details_for_slug(slug, content_id = SecureRandom.uuid) 90 | { 91 | "id" => "#{WEBSITE_ROOT}/api/organisations/#{slug}", 92 | "title" => titleize_slug(slug, title_case: true), 93 | "format" => (slug =~ /ministry/ ? "Ministerial department" : "Executive agency"), 94 | "updated_at" => "2013-03-25T13:06:42+00:00", 95 | "web_url" => "#{WEBSITE_ROOT}/government/organisations/#{slug}", 96 | "details" => { 97 | "slug" => slug, 98 | "abbreviation" => acronymize_slug(slug), 99 | "logo_formatted_name" => titleize_slug(slug, title_case: true), 100 | "organisation_brand_colour_class_name" => slug, 101 | "organisation_logo_type_class_name" => (slug =~ /ministry/ ? "single-identity" : "eo"), 102 | "closed_at" => nil, 103 | "govuk_status" => (slug =~ /ministry/ ? "live" : "joining"), 104 | "content_id" => content_id, 105 | }, 106 | "parent_organisations" => [ 107 | { 108 | "id" => "#{WEBSITE_ROOT}/api/organisations/#{slug}-parent-1", 109 | "web_url" => "#{WEBSITE_ROOT}/government/organisations/#{slug}-parent-1", 110 | }, 111 | ], 112 | "child_organisations" => [ 113 | { 114 | "id" => "#{WEBSITE_ROOT}/api/organisations/#{slug}-child-1", 115 | "web_url" => "#{WEBSITE_ROOT}/government/organisations/#{slug}-child-1", 116 | }, 117 | ], 118 | } 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/gds_api/test_helpers/search.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "gds_api/test_helpers/json_client_helper" 3 | 4 | module GdsApi 5 | module TestHelpers 6 | module Search 7 | SEARCH_ENDPOINT = Plek.find("search-api") 8 | 9 | def stub_any_search_post(index: nil) 10 | if index 11 | stub_request(:post, %r{#{SEARCH_ENDPOINT}/#{index}/documents}) 12 | .to_return(status: [202, "Accepted"]) 13 | else 14 | stub_request(:post, %r{#{SEARCH_ENDPOINT}/documents}) 15 | .to_return(status: [202, "Accepted"]) 16 | end 17 | end 18 | 19 | def assert_search_posted_item(attributes, index: nil, **options) 20 | url = if index 21 | SEARCH_ENDPOINT + "/#{index}/documents" 22 | else 23 | "#{SEARCH_ENDPOINT}/documents" 24 | end 25 | 26 | assert_requested(:post, url, **options) do |req| 27 | data = JSON.parse(req.body) 28 | attributes.to_a.all? do |key, value| 29 | data[key.to_s] == value 30 | end 31 | end 32 | end 33 | 34 | def stub_any_search 35 | stub_request(:get, %r{#{SEARCH_ENDPOINT}/search.json}) 36 | end 37 | 38 | def stub_any_search_to_return_no_results 39 | stub_any_search.to_return(body: { results: [] }.to_json) 40 | end 41 | 42 | def assert_search(options) 43 | assert_requested :get, "#{SEARCH_ENDPOINT}/search.json", **options 44 | end 45 | 46 | def stub_any_search_delete(index: nil) 47 | if index 48 | stub_request(:delete, %r{#{SEARCH_ENDPOINT}/#{index}/documents/.*}) 49 | else 50 | # use search-api's default index 51 | stub_request(:delete, %r{#{SEARCH_ENDPOINT}/documents/.*}) 52 | end 53 | end 54 | 55 | def stub_any_search_delete_content 56 | stub_request(:delete, %r{#{SEARCH_ENDPOINT}/content.*}) 57 | end 58 | 59 | def assert_search_deleted_item(id, index: nil, **options) 60 | if id =~ %r{^/} 61 | raise ArgumentError, "Search id must not start with a slash" 62 | end 63 | 64 | if index 65 | assert_requested( 66 | :delete, 67 | %r{#{SEARCH_ENDPOINT}/#{index}/documents/#{id}}, 68 | **options, 69 | ) 70 | else 71 | assert_requested( 72 | :delete, 73 | %r{#{SEARCH_ENDPOINT}/documents/#{id}}, 74 | **options, 75 | ) 76 | end 77 | end 78 | 79 | def assert_search_deleted_content(base_path, **options) 80 | assert_requested( 81 | :delete, 82 | %r{#{SEARCH_ENDPOINT}/content.*#{base_path}}, 83 | **options, 84 | ) 85 | end 86 | 87 | def stub_search_has_services_and_info_data_for_organisation 88 | stub_request_for(search_results_found) 89 | run_example_query 90 | end 91 | 92 | def stub_search_has_no_services_and_info_data_for_organisation 93 | stub_request_for(no_search_results_found) 94 | run_example_query 95 | end 96 | 97 | def stub_search_has_specialist_sector_organisations(_sub_sector) 98 | stub_request_for(sub_sector_organisations_results) 99 | run_example_query 100 | end 101 | 102 | def stub_search_has_no_policies_for_any_type 103 | stub_request(:get, %r{/search.json}) 104 | .to_return(body: no_search_results_found) 105 | end 106 | 107 | def stub_search_has_policies_for_every_type(options = {}) 108 | if options[:count] 109 | stub_request(:get, %r{/search.json.*count=#{options[:count]}.*}) 110 | .to_return(body: first_n_results(new_policies_results, n: options[:count])) 111 | else 112 | stub_request(:get, %r{/search.json}) 113 | .to_return(body: new_policies_results) 114 | end 115 | end 116 | 117 | private 118 | 119 | def stub_request_for(result_set) 120 | stub_request(:get, /example.com\/search/).to_return(body: result_set) 121 | end 122 | 123 | def run_example_query 124 | client.search(example_query) 125 | end 126 | 127 | def search_results_found 128 | File.read( 129 | File.expand_path( 130 | "../../../test/fixtures/services_and_info_fixture.json", 131 | __dir__, 132 | ), 133 | ) 134 | end 135 | 136 | def no_search_results_found 137 | File.read( 138 | File.expand_path( 139 | "../../../test/fixtures/no_services_and_info_data_found_fixture.json", 140 | __dir__, 141 | ), 142 | ) 143 | end 144 | 145 | def sub_sector_organisations_results 146 | File.read( 147 | File.expand_path( 148 | "../../../test/fixtures/sub_sector_organisations.json", 149 | __dir__, 150 | ), 151 | ) 152 | end 153 | 154 | def new_policies_results 155 | File.read( 156 | File.expand_path( 157 | "../../../test/fixtures/new_policies_for_dwp.json", 158 | __dir__, 159 | ), 160 | ) 161 | end 162 | 163 | def old_policies_results 164 | File.read( 165 | File.expand_path( 166 | "../../../test/fixtures/old_policies_for_dwp.json", 167 | __dir__, 168 | ), 169 | ) 170 | end 171 | 172 | def first_n_results(results, options) 173 | n = options[:n] 174 | results = JSON.parse(results) 175 | results["results"] = results["results"][0...n] 176 | 177 | results.to_json 178 | end 179 | 180 | def client 181 | GdsApi::Search.new("http://example.com") 182 | end 183 | 184 | def example_query 185 | { 186 | filter_organisations: %w[an-organisation-slug], 187 | facet_specialist_sectors: "1000,examples:4,example_scope:global,order:value.title", 188 | } 189 | end 190 | end 191 | end 192 | end 193 | --------------------------------------------------------------------------------