Skip to content

Commit

Permalink
Add new CERC subscriber API
Browse files Browse the repository at this point in the history
We now have two CERC APIs (forecast and subscriber), so we need to rename the other one to avoid confusion.
  • Loading branch information
jdudley1123 committed Nov 29, 2024
1 parent 62dac9a commit 15f855d
Show file tree
Hide file tree
Showing 13 changed files with 203 additions and 42 deletions.
10 changes: 7 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 6 additions & 3 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion app/models/cached_forecast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 0 additions & 16 deletions app/models/cerc_api_client.rb

This file was deleted.

21 changes: 21 additions & 0 deletions app/models/cerc_forecast_api_client.rb
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions app/models/cerc_subscriber_api_client.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion app/services/cerc_forecast_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
8 changes: 4 additions & 4 deletions spec/models/cached_forecast_spec.rb
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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: {
Expand All @@ -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
Expand Down
88 changes: 88 additions & 0 deletions spec/models/cerc_subscriber_api_client_spec.rb
Original file line number Diff line number Diff line change
@@ -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) { "[email protected]" }
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) { "[email protected]" }
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
8 changes: 4 additions & 4 deletions spec/services/cerc_forecast_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 15f855d

Please sign in to comment.