gds-api-adapters 69.2.0 → 71.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 294f4287f0eb5a9fad08a2732fba69502b5622e5a8ebbc8bef4acc54a98b5c71
4
- data.tar.gz: a9ecd5e2dbaf5e0fe31f678330300256b629e9ecd7b53c6f25b7c0e5ddaa4180
3
+ metadata.gz: fb0860ace845fe7016ae87832f88f46cbf9d27a660432f3314ad4b52cf4774f9
4
+ data.tar.gz: e3ad07862f196e9e65f16a1d3ff231133c45d293cc8ac4a998c126f5d08aa680
5
5
  SHA512:
6
- metadata.gz: 76d12236b2395137708802139460673ef5cb8e2634e84dfb29066dc69cfa2dbba08c049554cc74f363e2e9c22ce7b4d924b06e498704666c3c8fd60a4a0adfc4
7
- data.tar.gz: c1186893c71cd5daa83f62d52c0849954f1fc3395e224b934372bba08696691478553ce2c0a5ec25f2ba53ff7b5a7f08b936aee7bdf5160ad13604d3e09d19a8
6
+ metadata.gz: 8356685a336b23b4c0cb7f8abafd697cafd065be17f5508097e00d7503b6ae7aaaff5efa14a5430ed0f74121661d5df4d1614185717136a1b9e49d78e57ea369
7
+ data.tar.gz: 232183a69a8985bd6963c475ef852e2d4d28e481d9c09232bd8dbcc9f9db102d16f22df09cd99c4abef7072e1524be76d34230be9cdf99867de08f7e0cf5a4a2
data/Rakefile CHANGED
@@ -30,5 +30,5 @@ end
30
30
 
31
31
  desc "Run the linter against changed files"
32
32
  task :lint do
33
- sh "bundle exec rubocop --format clang"
33
+ sh "bundle exec rubocop"
34
34
  end
data/lib/gds_api.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "addressable"
2
2
  require "plek"
3
3
  require "time"
4
+ require "gds_api/account_api"
4
5
  require "gds_api/asset_manager"
5
6
  require "gds_api/calendars"
6
7
  require "gds_api/content_store"
@@ -21,6 +22,19 @@ require "gds_api/worldwide"
21
22
 
22
23
  # @api documented
23
24
  module GdsApi
25
+ # Creates a GdsApi::AccountApi adapter
26
+ #
27
+ # This will set a bearer token if a ACCOUNT_API_BEARER_TOKEN environment
28
+ # variable is set
29
+ #
30
+ # @return [GdsApi::AccountApi]
31
+ def self.account_api(options = {})
32
+ GdsApi::AccountApi.new(
33
+ Plek.find("account-api"),
34
+ { bearer_token: ENV["ACCOUNT_API_BEARER_TOKEN"] }.merge(options),
35
+ )
36
+ end
37
+
24
38
  # Creates a GdsApi::AssetManager adapter
25
39
  #
26
40
  # This will set a bearer token if a ASSET_MANAGER_BEARER_TOKEN environment
@@ -0,0 +1,148 @@
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 [String, nil] state_id identifier originally returned by #create_registration_state
15
+ # @param [String, nil] level_of_authentication either "level1" (require MFA) or "level0" (do not require MFA)
16
+ #
17
+ # @return [Hash] An authentication URL and the OAuth state parameter (for CSRF protection)
18
+ def get_sign_in_url(redirect_path: nil, state_id: nil, level_of_authentication: nil)
19
+ querystring = nested_query_string(
20
+ {
21
+ redirect_path: redirect_path,
22
+ state_id: state_id,
23
+ level_of_authentication: level_of_authentication,
24
+ }.compact,
25
+ )
26
+ get_json("#{endpoint}/api/oauth2/sign-in?#{querystring}")
27
+ end
28
+
29
+ # Validate an OAuth authentication response
30
+ #
31
+ # @param [String] code The OAuth code parameter, from the auth server.
32
+ # @param [String] state The OAuth state parameter, from the auth server.
33
+ #
34
+ # @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)
35
+ def validate_auth_response(code:, state:)
36
+ post_json("#{endpoint}/api/oauth2/callback", code: code, state: state)
37
+ end
38
+
39
+ # Register some initial state, to pass to get_sign_in_url, which is used to initialise the account if the user signs up
40
+ #
41
+ # @param [Hash, nil] attributes Initial attributes to store
42
+ #
43
+ # @return [Hash] The state ID to pass to get_sign_in_url
44
+ def create_registration_state(attributes:)
45
+ post_json("#{endpoint}/api/oauth2/state", attributes: attributes)
46
+ end
47
+
48
+ # Check if a user has an email subscription for the Transition Checker
49
+ #
50
+ # @param [String] govuk_account_session Value of the session header
51
+ #
52
+ # @return [Hash] Whether the user has a subscription, and a new session header
53
+ def check_for_email_subscription(govuk_account_session:)
54
+ get_json("#{endpoint}/api/transition-checker-email-subscription", auth_headers(govuk_account_session))
55
+ end
56
+
57
+ # Create or update a user's email subscription for the Transition Checker
58
+ #
59
+ # @param [String] govuk_account_session Value of the session header
60
+ # @param [String] slug The email topic slug
61
+ #
62
+ # @return [Hash] Whether the user has a subscription, and a new session header
63
+ def set_email_subscription(govuk_account_session:, slug:)
64
+ post_json("#{endpoint}/api/transition-checker-email-subscription", { slug: slug }, auth_headers(govuk_account_session))
65
+ end
66
+
67
+ # Look up the values of a user's attributes
68
+ #
69
+ # @param [String] attributes Names of the attributes to check
70
+ # @param [String] govuk_account_session Value of the session header
71
+ #
72
+ # @return [Hash] The attribute values (if present), and a new session header
73
+ def get_attributes(attributes:, govuk_account_session:)
74
+ querystring = nested_query_string({ attributes: attributes }.compact)
75
+ get_json("#{endpoint}/api/attributes?#{querystring}", auth_headers(govuk_account_session))
76
+ end
77
+
78
+ # Create or update attributes for a user
79
+ #
80
+ # @param [String] attributes Hash of new attribute values
81
+ # @param [String] govuk_account_session Value of the session header
82
+ #
83
+ # @return [Hash] A new session header
84
+ def set_attributes(attributes:, govuk_account_session:)
85
+ patch_json("#{endpoint}/api/attributes", { attributes: attributes }, auth_headers(govuk_account_session))
86
+ end
87
+
88
+ # Look up the names of a user's attributes
89
+ #
90
+ # @param [String] attributes Names of the attributes to check
91
+ # @param [String] govuk_account_session Value of the session header
92
+ #
93
+ # @return [Hash] The attribute names (if present), and a new session header
94
+ def get_attributes_names(attributes:, govuk_account_session:)
95
+ querystring = nested_query_string({ attributes: attributes }.compact)
96
+ get_json("#{endpoint}/api/attributes/names?#{querystring}", auth_headers(govuk_account_session))
97
+ end
98
+
99
+ # Look up all pages saved by a user in their Account
100
+ #
101
+ # @param [String] govuk_account_session Value of the session header
102
+ #
103
+ # @return [Hash] containing :saved_pages, an array of single saved page hashes def get_saved_pages(govuk_account_session:)
104
+ def get_saved_pages(govuk_account_session:)
105
+ get_json("#{endpoint}/api/saved_pages", auth_headers(govuk_account_session))
106
+ end
107
+
108
+ # Return a single page by unique URL
109
+ #
110
+ # @param [String] the path of a page to check
111
+ # @param [String] govuk_account_session Value of the session header
112
+ #
113
+ # @return [Hash] containing :saved_page, a hash of a single saved page value
114
+ def get_saved_page(page_path:, govuk_account_session:)
115
+ get_json("#{endpoint}/api/saved_pages/#{CGI.escape(page_path)}", auth_headers(govuk_account_session))
116
+ end
117
+
118
+ # Upsert a single saved page entry in a users account
119
+ #
120
+ # @param [String] the path of a page to check
121
+ # @param [String] govuk_account_session Value of the session header
122
+ #
123
+ # @return [Hash] A single saved page value (if sucessful)
124
+ def save_page(page_path:, govuk_account_session:)
125
+ put_json("#{endpoint}/api/saved_pages/#{CGI.escape(page_path)}", {}, auth_headers(govuk_account_session))
126
+ end
127
+
128
+ # Delete a single saved page entry from a users account
129
+ #
130
+ # @param [String] the path of a page to check
131
+ # @param [String] govuk_account_session Value of the session header
132
+ #
133
+ # @return [GdsApi::Response] A status code of 204 indicates the saved page has been successfully deleted.
134
+ # A status code of 404 indicates there is no saved page with this path.
135
+ def delete_saved_page(page_path:, govuk_account_session:)
136
+ delete_json("#{endpoint}/api/saved_pages/#{CGI.escape(page_path)}", {}, auth_headers(govuk_account_session))
137
+ end
138
+
139
+ private
140
+
141
+ def nested_query_string(params)
142
+ Rack::Utils.build_nested_query(params)
143
+ end
144
+
145
+ def auth_headers(govuk_account_session)
146
+ { AUTH_HEADER_NAME => govuk_account_session }
147
+ end
148
+ end
data/lib/gds_api/base.rb CHANGED
@@ -71,7 +71,7 @@ private
71
71
  case value
72
72
  when Array
73
73
  value.map do |v|
74
- "#{CGI.escape(key.to_s + '[]')}=#{CGI.escape(v.to_s)}"
74
+ "#{CGI.escape("#{key}[]")}=#{CGI.escape(v.to_s)}"
75
75
  end
76
76
  else
77
77
  "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
@@ -93,7 +93,7 @@ private
93
93
  def prefix_destination(redirect, path, query)
94
94
  uri = URI.parse(redirect["destination"])
95
95
  start_char = redirect["path"].length
96
- suffix = path[start_char..-1]
96
+ suffix = path[start_char..]
97
97
 
98
98
  if uri.path == "" && suffix[0] != "/"
99
99
  uri.path = "/#{suffix}"
@@ -14,49 +14,66 @@ module GdsApi
14
14
  end
15
15
 
16
16
  class EndpointNotFound < BaseError; end
17
+
17
18
  class TimedOutException < BaseError; end
19
+
18
20
  class InvalidUrl < BaseError; end
21
+
19
22
  class SocketErrorException < BaseError; end
20
23
 
21
24
  # Superclass for all 4XX and 5XX errors
22
25
  class HTTPErrorResponse < BaseError
23
- attr_accessor :code, :error_details
26
+ attr_accessor :code, :error_details, :http_body
24
27
 
25
- def initialize(code, message = nil, error_details = nil)
28
+ def initialize(code, message = nil, error_details = nil, http_body = nil)
26
29
  super(message)
27
30
  @code = code
28
31
  @error_details = error_details
32
+ @http_body = http_body
29
33
  end
30
34
  end
31
35
 
32
36
  # Superclass & fallback for all 4XX errors
33
37
  class HTTPClientError < HTTPErrorResponse; end
38
+
34
39
  class HTTPIntermittentClientError < HTTPClientError; end
35
40
 
36
41
  class HTTPNotFound < HTTPClientError; end
42
+
37
43
  class HTTPGone < HTTPClientError; end
44
+
38
45
  class HTTPPayloadTooLarge < HTTPClientError; end
46
+
39
47
  class HTTPUnauthorized < HTTPClientError; end
48
+
40
49
  class HTTPForbidden < HTTPClientError; end
50
+
41
51
  class HTTPConflict < HTTPClientError; end
52
+
42
53
  class HTTPUnprocessableEntity < HTTPClientError; end
54
+
43
55
  class HTTPBadRequest < HTTPClientError; end
56
+
44
57
  class HTTPTooManyRequests < HTTPIntermittentClientError; end
45
58
 
46
59
  # Superclass & fallback for all 5XX errors
47
60
  class HTTPServerError < HTTPErrorResponse; end
61
+
48
62
  class HTTPIntermittentServerError < HTTPServerError; end
49
63
 
50
64
  class HTTPInternalServerError < HTTPServerError; end
65
+
51
66
  class HTTPBadGateway < HTTPIntermittentServerError; end
67
+
52
68
  class HTTPUnavailable < HTTPIntermittentServerError; end
69
+
53
70
  class HTTPGatewayTimeout < HTTPIntermittentServerError; end
54
71
 
55
72
  module ExceptionHandling
56
73
  def build_specific_http_error(error, url, details = nil)
57
74
  message = "URL: #{url}\nResponse body:\n#{error.http_body}"
58
75
  code = error.http_code
59
- error_class_for_code(code).new(code, message, details)
76
+ error_class_for_code(code).new(code, message, details, error.http_body)
60
77
  end
61
78
 
62
79
  def error_class_for_code(code)
@@ -101,10 +101,10 @@ module GdsApi
101
101
  rescue RestClient::Exception => e
102
102
  # Attempt to parse the body as JSON if possible
103
103
  error_details = begin
104
- e.http_body ? JSON.parse(e.http_body) : nil
105
- rescue JSON::ParserError
106
- nil
107
- end
104
+ e.http_body ? JSON.parse(e.http_body) : nil
105
+ rescue JSON::ParserError
106
+ nil
107
+ end
108
108
  raise build_specific_http_error(e, url, error_details)
109
109
  end
110
110
 
@@ -89,37 +89,6 @@ class GdsApi::LinkCheckerApi < GdsApi::Base
89
89
  )
90
90
  end
91
91
 
92
- # Update or create a set of links to be monitored for a resource.
93
- #
94
- # Makes a +POST+ request to the link checker api to create a resource monitor.
95
- #
96
- # @param links [Array] A list of URIs to monitor.
97
- # @param reference [String] A unique id for the resource being monitored
98
- # @param app [String] The name of the service the call originated e.g. 'whitehall'
99
- # @return [MonitorReport] A +SimpleDelegator+ of the +GdsApi::Response+ which
100
- # responds to:
101
- # :id the ID of the created resource monitor
102
- #
103
- # @raise [HTTPErrorResponse] if the request returns an error
104
-
105
- def upsert_resource_monitor(links, app, reference)
106
- payload = {
107
- links: links,
108
- app: app,
109
- reference: reference,
110
- }
111
-
112
- response = post_json("#{endpoint}/monitor", payload)
113
-
114
- MonitorReport.new(response.to_hash)
115
- end
116
-
117
- class MonitorReport < SimpleDelegator
118
- def id
119
- self["id"]
120
- end
121
- end
122
-
123
92
  class LinkReport < SimpleDelegator
124
93
  def uri
125
94
  self["uri"]
@@ -24,6 +24,7 @@ module GdsApi
24
24
  PATTERN = /([-a-z]+)(?:\s*=\s*([^,\s]+))?,?+/i.freeze
25
25
 
26
26
  def initialize(value = nil)
27
+ super()
27
28
  parse(value)
28
29
  end
29
30
 
@@ -67,7 +67,7 @@ module GdsApi
67
67
  #
68
68
  # @param args [Hash] A valid search query. See search-api documentation for options.
69
69
  #
70
- # @see https://github.com/alphagov/search-api/blob/master/doc/search-api.md
70
+ # @see https://github.com/alphagov/search-api/blob/master/docs/search-api.md
71
71
  def search(args, additional_headers = {})
72
72
  request_url = "#{base_url}/search.json?#{Rack::Utils.build_nested_query(args)}"
73
73
  get_json(request_url, additional_headers)
@@ -77,7 +77,7 @@ module GdsApi
77
77
  #
78
78
  # @param searches [Array] An array valid search queries. Maximum of 6. See search-api documentation for options.
79
79
  #
80
- # # @see https://github.com/alphagov/search-api/blob/master/doc/search-api.md
80
+ # # @see https://github.com/alphagov/search-api/blob/master/docs/search-api.md
81
81
  def batch_search(searches, additional_headers = {})
82
82
  url_friendly_searches = searches.each_with_index.map do |search, index|
83
83
  { index => search }
@@ -95,7 +95,7 @@ module GdsApi
95
95
  # @param args [Hash] A valid search query. See search-api documentation for options.
96
96
  # @param page_size [Integer] Number of results in each page.
97
97
  #
98
- # @see https://github.com/alphagov/search-api/blob/master/doc/search-api.md
98
+ # @see https://github.com/alphagov/search-api/blob/master/docs/search-api.md
99
99
  def search_enum(args, page_size: 100, additional_headers: {})
100
100
  Enumerator.new do |yielder|
101
101
  (0..Float::INFINITY).step(page_size).each do |index|
@@ -120,7 +120,7 @@ module GdsApi
120
120
  # GOV.UK - we only allow deletion from metasearch
121
121
  # @return [GdsApi::Response] A status code of 202 indicates the document has been successfully queued.
122
122
  #
123
- # @see https://github.com/alphagov/search-api/blob/master/doc/documents.md
123
+ # @see https://github.com/alphagov/search-api/blob/master/docs/documents.md
124
124
  def add_document(*args)
125
125
  @api.add_document(*args)
126
126
  end
@@ -132,7 +132,7 @@ module GdsApi
132
132
  # and contacts, which may be deleted with `delete_document`.
133
133
  #
134
134
  # @param base_path Base path of the page on GOV.UK.
135
- # @see https://github.com/alphagov/search-api/blob/master/doc/content-api.md
135
+ # @see https://github.com/alphagov/search-api/blob/master/docs/content-api.md
136
136
  def delete_content(base_path)
137
137
  request_url = "#{base_url}/content?link=#{base_path}"
138
138
  delete_json(request_url)
@@ -145,7 +145,7 @@ module GdsApi
145
145
  # and contacts.
146
146
  #
147
147
  # @param base_path [String] Base path of the page on GOV.UK.
148
- # @see https://github.com/alphagov/search-api/blob/master/doc/content-api.md
148
+ # @see https://github.com/alphagov/search-api/blob/master/docs/content-api.md
149
149
  def get_content(base_path)
150
150
  request_url = "#{base_url}/content?link=#{base_path}"
151
151
  get_json(request_url)
@@ -0,0 +1,337 @@
1
+ require "json"
2
+
3
+ module GdsApi
4
+ module TestHelpers
5
+ module AccountApi
6
+ ACCOUNT_API_ENDPOINT = Plek.find("account-api")
7
+
8
+ def stub_account_api_get_sign_in_url(redirect_path: nil, state_id: nil, level_of_authentication: nil, auth_uri: "http://auth/provider", state: "state")
9
+ querystring = Rack::Utils.build_nested_query({ redirect_path: redirect_path, state_id: state_id, level_of_authentication: level_of_authentication }.compact)
10
+ stub_request(:get, "#{ACCOUNT_API_ENDPOINT}/api/oauth2/sign-in?#{querystring}")
11
+ .to_return(
12
+ status: 200,
13
+ body: { auth_uri: auth_uri, state: state }.to_json,
14
+ )
15
+ end
16
+
17
+ def stub_account_api_validates_auth_response(code: nil, state: nil, govuk_account_session: "govuk-account-session", redirect_path: "/", ga_client_id: "ga-client-id")
18
+ stub_request(:post, "#{ACCOUNT_API_ENDPOINT}/api/oauth2/callback")
19
+ .with(body: hash_including({ code: code, state: state }.compact))
20
+ .to_return(
21
+ status: 200,
22
+ body: { govuk_account_session: govuk_account_session, redirect_path: redirect_path, ga_client_id: ga_client_id }.to_json,
23
+ )
24
+ end
25
+
26
+ def stub_account_api_rejects_auth_response(code: nil, state: nil)
27
+ stub_request(:post, "#{ACCOUNT_API_ENDPOINT}/api/oauth2/callback")
28
+ .with(body: hash_including({ code: code, state: state }.compact))
29
+ .to_return(status: 401)
30
+ end
31
+
32
+ def stub_account_api_create_registration_state(attributes: nil, state_id: "state-id")
33
+ stub_request(:post, "#{ACCOUNT_API_ENDPOINT}/api/oauth2/state")
34
+ .with(body: hash_including({ attributes: attributes }.compact))
35
+ .to_return(
36
+ status: 200,
37
+ body: { state_id: state_id }.to_json,
38
+ )
39
+ end
40
+
41
+ def stub_account_api_has_email_subscription(**options)
42
+ stub_account_api_request(
43
+ :get,
44
+ "/api/transition-checker-email-subscription",
45
+ response_body: { has_subscription: true },
46
+ **options,
47
+ )
48
+ end
49
+
50
+ def stub_account_api_does_not_have_email_subscription(**options)
51
+ stub_account_api_request(
52
+ :get,
53
+ "/api/transition-checker-email-subscription",
54
+ response_body: { has_subscription: false },
55
+ **options,
56
+ )
57
+ end
58
+
59
+ def stub_account_api_unauthorized_get_email_subscription(**options)
60
+ stub_account_api_request(
61
+ :get,
62
+ "/api/transition-checker-email-subscription",
63
+ response_status: 401,
64
+ **options,
65
+ )
66
+ end
67
+
68
+ def stub_account_api_forbidden_get_email_subscription(needed_level_of_authentication: "level1", **options)
69
+ stub_account_api_request(
70
+ :get,
71
+ "/api/transition-checker-email-subscription",
72
+ response_status: 403,
73
+ response_body: { needed_level_of_authentication: needed_level_of_authentication },
74
+ **options,
75
+ )
76
+ end
77
+
78
+ def stub_account_api_set_email_subscription(slug: nil, **options)
79
+ stub_account_api_request(
80
+ :post,
81
+ "/api/transition-checker-email-subscription",
82
+ with: { body: hash_including({ slug: slug }.compact) },
83
+ **options,
84
+ )
85
+ end
86
+
87
+ def stub_account_api_unauthorized_set_email_subscription(slug: nil, **options)
88
+ stub_account_api_request(
89
+ :post,
90
+ "/api/transition-checker-email-subscription",
91
+ with: { body: hash_including({ slug: slug }.compact) },
92
+ response_status: 401,
93
+ **options,
94
+ )
95
+ end
96
+
97
+ def stub_account_api_forbidden_set_email_subscription(slug: nil, needed_level_of_authentication: "level1", **options)
98
+ stub_account_api_request(
99
+ :post,
100
+ "/api/transition-checker-email-subscription",
101
+ with: { body: hash_including({ slug: slug }.compact) },
102
+ response_status: 403,
103
+ response_body: { needed_level_of_authentication: needed_level_of_authentication },
104
+ **options,
105
+ )
106
+ end
107
+
108
+ def stub_account_api_has_attributes(attributes: [], values: {}, **options)
109
+ querystring = Rack::Utils.build_nested_query({ attributes: attributes }.compact)
110
+ stub_account_api_request(
111
+ :get,
112
+ "/api/attributes?#{querystring}",
113
+ response_body: { values: values },
114
+ **options,
115
+ )
116
+ end
117
+
118
+ def stub_account_api_unauthorized_has_attributes(attributes: [], **options)
119
+ querystring = Rack::Utils.build_nested_query({ attributes: attributes }.compact)
120
+ stub_account_api_request(
121
+ :get,
122
+ "/api/attributes?#{querystring}",
123
+ response_status: 401,
124
+ **options,
125
+ )
126
+ end
127
+
128
+ def stub_account_api_forbidden_has_attributes(attributes: [], needed_level_of_authentication: "level1", **options)
129
+ querystring = Rack::Utils.build_nested_query({ attributes: attributes }.compact)
130
+ stub_account_api_request(
131
+ :get,
132
+ "/api/attributes?#{querystring}",
133
+ response_status: 403,
134
+ response_body: { needed_level_of_authentication: needed_level_of_authentication },
135
+ **options,
136
+ )
137
+ end
138
+
139
+ def stub_account_api_set_attributes(attributes: nil, **options)
140
+ stub_account_api_request(
141
+ :patch,
142
+ "/api/attributes",
143
+ with: { body: hash_including({ attributes: attributes }.compact) },
144
+ **options,
145
+ )
146
+ end
147
+
148
+ def stub_account_api_unauthorized_set_attributes(attributes: nil, **options)
149
+ stub_account_api_request(
150
+ :patch,
151
+ "/api/attributes",
152
+ with: { body: hash_including({ attributes: attributes }.compact) },
153
+ response_status: 401,
154
+ **options,
155
+ )
156
+ end
157
+
158
+ def stub_account_api_forbidden_set_attributes(attributes: nil, needed_level_of_authentication: "level1", **options)
159
+ stub_account_api_request(
160
+ :patch,
161
+ "/api/attributes",
162
+ with: { body: hash_including({ attributes: attributes }.compact) },
163
+ response_status: 403,
164
+ response_body: { needed_level_of_authentication: needed_level_of_authentication },
165
+ **options,
166
+ )
167
+ end
168
+
169
+ def stub_account_api_get_attributes_names(attributes: [], **options)
170
+ querystring = Rack::Utils.build_nested_query({ attributes: attributes }.compact)
171
+ stub_account_api_request(
172
+ :get,
173
+ "/api/attributes/names?#{querystring}",
174
+ response_body: { values: attributes },
175
+ **options,
176
+ )
177
+ end
178
+
179
+ def stub_account_api_unauthorized_get_attributes_names(attributes: [], **options)
180
+ querystring = Rack::Utils.build_nested_query({ attributes: attributes }.compact)
181
+ stub_account_api_request(
182
+ :get,
183
+ "/api/attributes/names?#{querystring}",
184
+ response_status: 401,
185
+ **options,
186
+ )
187
+ end
188
+
189
+ def stub_account_api_forbidden_get_attributes_names(attributes: [], needed_level_of_authentication: "level1", **options)
190
+ querystring = Rack::Utils.build_nested_query({ attributes: attributes }.compact)
191
+ stub_account_api_request(
192
+ :get,
193
+ "/api/attributes/names?#{querystring}",
194
+ response_status: 403,
195
+ response_body: { needed_level_of_authentication: needed_level_of_authentication },
196
+ **options,
197
+ )
198
+ end
199
+
200
+ def stub_account_api_request(method, path, with: {}, response_status: 200, response_body: {}, govuk_account_session: nil, new_govuk_account_session: nil)
201
+ with.merge!(headers: { GdsApi::AccountApi::AUTH_HEADER_NAME => govuk_account_session }) if govuk_account_session
202
+ new_govuk_account_session = nil if response_status >= 400
203
+ to_return = { status: response_status, body: response_body.merge(govuk_account_session: new_govuk_account_session).compact.to_json }
204
+ if with.empty?
205
+ stub_request(method, "#{ACCOUNT_API_ENDPOINT}#{path}").to_return(**to_return)
206
+ else
207
+ stub_request(method, "#{ACCOUNT_API_ENDPOINT}#{path}").with(**with).to_return(**to_return)
208
+ end
209
+ end
210
+
211
+ ######################
212
+ # GET /api/saved_pages
213
+ ######################
214
+ def stub_account_api_returning_saved_pages(saved_pages: [], **options)
215
+ stub_account_api_request(
216
+ :get,
217
+ "/api/saved_pages",
218
+ response_body: { saved_pages: saved_pages },
219
+ **options,
220
+ )
221
+ end
222
+
223
+ def stub_account_api_unauthorized_get_saved_pages(**options)
224
+ stub_account_api_request(
225
+ :get,
226
+ "/api/saved_pages",
227
+ response_status: 401,
228
+ **options,
229
+ )
230
+ end
231
+
232
+ #################################
233
+ # GET /api/saved_pages/:page_path
234
+ #################################
235
+ def stub_account_api_get_saved_page(page_path:, **options)
236
+ stub_account_api_request(
237
+ :get,
238
+ "/api/saved_pages/#{CGI.escape(page_path)}",
239
+ response_body: { saved_page: { page_path: page_path } },
240
+ **options,
241
+ )
242
+ end
243
+
244
+ def stub_account_api_does_not_have_saved_page(page_path:, **options)
245
+ stub_account_api_request(
246
+ :get,
247
+ "/api/saved_pages/#{CGI.escape(page_path)}",
248
+ response_status: 404,
249
+ **options,
250
+ )
251
+ end
252
+
253
+ def stub_account_api_unauthorized_get_saved_page(page_path:, **options)
254
+ stub_account_api_request(
255
+ :get,
256
+ "/api/saved_pages/#{CGI.escape(page_path)}",
257
+ response_status: 401,
258
+ **options,
259
+ )
260
+ end
261
+
262
+ #################################
263
+ # PUT /api/saved_pages/:page_path
264
+ #################################
265
+ def stub_account_api_save_page(page_path:, **options)
266
+ stub_account_api_request(
267
+ :put,
268
+ "/api/saved_pages/#{CGI.escape(page_path)}",
269
+ response_body: { saved_page: { page_path: page_path } },
270
+ **options,
271
+ )
272
+ end
273
+
274
+ def stub_account_api_save_page_already_exists(page_path:, **options)
275
+ stub_account_api_save_page(page_path: page_path, **options)
276
+ end
277
+
278
+ def stub_account_api_save_page_cannot_save_page(page_path:, **options)
279
+ stub_account_api_request(
280
+ :put,
281
+ "/api/saved_pages/#{CGI.escape(page_path)}",
282
+ response_status: 422,
283
+ response_body: { **cannot_save_page_problem_detail({ page_path: page_path }) },
284
+ **options,
285
+ )
286
+ end
287
+
288
+ def stub_account_api_unauthorized_save_page(page_path:, **options)
289
+ stub_account_api_request(
290
+ :put,
291
+ "/api/saved_pages/#{CGI.escape(page_path)}",
292
+ response_status: 401,
293
+ **options,
294
+ )
295
+ end
296
+
297
+ def cannot_save_page_problem_detail(option = {})
298
+ {
299
+ title: "Cannot save page",
300
+ detail: "Cannot save page with path #{option['page_path']}, check it is not blank, and is a well formatted url path.",
301
+ type: "https://github.com/alphagov/account-api/blob/main/docs/api.md#cannot-save-page",
302
+ **option,
303
+ }
304
+ end
305
+
306
+ ####################################
307
+ # DELETE /api/saved-pages/:page_path
308
+ ####################################
309
+ def stub_account_api_delete_saved_page(page_path:, **options)
310
+ stub_account_api_request(
311
+ :delete,
312
+ "/api/saved_pages/#{CGI.escape(page_path)}",
313
+ response_status: 204,
314
+ **options,
315
+ )
316
+ end
317
+
318
+ def stub_account_api_delete_saved_page_does_not_exist(page_path:, **options)
319
+ stub_account_api_request(
320
+ :delete,
321
+ "/api/saved_pages/#{CGI.escape(page_path)}",
322
+ response_status: 404,
323
+ **options,
324
+ )
325
+ end
326
+
327
+ def stub_account_api_delete_saved_page_unauthorised(page_path:, **options)
328
+ stub_account_api_request(
329
+ :delete,
330
+ "/api/saved_pages/#{CGI.escape(page_path)}",
331
+ response_status: 401,
332
+ **options,
333
+ )
334
+ end
335
+ end
336
+ end
337
+ end