diff --git a/backend/.env.example b/backend/.env.example index ec043c3..053d8f6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,6 +1,7 @@ REDIS_URL=redis://redis:6379/0 +REDIS_CACHE_URL=redis://redis:6379/1 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_HOST=database ELASTICSEARCH_HOST=http://elasticsearch:9200 -COLLEGE_SCORE_CARD_API_KEY= +COLLEGE_SCORE_CARD_API_KEY= find_it_here_https://github.com/RTICWDT/open-data-maker/blob/master/API.md diff --git a/backend/.rubocop.yml b/backend/.rubocop.yml index 4c3021d..44f3dd8 100644 --- a/backend/.rubocop.yml +++ b/backend/.rubocop.yml @@ -102,6 +102,9 @@ RSpec/AnyInstance: RSpec/ImplicitSubject: EnforcedStyle: single_statement_only +RSpec/VerifiedDoubles: + Enabled: false + Style/Documentation: Enabled: false diff --git a/backend/app/actors/fetch_schools.rb b/backend/app/actors/fetch_schools.rb index acbbd7a..18f3af0 100644 --- a/backend/app/actors/fetch_schools.rb +++ b/backend/app/actors/fetch_schools.rb @@ -3,7 +3,7 @@ class FetchSchools < Actor FETCH_SCHOOLS_URL = "https://api.data.gov/ed/collegescorecard/v1/schools" - input :school_name_like + input :school_index_contract output :data @@ -26,7 +26,7 @@ def params api_key:, fields: "id,school.name,location", page: 0, - "school.name" => school_name_like + "school.name" => school_index_contract[:school_name_like] } end diff --git a/backend/app/contracts/school_contracts/index.rb b/backend/app/contracts/school_contracts/index.rb new file mode 100644 index 0000000..6dbc8d4 --- /dev/null +++ b/backend/app/contracts/school_contracts/index.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module SchoolContracts + class Index < ApplicationContract + params do + required(:school_name_like).filled(:string) + end + end +end diff --git a/backend/app/controllers/api/v1/schools_controller.rb b/backend/app/controllers/api/v1/schools_controller.rb new file mode 100644 index 0000000..b672edd --- /dev/null +++ b/backend/app/controllers/api/v1/schools_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Api + module V1 + class SchoolsController < Api::V1::ApiController + skip_before_action :authenticate_devise_api_token! + skip_after_action :verify_authorized + + def index + schools = Rails.cache.fetch(school_index_contract.to_json) do + FetchSchools.result(school_index_contract:).data + end + render json: schools, status: :ok + end + + private + + def school_index_contract + @school_index_contract ||= SchoolContracts::Index.call(permitted_params(:school_name_like)) + end + + def school_name_like + school_index_contract[:school_name_like] + end + end + end +end diff --git a/backend/config/environments/development.rb b/backend/config/environments/development.rb index 6d3dcde..ff8e6b0 100644 --- a/backend/config/environments/development.rb +++ b/backend/config/environments/development.rb @@ -38,6 +38,8 @@ config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } + elsif ENV["REDIS_CACHE_URL"] + config.cache_store = :redis_cache_store, { url: ENV["REDIS_CACHE_URL"] } else config.action_controller.perform_caching = false diff --git a/backend/config/environments/test.rb b/backend/config/environments/test.rb index 7b4cdd3..bd6009e 100644 --- a/backend/config/environments/test.rb +++ b/backend/config/environments/test.rb @@ -33,8 +33,13 @@ # Show full error reports and disable caching. config.consider_all_requests_local = true - config.action_controller.perform_caching = false - config.cache_store = :null_store + config.cache_store = if ENV["REDIS_CACHE_URL"] + config.action_controller.perform_caching = true + [:redis_cache_store, { url: ENV["REDIS_CACHE_URL"] }] + else + config.action_controller.perform_caching = false + :null_store + end # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = :none diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 2d886e9..aa9596d 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -19,8 +19,10 @@ namespace :api, defaults: { format: :json } do namespace :v1 do resources :users, only: %i[index create] + resources :schools, only: %i[index] end end + devise_scope :user do post "/api/v1/tokens", to: "devise/api/tokens#sign_in", as: "api_v1_sign_in_user_token" end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 99253a3..bba1650 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -12,7 +12,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_26_124316) do # These are extensions that must be enabled in order to support this database - enable_extension "citext" enable_extension "plpgsql" create_table "devise_api_tokens", force: :cascade do |t| @@ -31,81 +30,6 @@ t.index ["resource_owner_type", "resource_owner_id"], name: "index_devise_api_tokens_on_resource_owner" end - create_table "event_procedures", force: :cascade do |t| - t.bigint "procedure_id", null: false - t.bigint "patient_id", null: false - t.bigint "hospital_id", null: false - t.bigint "health_insurance_id", null: false - t.string "patient_service_number", null: false - t.datetime "date", null: false - t.boolean "urgency" - t.string "room_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "total_amount_cents" - t.bigint "user_id", null: false - t.boolean "payd", default: false, null: false - t.string "payment", default: "health_insurance", null: false - t.index ["health_insurance_id"], name: "index_event_procedures_on_health_insurance_id" - t.index ["hospital_id"], name: "index_event_procedures_on_hospital_id" - t.index ["patient_id"], name: "index_event_procedures_on_patient_id" - t.index ["procedure_id"], name: "index_event_procedures_on_procedure_id" - t.index ["user_id"], name: "index_event_procedures_on_user_id" - end - - create_table "health_insurances", force: :cascade do |t| - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "custom", default: false, null: false - t.integer "user_id" - t.index ["user_id"], name: "index_health_insurances_on_user_id" - end - - create_table "hospitals", force: :cascade do |t| - t.citext "name", null: false - t.citext "address", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["name"], name: "index_hospitals_on_name", unique: true - end - - create_table "medical_shifts", force: :cascade do |t| - t.bigint "hospital_id", null: false - t.string "workload", null: false - t.datetime "date", null: false - t.integer "amount_cents", default: 0, null: false - t.boolean "was_paid", default: false, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "user_id", null: false - t.index ["date"], name: "index_medical_shifts_on_date" - t.index ["hospital_id"], name: "index_medical_shifts_on_hospital_id" - t.index ["user_id"], name: "index_medical_shifts_on_user_id" - t.index ["was_paid"], name: "index_medical_shifts_on_was_paid" - end - - create_table "patients", force: :cascade do |t| - t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "user_id" - t.index ["user_id"], name: "index_patients_on_user_id" - end - - create_table "procedures", force: :cascade do |t| - t.string "name", null: false - t.citext "code" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "amount_cents", default: 0, null: false - t.text "description" - t.boolean "custom", default: false, null: false - t.integer "user_id" - t.index ["code"], name: "index_procedures_on_code", unique: true, where: "(custom = false)" - t.index ["user_id"], name: "index_procedures_on_user_id" - end - create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -133,9 +57,4 @@ t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" end - add_foreign_key "event_procedures", "health_insurances" - add_foreign_key "event_procedures", "hospitals" - add_foreign_key "event_procedures", "patients" - add_foreign_key "event_procedures", "procedures" - add_foreign_key "medical_shifts", "hospitals" end diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 858680f..501d47c 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -83,18 +83,6 @@ services: interval: 1s timeout: 3s retries: 30 - redis_cache: - image: docker.io/redis:latest - hostname: redis - command: redis-server - ports: - - "6380:6379" - healthcheck: - test: redis-cli ping - interval: 1s - timeout: 3s - retries: 30 - volumes: backend_data: database-data: diff --git a/backend/spec/actors/fetch_schools_spec.rb b/backend/spec/actors/fetch_schools_spec.rb index c0c3d61..7e4c059 100644 --- a/backend/spec/actors/fetch_schools_spec.rb +++ b/backend/spec/actors/fetch_schools_spec.rb @@ -5,30 +5,33 @@ RSpec.describe FetchSchools, type: :actor do describe ".call" do let(:school_name) { "The best school" } + let(:valid_params) do + { + api_key:, + fields: "id,school.name,location", + page: 0, + "school.name" => school_name + } + end - context "when setup is valid" do - let(:valid_params) do - { - api_key:, - fields: "id,school.name,location", - page: 0, - "school.name" => school_name - } - end + let(:school_index_contract) { SchoolContracts::Index.call({ school_name_like: school_name }) } - let(:success_response) { { results: schools }.to_json } + let(:success_response) { { results: schools }.to_json } - let(:schools) do - [ - { "id" => "1", - "school.name" => school_name, - "location.lat" => 42.374471, - "location.lon" => -71.118313 } - ] - end + let(:schools) do + [ + { "id" => "1", + "school.name" => school_name, + "location.lat" => 42.374471, + "location.lon" => -71.118313 } + ] + end + + let(:api_key) { "secret_api_key" } - let(:api_key) { "secret_api_key" } + let(:call) { described_class.result(school_index_contract:) } + context "when setup is valid" do before do allow_any_instance_of(described_class).to receive(:api_key).and_return(api_key) stub_request(:get, described_class::FETCH_SCHOOLS_URL) @@ -37,13 +40,11 @@ end it "is successful" do - result = described_class.result(school_name_like: school_name) - expect(result.success?).to be true + expect(call.success?).to be true end it "returns schools list" do - result = described_class.result(school_name_like: school_name) - expect(result.data).to eq(schools) + expect(call.data).to eq(schools) end end @@ -54,14 +55,12 @@ end it "is failure" do - result = described_class.result(school_name_like: school_name) - expect(result.failure?).to be true + expect(call.failure?).to be true end end it "returns error object" do - result = described_class.result(school_name_like: school_name) - expect(result.error).to eq(:missing_college_score_card_api_key) + expect(call.error).to eq(:missing_college_score_card_api_key) end end end diff --git a/backend/spec/requests/api/v1/schools_request_spec.rb b/backend/spec/requests/api/v1/schools_request_spec.rb new file mode 100644 index 0000000..13b4bdd --- /dev/null +++ b/backend/spec/requests/api/v1/schools_request_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Schools" do + describe "GET /api/v1/schools" do + let(:do_request) { get "/api/v1/schools", params: } + let(:result) { double("Result", data: schools) } + let(:school_name_like) { "Harvard" } + let(:schools) do + [ + { "id" => "1", + "school.name" => school_name_like, + "location.lat" => 42.374471, + "location.lon" => -71.118313 } + ] + end + let(:params) do + { + school_name_like: + } + end + let(:school_index_contract) { SchoolContracts::Index.call(params) } + + context "when schools are not cached" do + before do + allow(FetchSchools).to receive(:result).with(school_index_contract:).and_return(result) + do_request + end + + it { expect(response.parsed_body).to match(schools) } + end + + context "when schools are cached" do + let(:school_index_contract) { SchoolContracts::Index.call(params) } + + before do + Rails.cache.fetch(school_index_contract.to_json) do + schools + end + end + + it "does not calls FetchSchools.result" do + expect(FetchSchools).not_to receive(:result) + do_request + end + + it "returns schools from cache" do + do_request + expect(response.parsed_body).to match(schools) + end + end + end +end diff --git a/backend/spec/support/cache.rb b/backend/spec/support/cache.rb new file mode 100644 index 0000000..8e399ca --- /dev/null +++ b/backend/spec/support/cache.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:suite) do + Rails.cache.clear + end +end