diff --git a/.env.example b/.env.example index d7df3f83..4d26d2b2 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,11 @@ CANONICAL_HOSTNAME=example.com # this line completely ADDITIONAL_HOSTNAMES= -CERC_API_HOST_URL=https://cerc.example.com -CERC_API_KEY=SECRET-API-KEY -CERC_API_CACHE_LIMIT_MINS=30 +CERC_FORECAST_API_HOST_URL=https://cerc.example.com +CERC_FORECAST_API_KEY=SECRET-API-KEY +CERC_FORECAST_API_CACHE_LIMIT_MINS=30 + +CERC_SUBSCRIBE_API_HOST_URL=https://cerc.example.com +CERC_SUBSCRIBE_API_KEY=SECRET-API-KEY + MAPTILER_API_KEY=SECRET-DEV-KEY-FROM-MAPTILER # or prod key limited to Heroku origin diff --git a/.env.test b/.env.test index e3099c7a..b8fdd6a1 100644 --- a/.env.test +++ b/.env.test @@ -8,7 +8,10 @@ DATABASE_URL=postgres://postgres@localhost:5432/air-text-test HOSTNAME=localhost -CERC_API_HOST_URL=https://cerc.example.com, -CERC_API_KEY=SECRET-API-KEY, -CERC_API_CACHE_LIMIT_MINS=60, +CERC_SUBSCRIBE_API_HOST_URL=https://cerc.example.com +CERC_SUBSCRIBE_API_KEY=SECRET-API-KEY + +CERC_FORECAST_API_HOST_URL=https://cerc.example.com +CERC_FORECAST_API_KEY=SECRET-API-KEY +CERC_FORECAST_API_CACHE_LIMIT_MINS=60 MAPTILER_API_KEY=TOPSECRET \ No newline at end of file diff --git a/README.md b/README.md index b2c692d3..4a858c56 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,13 @@ To manage sensitive environment variables: ### Required environment variables -- `CERC_API_HOST_URL`: find the URL of the CERC API host in the 1Password vault -- `CERC_API_KEY`: find the API key in the 1Password vault -- `CERC_API_CACHE_LIMIT_MINS`: how often we expire our cached forecasts +- `CERC_FORECAST_API_HOST_URL`: find the URL of the CERC API host in the + 1Password vault +- `CERC_FORECAST_API_KEY`: find the API key in the 1Password vault +- `CERC_FORECAST_API_CACHE_LIMIT_MINS`: how often we expire our cached forecasts +- `CERC_SUBSCRIBE_API_HOST_URL`: find the URL of the CERC API host in the + 1Password vault +- `CERC_SUBSCRIBE_API_KEY`: find the API key in the 1Password vault - `MAPTILER_API_KEY`: used for vector tiles display within Leaflet. Dev and prod keys are in the 1Password vault diff --git a/app/models/cached_forecast.rb b/app/models/cached_forecast.rb index bcc48da9..7fda1f98 100644 --- a/app/models/cached_forecast.rb +++ b/app/models/cached_forecast.rb @@ -18,7 +18,7 @@ def self.stale? return true if latest_record.nil? - threshold = Time.current - ENV.fetch("CERC_API_CACHE_LIMIT_MINS").to_i.minutes + threshold = Time.current - ENV.fetch("CERC_FORECAST_API_CACHE_LIMIT_MINS").to_i.minutes latest_record.obtained_at < threshold end diff --git a/app/models/cerc_api_client.rb b/app/models/cerc_api_client.rb deleted file mode 100644 index 8f1773ed..00000000 --- a/app/models/cerc_api_client.rb +++ /dev/null @@ -1,16 +0,0 @@ -class CercApiClient - class << self - def latest_forecasts(zone = nil) - base_url = ENV.fetch("CERC_API_HOST_URL") - - query = { - "from" => Date.today, - "numdays" => 3, - "zone" => zone, - "key" => ENV.fetch("CERC_API_KEY") - }.compact - - HTTParty.get("#{base_url}/getforecast/all", query: query) - end - end -end diff --git a/app/models/cerc_forecast_api_client.rb b/app/models/cerc_forecast_api_client.rb new file mode 100644 index 00000000..0cc47f21 --- /dev/null +++ b/app/models/cerc_forecast_api_client.rb @@ -0,0 +1,21 @@ +class CercForecastApiClient + class << self + def latest_forecasts(zone = nil) + query = { + "from" => Date.today, + "numdays" => 3, + "zone" => zone + }.compact + + request("getforecast/all", query) + end + + private + + def request(endpoint, query = {}) + base_url = ENV.fetch("CERC_FORECAST_API_HOST_URL") + query["key"] = ENV.fetch("CERC_FORECAST_API_KEY") + HTTParty.get("#{base_url}/#{endpoint}", query: query) + end + end +end diff --git a/app/models/cerc_subscriber_api_client.rb b/app/models/cerc_subscriber_api_client.rb new file mode 100644 index 00000000..23ebed5f --- /dev/null +++ b/app/models/cerc_subscriber_api_client.rb @@ -0,0 +1,56 @@ +class CercSubscriberApiClient + class << self + def find_subscriber(email:, phone:) + # https://www.airtext.info/API/#/subscribers/get-subscriber + query = { + email: email, + phone: phone + } + + request("find-subscriber", :get, query) + end + + def get_subscriptions(subscriber_id) + # https://www.airtext.info/API/#/subscriptions/get-subscriptions + request("subscriptions/#{subscriber_id}", :get) + end + + def create_subscription(zone:, mode:, ampm:, subscriber_id: nil, phone: nil, email: nil, subscriber_details: nil) + # https://www.airtext.info/API/#/subscriptions/store-airtext-subscriber + query = { + subscriberId: subscriber_id, + zone: zone, + mode: mode, + phone: phone, + email: email, + ampm: ampm, + subscriberDetails: subscriber_details + } + + request("subscriptions", :post, query) + end + + def delete_subscription(subscriber_id, subscription_id) + # https://www.airtext.info/API/#/subscriptions/delete-subscription + query = { + subscriberId: subscriber_id, + subscriptionId: subscription_id + } + + request("subscriptions", :delete, query) + end + + private + + def request(endpoint, method, query = {}) + base_url = ENV.fetch("CERC_SUBSCRIBE_API_HOST_URL") + query["key"] = ENV.fetch("CERC_SUBSCRIBE_API_KEY") + + if method == :post + HTTParty.post("#{base_url}/#{endpoint}", body: query.compact) + else + HTTParty.get("#{base_url}/#{endpoint}", query: query.compact) + end + end + end +end diff --git a/app/services/cerc_forecast_service.rb b/app/services/cerc_forecast_service.rb index deea1b64..8bc7a913 100644 --- a/app/services/cerc_forecast_service.rb +++ b/app/services/cerc_forecast_service.rb @@ -13,7 +13,7 @@ def latest_forecasts(zone = nil) private def refresh_cache - cerc_forecasts = CercApiClient.latest_forecasts + cerc_forecasts = CercForecastApiClient.latest_forecasts obtained_at = Time.zone.parse(cerc_forecasts.fetch("forecastdate")) cerc_forecasts.fetch("zones").each do |zone| diff --git a/doc/architecture/decisions/0014-cache-cerc-api-responses-using-postgres.md b/doc/architecture/decisions/0014-cache-cerc-api-responses-using-postgres.md index d0db233f..b76cc8fd 100644 --- a/doc/architecture/decisions/0014-cache-cerc-api-responses-using-postgres.md +++ b/doc/architecture/decisions/0014-cache-cerc-api-responses-using-postgres.md @@ -48,7 +48,8 @@ Each entry in our cache will include these properties: ##### `obtained_at` The timestamp of when the forecast was fetched. We will use this to expire our -cache after `CERC_API_CACHE_LIMIT_MINS` with the `CachedForecast.stale?` test. +cache after `CERC_FORECAST_API_CACHE_LIMIT_MINS` with the +`CachedForecast.stale?` test. ##### `zone` diff --git a/spec/models/cached_forecast_spec.rb b/spec/models/cached_forecast_spec.rb index b40ccca1..940e154b 100644 --- a/spec/models/cached_forecast_spec.rb +++ b/spec/models/cached_forecast_spec.rb @@ -1,6 +1,6 @@ RSpec.describe CachedForecast do around do |example| - env_vars = {CERC_API_CACHE_LIMIT_MINS: "60"} + env_vars = {CERC_FORECAST_API_CACHE_LIMIT_MINS: "60"} ClimateControl.modify(env_vars) { example.run } end @@ -16,10 +16,10 @@ describe "::stale?" do def time_at_cache_limit - Time.current - ENV.fetch("CERC_API_CACHE_LIMIT_MINS").to_i.minutes + Time.current - ENV.fetch("CERC_FORECAST_API_CACHE_LIMIT_MINS").to_i.minutes end - context "when the last record is older than the CERC_API_CACHE_LIMIT_MINS" do + context "when the last record is older than the CERC_FORECAST_API_CACHE_LIMIT_MINS" do before do FactoryBot.create( :cached_forecast, @@ -32,7 +32,7 @@ def time_at_cache_limit end end - context "when the last record is younger than the CERC_API_CACHE_LIMIT_MINS" do + context "when the last record is younger than the CERC_FORECAST_API_CACHE_LIMIT_MINS" do before do FactoryBot.create( :cached_forecast, diff --git a/spec/models/cerc_api_client_spec.rb b/spec/models/cerc_forecast_api_client_spec.rb similarity index 82% rename from spec/models/cerc_api_client_spec.rb rename to spec/models/cerc_forecast_api_client_spec.rb index a741c6b4..d58a5452 100644 --- a/spec/models/cerc_api_client_spec.rb +++ b/spec/models/cerc_forecast_api_client_spec.rb @@ -1,8 +1,8 @@ -RSpec.describe CercApiClient do +RSpec.describe CercForecastApiClient do around do |example| env_vars = { - CERC_API_HOST_URL: "https://example.com", - CERC_API_KEY: "ABC123" + CERC_FORECAST_API_HOST_URL: "https://example.com", + CERC_FORECAST_API_KEY: "ABC123" } ClimateControl.modify(env_vars) { example.run } end @@ -11,7 +11,7 @@ it "makes a request to the API with the expected parameters" do allow(HTTParty).to receive(:get) - CercApiClient.latest_forecasts("North London") + CercForecastApiClient.latest_forecasts("North London") expect(HTTParty).to have_received(:get).with("https://example.com/getforecast/all", { query: { "zone" => "North London", @@ -47,7 +47,7 @@ before { allow(HTTParty).to receive(:get).and_return(forecasts_for_all_zones) } it "asks the CERC API for 3 days worth of forecasts for each zone" do - CercApiClient.latest_forecasts + CercForecastApiClient.latest_forecasts expect(HTTParty).to have_received(:get).with("https://example.com/getforecast/all", { query: { @@ -59,7 +59,7 @@ end it "returns 3 daily forecasts for each zone" do - expect(CercApiClient.latest_forecasts).to eq(forecasts_for_all_zones) + expect(CercForecastApiClient.latest_forecasts).to eq(forecasts_for_all_zones) end end end diff --git a/spec/models/cerc_subscriber_api_client_spec.rb b/spec/models/cerc_subscriber_api_client_spec.rb new file mode 100644 index 00000000..8186321d --- /dev/null +++ b/spec/models/cerc_subscriber_api_client_spec.rb @@ -0,0 +1,88 @@ +RSpec.describe CercSubscriberApiClient do + around do |example| + env_vars = { + CERC_SUBSCRIBE_API_HOST_URL: "https://example.com", + CERC_SUBSCRIBE_API_KEY: "ABC123" + } + ClimateControl.modify(env_vars) { example.run } + end + + describe ".find_subscriber" do + let(:email) { "name@example.com" } + let(:phone) { "555-555-5555" } + + it "makes a GET request to the find-subscriber endpoint" do + query = {email: email, phone: phone} + + expect(CercSubscriberApiClient).to receive(:request).with("find-subscriber", :get, query) + + CercSubscriberApiClient.find_subscriber(email: email, phone: phone) + end + end + + describe ".get_subscriptions" do + let(:subscriber_id) { 123 } + + it "makes a GET request to the subscriptions/:subscriber_id endpoint" do + expect(CercSubscriberApiClient).to receive(:request).with("subscriptions/#{subscriber_id}", :get) + + CercSubscriberApiClient.get_subscriptions(subscriber_id) + end + end + + describe ".create_subscription" do + let(:email) { "name@example.com" } + let(:phone) { "555-555-5555" } + let(:zone) { "zone" } + let(:mode) { "mode" } + let(:ampm) { "ampm" } + let(:subscriber_id) { 123 } + let(:subscriber_details) { {} } + + it "makes a POST request to the subscriptions endpoint" do + query = { + subscriberId: subscriber_id, + zone: zone, + mode: mode, + phone: phone, + email: email, + ampm: ampm, + subscriberDetails: subscriber_details + } + + expect(CercSubscriberApiClient).to receive(:request).with("subscriptions", :post, query) + + CercSubscriberApiClient.create_subscription(subscriber_id: subscriber_id, zone: zone, mode: mode, ampm: ampm, phone: phone, email: email, subscriber_details: subscriber_details) + end + end + + describe ".delete_subscription" do + it "makes a DELETE request to the subscriptions endpoint" do + subscriber_id = 123 + subscription_id = 456 + query = {subscriberId: subscriber_id, subscriptionId: subscription_id} + + expect(CercSubscriberApiClient).to receive(:request).with("subscriptions", :delete, query) + + CercSubscriberApiClient.delete_subscription(subscriber_id, subscription_id) + end + end + + describe ".request" do + it "makes a GET request to the base_url and endpoint" do + endpoint = "find-subscriber" + + expect(HTTParty).to receive(:get).with("https://example.com/#{endpoint}", query: {"key" => "ABC123"}) + + CercSubscriberApiClient.send(:request, endpoint, :get) + end + + it "makes a POST request to the base_url and endpoint" do + endpoint = "subscriptions" + + expect(HTTParty).to receive(:post).with("https://example.com/#{endpoint}", body: {"key" => "ABC123"}) + + CercSubscriberApiClient.send(:request, endpoint, :post) + end + end +end diff --git a/spec/services/cerc_forecast_service_spec.rb b/spec/services/cerc_forecast_service_spec.rb index 5031a63f..bfdb19fb 100644 --- a/spec/services/cerc_forecast_service_spec.rb +++ b/spec/services/cerc_forecast_service_spec.rb @@ -7,13 +7,13 @@ before do allow(CachedForecast).to receive(:stale?).and_return(true) - allow(CercApiClient).to receive(:latest_forecasts).and_return(latest_forecasts_from_api) + allow(CercForecastApiClient).to receive(:latest_forecasts).and_return(latest_forecasts_from_api) end it "asks the CercApiClient for the latest_forecasts (for all zones)" do CercForecastService.latest_forecasts(zone) - expect(CercApiClient).to have_received(:latest_forecasts).with(no_args) + expect(CercForecastApiClient).to have_received(:latest_forecasts).with(no_args) end it "caches a built forecast for each zone" do @@ -27,14 +27,14 @@ let(:latest_forecast_from_cache) { double("CachedForecast") } before do allow(CachedForecast).to receive(:stale?).and_return(false) - allow(CercApiClient).to receive(:latest_forecasts) + allow(CercForecastApiClient).to receive(:latest_forecasts) allow(CachedForecast).to receive(:latest_for).and_return(latest_forecast_from_cache) end it "does not ask the CercApiClient for the latest_forecasts" do CercForecastService.latest_forecasts(zone) - expect(CercApiClient).not_to have_received(:latest_forecasts) + expect(CercForecastApiClient).not_to have_received(:latest_forecasts) end it "returns the cached forecast for the given zone" do