├── 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 |
--------------------------------------------------------------------------------