├── .dockerignore ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── Gemfile ├── LICENSE ├── Makefile ├── README.md ├── Rakefile ├── bin ├── console ├── setup └── test_client.rb ├── lib └── notifications │ ├── client.rb │ └── client │ ├── helper_methods.rb │ ├── notification.rb │ ├── notifications_collection.rb │ ├── received_text.rb │ ├── received_text_collection.rb │ ├── request_error.rb │ ├── response_notification.rb │ ├── response_precompiled_letter.rb │ ├── response_template.rb │ ├── speaker.rb │ ├── template_collection.rb │ ├── template_preview.rb │ ├── uuid_validator.rb │ └── version.rb ├── notifications-ruby-client.gemspec ├── scripts └── run_with_docker.sh └── spec ├── factories ├── client_notifications.rb ├── client_notifications_collections.rb ├── client_request_errors.rb ├── notifications_client.rb ├── template_list.rb ├── template_preview.rb └── template_response.rb ├── notifications ├── client │ ├── email_notification_spec.rb │ ├── generate_template_preview_spec.rb │ ├── get_all_templates_spec.rb │ ├── get_notification_spec.rb │ ├── get_notifications_spec.rb │ ├── get_pdf_for_letter_spec.rb │ ├── get_received_texts_spec.rb │ ├── get_template_spec.rb │ ├── get_template_version_spec.rb │ ├── helper_methods_spec.rb │ ├── letter_notification_spec.rb │ ├── precompiled_letter_spec.rb │ ├── request_errors_spec.rb │ ├── sms_notification_spec.rb │ ├── speaker_spec.rb │ └── uuid_validator_spec.rb └── client_spec.rb ├── spec_helper.rb └── test_files └── test_pdf.pdf /.dockerignore: -------------------------------------------------------------------------------- 1 | environment*.sh 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What problem does the pull request solve? 4 | 5 | 6 | ## Checklist 7 | 8 | 9 | - [x] I’ve used the pull request template 10 | - [ ] I’ve written unit tests for these changes 11 | - [ ] I’ve updated the documentation in 12 | - [ ] [notifications-tech-docs repository](https://github.com/alphagov/notifications-tech-docs/blob/main/source/documentation/client_docs/_ruby.md) 13 | - [ ] `CHANGELOG.md` 14 | - [ ] I’ve bumped the version number (in `lib/notifications/client/version.rb`) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .DS_Store 11 | /vendor 12 | environment.sh 13 | docker.env 14 | .idea 15 | *.gem 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 6.2.0 2 | * Added fields related to cost data in response: 3 | * `is_cost_data_ready`: This field is true if cost data is ready, and false if it isn't (Boolean). 4 | * `cost_in_pounds`: Cost of the notification in pounds. The cost does not take free allowance into account (Float). 5 | * `cost_details.billable_sms_fragments`: Number of billable SMS fragments in your text message (SMS only) (Integer). 6 | * `cost_details.international_rate_multiplier`: For international SMS rate is multiplied by this value (SMS only) (Integer). 7 | * `cost_details.sms_rate`: Cost of 1 SMS fragment (SMS only) (Float). 8 | * `cost_details.billable_sheets_of_paper`: Number of sheets of paper in the letter you sent, that you will be charged for (letter only) (Integer). 9 | * `cost_details.postage`: Postage class of the notification sent (letter only) (String). 10 | 11 | ## 6.1.0 12 | 13 | * Adds a `one_click_unsubscribe_url` parameter to `send_email` so services can allow users to easily unsubscribe from distribution lists. 14 | * Adds a `one_click_unsubscribe_url` attribute to `Notification` class, so responses for get_notification include the unsubscribe link. 15 | 16 | ## 6.0.0 17 | 18 | * Removes the `is_csv` parameter from `prepare_upload` 19 | * Adds a `filename` parameter to `prepare_upload` to set the filename of the document upon download. See the documentation for more information. 20 | 21 | ## 5.4.0 22 | 23 | * Add support for new security features when sending a file by email: 24 | * `confirm_email_before_download` can be set to `true` to require the user to enter their email address before accessing the file. 25 | * `retention_period` can be set to `<1-78> weeks` to set how long the file should be made available. 26 | 27 | ## 5.3.0 28 | 29 | * Added `letter_contact_block` as a new attribute of the `Notifications::Client::Template` class. This affects the responses from the `get_template_by_id`, `get_template_version` and `get_all_templates` methods. 30 | 31 | ## 5.2.0 32 | 33 | * Add support for an optional `is_csv` parameter in the `prepare_upload()` function. This fixes a bug when sending a CSV file by email. This ensures that the file is downloaded as a CSV rather than a TXT file. 34 | 35 | ## 5.1.2 36 | 37 | * Change filesize too big exception message to refer to files rather than documents. 38 | 39 | ## 5.1.1 40 | 41 | * Exceptions now return the error message when calling `#to_s` on them. This will make services like Sentry correctly display the full error description. 42 | 43 | ## 5.1.0 44 | 45 | * Added new `get_pdf_for_letter` method 46 | * accepts a notification id argument 47 | * returns a string containing the final printable PDF for a precompiled or templated letter 48 | 49 | ## 5.0.0 50 | 51 | * Dropped support for Ruby 2.3. Official support for this version ended in March (https://www.ruby-lang.org/en/news/2019/03/31/support-of-ruby-2-3-has-ended/) 52 | 53 | ## 4.0.0 54 | 55 | * `RequestError.message` now returns a string, not an array of hashes – see https://github.com/alphagov/notifications-ruby-client/pull/72 56 | 57 | ## 3.1.0 58 | 59 | * Added `html` field to the TemplatePreview response, so users can see 60 | the rendered HTML of their email templates. 61 | 62 | ## 3.0.0 63 | 64 | * Changed response class for `send_precompiled_letter` request from `ResponseNotification` to a new response class: `ResponsePrecompiledLetter`. This may affect users sending precompiled letters. 65 | * Added an optional `postage` argument to `send_precompiled_letter` method, so users can specify postage when sending 66 | a precompiled letter via API. 67 | * Added postage to `Notification` class on the client. 68 | 69 | ## 2.10.0 70 | 71 | * Added subclasses of the `RequestError` class to handle specific types of errors. 72 | 73 | ## 2.9.0 74 | 75 | * Added the `send_precompiled_letter` method which allows the client to send letters as PDF files. 76 | * This requires two arguments - a reference for the letter and the PDF letter file. The file must conform to the Notify printing template. 77 | * Added support for document uploads using the `send_email` method. 78 | 79 | ## 2.8.0 80 | 81 | * Updated the Template class to have a `name` property, which is the name of the template as set in Notify. 82 | 83 | 84 | ## 2.7.0 85 | 86 | * The Notification class has a new `created_by_name` property. 87 | * If the notification was sent manually this will be the name of the sender. If the notification was sent through the API this will be `nil`. 88 | 89 | ## 2.6.0 90 | 91 | ### Changed 92 | * The client now validates that UUIDs derived from the API key are valid and raises a helpful error message if they are not. 93 | 94 | ## 2.5.0 95 | 96 | ### Changed 97 | * Added a new `get_received_texts` method. 98 | * an optional `older_than` argument can be specified to retrieve the next 250 received text messages older than the given received text id. If omitted 250 of the most recent received text messages are returned. 99 | 100 | 101 | ## 2.4.0 102 | 103 | ### Changed 104 | * It is now possible to have multiple SMS senders and to specify which sender an SMS notification should come from. Added the option to specify `sms_sender_id` when using the `send_sms` method. If no `sms_sender_id` is specified, the default sms sender will be used. 105 | * Replaced `factory_girl` development dependency with `factory_bot`, which is the [new name for Factory girl.](https://robots.thoughtbot.com/factory_bot) 106 | 107 | 108 | ## 2.3.0 109 | 110 | ### Changed 111 | * It is now possible to have multiple email to reply to addresses. Added the option to specify an `email_reply_to_id` 112 | when using the `send_email` method. If no `email_reply_to_id` is specified, the default email reply to address will be 113 | used. 114 | * Upgraded all dependencies 115 | * Minor code style changes and fixes for the tests 116 | 117 | ## 2.2.0 118 | 119 | ### Changed 120 | * Added a new `send_letter` method 121 | * Removed 'govuk-lint' gem as a development dependency 122 | 123 | 124 | ## 2.1.0 125 | 126 | ### Changed 127 | * Added methods to get templates and generate a preview of a template. 128 | * `get_template_by_id` - get the latest version of a template by id. 129 | * `get_template_version` - get the template by id and version. 130 | * `get_all_templates` - get all templates, can be filtered by template type. 131 | * `generate_template_preview` - get the contents of a template with the placeholders replaced with the given personalisation. 132 | * See the README for more information about the new template methods. 133 | 134 | 135 | ## 2.0.0 136 | 137 | ### Changed 138 | * Using version 2 of the notification-api. 139 | * A new `Notifications::Client` no longer requires the `service_id`, only the `api_key` is required. 140 | * `Notifications::Client.send_sms()` input parameters and the response object has changed, see the README for more information. 141 | ```ruby 142 | client.sendSms(phone_number, template_id, personalisation, reference) 143 | ``` 144 | * `Notifications::Client.send_email()` input parameters has changed and the response object, see the README for more information. 145 | ```ruby 146 | client.sendSms(phone_number, template_id, personalisation, reference) 147 | ``` 148 | * `reference` is a new optional argument of the send methods. The `reference` can be used as a unique reference for the notification. Because Notify does not require this reference to be unique you could also use this reference to identify a batch or group of notifications. 149 | * `Notifications::Client.get_all_notifications()` => the response object has changed. 150 | * You can also filter the collection of `Notifications` by `reference`. See the README for more information. 151 | * `Notifications::Client.get_notification(id)` => the response object has changed. See the README for more information. 152 | * Initializing a client only requires the api key. 153 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests are welcome. 4 | 5 | ## Setting Up 6 | 7 | ### Docker container 8 | 9 | This app uses dependencies that are difficult to install locally. In order to make local development easy, we run app commands through a Docker container. Run the following to set this up: 10 | 11 | ```shell 12 | make bootstrap-with-docker 13 | ``` 14 | 15 | Because the container caches things like packages, you will need to run this again if you change the package versions. 16 | 17 | ### `environment.sh` 18 | 19 | In the root directory of the repo, run: 20 | 21 | ``` 22 | notify-pass credentials/client-integration-tests > environment.sh 23 | ``` 24 | 25 | Unless you're part of the GOV.UK Notify team, you won't be able to run this command or the Integration Tests. However, the file still needs to exist - run `touch environment.sh` instead. 26 | 27 | ## Tests 28 | 29 | There are unit and integration tests that can be run to test functionality of the client. 30 | 31 | ### Unit Tests 32 | 33 | To run the unit tests: 34 | 35 | ``` 36 | make test-with-docker 37 | ``` 38 | 39 | ### Integration Tests 40 | 41 | To run the integration tests: 42 | 43 | ``` 44 | make integration-test-with-docker 45 | ``` 46 | 47 | ## Releasing (for notify developers only) 48 | 49 | To release manually, run `make publish-to-rubygems`. You will need to set the environment variable `GEM_HOST_API_KEY`, which can be found in the credentials repo under `credentials/rubygems/api_key`. 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.6-slim 2 | 3 | RUN \ 4 | echo "Install Debian packages" \ 5 | && apt-get update \ 6 | && apt-get install -y --no-install-recommends \ 7 | awscli \ 8 | gcc \ 9 | make \ 10 | curl \ 11 | git \ 12 | gnupg \ 13 | jq 14 | 15 | WORKDIR /var/project 16 | 17 | COPY . . 18 | RUN make bootstrap 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Government Digital Service 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | SHELL := /bin/bash 3 | 4 | .PHONY: help 5 | help: 6 | @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 7 | 8 | .PHONY: bootstrap 9 | bootstrap: ## Install build dependencies 10 | bundle install 11 | 12 | .PHONY: build 13 | build: bootstrap ## Build project (dummy task for CI) 14 | 15 | .PHONY: test 16 | test: ## Run tests 17 | bundle exec rake spec 18 | 19 | .PHONY: integration-test 20 | integration-test: ## Run integration tests 21 | bundle exec bin/test_client.rb 22 | 23 | .PHONY: bootstrap-with-docker 24 | bootstrap-with-docker: ## Prepare the Docker builder image 25 | docker build -t notifications-ruby-client . 26 | 27 | .PHONY: test-with-docker 28 | test-with-docker: ## Run tests inside a Docker container 29 | ./scripts/run_with_docker.sh make test 30 | 31 | .PHONY: integration-test-with-docker 32 | integration-test-with-docker: ## Run integration tests inside a Docker container 33 | ./scripts/run_with_docker.sh make integration-test 34 | 35 | .PHONY: get-client-version 36 | get-client-version: ## Retrieve client version number from source code 37 | @ruby -e "require './lib/notifications/client/version'; puts Notifications::Client::VERSION" 38 | 39 | .PHONY: publish-to-rubygems 40 | publish-to-rubygems: ## Create gemspec file and publish to rubygems 41 | $(if ${GEM_HOST_API_KEY},,$(error Must specify GEM_HOST_API_KEY)) 42 | gem build notifications-ruby-client.gemspec --output=release.gem 43 | gem push release.gem 44 | 45 | clean: 46 | rm -rf vendor 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOV.UK Notify Ruby client 2 | 3 | Use this client to send emails, text messages and letters using the [GOV.UK Notify](https://www.notifications.service.gov.uk) API. 4 | 5 | Useful links: 6 | 7 | - [Documentation](https://docs.notifications.service.gov.uk/ruby.html) 8 | - [Ruby gem](https://rubygems.org/gems/notifications-ruby-client) 9 | - [Changelog](https://github.com/alphagov/notifications-ruby-client/blob/main/CHANGELOG.md) 10 | - [Contributing to this client](https://github.com/alphagov/notifications-ruby-client/blob/main/CONTRIBUTING.md) 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "notifications/client" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/test_client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require './lib/notifications/client' 3 | 4 | def main 5 | client = Notifications::Client.new(ENV['API_KEY'], ENV['NOTIFY_API_URL']) 6 | test_get_email_template_by_id(client, ENV['EMAIL_TEMPLATE_ID']) 7 | test_get_sms_template_by_id(client, ENV['SMS_TEMPLATE_ID']) 8 | test_get_letter_template_by_id(client, ENV['LETTER_TEMPLATE_ID']) 9 | test_get_template_version(client, ENV['SMS_TEMPLATE_ID'], 1) 10 | test_get_all_templates(client) 11 | test_get_all_templates_filter_by_type(client) 12 | test_generate_template_preview(client, ENV['EMAIL_TEMPLATE_ID']) 13 | email_notification = test_send_email_endpoint(client) 14 | email_notification_with_document = test_send_email_endpoint_with_document(client) 15 | sms_notification = test_send_sms_endpoint(client) 16 | letter_notification = test_send_letter_endpoint(client) 17 | precompiled_letter_notification = test_send_precompiled_letter_endpoint(client) 18 | test_get_notification_by_id_endpoint(client, email_notification.id, 'email') 19 | test_get_notification_by_id_endpoint(client, email_notification_with_document.id, 'email') 20 | test_get_notification_by_id_endpoint(client, sms_notification.id, 'sms') 21 | test_get_notification_by_id_endpoint(client, letter_notification.id, 'letter') 22 | test_get_notification_by_id_endpoint(client, precompiled_letter_notification.id, 'precompiled_letter') 23 | test_get_all_notifications(client) 24 | test_get_received_texts 25 | test_get_pdf_for_letter(client, letter_notification.id) 26 | p 'ruby client integration tests pass' 27 | end 28 | 29 | def test_get_email_template_by_id(client, id) 30 | response = client.get_template_by_id(id) 31 | test_template_response(response, 'email', 'test_get_email_template_by_id') 32 | end 33 | 34 | def test_get_sms_template_by_id(client, id) 35 | response = client.get_template_by_id(id) 36 | test_template_response(response, 'sms', 'test_get_sms_template_by_id') 37 | end 38 | 39 | def test_get_letter_template_by_id(client, id) 40 | response = client.get_template_by_id(id) 41 | test_template_response(response, 'letter', 'test_get_letter_template_by_id') 42 | end 43 | 44 | def test_get_template_version(client, id, version) 45 | response = client.get_template_version(id, version) 46 | test_template_response(response, 'sms', 'test_get_template_version') 47 | end 48 | 49 | def test_get_all_templates(client) 50 | response = client.get_all_templates 51 | unless response.is_a?(Notifications::Client::TemplateCollection) 52 | raise 'failed test_get_all_templates response is not a Notifications::Client::TemplateCollection' 53 | end 54 | unless response.collection.length >= 3 55 | raise 'failed test_get_all_templates, expected at least 3 templates returned.' 56 | end 57 | test_template_response(response.collection.find { |template| template.id == ENV['LETTER_TEMPLATE_ID'] }, 'letter', 'test_get_all_templates') 58 | test_template_response(response.collection.find { |template| template.id == ENV['EMAIL_TEMPLATE_ID'] }, 'email', 'test_get_all_templates') 59 | test_template_response(response.collection.find { |template| template.id == ENV['SMS_TEMPLATE_ID'] }, 'sms', 'test_get_all_templates') 60 | end 61 | 62 | def test_get_all_templates_filter_by_type(client) 63 | response = client.get_all_templates('type' => 'sms') 64 | unless response.is_a?(Notifications::Client::TemplateCollection) 65 | raise 'failed test_get_all_templates response is not a Notifications::Client::TemplateCollection' 66 | end 67 | unless response.collection.length >= 1 68 | raise 'failed test_get_all_templates, expected at least 1 template to be returned.' 69 | end 70 | test_template_response(response.collection[0], 'sms', 'test_get_all_templates') 71 | end 72 | 73 | def test_generate_template_preview(client, id) 74 | response = client.generate_template_preview(id, personalisation: { "name" => "some name" }) 75 | test_template_preview(response) 76 | end 77 | 78 | def test_template_response(response, template_type, test_method) 79 | unless response.is_a?(Notifications::Client::Template) 80 | raise 'failed test_get_template_by_id response is not a Notifications::Client::Template' 81 | end 82 | unless response.id.is_a?(String) 83 | raise 'failed template id is not a String' 84 | end 85 | 86 | field_should_not_be_nil( 87 | expected_fields_in_template_response(template_type), 88 | response, 89 | test_method 90 | ) 91 | field_should_be_nil( 92 | expected_nil_fields_in_template_response(template_type), 93 | response, 94 | test_method 95 | ) 96 | end 97 | 98 | def test_template_preview(response) 99 | unless response.is_a?(Notifications::Client::TemplatePreview) 100 | raise 'failed test_generate_template_preview response is not a Notifications::Client::TemplatePreview' 101 | end 102 | unless response.id.is_a?(String) 103 | raise 'failed template id is not a String' 104 | end 105 | field_should_not_be_nil(expected_fields_in_template_preview, response, 'generate_template_preview') 106 | end 107 | 108 | def test_send_email_endpoint(client) 109 | email_resp = client.send_email( 110 | email_address: ENV['FUNCTIONAL_TEST_EMAIL'], 111 | template_id: ENV['EMAIL_TEMPLATE_ID'], 112 | personalisation: { "name" => "some name" }, 113 | reference: "some reference", 114 | email_reply_to_id: ENV['EMAIL_REPLY_TO_ID'], 115 | one_click_unsubscribe_url: "https://www.clovercouncil.gov.uk/unsubscribe?email_address=faye@example.com" 116 | ) 117 | test_notification_response_data_type(email_resp, 'email') 118 | email_resp 119 | end 120 | 121 | def test_send_email_endpoint_with_document(client) 122 | email_resp = File.open('spec/test_files/test_pdf.pdf', 'rb') do |f| 123 | client.send_email(email_address: ENV['FUNCTIONAL_TEST_EMAIL'], 124 | template_id: ENV['EMAIL_TEMPLATE_ID'], 125 | personalisation: { name: Notifications.prepare_upload(f) }, 126 | reference: "some reference", 127 | email_reply_to_id: ENV['EMAIL_REPLY_TO_ID'], 128 | one_click_unsubscribe_url: "https://www.clovercouncil.gov.uk/unsubscribe?email_address=faye@example.com") 129 | end 130 | 131 | test_notification_response_data_type(email_resp, 'email') 132 | email_resp 133 | end 134 | 135 | def test_send_sms_endpoint(client) 136 | sms_resp = client.send_sms(phone_number: ENV['FUNCTIONAL_TEST_NUMBER'], template_id: ENV['SMS_TEMPLATE_ID'], 137 | personalisation: { "name" => "some name" }, 138 | reference: "some reference", 139 | sms_sender_id: ENV['SMS_SENDER_ID']) 140 | test_notification_response_data_type(sms_resp, 'sms') 141 | sms_resp 142 | end 143 | 144 | def test_send_letter_endpoint(client) 145 | letter_resp = client.send_letter( 146 | template_id: ENV['LETTER_TEMPLATE_ID'], 147 | personalisation: { 148 | address_line_1: "Her Majesty The Queen", 149 | address_line_2: "Buckingham Palace", 150 | postcode: "SW1 1AA" 151 | }, 152 | reference: "some reference" 153 | ) 154 | test_notification_response_data_type(letter_resp, 'letter') 155 | letter_resp 156 | end 157 | 158 | def test_send_precompiled_letter_endpoint(client) 159 | precompiled_letter_resp = File.open('spec/test_files/test_pdf.pdf', 'rb') do |file| 160 | client.send_precompiled_letter("some reference", file, "first") 161 | end 162 | 163 | test_notification_response_data_type(precompiled_letter_resp, 'precompiled_letter') 164 | 165 | precompiled_letter_resp 166 | end 167 | 168 | def test_notification_response_data_type(notification, message_type) 169 | unless notification.is_a?(Notifications::Client::ResponseNotification) || (notification.is_a?(Notifications::Client::ResponsePrecompiledLetter) && message_type == "precompiled_letter") 170 | raise 'failed ' + message_type + ' response is not a Notifications::Client::ResponseNotification' 171 | end 172 | unless notification.id.is_a?(String) 173 | raise 'failed ' + message_type + 'id is not a String' 174 | end 175 | 176 | if message_type == 'precompiled_letter' 177 | field_should_not_be_nil(expected_fields_in_precompiled_letter_response, notification, 'send_precompiled_letter') 178 | if notification.postage != "first" 179 | raise "Postage should be set to 'first' for precompiled letter sending test. Right now it is set to #{notification.postage}" 180 | end 181 | return 182 | end 183 | 184 | field_should_not_be_nil(expected_fields_in_notification_response, notification, 'send_' + message_type) 185 | hash_key_should_not_be_nil(expected_fields_in_template, notification.send('template'), 'send_' + message_type + '.template') 186 | 187 | if message_type == 'email' 188 | hash_key_should_not_be_nil(expected_fields_in_email_content, notification.send('content'), 'send_' + message_type + '.content') 189 | elsif message_type == 'sms' 190 | hash_key_should_not_be_nil(expected_fields_in_sms_content, notification.send('content'), 'send_' + message_type + '.content') 191 | elsif message_type == 'letter' 192 | hash_key_should_not_be_nil(expected_fields_in_letter_content, notification.send('content'), 'send_' + message_type + '.content') 193 | end 194 | end 195 | 196 | def test_get_notification_by_id_endpoint(client, id, message_type) 197 | get_notification_response = nil 198 | 24.times do 199 | get_notification_response = client.get_notification(id) 200 | break if get_notification_response&.is_cost_data_ready 201 | sleep 5 202 | end 203 | 204 | raise "cost data didn't become ready in time" unless get_notification_response&.is_cost_data_ready 205 | 206 | unless get_notification_response.is_a?(Notifications::Client::Notification) 207 | raise 'get notification is not a Notifications::Client::Notification for id ' + id 208 | end 209 | 210 | if message_type == 'email' 211 | field_should_not_be_nil(expected_fields_in_email_notification, get_notification_response, 'Notifications::Client::Notification for type email') 212 | field_should_be_nil(expected_fields_in_email_notification_that_are_nil, get_notification_response, 'Notifications::Client::Notification for type email') 213 | hash_key_should_not_be_nil(expected_fields_in_template, get_notification_response.send('template'), 'Notifications::Client::Notification.template for type email') 214 | hash_should_be_empty(get_notification_response.send('cost_details'), 'Notifications::Client::Notification.cost_details for type sms') 215 | 216 | elsif message_type == 'sms' 217 | field_should_not_be_nil(expected_fields_in_sms_notification, get_notification_response, 'Notifications::Client::Notification for type sms') 218 | field_should_be_nil(expected_fields_in_sms_notification_that_are_nil, get_notification_response, 'Notifications::Client::Notification for type sms') 219 | hash_key_should_not_be_nil(expected_fields_in_template, get_notification_response.send('template'), 'Notifications::Client::Notification.template for type sms') 220 | hash_key_should_not_be_nil(expected_cost_details_fields, get_notification_response.send('cost_details'), 'Notifications::Client::Notification.cost_details for type sms') 221 | 222 | elsif message_type == 'letter' 223 | field_should_not_be_nil(expected_fields_in_letter_notification, get_notification_response, 'Notifications::Client::Notification for type letter') 224 | field_should_be_nil(expected_fields_in_letter_notification_that_are_nil, get_notification_response, 'Notifications::Client::Notification for type letter') 225 | hash_key_should_not_be_nil(expected_fields_in_template, get_notification_response.send('template'), 'Notifications::Client::Notification.template for type letter') 226 | hash_key_should_not_be_nil(expected_cost_details_fields, get_notification_response.send('cost_details'), 'Notifications::Client::Notification.cost_details for type sms') 227 | 228 | elsif message_type == 'precompiled_letter' 229 | field_should_not_be_nil(expected_fields_in_precompiled_letter_notification, get_notification_response, 'Notifications::Client::Notification for type precompiled letter') 230 | field_should_be_nil(expected_fields_in_precompiled_letter_notification_that_are_nil, get_notification_response, 'Notifications::Client::Notification for type precompiled letter') 231 | hash_key_should_not_be_nil(expected_fields_in_template, get_notification_response.send('template'), 'Notifications::Client::Notification.template for type precompiled letter') 232 | hash_key_should_not_be_nil(expected_cost_details_fields, get_notification_response.send('cost_details'), 'Notifications::Client::Notification.cost_details for type sms') 233 | 234 | end 235 | end 236 | 237 | def test_get_pdf_for_letter(client, id) 238 | response = nil 239 | 240 | 24.times do 241 | begin 242 | response = client.get_pdf_for_letter(id) 243 | break 244 | rescue Notifications::Client::BadRequestError 245 | sleep(5) 246 | end 247 | end 248 | 249 | raise "pdf didn't become ready in time" if response.nil? 250 | raise "get_pdf_for_letter response for #{id} is not a PDF: #{response}" unless response.start_with?('%PDF-') 251 | end 252 | 253 | def hash_key_should_not_be_nil(fields, obj, method_name) 254 | fields.each do |field| 255 | if obj.has_value?(:"#{field}") 256 | raise 'contract test failed because ' + field + ' should not be nil for ' + method_name + ' response' 257 | end 258 | end 259 | end 260 | 261 | def hash_should_be_empty(hash, method_name) 262 | if !hash.empty? 263 | raise "contract test failed because #{hash} should be empty for #{method_name} response" 264 | end 265 | end 266 | 267 | def field_should_not_be_nil(fields, obj, method_name) 268 | fields.each do |field| 269 | if obj.send(:"#{field}") == nil 270 | raise 'contract test failed because ' + field + ' should not be nil for ' + method_name + ' response' 271 | end 272 | end 273 | end 274 | 275 | def field_should_be_nil(fields, obj, method_name) 276 | fields.each do |field| 277 | if obj.send(:"#{field}") != nil 278 | raise 'contract test failed because ' + field + ' should be nil for ' + method_name + ' response' 279 | end 280 | end 281 | end 282 | 283 | def expected_fields_in_template_response(template_type) 284 | { 285 | "email" => ["id", "name", "type", "created_at", "created_by", "version", "body", "subject"], 286 | "sms" => ["id", "name", "type", "created_at", "created_by", "version", "body"], 287 | "letter" => ["id", "name", "type", "created_at", "created_by", "version", "body", "subject", 288 | "letter_contact_block"], 289 | }[template_type] 290 | end 291 | 292 | def expected_nil_fields_in_template_response(template_type) 293 | { 294 | "email" => ["letter_contact_block"], 295 | "sms" => ["subject", "letter_contact_block"], 296 | "letter" => [], 297 | }[template_type] 298 | end 299 | 300 | def expected_fields_in_template_preview 301 | %w(id 302 | body 303 | version 304 | type 305 | html) 306 | end 307 | 308 | def expected_fields_in_notification_response 309 | %w(id 310 | reference 311 | content 312 | template 313 | uri) 314 | end 315 | 316 | def expected_fields_in_precompiled_letter_response 317 | %w(id 318 | reference 319 | postage) 320 | end 321 | 322 | def expected_fields_in_email_content 323 | %w(from_email 324 | body 325 | subject 326 | one_click_unsubscribe_url) 327 | end 328 | 329 | def expected_fields_in_sms_content 330 | %w(body 331 | from_number) 332 | end 333 | 334 | def expected_fields_in_letter_content 335 | %w( 336 | body 337 | subject 338 | ) 339 | end 340 | 341 | def expected_fields_in_email_notification 342 | %w(id 343 | email_address 344 | type 345 | status 346 | template 347 | body 348 | subject 349 | created_at 350 | one_click_unsubscribe_url 351 | is_cost_data_ready 352 | cost_in_pounds 353 | cost_details 354 | ) 355 | end 356 | 357 | def expected_fields_in_email_notification_that_are_nil 358 | %w(phone_number 359 | line_1 360 | line_2 361 | line_3 362 | line_4 363 | line_5 364 | line_5 365 | line_6 366 | postcode 367 | created_by_name 368 | postage) 369 | end 370 | 371 | def expected_fields_in_sms_notification 372 | %w(id 373 | phone_number 374 | type 375 | status 376 | template 377 | body 378 | created_at 379 | is_cost_data_ready 380 | cost_in_pounds 381 | cost_details) 382 | end 383 | 384 | def expected_fields_in_sms_notification_that_are_nil 385 | %w(email_address 386 | line_1 387 | line_2 388 | line_3 389 | line_4 390 | line_5 391 | line_5 392 | line_6 393 | postcode 394 | subject 395 | created_by_name 396 | postage 397 | one_click_unsubscribe_url) 398 | end 399 | 400 | def expected_fields_in_letter_notification 401 | %w( 402 | id 403 | type 404 | status 405 | template 406 | body 407 | subject 408 | line_1 409 | line_2 410 | postcode 411 | created_at 412 | postage 413 | is_cost_data_ready 414 | cost_in_pounds 415 | cost_details 416 | ) 417 | end 418 | 419 | def expected_fields_in_letter_notification_that_are_nil 420 | %w( 421 | phone_number 422 | email_address 423 | line_3 424 | line_4 425 | line_5 426 | line_5 427 | line_6 428 | created_by_name 429 | one_click_unsubscribe_url 430 | ) 431 | end 432 | 433 | def expected_fields_in_precompiled_letter_notification 434 | %w( 435 | body 436 | created_at 437 | id 438 | line_1 439 | reference 440 | status 441 | subject 442 | template 443 | type 444 | postage 445 | ) 446 | end 447 | 448 | def expected_fields_in_precompiled_letter_notification_that_are_nil 449 | %w( 450 | created_by_name 451 | email_address 452 | line_2 453 | line_3 454 | line_4 455 | line_5 456 | line_6 457 | phone_number 458 | postcode 459 | sent_at 460 | one_click_unsubscribe_url 461 | ) 462 | end 463 | 464 | def expected_fields_in_template 465 | %w(id 466 | version 467 | uri) 468 | end 469 | 470 | def expected_cost_details_fields 471 | %w(billable_sms_fragments 472 | international_rate_multiplier 473 | sms_rate 474 | billable_sheets_of_paper 475 | postage) 476 | end 477 | 478 | def expected_fields_in_received_text_response 479 | %w(id 480 | created_at 481 | content 482 | notify_number 483 | service_id 484 | user_number) 485 | end 486 | 487 | def test_get_all_notifications(client) 488 | notifications = client.get_notifications 489 | unless notifications.is_a?(Notifications::Client::NotificationsCollection) 490 | raise 'get all notifications is not Notifications::Client::NotificationsCollection' 491 | end 492 | field_should_not_be_nil(expected_fields_for_get_all_notifications, notifications, 'get_notifications') 493 | end 494 | 495 | def test_get_received_texts 496 | client = Notifications::Client.new(ENV['INBOUND_SMS_QUERY_KEY'], ENV['NOTIFY_API_URL']) 497 | response = client.get_received_texts 498 | unless response.is_a?(Notifications::Client::ReceivedTextCollection) 499 | raise 'failed test_get_received_texts response is not a Notifications::Client::ReceivedTextCollection' 500 | end 501 | unless response.collection.length >= 0 502 | raise 'failed test_get_received_texts, expected at least 1 received text returned.' 503 | end 504 | test_received_text_response(response.collection[0], 'test_received_text_response') 505 | end 506 | 507 | def test_received_text_response(response, test_method) 508 | unless response.is_a?(Notifications::Client::ReceivedText) 509 | raise 'failed test_get_received_texts response is not a Notifications::Client::ReceivedText' 510 | end 511 | unless response.id.is_a?(String) 512 | raise 'failed received text id is not a String' 513 | end 514 | field_should_not_be_nil(expected_fields_in_received_text_response, response, test_method) 515 | end 516 | 517 | def expected_fields_for_get_all_notifications 518 | %W(links 519 | collection) 520 | end 521 | 522 | if __FILE__ == $PROGRAM_NAME 523 | main 524 | end 525 | -------------------------------------------------------------------------------- /lib/notifications/client.rb: -------------------------------------------------------------------------------- 1 | require_relative "client/version" 2 | require_relative "client/speaker" 3 | require_relative "client/notification" 4 | require_relative "client/response_notification" 5 | require_relative "client/response_precompiled_letter" 6 | require_relative "client/notifications_collection" 7 | require_relative "client/received_text" 8 | require_relative "client/received_text_collection" 9 | require_relative "client/response_template" 10 | require_relative "client/template_collection" 11 | require_relative "client/template_preview" 12 | require_relative "client/uuid_validator" 13 | require_relative "client/helper_methods" 14 | require "forwardable" 15 | 16 | module Notifications 17 | class Client 18 | attr_reader :speaker 19 | 20 | PRODUCTION_BASE_URL = "https://api.notifications.service.gov.uk".freeze 21 | MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB limit on uploaded documents 22 | 23 | extend Forwardable 24 | def_delegators :speaker, :service_id, :secret_token, :base_url, :base_url= 25 | 26 | ## 27 | # @see Notifications::Client::Speaker#initialize 28 | def initialize(*args) 29 | @speaker = Speaker.new(*args) 30 | end 31 | 32 | ## 33 | # @see Notifications::Client::Speaker#post 34 | # @return [ResponseNotification] 35 | def send_email(args) 36 | ResponseNotification.new( 37 | speaker.post("email", args) 38 | ) 39 | end 40 | 41 | ## 42 | # @see Notifications::Client::Speaker#post 43 | # @return [ResponseNotification] 44 | def send_sms(args) 45 | ResponseNotification.new( 46 | speaker.post("sms", args) 47 | ) 48 | end 49 | 50 | ## 51 | # @see Notifications::Client::Speaker#post 52 | # @return [ResponseNotification] 53 | def send_letter(args) 54 | ResponseNotification.new( 55 | speaker.post("letter", args) 56 | ) 57 | end 58 | 59 | ## 60 | # @param reference [String] 61 | # @param pdf_file [File] 62 | # @see Notifications::Client::Speaker#post_precompiled_letter 63 | # @return [ResponsePrecompiledLetter] 64 | def send_precompiled_letter(reference, pdf_file, postage = nil) 65 | ResponsePrecompiledLetter.new( 66 | speaker.post_precompiled_letter(reference, pdf_file, postage) 67 | ) 68 | end 69 | 70 | ## 71 | # @param id [String] 72 | # @see Notifications::Client::Speaker#get 73 | # @return [String] 74 | def get_pdf_for_letter(id) 75 | speaker.get_pdf_for_letter(id) 76 | end 77 | 78 | ## 79 | # @param id [String] 80 | # @see Notifications::Client::Speaker#get 81 | # @return [Notification] 82 | def get_notification(id) 83 | Notification.new( 84 | speaker.get(id) 85 | ) 86 | end 87 | 88 | ## 89 | # @param options [Hash] 90 | # @option options [String] :template_type 91 | # sms or email 92 | # @option options [String] :status 93 | # sending, delivered, permanently failed, 94 | # temporarily failed, or technical failure 95 | # @option options [String] :reference 96 | # your reference for the notification 97 | # @option options [String] :older_than 98 | # notification id to return notificaitons that are older than this id. 99 | # @see Notifications::Client::Speaker#get 100 | # @return [NotificationsCollection] 101 | def get_notifications(options = {}) 102 | NotificationsCollection.new( 103 | speaker.get(nil, options) 104 | ) 105 | end 106 | 107 | ## 108 | # @param id [String] 109 | # @return [Template] 110 | def get_template_by_id(id, options = {}) 111 | path = "/v2/template/" << id 112 | Template.new( 113 | speaker.get_with_url(path, options) 114 | ) 115 | end 116 | 117 | ## 118 | # @param id [String] 119 | # @param version [int] 120 | # @return [Template] 121 | def get_template_version(id, version, options = {}) 122 | path = "/v2/template/" << id << "/version/" << version.to_s 123 | Template.new( 124 | speaker.get_with_url(path, options) 125 | ) 126 | end 127 | 128 | ## 129 | # @option options [String] :type 130 | # email, sms, letter 131 | # @return [TemplateCollection] 132 | def get_all_templates(options = {}) 133 | path = "/v2/templates" 134 | TemplateCollection.new( 135 | speaker.get_with_url(path, options) 136 | ) 137 | end 138 | 139 | ## 140 | # @param options [String] 141 | # @option personalisation [Hash] 142 | # @return [TemplatePreview] 143 | def generate_template_preview(id, options = {}) 144 | path = "/v2/template/" << id << "/preview" 145 | TemplatePreview.new( 146 | speaker.post_with_url(path, options) 147 | ) 148 | end 149 | 150 | ## 151 | # @option options [String] :older_than 152 | # received text id to return received texts that are older than this id. 153 | # @return [ReceivedTextCollection] 154 | def get_received_texts(options = {}) 155 | path = "/v2/received-text-messages" 156 | ReceivedTextCollection.new( 157 | speaker.get_with_url(path, options) 158 | ) 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/notifications/client/helper_methods.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | module Notifications 4 | def self.prepare_upload(file, filename: nil, confirm_email_before_download: nil, retention_period: nil) 5 | raise ArgumentError.new("File is larger than 2MB") if file.size > Client::MAX_FILE_UPLOAD_SIZE 6 | 7 | data = { file: Base64.strict_encode64(file.read) } 8 | 9 | data[:filename] = filename 10 | data[:confirm_email_before_download] = confirm_email_before_download 11 | data[:retention_period] = retention_period 12 | 13 | data 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/notifications/client/notification.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module Notifications 4 | class Client 5 | class Notification 6 | FIELDS = %i( 7 | id 8 | reference 9 | email_address 10 | phone_number 11 | line_1 12 | line_2 13 | line_3 14 | line_4 15 | line_5 16 | line_6 17 | postcode 18 | postage 19 | type 20 | status 21 | template 22 | body 23 | subject 24 | sent_at 25 | created_at 26 | completed_at 27 | created_by_name 28 | one_click_unsubscribe_url 29 | cost_in_pounds 30 | is_cost_data_ready 31 | cost_details 32 | ).freeze 33 | 34 | attr_reader(*FIELDS) 35 | 36 | def initialize(notification) 37 | FIELDS.each do |field| 38 | instance_variable_set(:"@#{field}", notification.fetch(field.to_s, nil)) 39 | end 40 | end 41 | 42 | %i( 43 | sent_at 44 | created_at 45 | completed_at 46 | ).each do |field| 47 | define_method field do 48 | begin 49 | value = instance_variable_get(:"@#{field}") 50 | Time.parse value 51 | rescue StandardError 52 | value 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/notifications/client/notifications_collection.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class Client 3 | class NotificationsCollection 4 | attr_reader :links, 5 | :collection 6 | 7 | def initialize(response) 8 | @links = response["links"] 9 | @collection = collection_from(response["notifications"]) 10 | end 11 | 12 | def collection_from(notifications) 13 | notifications.map do |notification| 14 | Notification.new(notification) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/notifications/client/received_text.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module Notifications 4 | class Client 5 | class ReceivedText 6 | FIELDS = %i( 7 | id 8 | created_at 9 | content 10 | notify_number 11 | service_id 12 | user_number 13 | ).freeze 14 | 15 | attr_reader(*FIELDS) 16 | 17 | def initialize(received_text) 18 | FIELDS.each do |field| 19 | instance_variable_set(:"@#{field}", received_text.fetch(field.to_s, nil)) 20 | end 21 | end 22 | 23 | def created_at 24 | value = instance_variable_get(:@created_at) 25 | Time.parse(value) 26 | rescue StandardError 27 | value 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/notifications/client/received_text_collection.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class Client 3 | class ReceivedTextCollection 4 | attr_reader :links, :collection 5 | 6 | def initialize(response) 7 | @links = response["links"] 8 | @collection = collection_from(response["received_text_messages"]) 9 | end 10 | 11 | def collection_from(received_texts) 12 | received_texts.map do |received_text| 13 | ReceivedText.new(received_text) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/notifications/client/request_error.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class Client 3 | class RequestError < StandardError 4 | attr_reader :code, :body 5 | 6 | def initialize(response) 7 | @code = response.code 8 | @body = parse_body(response.body) 9 | super(build_message) 10 | end 11 | 12 | private 13 | 14 | def parse_body(body) 15 | JSON.parse(body) 16 | rescue JSON::ParserError 17 | body 18 | end 19 | 20 | def build_message 21 | return body if body.is_a?(String) 22 | 23 | error_messages = body.fetch('errors') 24 | .map { |e| "#{e.fetch('error')}: #{e.fetch('message')}" } 25 | error_messages.join(", ") 26 | end 27 | end 28 | 29 | class ClientError < RequestError; end 30 | class BadRequestError < ClientError; end 31 | class AuthError < ClientError; end 32 | class NotFoundError < ClientError; end 33 | class RateLimitError < ClientError; end 34 | 35 | class ServerError < RequestError; end 36 | 37 | module ErrorHandling 38 | def build_error(response) 39 | error_class_for_code(response.code.to_i).new(response) 40 | end 41 | 42 | def error_class_for_code(code) 43 | case code 44 | when 400 45 | BadRequestError 46 | when 403 47 | AuthError 48 | when 404 49 | NotFoundError 50 | when 429 51 | RateLimitError 52 | when (400..499) 53 | ClientError 54 | when (500..599) 55 | ServerError 56 | else 57 | RequestError 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/notifications/client/response_notification.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class Client 3 | class ResponseNotification 4 | FIELDS = %i( 5 | id 6 | reference 7 | content 8 | template 9 | uri 10 | ).freeze 11 | 12 | attr_reader(*FIELDS) 13 | 14 | def initialize(notification) 15 | FIELDS.each do |field| 16 | instance_variable_set(:"@#{field}", notification.fetch(field.to_s, nil)) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/notifications/client/response_precompiled_letter.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class Client 3 | class ResponsePrecompiledLetter 4 | FIELDS = %i( 5 | id 6 | reference 7 | postage 8 | ).freeze 9 | 10 | attr_reader(*FIELDS) 11 | 12 | def initialize(notification) 13 | FIELDS.each do |field| 14 | instance_variable_set(:"@#{field}", notification.fetch(field.to_s, nil)) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/notifications/client/response_template.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module Notifications 4 | class Client 5 | class Template 6 | FIELDS = %i( 7 | id 8 | type 9 | name 10 | created_at 11 | updated_at 12 | created_by 13 | version 14 | body 15 | subject 16 | letter_contact_block 17 | ).freeze 18 | 19 | attr_reader(*FIELDS) 20 | 21 | def initialize(notification) 22 | FIELDS.each do |field| 23 | instance_variable_set(:"@#{field}", notification.fetch(field.to_s, nil)) 24 | end 25 | end 26 | 27 | %i( 28 | created_at 29 | updated_at 30 | ).each do |field| 31 | define_method field do 32 | begin 33 | value = instance_variable_get(:"@#{field}") 34 | Time.parse value 35 | rescue StandardError 36 | value 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/notifications/client/speaker.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | require "net/https" 3 | require "uri" 4 | require "jwt" 5 | require_relative "request_error" 6 | 7 | module Notifications 8 | class Client 9 | class Speaker 10 | include ErrorHandling 11 | 12 | attr_reader :base_url 13 | attr_reader :service_id 14 | attr_reader :secret_token 15 | 16 | BASE_PATH = "/v2/notifications".freeze 17 | USER_AGENT = "NOTIFY-API-RUBY-CLIENT/#{Notifications::Client::VERSION}".freeze 18 | 19 | ## 20 | # @param secret [String] your service API secret 21 | # @param base_url [String] host URL. This is the address to perform the requests. 22 | # If left nil the production url is used. 23 | def initialize(secret_token = nil, base_url = nil) 24 | @service_id = secret_token[secret_token.length - 73..secret_token.length - 38] 25 | @secret_token = secret_token[secret_token.length - 36..secret_token.length] 26 | @base_url = base_url || PRODUCTION_BASE_URL 27 | 28 | validate_uuids! 29 | end 30 | 31 | ## 32 | # @param kind [String] 'email', 'sms' or 'letter' 33 | # @param form_data [Hash] 34 | # @option form_data [String] :phone_number 35 | # phone number of the sms recipient 36 | # @option form_data [String] :email_address 37 | # email address of the email recipent 38 | # @option form_data [String] :template 39 | # template to render in notification 40 | # @option form_data [Hash] :personalisation 41 | # fields to use in the template 42 | # @option form_data [String] :reference 43 | # A reference specified by the service for the notification. Get all notifications can be filtered by this reference. 44 | # This reference can be unique or used used to refer to a batch of notifications. 45 | # Can be an empty string or nil, when you do not require a reference for the notifications. 46 | # @option form_data [String] :email_reply_to_id 47 | # id of the email address that replies to email notifications will be sent to 48 | # @option form_data [String] :sms_sender_id 49 | # id of the sender to be used for an sms notification 50 | # @option form_data [String] :one_click_unsubscribe_url 51 | # link that end user can click to unsubscribe from the distribution list. We will pass this link in the email headers. 52 | # @see #perform_request! 53 | def post(kind, form_data) 54 | request = Net::HTTP::Post.new( 55 | "#{BASE_PATH}/#{kind}", 56 | headers 57 | ) 58 | request.body = form_data.is_a?(Hash) ? form_data.to_json : form_data 59 | perform_request!(request) 60 | end 61 | 62 | ## 63 | # @param id [String] 64 | # @param options [Hash] query 65 | # @see #perform_request! 66 | def get(id = nil, options = {}) 67 | path = BASE_PATH.dup 68 | path << "/" << id if id 69 | path << "?" << URI.encode_www_form(options) if options.any? 70 | request = Net::HTTP::Get.new(path, headers) 71 | perform_request!(request) 72 | end 73 | 74 | ## 75 | # @param url path of endpoint 76 | # @param id [String] 77 | # @param options [Hash] query 78 | # @see #perform_request! 79 | def get_with_url(url, options = {}) 80 | path = url 81 | path << "?" << URI.encode_www_form(options) if options.any? 82 | request = Net::HTTP::Get.new(path, headers) 83 | perform_request!(request) 84 | end 85 | 86 | ## 87 | # @param url [String] path of the endpoint 88 | # @param form_data [Hash] 89 | # @option form_data [String] :template_id 90 | # id of the template to render 91 | # @option form_data [Hash] :personalisation 92 | # fields to use in the template 93 | # @see #perform_request! 94 | def post_with_url(url, form_data) 95 | request = Net::HTTP::Post.new( 96 | url, 97 | headers 98 | ) 99 | request.body = form_data.is_a?(Hash) ? form_data.to_json : form_data 100 | perform_request!(request) 101 | end 102 | 103 | ## 104 | # @param reference [String] reference of the notification 105 | # @param pdf_file [File] PDF file opened for reading 106 | # @see #perform_request! 107 | def post_precompiled_letter(reference, pdf_file, postage = nil) 108 | content = Base64.strict_encode64(pdf_file.read) 109 | form_data = { reference: reference, content: content } 110 | 111 | if postage != nil 112 | form_data[:postage] = postage 113 | end 114 | 115 | request = Net::HTTP::Post.new( 116 | "#{BASE_PATH}/letter", 117 | headers 118 | ) 119 | request.body = form_data.to_json 120 | perform_request!(request) 121 | end 122 | 123 | def get_pdf_for_letter(id) 124 | path = "/v2/notifications/" << id << "/pdf" 125 | request = Net::HTTP::Get.new(path, headers) 126 | 127 | # can't use `perform_request!` because we're just returning raw binary data 128 | response = open(request) 129 | if response.is_a?(Net::HTTPClientError) || response.is_a?(Net::HTTPServerError) 130 | raise build_error(response) 131 | else 132 | response.body 133 | end 134 | end 135 | 136 | private 137 | 138 | ## 139 | # @return [Hash] JSON parsed response 140 | # @raise [RequestError] if request is 141 | # not successful 142 | def perform_request!(request) 143 | response = open(request) 144 | if response.is_a?(Net::HTTPClientError) || response.is_a?(Net::HTTPServerError) 145 | raise build_error(response) 146 | else 147 | JSON.parse(response.body) 148 | end 149 | end 150 | 151 | def open(request) 152 | uri = URI.parse(@base_url) 153 | Net::HTTP.start(uri.host, uri.port, :ENV, use_ssl: uri.scheme == 'https') do |http| 154 | http.request(request) 155 | end 156 | end 157 | 158 | def headers 159 | { 160 | "User-agent" => USER_AGENT, 161 | "Content-Type" => "application/json", 162 | "Authorization" => "Bearer " + jwt_token 163 | } 164 | end 165 | 166 | def jwt_token 167 | payload = { 168 | iss: @service_id, 169 | iat: Time.now.to_i 170 | } 171 | JWT.encode payload, @secret_token, "HS256" 172 | end 173 | 174 | def validate_uuids! 175 | contextual_message = [ 176 | "This error is probably caused by initializing the Notifications client with an invalid API key.", 177 | "You can generate a new API key by logging into Notify and visiting the 'API integration' page:", 178 | "https://www.notifications.service.gov.uk", 179 | ].join("\n") 180 | 181 | UuidValidator.validate!(@service_id, contextual_message) 182 | UuidValidator.validate!(@secret_token, contextual_message) 183 | end 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/notifications/client/template_collection.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class Client 3 | class TemplateCollection 4 | attr_reader :collection 5 | def initialize(response) 6 | @collection = collection_from(response["templates"]) 7 | end 8 | 9 | def collection_from(templates) 10 | templates.map do |template| 11 | Template.new(template) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notifications/client/template_preview.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class Client 3 | class TemplatePreview 4 | FIELDS = %i( 5 | id 6 | version 7 | body 8 | subject 9 | type 10 | html 11 | ).freeze 12 | 13 | attr_reader(*FIELDS) 14 | 15 | def initialize(notification) 16 | FIELDS.each do |field| 17 | instance_variable_set(:"@#{field}", notification.fetch(field.to_s, nil)) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/notifications/client/uuid_validator.rb: -------------------------------------------------------------------------------- 1 | module Notifications 2 | class UuidValidator 3 | HEX = /[0-9a-f]/ 4 | REGEX = /^#{HEX}{8}-#{HEX}{4}-#{HEX}{4}-#{HEX}{4}-#{HEX}{12}$/ 5 | 6 | attr_accessor :uuid 7 | 8 | def initialize(uuid) 9 | self.uuid = uuid 10 | end 11 | 12 | def valid? 13 | !!(uuid && uuid.match(REGEX)) 14 | end 15 | 16 | def self.validate!(uuid, contextual_message = nil) 17 | return if new(uuid).valid? 18 | 19 | message = "#{uuid.inspect} is not a valid uuid" 20 | message += "\n#{contextual_message}" if contextual_message 21 | 22 | raise ArgumentError, message 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/notifications/client/version.rb: -------------------------------------------------------------------------------- 1 | # Version numbering follows Semantic Versionning: 2 | # 3 | # Given a version number MAJOR.MINOR.PATCH, increment the: 4 | # - MAJOR version when you make incompatible API changes, 5 | # - MINOR version when you add functionality in a backwards-compatible manner, and 6 | # - PATCH version when you make backwards-compatible bug fixes. 7 | # 8 | # -- http://semver.org/ 9 | 10 | module Notifications 11 | class Client 12 | VERSION = "6.2.0".freeze 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /notifications-ruby-client.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'notifications/client/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "notifications-ruby-client" 8 | spec.version = Notifications::Client::VERSION 9 | spec.authors = [ 10 | "Government Digital Service" 11 | ] 12 | 13 | spec.email = ["notify@digital.cabinet-office.gov.uk"] 14 | 15 | spec.summary = "Ruby client for GOV.UK Notifications API" 16 | spec.homepage = "https://github.com/alphagov/notifications-ruby-client" 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | spec.bindir = "exe" 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_runtime_dependency "jwt", ">= 1.5", "< 3" 24 | 25 | spec.add_development_dependency "bundler", "~> 1.7" 26 | spec.add_development_dependency "rake", "~> 13.0" 27 | spec.add_development_dependency "rspec", "~> 3.7" 28 | spec.add_development_dependency "webmock", "~> 3.4" 29 | spec.add_development_dependency "factory_bot", "~> 6.1", "<6.4.5" 30 | end 31 | -------------------------------------------------------------------------------- /scripts/run_with_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DOCKER_IMAGE_NAME=notifications-ruby-client 3 | 4 | source environment.sh 5 | 6 | docker run \ 7 | --rm \ 8 | -v "`pwd`:/var/project" \ 9 | -e NOTIFY_API_URL=${NOTIFY_API_URL} \ 10 | -e API_KEY=${API_KEY} \ 11 | -e FUNCTIONAL_TEST_NUMBER=${FUNCTIONAL_TEST_NUMBER} \ 12 | -e FUNCTIONAL_TEST_EMAIL=${FUNCTIONAL_TEST_EMAIL} \ 13 | -e EMAIL_TEMPLATE_ID=${EMAIL_TEMPLATE_ID} \ 14 | -e SMS_TEMPLATE_ID=${SMS_TEMPLATE_ID} \ 15 | -e LETTER_TEMPLATE_ID=${LETTER_TEMPLATE_ID} \ 16 | -e EMAIL_REPLY_TO_ID=${EMAIL_REPLY_TO_ID} \ 17 | -e SMS_SENDER_ID=${SMS_SENDER_ID} \ 18 | -e API_SENDING_KEY=${API_SENDING_KEY} \ 19 | -e INBOUND_SMS_QUERY_KEY=${INBOUND_SMS_QUERY_KEY} \ 20 | -it \ 21 | ${DOCKER_IMAGE_NAME} \ 22 | ${@} 23 | -------------------------------------------------------------------------------- /spec/factories/client_notifications.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_notification, 3 | class: Notifications::Client::Notification do 4 | initialize_with do 5 | new(body) 6 | end 7 | 8 | body do 9 | { 10 | "id" => "f163deaf-2d3f-4ec6-98fc-f23fa511518f", 11 | "reference" => "your_reference_string", 12 | "phone_number" => "07515 987 456", 13 | "email_address" => nil, 14 | "line_1" => nil, 15 | "line_2" => nil, 16 | "line_3" => nil, 17 | "line_4" => nil, 18 | "line_5" => nil, 19 | "line_6" => nil, 20 | "postcode" => nil, 21 | "postage" => nil, 22 | "type" => "sms", 23 | "status" => "delivered", 24 | "template" => 25 | { 26 | "id" => "5e427b42-4e98-46f3-a047-32c4a87d26bb", 27 | "uri" => "/v2/templates/5e427b42-4e98-46f3-a047-32c4a87d26bb", 28 | "version" => 1 29 | }, 30 | "body" => "Body of the message", 31 | "subject" => nil, 32 | "created_at" => "2016-11-29T11:12:30.12354Z", 33 | "sent_at" => "2016-11-29T11:12:40.12354Z", 34 | "completed_at" => "2016-11-29T11:12:52.12354Z", 35 | "created_by_name" => "A. Sender", 36 | "is_cost_data_ready" => true, 37 | "cost_in_pounds" => 0.5, 38 | "cost_details" => { 39 | "billable_sms_fragments" => 1, 40 | "international_rate_multiplier" => 1.0, 41 | "sms_rate" => 0.05 42 | } 43 | } 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/factories/client_notifications_collections.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_notifications_collection, 3 | class: Notifications::Client::NotificationsCollection do 4 | initialize_with do 5 | new(body) 6 | end 7 | 8 | body do 9 | { 10 | "links" => { 11 | "current" => "/v2/notifications?page=3&template_type=sms&status=delivered", 12 | "next" => "/v2/notifications?page=3&template_type=sms&status=delivered" 13 | }, 14 | "notifications" => 2.times.map { 15 | attributes_for(:client_notification)[:body] 16 | } 17 | } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/factories/client_request_errors.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_request_error, 3 | class: Notifications::Client::RequestError do 4 | code { '403' } 5 | body do 6 | { 7 | 'status_code' => 400, 8 | 'errors' => ['error' => 'BadRequest', 9 | 'message' => 'Invalid token: expired'] 10 | } 11 | end 12 | 13 | initialize_with do 14 | new( 15 | OpenStruct.new(code: code, body: body.to_json) 16 | ) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/factories/notifications_client.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :notifications_client, 3 | class: Notifications::Client do 4 | base_url { nil } 5 | jwt_secret { "test-key-fa80e418-ff49-445c-a29b-92c04a181207-7aaec57c-2dc9-4d31-8f5c-7225fe79516a" } 6 | initialize_with do 7 | new(jwt_secret, base_url) 8 | end 9 | end 10 | 11 | factory :notifications_client_combined, 12 | class: Notifications::Client do 13 | jwt_secret { "test_key-fa80e418-ff49-445c-a29b-92c04a181207-7aaec57c-2dc9-4d31-8f5c-7225fe79516a" } 14 | 15 | initialize_with do 16 | new(jwt_secret) 17 | end 18 | end 19 | 20 | factory :notifications_client_combined_with_base_url, 21 | class: Notifications::Client do 22 | base_url { "http://example.com" } 23 | jwt_secret { "test_key-fa80e418-ff49-445c-a29b-92c04a181207-7aaec57c-2dc9-4d31-8f5c-7225fe79516a" } 24 | 25 | initialize_with do 26 | new(jwt_secret, base_url) 27 | end 28 | end 29 | 30 | factory :notifications_client_with_invalid_api_key, 31 | class: Notifications::Client do 32 | base_url { nil } 33 | jwt_secret { "test_key-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } 34 | 35 | initialize_with do 36 | new(jwt_secret, base_url) 37 | end 38 | end 39 | 40 | ## 41 | # stubbed. response from API 42 | factory :notifications_client_post_sms_response, 43 | class: Notifications::Client::ResponseNotification do 44 | body do 45 | { 46 | "id" => "aceed36e-6aee-494c-a09f-88b68904bad6", 47 | "reference" => nil, 48 | "content" => { 49 | "body" => "Hello we got your application", 50 | "from_number" => "40604" 51 | }, 52 | "template" => { 53 | "id" => "f6895ff7-86e0-4d38-80ab-c9525856c3ff", 54 | "version" => 1, 55 | "uri" => "/v2/templates/f6895ff7-86e0-4d38-80ab-c9525856c3ff" 56 | }, 57 | "uri" => "/notifications/aceed36e-6aee-494c-a09f-88b68904bad6" 58 | } 59 | end 60 | 61 | initialize_with do 62 | new(body) 63 | end 64 | end 65 | 66 | ## 67 | # stubbed. response from API 68 | factory :notifications_client_post_email_response, 69 | class: Notifications::Client::ResponseNotification do 70 | body do 71 | { 72 | "id" => "aceed36e-6aee-494c-a09f-88b68904bad6", 73 | "reference" => nil, 74 | "content" => { 75 | "body" => "Hello we got your application", 76 | "subject" => "Application recieved", 77 | "from_email" => "40604" 78 | }, 79 | "template" => { 80 | "id" => "f6895ff7-86e0-4d38-80ab-c9525856c3ff", 81 | "version" => 1, 82 | "uri" => "/v2/templates/f6895ff7-86e0-4d38-80ab-c9525856c3ff" 83 | }, 84 | "uri" => "/notifications/aceed36e-6aee-494c-a09f-88b68904bad6" 85 | } 86 | end 87 | 88 | initialize_with do 89 | new(body) 90 | end 91 | end 92 | 93 | ## 94 | # stubbed. response from API 95 | factory :notifications_client_post_letter_response, 96 | class: Notifications::Client::ResponseNotification do 97 | body do 98 | { 99 | "id" => "aceed36e-6aee-494c-a09f-88b68904bad6", 100 | "reference" => nil, 101 | "content" => { 102 | "body" => "Hello we got your application", 103 | "subject" => "Application recieved" 104 | }, 105 | "scheduled_for" => nil, 106 | "template" => { 107 | "id" => "f6895ff7-86e0-4d38-80ab-c9525856c3ff", 108 | "version" => 1, 109 | "uri" => "/v2/templates/f6895ff7-86e0-4d38-80ab-c9525856c3ff" 110 | }, 111 | "uri" => "/notifications/aceed36e-6aee-494c-a09f-88b68904bad6" 112 | } 113 | end 114 | 115 | initialize_with do 116 | new(body) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/factories/template_list.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_template_collection, 3 | class: Notifications::Client::TemplateCollection do 4 | initialize_with do 5 | new(body) 6 | end 7 | 8 | body do 9 | { 10 | "templates" => 2.times.map { 11 | attributes_for(:client_template_response)[:body] 12 | } 13 | } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/factories/template_preview.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_template_preview, 3 | class: Notifications::Client::TemplatePreview do 4 | initialize_with do 5 | new(body) 6 | end 7 | 8 | body do 9 | { 10 | "id" => "f163deaf-2d3f-4ec6-98fc-f23fa511518f", 11 | "body" => "Contents of template Mr Big Nose", 12 | "subject" => "Subject of the email", 13 | "version" => "2", 14 | "type" => "email", 15 | "html" => "

Contents of template Mr Big Nose

" 16 | } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/factories/template_response.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :client_template_response, 3 | class: Notifications::Client::Template do 4 | initialize_with do 5 | new(body) 6 | end 7 | 8 | body do 9 | { 10 | "id" => "f163deaf-2d3f-4ec6-98fc-f23fa511518f", 11 | "name" => "My template name", 12 | "type" => "letter", 13 | "created_at" => "2016-11-29T11:12:30.12354Z", 14 | "updated_at" => "2016-11-29T11:12:40.12354Z", 15 | "created_by" => "jane.doe@gmail.com", 16 | "body" => "Contents of template ((place_holder))", 17 | "subject" => "Subject of the letter", 18 | "version" => "2", 19 | "letter_contact_block" => "The return address" 20 | } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/notifications/client/email_notification_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | let(:uri) { URI.parse(Notifications::Client::PRODUCTION_BASE_URL) } 4 | let(:mocked_response) { 5 | attributes_for(:notifications_client_post_email_response)[:body] 6 | } 7 | 8 | before do 9 | stub_request( 10 | :post, 11 | "https://#{uri.host}:#{uri.port}/v2/notifications/email" 12 | ).to_return( 13 | body: mocked_response.to_json, 14 | status: 201, 15 | headers: { "Content-Type" => "application/json" } 16 | ) 17 | end 18 | 19 | describe "#send_email" do 20 | let!(:sent_email) { 21 | client.send_email( 22 | email_address: "email@gov.uk", 23 | template_id: "f6895ff7-86e0-4d38-80ab-c9525856c3ff" 24 | ) 25 | } 26 | 27 | it "should send valid notification" do 28 | expect(sent_email).to be_a( 29 | Notifications::Client::ResponseNotification 30 | ) 31 | end 32 | 33 | %w( 34 | id 35 | content 36 | uri 37 | template 38 | ).each do |field| 39 | it "expect to include #{field}" do 40 | expect( 41 | sent_email.send(field) 42 | ).to_not be_nil 43 | end 44 | end 45 | 46 | it "hits the correct API endpoint" do 47 | expect(WebMock).to have_requested(:post, "https://#{uri.host}:#{uri.port}/v2/notifications/email"). 48 | with(body: { email_address: "email@gov.uk", template_id: "f6895ff7-86e0-4d38-80ab-c9525856c3ff" }) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/notifications/client/generate_template_preview_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | let(:uri) { 5 | URI.parse(Notifications::Client::PRODUCTION_BASE_URL) 6 | } 7 | 8 | describe "generate template preview" do 9 | before do 10 | stub_request( 11 | :post, 12 | "https://#{uri.host}:#{uri.port}/v2/template/#{id}/preview" 13 | ).to_return(body: mocked_response.to_json) 14 | end 15 | 16 | let(:id) { 17 | "1" 18 | } 19 | let(:personalisation) { 20 | { name: "Mr Big Nose" } 21 | } 22 | 23 | let!(:template_preview) { 24 | client.generate_template_preview(id, personalisation) 25 | } 26 | 27 | let(:mocked_response) { 28 | attributes_for(:client_template_preview)[:body] 29 | } 30 | 31 | it "expects template preview" do 32 | expect(template_preview).to be_a( 33 | Notifications::Client::TemplatePreview 34 | ) 35 | end 36 | 37 | %w( 38 | id 39 | body 40 | subject 41 | version 42 | html 43 | ).each do |field| 44 | it "expect to include #{field}" do 45 | expect( 46 | template_preview.send(field) 47 | ).to_not be_nil 48 | end 49 | end 50 | 51 | it "hits the correct API endpoint" do 52 | expect(WebMock).to have_requested(:post, "https://#{uri.host}:#{uri.port}/v2/template/#{id}/preview"). 53 | with(body: { name: "Mr Big Nose" }) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/notifications/client/get_all_templates_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | let(:uri) { 5 | URI.parse(Notifications::Client::PRODUCTION_BASE_URL) 6 | } 7 | 8 | let(:mocked_response) { 9 | attributes_for(:client_template_collection)[:body] 10 | } 11 | 12 | describe "get all templates" do 13 | before do 14 | stub_request( 15 | :get, 16 | "https://#{uri.host}:#{uri.port}/v2/templates" 17 | ).to_return(body: mocked_response.to_json) 18 | end 19 | 20 | let!(:templates) { client.get_all_templates } 21 | 22 | it "expects TemplateCollection" do 23 | expect(templates).to be_a( 24 | Notifications::Client::TemplateCollection 25 | ) 26 | end 27 | 28 | it "collection contains all templates" do 29 | expect( 30 | templates.collection.count 31 | ).to eq(mocked_response["templates"].count) 32 | end 33 | 34 | it "collection has templates" do 35 | expect( 36 | templates.collection.sample 37 | ).to be_a(Notifications::Client::Template) 38 | end 39 | 40 | it "hits the correct API endpoint with no parameters" do 41 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/templates") 42 | end 43 | end 44 | 45 | describe "get templates by query" do 46 | before do 47 | stub_request( 48 | :get, 49 | "https://#{uri.host}:#{uri.port}/v2/templates?template_type=sms" 50 | ).to_return(body: mocked_response.to_json) 51 | end 52 | 53 | it "hits the correct API endpoint when filtering by template type" do 54 | args = { template_type: "sms" } 55 | 56 | client.get_all_templates(args) 57 | 58 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/templates").with(query: args) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/notifications/client/get_notification_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | let(:uri) { URI.parse(Notifications::Client::PRODUCTION_BASE_URL) } 4 | 5 | describe "get a notification by id" do 6 | before do 7 | stub_request( 8 | :get, 9 | "https://#{uri.host}:#{uri.port}/v2/notifications/#{id}" 10 | ).to_return(body: mocked_response.to_json) 11 | end 12 | 13 | let(:id) { "1" } 14 | let!(:notification) { client.get_notification(id) } 15 | let(:mocked_response) { attributes_for(:client_notification)[:body] } 16 | 17 | it "expects notification" do 18 | expect(notification).to be_a( 19 | Notifications::Client::Notification 20 | ) 21 | end 22 | 23 | %w( 24 | id 25 | reference 26 | phone_number 27 | type 28 | status 29 | template 30 | body 31 | created_at 32 | sent_at 33 | completed_at 34 | created_by_name 35 | cost_in_pounds 36 | is_cost_data_ready 37 | cost_details 38 | ).each do |field| 39 | it "expect to include #{field}" do 40 | expect( 41 | notification.send(field) 42 | ).to_not be_nil 43 | end 44 | end 45 | 46 | it "parses the time correctly" do 47 | expect(notification.created_at.to_s).to eq("2016-11-29 11:12:30 UTC") 48 | end 49 | 50 | it "hits the correct API endpoint" do 51 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/notifications/#{id}") 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/notifications/client/get_notifications_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | let(:uri) { 5 | URI.parse(Notifications::Client::PRODUCTION_BASE_URL) 6 | } 7 | 8 | describe "get all notifications" do 9 | before do 10 | stub_request( 11 | :get, 12 | "https://#{uri.host}:#{uri.port}/v2/notifications" 13 | ).to_return(body: mocked_response.to_json) 14 | end 15 | 16 | let!(:notifications) { 17 | client.get_notifications 18 | } 19 | 20 | let(:mocked_response) { 21 | attributes_for(:client_notifications_collection)[:body] 22 | } 23 | 24 | it "collection contains all notifications" do 25 | expect( 26 | notifications.collection.count 27 | ).to eq(mocked_response["notifications"].count) 28 | end 29 | 30 | it "collection has notifications" do 31 | expect( 32 | notifications.collection.sample 33 | ).to be_a(Notifications::Client::Notification) 34 | end 35 | 36 | it "requests all notifications with no parameters" do 37 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/notifications") 38 | end 39 | end 40 | 41 | describe "get notifications by query" do 42 | before do 43 | stub_request( 44 | :get, 45 | "https://#{uri.host}:#{uri.port}/v2/notifications?#{request_path}" 46 | ).to_return(body: mocked_response.to_json) 47 | end 48 | 49 | let(:options) { 50 | { 51 | "template_type" => "sms", 52 | "status" => "delivered" 53 | } 54 | } 55 | 56 | let!(:notifications) { 57 | client.get_notifications( 58 | options 59 | ) 60 | } 61 | 62 | let(:mocked_response) { 63 | attributes_for( 64 | :client_notifications_collection 65 | )[:body].merge(options) 66 | } 67 | 68 | let(:request_path) { 69 | URI.encode_www_form(options) 70 | } 71 | 72 | it "expect to request with right parameters" do 73 | expect(WebMock).to have_requested( 74 | :get, 75 | "https://#{uri.host}:#{uri.port}/v2/notifications" 76 | ).with(query: options) 77 | end 78 | 79 | it "expects notifications collection" do 80 | expect(notifications).to be_a( 81 | Notifications::Client::NotificationsCollection 82 | ) 83 | end 84 | 85 | %w(links).each do |field| 86 | it "should contain service #{field}" do 87 | expect( 88 | notifications.send(field) 89 | ).to eq(mocked_response.fetch(field)) 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/notifications/client/get_pdf_for_letter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | let(:uri) { 5 | URI.parse(Notifications::Client::PRODUCTION_BASE_URL) 6 | } 7 | 8 | describe "get a pdf for a letter notification" do 9 | before do 10 | stub_request( 11 | :get, 12 | "https://#{uri.host}:#{uri.port}/v2/notifications/#{id}/pdf" 13 | ).to_return(body: mocked_response) 14 | end 15 | 16 | let(:id) { 17 | "1" 18 | } 19 | 20 | let!(:pdf) { 21 | client.get_pdf_for_letter(id) 22 | } 23 | 24 | let(:mocked_response) { 25 | 'foobar' 26 | } 27 | 28 | it "expects pdf to be returned" do 29 | expect(pdf).to equal(mocked_response) 30 | end 31 | 32 | it "hits the correct API endpoint" do 33 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/notifications/#{id}/pdf") 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/notifications/client/get_received_texts_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | let(:uri) { 5 | URI.parse(Notifications::Client::PRODUCTION_BASE_URL) 6 | } 7 | 8 | describe "get received texts requests" do 9 | it "hits the received text message endpoint without older_than" do 10 | stub_request( 11 | :get, 12 | "https://#{uri.host}:#{uri.port}/v2/received-text-messages" 13 | ).to_return(body: { "received_text_messages": [] }.to_json) 14 | 15 | client.get_received_texts 16 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/received-text-messages") 17 | end 18 | 19 | it "hits the received text message endpoint with older_than" do 20 | stub_request( 21 | :get, 22 | "https://#{uri.host}:#{uri.port}/v2/received-text-messages?older_than=received-text-id" 23 | ).to_return(body: { "received_text_messages": [] }.to_json) 24 | 25 | client.get_received_texts(older_than: "received-text-id") 26 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/received-text-messages"). 27 | with(query: { "older_than" => "received-text-id" }) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/notifications/client/get_template_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | let(:uri) { 5 | URI.parse(Notifications::Client::PRODUCTION_BASE_URL) 6 | } 7 | 8 | describe "get a template by id" do 9 | before do 10 | stub_request( 11 | :get, 12 | "https://#{uri.host}:#{uri.port}/v2/template/#{id}" 13 | ).to_return(body: mocked_response.to_json) 14 | end 15 | 16 | let(:id) { 17 | "1" 18 | } 19 | 20 | let!(:template) { 21 | client.get_template_by_id(id) 22 | } 23 | 24 | let(:mocked_response) { 25 | attributes_for(:client_template_response)[:body] 26 | } 27 | 28 | it "expects template" do 29 | expect(template).to be_a( 30 | Notifications::Client::Template 31 | ) 32 | end 33 | 34 | %w( 35 | id 36 | name 37 | type 38 | body 39 | created_at 40 | updated_at 41 | created_by 42 | subject 43 | version 44 | letter_contact_block 45 | ).each do |field| 46 | it "expect to include #{field}" do 47 | expect( 48 | template.send(field) 49 | ).to_not be_nil 50 | end 51 | end 52 | 53 | it "parses the time correctly" do 54 | expect(template.created_at.to_s).to eq("2016-11-29 11:12:30 UTC") 55 | end 56 | 57 | it "hits the correct API endpoint" do 58 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/template/#{id}") 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/notifications/client/get_template_version_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | let(:uri) { 5 | URI.parse(Notifications::Client::PRODUCTION_BASE_URL) 6 | } 7 | 8 | describe "get a template by id and version" do 9 | before do 10 | stub_request( 11 | :get, 12 | "https://#{uri.host}:#{uri.port}/v2/template/#{id}/version/#{version}" 13 | ).to_return(body: mocked_response.to_json) 14 | end 15 | 16 | let(:id) { 17 | "1" 18 | } 19 | let(:version) { 20 | "1" 21 | } 22 | let!(:template) { 23 | client.get_template_version(id, version) 24 | } 25 | 26 | let(:mocked_response) { 27 | attributes_for(:client_template_response)[:body] 28 | } 29 | 30 | it "expects template" do 31 | expect(template).to be_a( 32 | Notifications::Client::Template 33 | ) 34 | end 35 | 36 | %w( 37 | id 38 | name 39 | type 40 | body 41 | created_at 42 | updated_at 43 | created_by 44 | subject 45 | version 46 | letter_contact_block 47 | ).each do |field| 48 | it "expect to include #{field}" do 49 | expect( 50 | template.send(field) 51 | ).to_not be_nil 52 | end 53 | end 54 | 55 | it "hits the correct API endpoint" do 56 | expect(WebMock).to have_requested(:get, "https://#{uri.host}:#{uri.port}/v2/template/#{id}/version/#{version}") 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/notifications/client/helper_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | describe Notifications do 4 | describe ".prepare_upload" do 5 | it "encodes a File object" do 6 | File.open('spec/test_files/test_pdf.pdf', 'rb') do |f| 7 | encoded_content = Base64.strict_encode64(f.read) 8 | f.rewind 9 | 10 | result = Notifications.prepare_upload(f) 11 | f.rewind 12 | 13 | expect(result).to eq(file: encoded_content, filename: nil, confirm_email_before_download: nil, retention_period: nil) 14 | expect(Base64.strict_decode64(encoded_content)).to eq(f.read) 15 | end 16 | end 17 | 18 | it "encodes a StringIO object" do 19 | input_string = StringIO.new("My document to send") 20 | expect(Notifications.prepare_upload(input_string)).to eq(file: "TXkgZG9jdW1lbnQgdG8gc2VuZA==", filename: nil, confirm_email_before_download: nil, retention_period: nil) 21 | end 22 | 23 | it "allows filename to be set" do 24 | input_string = StringIO.new("My document to send") 25 | expect(Notifications.prepare_upload(input_string, filename: "report.csv")).to eq(file: "TXkgZG9jdW1lbnQgdG8gc2VuZA==", filename: "report.csv", confirm_email_before_download: nil, retention_period: nil) 26 | end 27 | 28 | it "allows confirm_email_before_download to be set to true" do 29 | input_string = StringIO.new("My document to send") 30 | expect(Notifications.prepare_upload(input_string, confirm_email_before_download: true)).to eq(file: "TXkgZG9jdW1lbnQgdG8gc2VuZA==", filename: nil, confirm_email_before_download: true, retention_period: nil) 31 | end 32 | 33 | it "allows retention_period to be set" do 34 | input_string = StringIO.new("My document to send") 35 | expect(Notifications.prepare_upload(input_string, retention_period: '1 weeks')).to eq(file: "TXkgZG9jdW1lbnQgdG8gc2VuZA==", filename: nil, confirm_email_before_download: nil, retention_period: '1 weeks') 36 | end 37 | 38 | it "raises an error when the file size is too large" do 39 | File.open('spec/test_files/test_pdf.pdf', 'rb') do |file| 40 | allow(file).to receive(:size).and_return(Notifications::Client::MAX_FILE_UPLOAD_SIZE + 1) 41 | 42 | expect { Notifications.prepare_upload(file) }.to raise_error(ArgumentError, "File is larger than 2MB") 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/notifications/client/letter_notification_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | let(:uri) { URI.parse(Notifications::Client::PRODUCTION_BASE_URL) } 4 | let(:mocked_response) { 5 | attributes_for(:notifications_client_post_letter_response)[:body] 6 | } 7 | 8 | before do 9 | stub_request( 10 | :post, 11 | "https://#{uri.host}:#{uri.port}/v2/notifications/letter" 12 | ).to_return( 13 | body: mocked_response.to_json, 14 | status: 201, 15 | headers: { "Content-Type" => "application/json" } 16 | ) 17 | end 18 | 19 | describe "#send_letter" do 20 | let!(:sent_letter) { 21 | client.send_letter( 22 | template_id: "f6895ff7-86e0-4d38-80ab-c9525856c3ff", 23 | personalisation: { 24 | address_line_1: "The Occupier", 25 | address_line_2: "123 High Street", 26 | postcode: "SW14 6BH" 27 | } 28 | ) 29 | } 30 | 31 | it "should send a valid notification" do 32 | expect(sent_letter).to be_a( 33 | Notifications::Client::ResponseNotification 34 | ) 35 | end 36 | 37 | %w( 38 | id 39 | content 40 | uri 41 | template 42 | ).each do |field| 43 | it "expect to include #{field}" do 44 | expect( 45 | sent_letter.send(field) 46 | ).to_not be_nil 47 | end 48 | end 49 | 50 | it "hits the correct API endpoint" do 51 | expect(WebMock).to have_requested(:post, "https://#{uri.host}:#{uri.port}/v2/notifications/letter"). 52 | with(body: { 53 | template_id: "f6895ff7-86e0-4d38-80ab-c9525856c3ff", 54 | personalisation: { 55 | address_line_1: "The Occupier", 56 | address_line_2: "123 High Street", 57 | postcode: "SW14 6BH" 58 | } 59 | }) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/notifications/client/precompiled_letter_spec.rb: -------------------------------------------------------------------------------- 1 | require "stringio" 2 | 3 | describe Notifications::Client do 4 | let(:client) { build :notifications_client } 5 | let(:uri) { URI.parse(Notifications::Client::PRODUCTION_BASE_URL) } 6 | 7 | describe "#send_precompiled_letter" do 8 | before do 9 | stub_request(:post, "https://#{uri.host}/v2/notifications/letter"). 10 | to_return( 11 | body: { "id" => "12345", "reference" => "my letter", "postage" => "first" }.to_json, 12 | status: 201, 13 | headers: { "Content-Type" => "application/json" } 14 | ) 15 | end 16 | 17 | it "hits the correct API endpoint with an encoded File object" do 18 | pdf_file = File.open('spec/test_files/test_pdf.pdf', 'rb') 19 | encoded_content = Base64.strict_encode64(pdf_file.read) 20 | pdf_file.rewind 21 | 22 | client.send_precompiled_letter('12345', pdf_file) 23 | pdf_file.close 24 | 25 | expect(WebMock).to have_requested(:post, "https://#{uri.host}/v2/notifications/letter"). 26 | with(body: { reference: "12345", content: encoded_content }) 27 | end 28 | 29 | it "hits the correct API endpoint with an encoded StringIO object" do 30 | input_string = StringIO.new("My precompiled letter") 31 | encoded_content = Base64.strict_encode64(input_string.read) 32 | input_string.rewind 33 | 34 | client.send_precompiled_letter('12345', input_string) 35 | 36 | expect(WebMock).to have_requested(:post, "https://#{uri.host}/v2/notifications/letter"). 37 | with(body: { reference: "12345", content: encoded_content }) 38 | end 39 | 40 | it "hits the correct API endpoint with postage" do 41 | input_string = StringIO.new("My precompiled letter") 42 | encoded_content = Base64.strict_encode64(input_string.read) 43 | input_string.rewind 44 | 45 | client.send_precompiled_letter('12345', input_string, 'first') 46 | 47 | expect(WebMock).to have_requested(:post, "https://#{uri.host}/v2/notifications/letter"). 48 | with(body: { reference: "12345", content: encoded_content, postage: 'first' }) 49 | end 50 | 51 | it "returns a ResponsePrecompiledLetter with an id, postage and reference" do 52 | response = File.open('spec/test_files/test_pdf.pdf', 'rb') do |file| 53 | client.send_precompiled_letter('my letter', file, 'first') 54 | end 55 | 56 | expect(response).to be_a(Notifications::Client::ResponsePrecompiledLetter) 57 | expect(response.id).to eq('12345') 58 | expect(response.reference).to eq('my letter') 59 | expect(response.postage).to eq('first') 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/notifications/client/request_errors_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | let(:uri) { URI.parse(Notifications::Client::PRODUCTION_BASE_URL) } 4 | 5 | def stub_error_request(code, body: nil) 6 | url = "https://#{uri.host}:#{uri.port}/v2/notifications/1" 7 | body = attributes_for(:client_request_error)[:body] unless body 8 | stub_request(:get, url).to_return(status: code, body: body.to_json) 9 | end 10 | 11 | def expect_error(error_class = Notifications::Client::RequestError, message = nil) 12 | expect { client.get_notification("1") }.to raise_error(error_class, message) 13 | end 14 | 15 | shared_examples "raises an error" do |error_class, message| 16 | it "should raise a #{error_class}" do 17 | expect_error(error_class, message) 18 | end 19 | 20 | it "should be a subclass of Notifications::Client::RequestError" do 21 | expect_error 22 | end 23 | end 24 | 25 | describe "bad request error" do 26 | before { stub_error_request(400) } 27 | include_examples "raises an error", Notifications::Client::BadRequestError 28 | end 29 | 30 | describe "authorisation error" do 31 | before { stub_error_request(403) } 32 | include_examples "raises an error", Notifications::Client::AuthError 33 | end 34 | 35 | describe "not found error" do 36 | before { stub_error_request(404) } 37 | include_examples "raises an error", Notifications::Client::NotFoundError 38 | end 39 | 40 | describe "rate limit error" do 41 | before { stub_error_request(429) } 42 | include_examples "raises an error", Notifications::Client::RateLimitError 43 | end 44 | 45 | describe "other client error" do 46 | before { stub_error_request(487) } 47 | include_examples "raises an error", Notifications::Client::ClientError 48 | end 49 | 50 | describe "server error" do 51 | before do 52 | stub_error_request(503, body: { 53 | 'status_code' => 503, 54 | 'errors' => ['error' => 'BadRequestError', 'message' => 'App error'] 55 | }) 56 | end 57 | 58 | include_examples "raises an error", Notifications::Client::ServerError, "BadRequestError: App error" 59 | end 60 | 61 | describe "#message" do 62 | before do 63 | stub_error_request(401, body: { 64 | 'status_code' => 401, 65 | 'errors' => ['error' => 'BadRequestError', 'message' => 'Can’t send to this recipient using a team-only API key'] 66 | }) 67 | end 68 | 69 | it "returns the message" do 70 | expect { client.get_notification('1') }.to raise_error do |error| 71 | expect(error.to_s).to eql('BadRequestError: Can’t send to this recipient using a team-only API key') 72 | expect(error.message).to eql('BadRequestError: Can’t send to this recipient using a team-only API key') 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/notifications/client/sms_notification_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | let(:uri) { URI.parse(Notifications::Client::PRODUCTION_BASE_URL) } 4 | let(:mocked_response) { 5 | attributes_for(:notifications_client_post_sms_response)[:body] 6 | } 7 | 8 | before do 9 | stub_request( 10 | :post, 11 | "https://#{uri.host}:#{uri.port}/v2/notifications/sms" 12 | ).to_return( 13 | body: mocked_response.to_json, 14 | status: 201, 15 | headers: { "Content-Type" => "application/json" } 16 | ) 17 | end 18 | 19 | describe "#send_sms" do 20 | let!(:sent_sms) { 21 | client.send_sms( 22 | phone_number: "+44 7700 900 404", 23 | template_id: "f6895ff7-86e0-4d38-80ab-c9525856c3ff" 24 | ) 25 | } 26 | 27 | it "should send a valid notification" do 28 | expect(sent_sms).to be_a( 29 | Notifications::Client::ResponseNotification 30 | ) 31 | end 32 | 33 | %w( 34 | id 35 | content 36 | uri 37 | template 38 | ).each do |field| 39 | it "expect to include #{field}" do 40 | expect( 41 | sent_sms.send(field) 42 | ).to_not be_nil 43 | end 44 | end 45 | 46 | it "hits the correct API endpoint" do 47 | expect(WebMock).to have_requested(:post, "https://#{uri.host}:#{uri.port}/v2/notifications/sms"). 48 | with(body: { phone_number: "+44 7700 900 404", template_id: "f6895ff7-86e0-4d38-80ab-c9525856c3ff" }) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/notifications/client/speaker_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | let(:client) { build :notifications_client } 3 | 4 | describe "client jwt token" do 5 | let(:jwt_token) { 6 | client.speaker.send(:jwt_token) 7 | } 8 | let(:secret) { "7aaec57c-2dc9-4d31-8f5c-7225fe79516a" } 9 | let(:service_id) { "fa80e418-ff49-445c-a29b-92c04a181207" } 10 | 11 | 12 | let(:decoded_payload) { 13 | JWT.decode( 14 | jwt_token, 15 | secret, 16 | true, 17 | algorithm: "HS256" 18 | ) 19 | } 20 | 21 | it "should have valid payload" do 22 | expect( 23 | decoded_payload.first["iss"] 24 | ).to eq(service_id) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/notifications/client/uuid_validator_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::UuidValidator do 2 | def assert_valid(uuid) 3 | expect(described_class.new(uuid)).to be_valid, 4 | "uuid: #{uuid} should be valid" 5 | end 6 | 7 | def assert_invalid(uuid) 8 | expect(described_class.new(uuid)).not_to be_valid, 9 | "uuid: #{uuid} should be invalid" 10 | end 11 | 12 | it "accepts valid uuids" do 13 | assert_valid("00000000-0000-0000-0000-000000000000") 14 | assert_valid("01234567-89ab-cdef-edcb-a98765432100") 15 | 16 | 100.times { assert_valid(SecureRandom.uuid) } 17 | end 18 | 19 | it "rejects invalid uuids" do 20 | assert_invalid("not-a-valid-uuid") 21 | assert_invalid("abcdefg-0000-0000-0000-0000000000000") 22 | assert_invalid("000000000000000000000000000000000000") 23 | assert_invalid("00000000-0000-0000-0000-0000000000") 24 | assert_invalid("00000000------0000-0000-000000000000") 25 | assert_invalid(nil) 26 | end 27 | 28 | describe ".validate!" do 29 | context "for a valid uuid" do 30 | let(:uuid) { "00000000-0000-0000-0000-000000000000" } 31 | 32 | it "does not raise" do 33 | expect { described_class.validate!(uuid) }.not_to raise_error 34 | end 35 | end 36 | 37 | context "for an invalid uuid" do 38 | let(:uuid) { "not-a-valid-uuid" } 39 | 40 | it "raises a helpful error" do 41 | expect { described_class.validate!(uuid) } 42 | .to raise_error(ArgumentError, '"not-a-valid-uuid" is not a valid uuid') 43 | 44 | expect { described_class.validate!(nil) } 45 | .to raise_error(ArgumentError, 'nil is not a valid uuid') 46 | end 47 | 48 | context "when a contextual message is provided" do 49 | it "includes it in the error message" do 50 | expect { described_class.validate!(uuid, "please check that ...") } 51 | .to raise_error(ArgumentError, /please check that .../) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/notifications/client_spec.rb: -------------------------------------------------------------------------------- 1 | describe Notifications::Client do 2 | it "has a version number" do 3 | expect(Notifications::Client::VERSION).not_to be nil 4 | end 5 | 6 | describe "with combined API key" do 7 | let(:client) { build :notifications_client_combined } 8 | 9 | it "should extract service ID" do 10 | expect( 11 | client.service_id 12 | ).to eq("fa80e418-ff49-445c-a29b-92c04a181207") 13 | end 14 | 15 | it "should extract secret" do 16 | expect( 17 | client.secret_token 18 | ).to eq("7aaec57c-2dc9-4d31-8f5c-7225fe79516a") 19 | end 20 | 21 | it "should have use default base URL" do 22 | expect( 23 | client.base_url 24 | ).to eq(Notifications::Client::PRODUCTION_BASE_URL) 25 | end 26 | end 27 | 28 | 29 | describe "with combined API key and non-default base URL" do 30 | let(:client) { build :notifications_client_combined_with_base_url } 31 | 32 | it "should extract service ID" do 33 | expect( 34 | client.service_id 35 | ).to eq("fa80e418-ff49-445c-a29b-92c04a181207") 36 | end 37 | 38 | it "should extract secret" do 39 | expect( 40 | client.secret_token 41 | ).to eq("7aaec57c-2dc9-4d31-8f5c-7225fe79516a") 42 | end 43 | 44 | it "should use custom base URL" do 45 | expect( 46 | client.base_url 47 | ).to eq("http://example.com") 48 | end 49 | end 50 | 51 | describe "with an invalid API key" do 52 | let(:client) { build :notifications_client_with_invalid_api_key } 53 | 54 | it "raises a helpful error" do 55 | expect { client }.to raise_error(ArgumentError, /is not a valid uuid/) 56 | end 57 | 58 | it "includes contextual information in the error" do 59 | expect { client }.to raise_error(ArgumentError, /this error is probably caused by/i) 60 | end 61 | end 62 | 63 | describe "#base_url" do 64 | describe "default base url" do 65 | let(:client) { build :notifications_client } 66 | 67 | it { 68 | expect(client.base_url).to eq( 69 | Notifications::Client::PRODUCTION_BASE_URL 70 | ) 71 | } 72 | end 73 | 74 | describe "set base url" do 75 | let(:new_url) { 76 | "https://test.notifications.service.gov.uk" 77 | } 78 | 79 | let(:client) { 80 | build :notifications_client, base_url: new_url 81 | } 82 | 83 | it { 84 | expect(client.base_url).to eq(new_url) 85 | } 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'notifications/client' 3 | require 'webmock/rspec' 4 | require 'factory_bot' 5 | 6 | Dir[Dir.pwd + "/spec/support/**/*.rb"].each { |f| require f } 7 | 8 | RSpec.configure do |config| 9 | config.include FactoryBot::Syntax::Methods 10 | 11 | config.before(:suite) do 12 | FactoryBot.find_definitions 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/test_files/test_pdf.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/notifications-ruby-client/93756391ca90b555285182167dbb72eb07608652/spec/test_files/test_pdf.pdf --------------------------------------------------------------------------------