Skip to content

Commit

Permalink
Merge branch 'master' into meb1990-email-confirmation
Browse files Browse the repository at this point in the history
  • Loading branch information
tsr-rise8 authored Mar 6, 2024
2 parents faa9b55 + 85df51d commit cacdfeb
Show file tree
Hide file tree
Showing 13 changed files with 431 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ GEM
ffi
ssrf_filter (1.1.2)
staccato (0.5.3)
statsd-instrument (3.6.1)
statsd-instrument (3.7.0)
strong_migrations (1.7.0)
activerecord (>= 5.2)
super_diff (0.11.0)
Expand Down
20 changes: 20 additions & 0 deletions app/models/schema_contract/validation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module SchemaContract
class Validation < ApplicationRecord
self.table_name = 'schema_contract_validations'

attribute :contract_name, :string
attribute :user_uuid, :string
attribute :response, :jsonb
attribute :error_details, :string

validates :contract_name, presence: true
validates :user_uuid, presence: true
validates :response, presence: true
validates :status, presence: true

enum status: { initialized: 0, success: 1, schema_errors_found: 2, schema_not_found: 3, error: 4 },
_default: :initialized
end
end
21 changes: 21 additions & 0 deletions app/models/schema_contract/validation_initiator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module SchemaContract
class ValidationInitiator
def self.call(user:, response:, contract_name:)
if response.success? && Flipper.enabled?("schema_contract_#{contract_name}")
return if SchemaContract::Validation.where(contract_name:, created_at: Time.zone.today.all_day).any?

record = SchemaContract::Validation.create(
contract_name:, user_uuid: user.uuid, response: response.to_json, status: 'initialized'
)
Rails.logger.info('Initiating schema contract validation', { contract_name:, record_id: record.id })
SchemaContract::ValidationJob.perform_async(record.id)
end
rescue => e
# blanket rescue to avoid blocking main thread execution
message = { response:, contract_name:, error_details: e.message }
Rails.logger.error('Error creating schema contract job', message)
end
end
end
55 changes: 55 additions & 0 deletions app/models/schema_contract/validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module SchemaContract
class Validator
class SchemaContractValidationError < StandardError; end

def initialize(record_id)
@record_id = record_id
end

def validate
errors = JSON::Validator.fully_validate(parsed_schema, record.response)
if errors.any?
@result = :schema_errors_found
record.update(error_details: errors)
detailed_message = { error_type: 'Schema discrepancy found', record_id: @record_id, response: record.response,
details: errors }
raise SchemaContractValidationError, detailed_message
else
@result = :success
end
rescue SchemaContractValidationError => e
raise e
rescue => e
@result = :error
detailed_message = { error_type: 'Unknown', record_id: @record_id, details: e.message }
raise SchemaContractValidationError, detailed_message
ensure
record&.update(status: @result) if defined?(@record)
end

private

def record
@record ||= Validation.find(@record_id)
end

def schema_file
@schema_file ||= begin
path = Settings.schema_contract[record.contract_name]
if path.nil?
@result = :schema_not_found
raise SchemaContractValidationError, "No schema file #{record.contract_name} found."
end

Rails.root.join(path)
end
end

def parsed_schema
file_contents = File.read(schema_file)
JSON.parse(file_contents)
end
end
end
13 changes: 13 additions & 0 deletions app/sidekiq/schema_contract/validation_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module SchemaContract
class ValidationJob
include Sidekiq::Job

sidekiq_options(retry: false)

def perform(contract_name)
SchemaContract::Validator.new(contract_name).validate
end
end
end
2 changes: 1 addition & 1 deletion config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ features:
description: Enables new review page navigation for users completing the Financial Status Report (FSR) form.
enable_in_development: true
find_a_representative_enabled:
actor_type: user
actor_type: cookie_id
description: Enables Find a Representative tool
enable_in_development: true
find_a_representative_enable_frontend:
Expand Down
3 changes: 3 additions & 0 deletions config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1669,3 +1669,6 @@ vye:
r: 8
p: 1
length: 16

schema_contract:
test_index: 'spec/fixtures/schema_contract/test_schema.json'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddStateToForm526Submissions < ActiveRecord::Migration[7.0]
def change
add_column :form526_submissions, :aasm_state, :string
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions spec/factories/schema_contract/validations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

FactoryBot.define do
factory :schema_contract_validation, class: 'SchemaContract::Validation' do
contract_name { 'test_index' }
user_uuid { '1234' }
response { { key: 'value' } }
status { 'initialized' }
end
end
43 changes: 43 additions & 0 deletions spec/fixtures/schema_contract/test_schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-04/schema",
"type": "object",
"required": [
"data",
"meta"
],
"additionalProperties": false,
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"required": [
"required_string",
"required_object"
],
"additionalProperties": false,
"properties": {
"required_string": { "type": "string" },
"optional_nullable_string": { "type": ["string", "null"] },
"required_object": {
"type": "object",
"required": [
"required_nested_string"
],
"additionalProperties": false,
"properties": {
"required_nested_string": { "type": "string" },
"optional_nested_int": { "type": "integer" }
}
}
}
}
},
"meta": {
"type": "array",
"items": {
"type": "object"
}
}
}
}
59 changes: 59 additions & 0 deletions spec/models/schema_contract/validation_initiator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require 'rails_helper'
require_relative Rails.root.join('app', 'models', 'schema_contract', 'validation_initiator')

describe SchemaContract::ValidationInitiator do
describe '.call' do
let(:user) { create(:user) }
let(:response) do
OpenStruct.new({ success?: true, status: 200, body: { key: 'value' } })
end

before do
Timecop.freeze
Flipper.enable(:schema_contract_test_index)
end

context 'when a record already exists for the current day' do
before do
create(:schema_contract_validation, contract_name: 'test_index', user_uuid: '1234', response:,
status: 'initialized')
end

it 'does not create a record or enqueue a job' do
expect(SchemaContract::ValidationJob).not_to receive(:perform_async)

expect do
SchemaContract::ValidationInitiator.call(user:, response:, contract_name: 'test_index')
end.not_to change(SchemaContract::Validation, :count)
end
end

context 'when no record exists for the current day' do
before do
create(:schema_contract_validation, contract_name: 'test_index', user_uuid: '1234', response:,
status: 'initialized', created_at: Time.zone.yesterday.beginning_of_day)
end

it 'creates one with provided details and enqueues a job' do
expect(SchemaContract::ValidationJob).to receive(:perform_async)

expect do
SchemaContract::ValidationInitiator.call(user:, response:, contract_name: 'test_index')
end.to change(SchemaContract::Validation, :count).by(1)
end
end

context 'when an error is encountered' do
it 'logs but does not raise the error' do
allow(SchemaContract::Validation).to receive(:create).with(any_args).and_raise(ArgumentError)
error_message = { response:, contract_name: 'test_index', error_details: 'ArgumentError' }
expect(Rails.logger).to receive(:error).with('Error creating schema contract job', error_message)
expect do
SchemaContract::ValidationInitiator.call(user:, response:, contract_name: 'test_index')
end.not_to raise_error
end
end
end
end
Loading

0 comments on commit cacdfeb

Please sign in to comment.