dual-write path for postgres auditors
refs CNVS-48876 flag = none add setting for auditors read/write paths map settings into boolean helpers create config values for AR writing path split backend of event_stream by strategy and confirm writing to both destinations functions test dual writing from config wrap tests around attribute mapping from event stream to active record dual write from all 3 auditor classes via a shared model mixin TEST PLAN: * update dynamic settings to include dual write pattern * login a few times * publish a course * grade an assignment * make sure new auditor records are in cassandra (auditors API calls is fine) * make sure companion records are in the auditor postres table (auditor_authentication_records, auditor_course_records, auditor_grade_change_records) Change-Id: I9b85fc926f7363876f89c82a3fdceb253244fb57 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/234334 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com> QA-Review: Ethan Vizitei <evizitei@instructure.com>
This commit is contained in:
parent
17ae63a596
commit
432419fbcd
|
@ -17,22 +17,54 @@
|
|||
#
|
||||
|
||||
module Auditors
|
||||
def self.stream(&block)
|
||||
::EventStream::Stream.new(&block).tap do |stream|
|
||||
stream.raise_on_error = Rails.env.test?
|
||||
class << self
|
||||
def stream(&block)
|
||||
::EventStream::Stream.new(&block).tap do |stream|
|
||||
stream.raise_on_error = Rails.env.test?
|
||||
|
||||
stream.on_insert do |record|
|
||||
Canvas::EventStreamLogger.info('AUDITOR', identifier, 'insert', record.to_json)
|
||||
end
|
||||
stream.on_insert do |record|
|
||||
Canvas::EventStreamLogger.info('AUDITOR', identifier, 'insert', record.to_json)
|
||||
end
|
||||
|
||||
stream.on_error do |operation, record, exception|
|
||||
next unless Canvas::Cassandra::DatabaseBuilder.configured?(:auditors)
|
||||
Canvas::EventStreamLogger.error('AUDITOR', identifier, operation, record.to_json, exception.message.to_s)
|
||||
stream.on_error do |operation, record, exception|
|
||||
next unless Canvas::Cassandra::DatabaseBuilder.configured?(:auditors)
|
||||
Canvas::EventStreamLogger.error('AUDITOR', identifier, operation, record.to_json, exception.message.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.logger
|
||||
Rails.logger
|
||||
def logger
|
||||
Rails.logger
|
||||
end
|
||||
|
||||
def write_to_cassandra?
|
||||
write_paths.include?('cassandra')
|
||||
end
|
||||
|
||||
def write_to_postgres?
|
||||
write_paths.include?('active_record')
|
||||
end
|
||||
|
||||
def read_from_cassandra?
|
||||
read_path == 'cassandra'
|
||||
end
|
||||
|
||||
def read_from_postgres?
|
||||
read_path == 'active_record'
|
||||
end
|
||||
|
||||
def read_path
|
||||
config&.[]('read_path') || 'cassandra'
|
||||
end
|
||||
|
||||
def write_paths
|
||||
paths = [config&.[]('write_paths')].flatten.compact
|
||||
paths.empty? ? ['cassandra'] : paths
|
||||
end
|
||||
|
||||
def config(shard=Shard.current)
|
||||
settings = Canvas::DynamicSettings.find(tree: :private, cluster: shard.database_server.id)
|
||||
YAML.safe_load(settings['auditors.yml'] || '{}')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,5 +22,19 @@ module Auditors::ActiveRecord
|
|||
self.partitioning_interval = :months
|
||||
self.partitioning_field = 'created_at'
|
||||
self.table_name = 'auditor_authentication_records'
|
||||
|
||||
class << self
|
||||
include Auditors::ActiveRecord::Model
|
||||
|
||||
def ar_attributes_from_event_stream(record)
|
||||
attrs_hash = record.attributes.except('id')
|
||||
attrs_hash['request_id'] ||= "MISSING"
|
||||
attrs_hash['uuid'] = record.id
|
||||
attrs_hash['account_id'] = record.pseudonym.account_id
|
||||
attrs_hash['user_id'] = record.pseudonym.user_id
|
||||
attrs_hash['pseudonym_id'] = record.pseudonym.id
|
||||
attrs_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,5 +22,22 @@ module Auditors::ActiveRecord
|
|||
self.partitioning_interval = :months
|
||||
self.partitioning_field = 'created_at'
|
||||
self.table_name = 'auditor_course_records'
|
||||
|
||||
class << self
|
||||
include Auditors::ActiveRecord::Model
|
||||
|
||||
def ar_attributes_from_event_stream(record)
|
||||
attrs_hash = record.attributes.except('id')
|
||||
attrs_hash['request_id'] ||= "MISSING"
|
||||
attrs_hash['uuid'] = record.id
|
||||
attrs_hash['course_id'] = record.course.id
|
||||
attrs_hash['account_id'] = record.course.account_id
|
||||
attrs_hash['user_id'] = record.user.id
|
||||
if record.sis_batch_id.present?
|
||||
attrs_hash['sis_batch_id'] = record.sis_batch.id
|
||||
end
|
||||
attrs_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,5 +22,25 @@ module Auditors::ActiveRecord
|
|||
self.partitioning_interval = :months
|
||||
self.partitioning_field = 'created_at'
|
||||
self.table_name = 'auditor_grade_change_records'
|
||||
|
||||
class << self
|
||||
include Auditors::ActiveRecord::Model
|
||||
|
||||
def ar_attributes_from_event_stream(record)
|
||||
attrs_hash = record.attributes.except('id', 'version_number')
|
||||
attrs_hash['request_id'] ||= "MISSING"
|
||||
attrs_hash['uuid'] = record.id
|
||||
attrs_hash['account_id'] = record.account.id
|
||||
attrs_hash['root_account_id'] = record.root_account.id
|
||||
attrs_hash['assignment_id'] = record.assignment.id
|
||||
attrs_hash['context_id'] = record.context.id
|
||||
attrs_hash['grader_id'] = record.grader&.id
|
||||
attrs_hash['graded_anonymously'] ||= false
|
||||
attrs_hash['student_id'] = record.student.id
|
||||
attrs_hash['submission_id'] = record.submission.id
|
||||
attrs_hash['submission_version_number'] = record.submission.version_number
|
||||
attrs_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
module Auditors::ActiveRecord
|
||||
# The classes that include this are adapters taking the event_stream
|
||||
# view of attributes for a log and mapping them
|
||||
# the way we would store such data in the db itself.
|
||||
# shard-local ids is the main change, but also sometimes
|
||||
# transforming reserved attribute names like "version_id".
|
||||
# the only required method for each includer is
|
||||
# "ar_attributes_from_event_stream"
|
||||
module Model
|
||||
def ar_attributes_from_event_stream(_record)
|
||||
# here is where hash of attributes should be produced
|
||||
raise "Not Implemented!"
|
||||
end
|
||||
|
||||
def create_from_event_stream!(record)
|
||||
create!(ar_attributes_from_event_stream(record))
|
||||
end
|
||||
|
||||
def update_from_event_stream!(record)
|
||||
db_rec = find_by!(uuid: record.attributes['id'])
|
||||
db_rec.update_attributes!(ar_attributes_from_event_stream(record))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -55,6 +55,8 @@ class Auditors::Authentication
|
|||
end
|
||||
|
||||
Stream = Auditors.stream do
|
||||
backend_strategy :cassandra
|
||||
active_record_type Auditors::ActiveRecord::AuthenticationRecord
|
||||
database -> { Canvas::Cassandra::DatabaseBuilder.from_config(:auditors) }
|
||||
table :authentications
|
||||
record_type Auditors::Authentication::Record
|
||||
|
@ -81,10 +83,13 @@ class Auditors::Authentication
|
|||
|
||||
def self.record(pseudonym, event_type)
|
||||
return unless pseudonym
|
||||
event_record = nil
|
||||
pseudonym.shard.activate do
|
||||
record = Auditors::Authentication::Record.generate(pseudonym, event_type)
|
||||
Auditors::Authentication::Stream.insert(record)
|
||||
event_record = Auditors::Authentication::Record.generate(pseudonym, event_type)
|
||||
Auditors::Authentication::Stream.insert(event_record, {backend_strategy: :cassandra}) if Auditors.write_to_cassandra?
|
||||
Auditors::Authentication::Stream.insert(event_record, {backend_strategy: :active_record}) if Auditors.write_to_postgres?
|
||||
end
|
||||
event_record
|
||||
end
|
||||
|
||||
def self.for_account(account, options={})
|
||||
|
|
|
@ -110,6 +110,8 @@ class Auditors::Course
|
|||
end
|
||||
|
||||
Stream = Auditors.stream do
|
||||
backend_strategy :cassandra
|
||||
active_record_type Auditors::ActiveRecord::CourseRecord
|
||||
database -> { Canvas::Cassandra::DatabaseBuilder.from_config(:auditors) }
|
||||
table :courses
|
||||
record_type Auditors::Course::Record
|
||||
|
@ -198,10 +200,13 @@ class Auditors::Course
|
|||
data[k] = change.map{|v| v.is_a?(String) ? CanvasTextHelper.truncate_text(v, :max_length => 1000) : v}
|
||||
end
|
||||
end
|
||||
event_record = nil
|
||||
course.shard.activate do
|
||||
record = Auditors::Course::Record.generate(course, user, event_type, data, opts)
|
||||
Auditors::Course::Stream.insert(record)
|
||||
event_record = Auditors::Course::Record.generate(course, user, event_type, data, opts)
|
||||
Auditors::Course::Stream.insert(event_record, {backend_strategy: :cassandra}) if Auditors.write_to_cassandra?
|
||||
Auditors::Course::Stream.insert(event_record, {backend_strategy: :active_record}) if Auditors.write_to_postgres?
|
||||
end
|
||||
event_record
|
||||
end
|
||||
|
||||
def self.for_course(course, options={})
|
||||
|
|
|
@ -147,7 +147,10 @@ class Auditors::GradeChange
|
|||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/BlockLength
|
||||
Stream = Auditors.stream do
|
||||
backend_strategy :cassandra
|
||||
active_record_type Auditors::ActiveRecord::GradeChangeRecord
|
||||
database -> { Canvas::Cassandra::DatabaseBuilder.from_config(:auditors) }
|
||||
table :grade_changes
|
||||
record_type Auditors::GradeChange::Record
|
||||
|
@ -232,14 +235,20 @@ class Auditors::GradeChange
|
|||
end
|
||||
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
||||
|
||||
def self.record(skip_insert: false, submission:, event_type: nil)
|
||||
return unless submission
|
||||
event_record = nil
|
||||
submission.shard.activate do
|
||||
record = Auditors::GradeChange::Record.generate(submission, event_type)
|
||||
Canvas::LiveEvents.grade_changed(submission, record.previous_submission, record.previous_assignment)
|
||||
Auditors::GradeChange::Stream.insert(record) unless skip_insert
|
||||
event_record = Auditors::GradeChange::Record.generate(submission, event_type)
|
||||
Canvas::LiveEvents.grade_changed(submission, event_record.previous_submission, event_record.previous_assignment)
|
||||
unless skip_insert
|
||||
Auditors::GradeChange::Stream.insert(event_record, {backend_strategy: :cassandra}) if Auditors.write_to_cassandra?
|
||||
Auditors::GradeChange::Stream.insert(event_record, {backend_strategy: :active_record}) if Auditors.write_to_postgres?
|
||||
end
|
||||
end
|
||||
event_record
|
||||
end
|
||||
|
||||
def self.for_root_account_student(account, student, options={})
|
||||
|
|
|
@ -13,6 +13,10 @@ development:
|
|||
address-book:
|
||||
app-host: "http://address-book.docker"
|
||||
secret: "opensesame"
|
||||
auditors.yml: |
|
||||
write_paths:
|
||||
- cassandra
|
||||
read_path: cassandra
|
||||
canvas:
|
||||
encryption-secret: "astringthatisactually32byteslong"
|
||||
signing-secret: "astringthatisactually32byteslong"
|
||||
|
@ -51,4 +55,6 @@ development:
|
|||
jwk-future.json: '{"kty":"RSA","e":"AQAB","n":"uX1MpfEMQCBUMcj0sBYI-iFaG5Nodp3C6OlN8uY60fa5zSBd83-iIL3n_qzZ8VCluuTLfB7rrV_tiX727XIEqQ","kid":"2018-07-18T22:33:20Z","d":"pYwR64x-LYFtA13iHIIeEvfPTws50ZutyGfpHN-kIZz3k-xVpun2Hgu0hVKZMxcZJ9DkG8UZPqD-zTDbCmCyLQ","p":"6OQ2bi_oY5fE9KfQOcxkmNhxDnIKObKb6TVYqOOz2JM","q":"y-UBef95njOrqMAxJH1QPds3ltYWr8QgGgccmcATH1M","dp":"Ol_xkL7rZgNFt_lURRiJYpJmDDPjgkDVuafIeFTS4Ic","dq":"RtzDY5wXr5TzrwWEztLCpYzfyAuF_PZj1cfs976apsM","qi":"XA5wnwIrwe5MwXpaBijZsGhKJoypZProt47aVCtWtPE"}'
|
||||
private:
|
||||
canvas:
|
||||
datadog_apm.yml: "sample_rate: 0.0\nhost_sample_rate: 0.0"
|
||||
datadog_apm.yml: |
|
||||
sample_rate: 0.0
|
||||
host_sample_rate: 0.0
|
||||
|
|
|
@ -23,6 +23,7 @@ require 'inst_statsd'
|
|||
|
||||
module EventStream
|
||||
require 'event_stream/attr_config'
|
||||
require 'event_stream/backend'
|
||||
require 'event_stream/record'
|
||||
require 'event_stream/failure'
|
||||
require 'event_stream/stream'
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
require 'event_stream/backend/strategy'
|
||||
require 'event_stream/backend/cassandra'
|
||||
require 'event_stream/backend/active_record'
|
||||
|
||||
module EventStream
|
||||
module Backend
|
||||
def self.for_strategy(stream, strategy_name)
|
||||
const_get(strategy_name.to_s.classify, false).new(stream)
|
||||
rescue NameError
|
||||
raise "Unknown EventStream Strategy: #{strategy_name}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
module EventStream::Backend
|
||||
class ActiveRecord
|
||||
include Strategy
|
||||
attr_accessor :stream, :active_record_type
|
||||
|
||||
def initialize(stream_obj)
|
||||
@stream = stream_obj
|
||||
@active_record_type = stream_obj.active_record_type
|
||||
end
|
||||
|
||||
class Unavailable < RuntimeError; end
|
||||
|
||||
def available?
|
||||
active_record_type.connection.active?
|
||||
end
|
||||
|
||||
def execute(operation, record)
|
||||
unless available?
|
||||
stream.run_callbacks(:error, operation, record, Unavailable.new)
|
||||
return
|
||||
end
|
||||
|
||||
send(operation, record)
|
||||
stream.run_callbacks(operation, record)
|
||||
rescue StandardError => exception
|
||||
stream.run_callbacks(:error, operation, record, exception)
|
||||
raise if stream.raise_on_error
|
||||
end
|
||||
|
||||
private
|
||||
def insert(record)
|
||||
active_record_type.create_from_event_stream!(record)
|
||||
end
|
||||
|
||||
def update(record)
|
||||
active_record_type.update_from_event_stream!(record)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
module EventStream::Backend
|
||||
class Cassandra
|
||||
include Strategy
|
||||
attr_accessor :stream, :database
|
||||
|
||||
def initialize(stream_obj)
|
||||
@stream = stream_obj
|
||||
@database = stream_obj.database
|
||||
end
|
||||
|
||||
class Unavailable < RuntimeError; end
|
||||
|
||||
def available?
|
||||
!!database && database.available?
|
||||
end
|
||||
|
||||
def execute(operation, record)
|
||||
unless stream.available?
|
||||
stream.run_callbacks(:error, operation, record, Unavailable.new)
|
||||
return
|
||||
end
|
||||
|
||||
ttl_seconds = stream.ttl_seconds(record.created_at)
|
||||
return if ttl_seconds < 0
|
||||
|
||||
database.batch do
|
||||
stream.database.send(:"#{operation}_record", stream.table, {stream.id_column => record.id}, stream.operation_payload(operation, record), ttl_seconds)
|
||||
stream.run_callbacks(operation, record)
|
||||
end
|
||||
rescue StandardError => exception
|
||||
stream.run_callbacks(:error, operation, record, exception)
|
||||
raise if stream.raise_on_error
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
module EventStream::Backend
|
||||
# this is supposed to be a bit like an interface.
|
||||
# Backend classes that include this module should know
|
||||
# what methods they're expected to provide an implementation for.
|
||||
# See the cassandra.rb and active_record.rb fils in this directory for
|
||||
# examples
|
||||
module Strategy
|
||||
def available?
|
||||
raise "Not Implemented"
|
||||
end
|
||||
|
||||
def execute(_operation, _record)
|
||||
raise "Not Implemented"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,12 +19,14 @@
|
|||
class EventStream::Stream
|
||||
include EventStream::AttrConfig
|
||||
|
||||
attr_config :database, :default => nil
|
||||
attr_config :database, :default => nil # only needed if backend_strategy is :cassandra
|
||||
attr_config :table, :type => String
|
||||
attr_config :id_column, :type => String, :default => 'id'
|
||||
attr_config :record_type, :default => EventStream::Record
|
||||
attr_config :time_to_live, :type => Integer, :default => 1.year
|
||||
attr_config :read_consistency_level, :default => nil
|
||||
attr_config :time_to_live, :type => Integer, :default => 1.year # only honored for cassandra strategy
|
||||
attr_config :read_consistency_level, :default => nil # only honored for cassandra strategy
|
||||
attr_config :backend_strategy, default: :cassandra # one of [:cassandra, :active_record]
|
||||
attr_config :active_record_type, default: nil # only needed if backend_strategy is :active_record
|
||||
|
||||
attr_accessor :raise_on_error
|
||||
|
||||
|
@ -45,8 +47,9 @@ class EventStream::Stream
|
|||
add_callback(:insert, callback)
|
||||
end
|
||||
|
||||
def insert(record)
|
||||
execute(:insert, record)
|
||||
def insert(record, options={})
|
||||
backend = backend_for(options.fetch(:backend_strategy, backend_strategy))
|
||||
backend.execute(:insert, record)
|
||||
record
|
||||
end
|
||||
|
||||
|
@ -54,8 +57,9 @@ class EventStream::Stream
|
|||
add_callback(:update, callback)
|
||||
end
|
||||
|
||||
def update(record)
|
||||
execute(:update, record)
|
||||
def update(record, options={})
|
||||
backend = backend_for(options.fetch(:backend_strategy, backend_strategy))
|
||||
backend.execute(:update, record)
|
||||
record
|
||||
end
|
||||
|
||||
|
@ -116,40 +120,26 @@ class EventStream::Stream
|
|||
"SELECT * FROM #{table} %CONSISTENCY% WHERE #{id_column} IN (?)"
|
||||
end
|
||||
|
||||
def run_callbacks(operation, *args)
|
||||
callbacks_for(operation).each do |callback|
|
||||
instance_exec(*args, &callback)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def backend_for(strategy)
|
||||
@backends ||= {}
|
||||
@backends[strategy] ||= EventStream::Backend.for_strategy(self, strategy)
|
||||
end
|
||||
|
||||
def callbacks_for(operation)
|
||||
@callbacks ||= {}
|
||||
@callbacks[operation] ||= []
|
||||
end
|
||||
|
||||
class Unavailable < Exception; end
|
||||
|
||||
def execute(operation, record)
|
||||
unless available?
|
||||
run_callbacks(:error, operation, record, Unavailable.new)
|
||||
return
|
||||
end
|
||||
|
||||
ttl_seconds = self.ttl_seconds(record.created_at)
|
||||
return if ttl_seconds < 0
|
||||
|
||||
database.batch do
|
||||
database.send(:"#{operation}_record", table, {id_column => record.id}, operation_payload(operation, record), ttl_seconds)
|
||||
run_callbacks(operation, record)
|
||||
end
|
||||
rescue Exception => exception
|
||||
run_callbacks(:error, operation, record, exception)
|
||||
raise if raise_on_error
|
||||
end
|
||||
|
||||
def add_callback(operation, callback)
|
||||
callbacks_for(operation) << callback
|
||||
end
|
||||
|
||||
def run_callbacks(operation, *args)
|
||||
callbacks_for(operation).each do |callback|
|
||||
instance_exec(*args, &callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe EventStream::Backend::ActiveRecord do
|
||||
let(:ar_type) do
|
||||
Class.new do
|
||||
class << self
|
||||
def reset!
|
||||
@recs = []
|
||||
end
|
||||
|
||||
def written_recs
|
||||
@recs ||= []
|
||||
end
|
||||
|
||||
def create_from_event_stream!(rec)
|
||||
@recs ||= []
|
||||
@recs << rec
|
||||
end
|
||||
|
||||
def connection
|
||||
self
|
||||
end
|
||||
|
||||
def active?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:stream) do
|
||||
ar_cls = ar_type
|
||||
s = EventStream::Stream.new do
|
||||
table "test_table"
|
||||
active_record_type ar_cls
|
||||
end
|
||||
s.raise_on_error = true
|
||||
s
|
||||
end
|
||||
|
||||
let(:event_record) { OpenStruct.new(field: "value") }
|
||||
|
||||
describe "executing operations" do
|
||||
after(:each) do
|
||||
ar_type.reset!
|
||||
end
|
||||
|
||||
it "proxies calls through provided AR model" do
|
||||
ar_backend = EventStream::Backend::ActiveRecord.new(stream)
|
||||
ar_backend.execute(:insert, event_record)
|
||||
expect(ar_type.written_recs.first).to eq(event_record)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,77 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe EventStream::Backend::Cassandra do
|
||||
let(:database) do
|
||||
database = double('database')
|
||||
|
||||
def database.batch
|
||||
yield
|
||||
end
|
||||
|
||||
def database.update_record(*args); end
|
||||
|
||||
def database.insert_record(*args)
|
||||
@inserted ||= []
|
||||
@inserted << args
|
||||
end
|
||||
|
||||
# rubocop:disable Style/TrivialAccessors
|
||||
def database.inserted
|
||||
@inserted
|
||||
end
|
||||
# rubocop:enable Style/TrivialAccessors
|
||||
|
||||
def database.update(*args); end
|
||||
|
||||
def database.available?
|
||||
true
|
||||
end
|
||||
|
||||
def database.keyspace
|
||||
'test_db'
|
||||
end
|
||||
|
||||
database
|
||||
end
|
||||
|
||||
let(:stream) do
|
||||
db = database
|
||||
s = EventStream::Stream.new do
|
||||
table "test_table"
|
||||
database db
|
||||
end
|
||||
s.raise_on_error = true
|
||||
s
|
||||
end
|
||||
|
||||
let(:event_record) do
|
||||
OpenStruct.new(field: "value", created_at: Time.zone.now, id: "big-uuid")
|
||||
end
|
||||
|
||||
describe "executing operations" do
|
||||
it "proxies calls through provided cassandra db" do
|
||||
cass_backend = EventStream::Backend::Cassandra.new(stream)
|
||||
cass_backend.execute(:insert, event_record)
|
||||
expect(database.inserted.size).to eq(1)
|
||||
expect(database.inserted.first[1]['id']).to eq('big-uuid')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,44 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
require 'spec_helper'
|
||||
|
||||
describe EventStream::Backend do
|
||||
describe "backend selection from strategy" do
|
||||
let(:stream) do
|
||||
EventStream::Stream.new do
|
||||
table "test_table"
|
||||
end
|
||||
end
|
||||
|
||||
it "has a cassandra strategy" do
|
||||
expect(EventStream::Backend.for_strategy(stream, :cassandra)).to(
|
||||
be_a(EventStream::Backend::Cassandra)
|
||||
)
|
||||
end
|
||||
|
||||
it "has an AR strategy" do
|
||||
expect(EventStream::Backend.for_strategy(stream, :active_record)).to(
|
||||
be_a(EventStream::Backend::ActiveRecord)
|
||||
)
|
||||
end
|
||||
|
||||
it "rejects other strategies" do
|
||||
expect { EventStream::Backend.for_strategy(stream, :redis) }.to raise_error(RuntimeError)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -33,7 +33,7 @@ describe EventStream::Failure do
|
|||
allow(@stream).to receive(:operation_payload).with(:insert, @record).and_return(@record.attributes)
|
||||
allow(@stream).to receive(:operation_payload).with(:update, @record).and_return(@record.changes)
|
||||
|
||||
@exception = Exception.new
|
||||
@exception = StandardError.new
|
||||
allow(@exception).to receive(:message).and_return(double('exception_message', :to_s => 'exception_message_string'))
|
||||
allow(@exception).to receive(:backtrace).and_return([42])
|
||||
end
|
||||
|
|
|
@ -437,10 +437,11 @@ describe EventStream::Stream do
|
|||
allow(@stream).to receive(:database).and_return(@database)
|
||||
@record = double(
|
||||
:id => 'id',
|
||||
:created_at => Time.now,
|
||||
:created_at => Time.zone.now,
|
||||
:attributes => {'attribute' => 'attribute_value'},
|
||||
:changes => {'changed_attribute' => 'changed_value'})
|
||||
@exception = Exception.new
|
||||
:changes => {'changed_attribute' => 'changed_value'}
|
||||
)
|
||||
@exception = StandardError.new
|
||||
end
|
||||
|
||||
shared_examples_for "error callbacks" do
|
||||
|
@ -463,7 +464,7 @@ describe EventStream::Stream do
|
|||
@stream.raise_on_error = true
|
||||
@stream.on_error{ spy.trigger }
|
||||
expect(spy).to receive(:trigger).once
|
||||
expect{ @stream.insert(@record) }.to raise_exception(Exception)
|
||||
expect{ @stream.insert(@record) }.to raise_exception(StandardError)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -19,7 +19,45 @@
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../../sharding_spec_helper.rb')
|
||||
|
||||
describe Auditors::ActiveRecord::AuthenticationRecord do
|
||||
let(:request_id){ 'abcde-12345'}
|
||||
|
||||
before(:each) do
|
||||
allow(RequestContextGenerator).to receive_messages(request_id: request_id)
|
||||
end
|
||||
|
||||
it "it appropriately connected to a table" do
|
||||
Auditors::ActiveRecord::AuthenticationRecord.delete_all
|
||||
expect(Auditors::ActiveRecord::AuthenticationRecord.count).to eq(0)
|
||||
end
|
||||
|
||||
describe "mapping from event stream record" do
|
||||
let(:user_record){ user_with_pseudonym }
|
||||
let(:pseudonym_record){ user_record.pseudonym }
|
||||
let(:es_record){ Auditors::Authentication::Record.generate(pseudonym_record, 'login') }
|
||||
|
||||
it "is creatable from an event_stream record of the correct type" do
|
||||
ar_rec = Auditors::ActiveRecord::AuthenticationRecord.create_from_event_stream!(es_record)
|
||||
expect(ar_rec.id).to_not be_nil
|
||||
expect(ar_rec.uuid).to eq(es_record.id)
|
||||
expect(ar_rec.request_id).to eq(request_id)
|
||||
expect(ar_rec.pseudonym_id).to eq(pseudonym_record.id)
|
||||
expect(ar_rec.account_id).to eq(pseudonym_record.account_id)
|
||||
expect(ar_rec.user_id).to eq(user_record.id)
|
||||
end
|
||||
|
||||
it "is updatable from ES record" do
|
||||
ar_rec = Auditors::ActiveRecord::AuthenticationRecord.create_from_event_stream!(es_record)
|
||||
es_record.request_id = "aaa-111-bbb-222"
|
||||
Auditors::ActiveRecord::AuthenticationRecord.update_from_event_stream!(es_record)
|
||||
expect(ar_rec.reload.request_id).to eq("aaa-111-bbb-222")
|
||||
end
|
||||
|
||||
it "fails predictably on attempted update to missing value" do
|
||||
unpersisted_rec = es_record
|
||||
expect do
|
||||
Auditors::ActiveRecord::AuthenticationRecord.update_from_event_stream!(unpersisted_rec)
|
||||
end.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -19,7 +19,37 @@
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../../sharding_spec_helper.rb')
|
||||
|
||||
describe Auditors::ActiveRecord::CourseRecord do
|
||||
let(:request_id){ 'abcde-12345'}
|
||||
|
||||
it "it appropriately connected to a table" do
|
||||
expect(Auditors::ActiveRecord::CourseRecord.count).to eq(0)
|
||||
end
|
||||
|
||||
describe "mapping from event stream record" do
|
||||
let(:course_enrollment){ course_with_student }
|
||||
let(:course_record){ course_enrollment.course }
|
||||
let(:user_record){ course_enrollment.user }
|
||||
let(:event_data){ {"data-key" => "data-val"} }
|
||||
let(:es_record){ Auditors::Course::Record.generate(course_record, user_record, 'unconcluded', event_data) }
|
||||
|
||||
it "is creatable from an event_stream record of the correct type" do
|
||||
ar_rec = Auditors::ActiveRecord::CourseRecord.create_from_event_stream!(es_record)
|
||||
expect(ar_rec.id).to_not be_nil
|
||||
expect(ar_rec.uuid).to eq(es_record.id)
|
||||
expect(ar_rec.course_id).to eq(course_record.id)
|
||||
expect(ar_rec.user_id).to eq(user_record.id)
|
||||
expect(ar_rec.event_source).to eq("manual")
|
||||
expect(ar_rec.event_type).to eq("unconcluded")
|
||||
expect(JSON.parse(ar_rec.data)).to eq({"data-key" => "data-val"})
|
||||
expect(ar_rec.sis_batch_id).to be_nil
|
||||
expect(ar_rec.created_at).to_not be_nil
|
||||
end
|
||||
|
||||
it "is updatable from ES record" do
|
||||
ar_rec = Auditors::ActiveRecord::CourseRecord.create_from_event_stream!(es_record)
|
||||
es_record.request_id = "aaa-111-bbb-222"
|
||||
Auditors::ActiveRecord::CourseRecord.update_from_event_stream!(es_record)
|
||||
expect(ar_rec.reload.request_id).to eq("aaa-111-bbb-222")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,7 +19,40 @@
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../../sharding_spec_helper.rb')
|
||||
|
||||
describe Auditors::ActiveRecord::GradeChangeRecord do
|
||||
let(:request_id){ 'abcde-12345'}
|
||||
|
||||
it "it appropriately connected to a table" do
|
||||
expect(Auditors::ActiveRecord::GradeChangeRecord.count).to eq(0)
|
||||
end
|
||||
|
||||
describe "mapping from event stream record" do
|
||||
let(:submission_record){ graded_submission_model }
|
||||
let(:es_record){ Auditors::GradeChange::Record.generate(submission_record) }
|
||||
|
||||
it "is creatable from an event_stream record of the correct type" do
|
||||
ar_rec = Auditors::ActiveRecord::GradeChangeRecord.create_from_event_stream!(es_record)
|
||||
expect(ar_rec.id).to_not be_nil
|
||||
expect(ar_rec.uuid).to eq(es_record.id)
|
||||
course = submission_record.assignment.context
|
||||
expect(ar_rec.grade_after).to eq(es_record.grade_after)
|
||||
expect(ar_rec.account_id).to eq(course.account.id)
|
||||
expect(ar_rec.root_account_id).to eq(course.account.root_account.id)
|
||||
expect(ar_rec.assignment_id).to eq(submission_record.assignment_id)
|
||||
expect(ar_rec.event_type).to eq("grade_change")
|
||||
expect(ar_rec.context_id).to eq(course.id)
|
||||
expect(ar_rec.context_type).to eq('Course')
|
||||
expect(ar_rec.grader_id).to eq(submission_record.grader_id)
|
||||
expect(ar_rec.student_id).to eq(submission_record.user_id)
|
||||
expect(ar_rec.submission_id).to eq(submission_record.id)
|
||||
expect(ar_rec.submission_version_number).to eq(submission_record.version_number)
|
||||
expect(ar_rec.created_at).to_not be_nil
|
||||
end
|
||||
|
||||
it "is updatable from ES record" do
|
||||
ar_rec = Auditors::ActiveRecord::GradeChangeRecord.create_from_event_stream!(es_record)
|
||||
es_record.request_id = "aaa-111-bbb-222"
|
||||
Auditors::ActiveRecord::GradeChangeRecord.update_from_event_stream!(es_record)
|
||||
expect(ar_rec.reload.request_id).to eq("aaa-111-bbb-222")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,207 +20,237 @@ require File.expand_path(File.dirname(__FILE__) + '/../../sharding_spec_helper.r
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../cassandra_spec_helper')
|
||||
|
||||
describe Auditors::Authentication do
|
||||
include_examples "cassandra audit logs"
|
||||
|
||||
let(:request_id) { 42 }
|
||||
|
||||
before do
|
||||
before(:each) do
|
||||
shard_class = Class.new {
|
||||
define_method(:activate) { |&b| b.call }
|
||||
}
|
||||
|
||||
EventStream.current_shard_lookup = lambda {
|
||||
shard_class.new
|
||||
}
|
||||
|
||||
allow(RequestContextGenerator).to receive_messages( :request_id => request_id )
|
||||
@account = Account.default
|
||||
user_with_pseudonym(active_all: true)
|
||||
@event = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
allow(RequestContextGenerator).to receive_messages(request_id: request_id)
|
||||
end
|
||||
|
||||
context "nominal cases" do
|
||||
it "should include event for pseudonym" do
|
||||
expect(Auditors::Authentication.for_pseudonym(@pseudonym).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
let(:request_id) { 42 }
|
||||
|
||||
it "should include event for account" do
|
||||
expect(Auditors::Authentication.for_account(@account).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
describe "with cassandra backend" do
|
||||
include_examples "cassandra audit logs"
|
||||
|
||||
it "should include event at user" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
|
||||
it "should set request_id" do
|
||||
expect(@event.request_id).to eq request_id.to_s
|
||||
end
|
||||
|
||||
it "doesn't record an error when not configured" do
|
||||
allow(Auditors::Authentication::Stream).to receive(:database).and_return(nil)
|
||||
expect(Canvas::Cassandra::DatabaseBuilder).to receive(:configured?).with(:auditors).once.and_return(false)
|
||||
expect(Canvas::EventStreamLogger).to receive(:error).never
|
||||
Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
end
|
||||
|
||||
context "with a second account (same user)" do
|
||||
before do
|
||||
@account = account_model
|
||||
user_with_pseudonym(user: @user, account: @account, active_all: true)
|
||||
end
|
||||
|
||||
it "should not include cross-account events for pseudonym" do
|
||||
expect(Auditors::Authentication.for_pseudonym(@pseudonym).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
end
|
||||
|
||||
it "should not include cross-account events for account" do
|
||||
expect(Auditors::Authentication.for_account(@account).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
end
|
||||
|
||||
it "should include cross-account events for user" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a second user (same account)" do
|
||||
before do
|
||||
allow(Auditors).to receive(:config).and_return({'write_paths' => ['cassandra'], 'read_path' => 'cassandra'})
|
||||
@account = Account.default
|
||||
user_with_pseudonym(active_all: true)
|
||||
@event = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
|
||||
it "should not include cross-user events for pseudonym" do
|
||||
expect(Auditors::Authentication.for_pseudonym(@pseudonym).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
context "nominal cases" do
|
||||
it "returns the event on generation" do
|
||||
expect(@event.class).to eq(Auditors::Authentication::Record)
|
||||
end
|
||||
|
||||
it "should include event for pseudonym" do
|
||||
expect(Auditors::Authentication.for_pseudonym(@pseudonym).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
|
||||
it "should include event for account" do
|
||||
expect(Auditors::Authentication.for_account(@account).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
|
||||
it "should include event at user" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
|
||||
it "should set request_id" do
|
||||
expect(@event.request_id).to eq request_id.to_s
|
||||
end
|
||||
|
||||
it "doesn't record an error when not configured" do
|
||||
allow(Auditors::Authentication::Stream).to receive(:database).and_return(nil)
|
||||
expect(Canvas::Cassandra::DatabaseBuilder).to receive(:configured?).with(:auditors).once.and_return(false)
|
||||
expect(Canvas::EventStreamLogger).to receive(:error).never
|
||||
Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
end
|
||||
|
||||
it "should include cross-user events for account" do
|
||||
expect(Auditors::Authentication.for_account(@account).paginate(:per_page => 1)).
|
||||
context "with a second account (same user)" do
|
||||
before do
|
||||
@account = account_model
|
||||
user_with_pseudonym(user: @user, account: @account, active_all: true)
|
||||
end
|
||||
|
||||
it "should not include cross-account events for pseudonym" do
|
||||
expect(Auditors::Authentication.for_pseudonym(@pseudonym).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
end
|
||||
|
||||
it "should not include cross-account events for account" do
|
||||
expect(Auditors::Authentication.for_account(@account).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
end
|
||||
|
||||
it "should include cross-account events for user" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a second user (same account)" do
|
||||
before do
|
||||
user_with_pseudonym(active_all: true)
|
||||
end
|
||||
|
||||
it "should not include cross-user events for pseudonym" do
|
||||
expect(Auditors::Authentication.for_pseudonym(@pseudonym).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
end
|
||||
|
||||
it "should include cross-user events for account" do
|
||||
expect(Auditors::Authentication.for_account(@account).paginate(:per_page => 1)).
|
||||
to include(@event)
|
||||
end
|
||||
|
||||
it "should not include cross-user events for user" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
describe "options forwarding" do
|
||||
before do
|
||||
@event2 = @pseudonym.shard.activate do
|
||||
record = Auditors::Authentication::Record.new(
|
||||
'id' => SecureRandom.uuid,
|
||||
'created_at' => 1.day.ago,
|
||||
'pseudonym' => @pseudonym,
|
||||
'event_type' => 'login'
|
||||
)
|
||||
Auditors::Authentication::Stream.insert(record)
|
||||
end
|
||||
end
|
||||
|
||||
it "should recognize :oldest for pseudonyms" do
|
||||
page = Auditors::Authentication.
|
||||
for_pseudonym(@pseudonym, oldest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest for pseudonyms" do
|
||||
page = Auditors::Authentication.
|
||||
for_pseudonym(@pseudonym, newest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
end
|
||||
|
||||
it "should recognize :oldest for accounts" do
|
||||
page = Auditors::Authentication.
|
||||
for_account(@account, oldest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest for accounts" do
|
||||
page = Auditors::Authentication.
|
||||
for_account(@account, newest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
end
|
||||
|
||||
it "should recognize :oldest for users" do
|
||||
page = Auditors::Authentication.
|
||||
for_user(@user, oldest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest for users" do
|
||||
page = Auditors::Authentication.
|
||||
for_user(@user, newest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
describe "sharding" do
|
||||
specs_require_sharding
|
||||
|
||||
context "different shard, same database server" do
|
||||
before do
|
||||
@shard1.activate do
|
||||
@account = account_model
|
||||
user_with_pseudonym(account: @account, active_all: true)
|
||||
@event1 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
user_with_pseudonym(user: @user, active_all: true)
|
||||
@event2 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
|
||||
it "should include events from the user's native shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event1)
|
||||
end
|
||||
|
||||
it "should include events from the other pseudonym's shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event2)
|
||||
end
|
||||
|
||||
it "should not include duplicate events" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 4).
|
||||
size).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context "different shard, different database server" do
|
||||
before do
|
||||
@shard2.activate do
|
||||
@account = account_model
|
||||
user_with_pseudonym(account: @account, active_all: true)
|
||||
@event1 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
user_with_pseudonym(user: @user, active_all: true)
|
||||
@event2 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
|
||||
it "should include events from the user's native shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event1)
|
||||
end
|
||||
|
||||
it "should include events from the other pseudonym's shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with dual writing enabled to postgres" do
|
||||
before do
|
||||
allow(Auditors).to receive(:config).and_return({'write_paths' => ['cassandra', 'active_record'], 'read_path' => 'cassandra'})
|
||||
@account = Account.default
|
||||
user_with_pseudonym(active_all: true)
|
||||
@event = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
|
||||
it "writes to cassandra" do
|
||||
expect(Auditors.write_to_cassandra?).to eq(true)
|
||||
expect(Auditors::Authentication.for_pseudonym(@pseudonym).paginate(per_page: 1)).
|
||||
to include(@event)
|
||||
end
|
||||
|
||||
it "should not include cross-user events for user" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 1)).
|
||||
not_to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
describe "options forwarding" do
|
||||
before do
|
||||
@event2 = @pseudonym.shard.activate do
|
||||
record = Auditors::Authentication::Record.new(
|
||||
'id' => SecureRandom.uuid,
|
||||
'created_at' => 1.day.ago,
|
||||
'pseudonym' => @pseudonym,
|
||||
'event_type' => 'login')
|
||||
Auditors::Authentication::Stream.insert(record)
|
||||
end
|
||||
end
|
||||
|
||||
it "should recognize :oldest for pseudonyms" do
|
||||
page = Auditors::Authentication.
|
||||
for_pseudonym(@pseudonym, oldest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest for pseudonyms" do
|
||||
page = Auditors::Authentication.
|
||||
for_pseudonym(@pseudonym, newest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
end
|
||||
|
||||
it "should recognize :oldest for accounts" do
|
||||
page = Auditors::Authentication.
|
||||
for_account(@account, oldest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest for accounts" do
|
||||
page = Auditors::Authentication.
|
||||
for_account(@account, newest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
end
|
||||
|
||||
it "should recognize :oldest for users" do
|
||||
page = Auditors::Authentication.
|
||||
for_user(@user, oldest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest for users" do
|
||||
page = Auditors::Authentication.
|
||||
for_user(@user, newest: 12.hours.ago).
|
||||
paginate(:per_page => 1)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
describe "sharding" do
|
||||
specs_require_sharding
|
||||
|
||||
context "different shard, same database server" do
|
||||
before do
|
||||
@shard1.activate do
|
||||
@account = account_model
|
||||
user_with_pseudonym(account: @account, active_all: true)
|
||||
@event1 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
user_with_pseudonym(user: @user, active_all: true)
|
||||
@event2 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
|
||||
it "should include events from the user's native shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event1)
|
||||
end
|
||||
|
||||
it "should include events from the other pseudonym's shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event2)
|
||||
end
|
||||
|
||||
it "should not include duplicate events" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 4).
|
||||
size).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context "different shard, different database server" do
|
||||
before do
|
||||
@shard2.activate do
|
||||
@account = account_model
|
||||
user_with_pseudonym(account: @account, active_all: true)
|
||||
@event1 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
user_with_pseudonym(user: @user, active_all: true)
|
||||
@event2 = Auditors::Authentication.record(@pseudonym, 'login')
|
||||
end
|
||||
|
||||
it "should include events from the user's native shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event1)
|
||||
end
|
||||
|
||||
it "should include events from the other pseudonym's shard" do
|
||||
expect(Auditors::Authentication.for_user(@user).paginate(:per_page => 2)).
|
||||
to include(@event2)
|
||||
end
|
||||
it "writes to postgres" do
|
||||
expect(Auditors.write_to_postgres?).to eq(true)
|
||||
pg_record = Auditors::ActiveRecord::AuthenticationRecord.where(uuid: @event.id).first
|
||||
expect(pg_record.pseudonym_id).to eq(@pseudonym.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,12 +20,10 @@ require File.expand_path(File.dirname(__FILE__) + '/../../sharding_spec_helper.r
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../cassandra_spec_helper')
|
||||
|
||||
describe Auditors::Course do
|
||||
include_examples "cassandra audit logs"
|
||||
|
||||
let(:request_id) { 42 }
|
||||
|
||||
before do
|
||||
allow(RequestContextGenerator).to receive_messages( :request_id => request_id )
|
||||
allow(RequestContextGenerator).to receive_messages(:request_id => request_id)
|
||||
|
||||
@account = Account.default
|
||||
@sub_account = Account.create!(:parent_account => @account)
|
||||
|
@ -34,159 +32,189 @@ describe Auditors::Course do
|
|||
course_with_teacher(course_name: "Course 1", account: @sub_sub_account)
|
||||
|
||||
@course.name = "Course 2"
|
||||
@course.start_at = Date.today
|
||||
@course.conclude_at = Date.today + 7.days
|
||||
@course.start_at = Time.zone.today
|
||||
@course.conclude_at = Time.zone.today + 7.days
|
||||
end
|
||||
|
||||
context "nominal cases" do
|
||||
it "should include event" do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(Auditors::Course.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::Course.for_account(@course.account).paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
|
||||
it "should set request_id" do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(@event.request_id).to eq request_id.to_s
|
||||
end
|
||||
|
||||
it "should truncate super long changes" do
|
||||
@course.syllabus_body = "ohnoes" * 10_000
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(@event.attributes["data"].length < 3_000).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "event source" do
|
||||
it "should default event source to :manual" do
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes)
|
||||
expect(@event.event_source).to eq :manual
|
||||
end
|
||||
|
||||
it "should log event with api source" do
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes, source: :api)
|
||||
expect(@event.event_source).to eq :api
|
||||
end
|
||||
|
||||
it "should log event with sis_batch_id and event source of sis" do
|
||||
sis_batch = @account.root_account.sis_batches.create
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes, source: :sis, sis_batch: sis_batch)
|
||||
expect(@event.event_source).to eq :sis
|
||||
expect(@event.sis_batch_id).to eq sis_batch.id
|
||||
end
|
||||
end
|
||||
|
||||
context "type specific" do
|
||||
it "should log created event" do
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "created"
|
||||
expect(@event.event_data).to eq @course.changes
|
||||
end
|
||||
|
||||
it "should log updated event" do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "updated"
|
||||
expect(@event.event_data).to eq @course.changes
|
||||
end
|
||||
|
||||
it "should log concluded event" do
|
||||
@event = Auditors::Course.record_concluded(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "concluded"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log unconcluded event" do
|
||||
@event = Auditors::Course.record_unconcluded(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "unconcluded"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log published event" do
|
||||
@event = Auditors::Course.record_published(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "published"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log deleted event" do
|
||||
@event = Auditors::Course.record_deleted(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "deleted"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log restored event" do
|
||||
@event = Auditors::Course.record_restored(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "restored"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log copied event" do
|
||||
@course, @copy_course = @course, course_factory(active_all: true)
|
||||
@from_event, @to_event = Auditors::Course.record_copied(@course, @copy_course, @teacher, source: :api)
|
||||
|
||||
expect(@from_event.course).to eq @copy_course
|
||||
expect(@from_event.event_type).to eq "copied_from"
|
||||
expect(@from_event.event_data).to eq({ :"copied_from" => Shard.global_id_for(@course) })
|
||||
|
||||
expect(@to_event.course).to eq @course
|
||||
expect(@to_event.event_type).to eq "copied_to"
|
||||
expect(@to_event.event_data).to eq({ :"copied_to" => Shard.global_id_for(@copy_course) })
|
||||
end
|
||||
|
||||
it "should log reset event" do
|
||||
@course, @new_course = @course, course_factory(active_all: true)
|
||||
@from_event, @to_event = Auditors::Course.record_reset(@course, @new_course, @teacher, source: :api)
|
||||
|
||||
expect(@from_event.course).to eq @new_course
|
||||
expect(@from_event.event_type).to eq "reset_from"
|
||||
expect(@from_event.event_data).to eq({ :"reset_from" => Shard.global_id_for(@course) })
|
||||
|
||||
expect(@to_event.course).to eq @course
|
||||
expect(@to_event.event_type).to eq "reset_to"
|
||||
expect(@to_event.event_data).to eq({ :"reset_to" => Shard.global_id_for(@new_course) })
|
||||
end
|
||||
end
|
||||
|
||||
describe "options forwarding" do
|
||||
describe "with cassandra backend" do
|
||||
before do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
|
||||
record = Auditors::Course::Record.new(
|
||||
'course' => @course,
|
||||
'user' => @teacher,
|
||||
'event_type' => 'updated',
|
||||
'event_data' => @course.changes,
|
||||
'event_source' => 'manual',
|
||||
'sis_batch_id' => nil,
|
||||
'created_at' => 1.day.ago
|
||||
)
|
||||
@event2 = Auditors::Course::Stream.insert(record)
|
||||
allow(Auditors).to receive(:config).and_return({'write_paths' => ['cassandra'], 'read_path' => 'cassandra'})
|
||||
end
|
||||
|
||||
it "should recognize :oldest" do
|
||||
page = Auditors::Course.for_course(@course, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
include_examples "cassandra audit logs"
|
||||
|
||||
acct_page = Auditors::Course.for_account(@course.account, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(acct_page).to include(@event)
|
||||
expect(acct_page).not_to include(@event2)
|
||||
context "nominal cases" do
|
||||
it "should include event" do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(Auditors::Course.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::Course.for_account(@course.account).paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
|
||||
it "should set request_id" do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(@event.request_id).to eq request_id.to_s
|
||||
end
|
||||
|
||||
it "should truncate super long changes" do
|
||||
@course.syllabus_body = "ohnoes" * 10_000
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(@event.attributes["data"].length < 3_000).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
it "should recognize :newest" do
|
||||
page = Auditors::Course.for_course(@course, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
context "event source" do
|
||||
it "should default event source to :manual" do
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes)
|
||||
expect(@event.event_source).to eq :manual
|
||||
end
|
||||
|
||||
acct_page = Auditors::Course.for_account(@course.account, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(acct_page).to include(@event2)
|
||||
expect(acct_page).not_to include(@event)
|
||||
it "should log event with api source" do
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes, source: :api)
|
||||
expect(@event.event_source).to eq :api
|
||||
end
|
||||
|
||||
it "should log event with sis_batch_id and event source of sis" do
|
||||
sis_batch = @account.root_account.sis_batches.create
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes, source: :sis, sis_batch: sis_batch)
|
||||
expect(@event.event_source).to eq :sis
|
||||
expect(@event.sis_batch_id).to eq sis_batch.global_id
|
||||
end
|
||||
end
|
||||
|
||||
context "type specific" do
|
||||
it "should log created event" do
|
||||
@event = Auditors::Course.record_created(@course, @teacher, @course.changes)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "created"
|
||||
expect(@event.event_data).to eq @course.changes
|
||||
end
|
||||
|
||||
it "should log updated event" do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "updated"
|
||||
expect(@event.event_data).to eq @course.changes
|
||||
end
|
||||
|
||||
it "should log concluded event" do
|
||||
@event = Auditors::Course.record_concluded(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "concluded"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log unconcluded event" do
|
||||
@event = Auditors::Course.record_unconcluded(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "unconcluded"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log published event" do
|
||||
@event = Auditors::Course.record_published(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "published"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log deleted event" do
|
||||
@event = Auditors::Course.record_deleted(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "deleted"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log restored event" do
|
||||
@event = Auditors::Course.record_restored(@course, @teacher)
|
||||
expect(@event.course).to eq @course
|
||||
expect(@event.event_type).to eq "restored"
|
||||
expect(@event.event_data).to eq({})
|
||||
end
|
||||
|
||||
it "should log copied event" do
|
||||
@course, @copy_course = @course, course_factory(active_all: true)
|
||||
@from_event, @to_event = Auditors::Course.record_copied(@course, @copy_course, @teacher, source: :api)
|
||||
|
||||
expect(@from_event.course).to eq @copy_course
|
||||
expect(@from_event.event_type).to eq "copied_from"
|
||||
expect(@from_event.event_data[:copied_from]).to eq(Shard.global_id_for(@course))
|
||||
|
||||
expect(@to_event.course).to eq @course
|
||||
expect(@to_event.event_type).to eq "copied_to"
|
||||
expect(@to_event.event_data[:copied_to]).to eq(Shard.global_id_for(@copy_course))
|
||||
end
|
||||
|
||||
it "should log reset event" do
|
||||
@course, @new_course = @course, course_factory(active_all: true)
|
||||
@from_event, @to_event = Auditors::Course.record_reset(@course, @new_course, @teacher, source: :api)
|
||||
|
||||
expect(@from_event.course).to eq @new_course
|
||||
expect(@from_event.event_type).to eq "reset_from"
|
||||
expect(@from_event.event_data[:reset_from]).to eq(Shard.global_id_for(@course))
|
||||
|
||||
expect(@to_event.course).to eq @course
|
||||
expect(@to_event.event_type).to eq "reset_to"
|
||||
expect(@to_event.event_data[:reset_to]).to eq(Shard.global_id_for(@new_course))
|
||||
end
|
||||
end
|
||||
|
||||
describe "options forwarding" do
|
||||
before do
|
||||
@event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
|
||||
record = Auditors::Course::Record.new(
|
||||
'course' => @course,
|
||||
'user' => @teacher,
|
||||
'event_type' => 'updated',
|
||||
'event_data' => @course.changes,
|
||||
'event_source' => 'manual',
|
||||
'sis_batch_id' => nil,
|
||||
'created_at' => 1.day.ago
|
||||
)
|
||||
@event2 = Auditors::Course::Stream.insert(record)
|
||||
end
|
||||
|
||||
it "should recognize :oldest" do
|
||||
page = Auditors::Course.for_course(@course, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
|
||||
acct_page = Auditors::Course.for_account(@course.account, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(acct_page).to include(@event)
|
||||
expect(acct_page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest" do
|
||||
page = Auditors::Course.for_course(@course, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
|
||||
acct_page = Auditors::Course.for_account(@course.account, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(acct_page).to include(@event2)
|
||||
expect(acct_page).not_to include(@event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with dual writing enabled to postgres" do
|
||||
before do
|
||||
allow(Auditors).to receive(:config).and_return({'write_paths' => ['cassandra', 'active_record'], 'read_path' => 'cassandra'})
|
||||
end
|
||||
|
||||
it "writes to cassandra" do
|
||||
event = Auditors::Course.record_updated(@course, @teacher, @course.changes)
|
||||
expect(Auditors.write_to_cassandra?).to eq(true)
|
||||
expect(Auditors::Course.for_course(@course).paginate(:per_page => 5)).to include(event)
|
||||
end
|
||||
|
||||
it "writes to postgres" do
|
||||
sis_batch = @account.root_account.sis_batches.create
|
||||
event = Auditors::Course.record_created(@course, @teacher, @course.changes, source: :sis, sis_batch: sis_batch)
|
||||
expect(Auditors.write_to_postgres?).to eq(true)
|
||||
pg_record = Auditors::ActiveRecord::CourseRecord.where(uuid: event.id).first
|
||||
expect(pg_record).to_not be_nil
|
||||
expect(pg_record.course_id).to eq(@course.id)
|
||||
expect(pg_record.sis_batch_id).to eq(sis_batch.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,12 +20,11 @@ require File.expand_path(File.dirname(__FILE__) + '/../../sharding_spec_helper.r
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../cassandra_spec_helper')
|
||||
|
||||
describe Auditors::GradeChange do
|
||||
include_examples "cassandra audit logs"
|
||||
|
||||
let(:request_id) { 42 }
|
||||
|
||||
before do
|
||||
allow(RequestContextGenerator).to receive_messages( :request_id => request_id )
|
||||
before(:each) do
|
||||
allow(RequestContextGenerator).to receive_messages(request_id: request_id)
|
||||
|
||||
shard_class = Class.new {
|
||||
define_method(:activate) { |&b| b.call }
|
||||
|
@ -44,227 +43,254 @@ describe Auditors::GradeChange do
|
|||
|
||||
@assignment = @course.assignments.create!(:title => 'Assignment', :points_possible => 10)
|
||||
@submission = @assignment.grade_student(@student, grade: 8, grader: @teacher).first
|
||||
@event_time = Time.at(1.hour.ago.to_i) # cassandra doesn't remember microseconds
|
||||
Timecop.freeze(@event_time) { @event = Auditors::GradeChange.record(submission: @submission) }
|
||||
@event_time = Time.zone.at(1.hour.ago.to_i) # cassandra doesn't remember microseconds
|
||||
end
|
||||
|
||||
def test_course_and_other_contexts
|
||||
# course assignment
|
||||
contexts = { assignment: @assignment }
|
||||
yield contexts
|
||||
# course assignment grader
|
||||
contexts[:grader] = @teacher
|
||||
yield contexts
|
||||
# course assignment grader student
|
||||
contexts[:student] = @student
|
||||
yield contexts
|
||||
# course assignment student
|
||||
contexts.delete(:grader)
|
||||
yield contexts
|
||||
# course grader
|
||||
contexts = { grader: @teacher }
|
||||
yield contexts
|
||||
# course grader student
|
||||
contexts[:student] = @student
|
||||
yield contexts
|
||||
# course student
|
||||
contexts.delete(:grader)
|
||||
yield contexts
|
||||
end
|
||||
describe "with cassandra backend" do
|
||||
include_examples "cassandra audit logs"
|
||||
before do
|
||||
allow(Auditors).to receive(:config).and_return({'write_paths' => ['cassandra'], 'read_path' => 'cassandra'})
|
||||
Timecop.freeze(@event_time) { @event = Auditors::GradeChange.record(submission: @submission) }
|
||||
end
|
||||
|
||||
context "nominal cases" do
|
||||
it "should include event" do
|
||||
expect(@event.created_at).to eq @event_time
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_grader(@account, @teacher).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
def test_course_and_other_contexts
|
||||
# course assignment
|
||||
contexts = { assignment: @assignment }
|
||||
yield contexts
|
||||
# course assignment grader
|
||||
contexts[:grader] = @teacher
|
||||
yield contexts
|
||||
# course assignment grader student
|
||||
contexts[:student] = @student
|
||||
yield contexts
|
||||
# course assignment student
|
||||
contexts.delete(:grader)
|
||||
yield contexts
|
||||
# course grader
|
||||
contexts = { grader: @teacher }
|
||||
yield contexts
|
||||
# course grader student
|
||||
contexts[:student] = @student
|
||||
yield contexts
|
||||
# course student
|
||||
contexts.delete(:grader)
|
||||
yield contexts
|
||||
end
|
||||
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).
|
||||
context "nominal cases" do
|
||||
it "should include event" do
|
||||
expect(@event.created_at).to eq @event_time
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_grader(@account, @teacher).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "should include event for nil grader" do
|
||||
# We don't want to index events for nil graders.
|
||||
|
||||
@submission = @assignment.grade_student(@student, grade: 6, grader: @teacher).first
|
||||
@event = Auditors::GradeChange.record(submission: @submission)
|
||||
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment,
|
||||
student: @student}).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {student: @student}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
|
||||
it "should include event for auto grader" do
|
||||
# Currently we are not indexing events for auto grader in cassandra.
|
||||
|
||||
@submission.score = 5
|
||||
@submission.grader_id = -1
|
||||
@event = Auditors::GradeChange.record(submission: @submission)
|
||||
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment,
|
||||
student: @student}).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {student: @student}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
end
|
||||
|
||||
it "should set request_id" do
|
||||
expect(@event.request_id).to eq request_id.to_s
|
||||
end
|
||||
end
|
||||
|
||||
it "should include event for nil grader" do
|
||||
# We don't want to index events for nil graders.
|
||||
it "reports excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
@event = Auditors::GradeChange.record(submission: @excused)
|
||||
|
||||
@submission = @assignment.grade_student(@student, grade: 6, grader: @teacher).first
|
||||
@event = Auditors::GradeChange.record(submission: @submission)
|
||||
for_assignment = Auditors::GradeChange.for_assignment(@assignment)
|
||||
for_course = Auditors::GradeChange.for_course(@course)
|
||||
for_root_account_student = Auditors::GradeChange.for_root_account_student(@account, @student)
|
||||
expect(for_assignment.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_course.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_root_account_student.paginate(per_page: 5)).to include(@event)
|
||||
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment,
|
||||
student: @student}).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {student: @student}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).paginate(per_page: 5)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "should include event for auto grader" do
|
||||
# Currently we are not indexing events for auto grader in cassandra.
|
||||
it "reports formerly excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
Auditors::GradeChange.record(submission: @excused)
|
||||
@unexcused = @assignment.grade_student(@student, grader: @teacher, excused: false).first
|
||||
@event = Auditors::GradeChange.record(submission: @unexcused)
|
||||
|
||||
for_assignment = Auditors::GradeChange.for_assignment(@assignment)
|
||||
for_course = Auditors::GradeChange.for_course(@course)
|
||||
for_root_account_student = Auditors::GradeChange.for_root_account_student(@account, @student)
|
||||
|
||||
expect(for_assignment.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_course.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_root_account_student.paginate(per_page: 5)).to include(@event)
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).paginate(per_page: 5)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "records excused_before and excused_after as booleans on initial grading" do
|
||||
expect(@event.excused_before).to be(false)
|
||||
expect(@event.excused_after).to be(false)
|
||||
end
|
||||
|
||||
it "records excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
@event = Auditors::GradeChange.record(submission: @excused)
|
||||
|
||||
expect(@event.grade_before).to eq(@submission.grade)
|
||||
expect(@event.grade_after).to be_nil
|
||||
expect(@event.excused_before).to be(false)
|
||||
expect(@event.excused_after).to be(true)
|
||||
end
|
||||
|
||||
it "records formerly excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
Auditors::GradeChange.record(submission: @excused)
|
||||
@unexcused = @assignment.grade_student(@student, grader: @teacher, excused: false).first
|
||||
@event = Auditors::GradeChange.record(submission: @unexcused)
|
||||
|
||||
expect(@event.grade_before).to be_nil
|
||||
expect(@event.grade_after).to be_nil
|
||||
expect(@event.excused_before).to be(true)
|
||||
expect(@event.excused_after).to be(false)
|
||||
end
|
||||
|
||||
it "records regraded submissions" do
|
||||
@submission.score = 5
|
||||
@submission.grader_id = -1
|
||||
@submission.with_versioning(:explicit => true, &:save!)
|
||||
@event = Auditors::GradeChange.record(submission: @submission)
|
||||
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course(@course).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_root_account_student(@account, @student).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {assignment: @assignment,
|
||||
student: @student}).paginate(:per_page => 5)).to include(@event)
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, {student: @student}).
|
||||
paginate(:per_page => 5)).to include(@event)
|
||||
expect(@event.score_before).to eq 8
|
||||
expect(@event.score_after).to eq 5
|
||||
end
|
||||
|
||||
it "should set request_id" do
|
||||
expect(@event.request_id).to eq request_id.to_s
|
||||
it "records grades affected by assignment update" do
|
||||
@assignment.points_possible = 15
|
||||
@assignment.save!
|
||||
@submission.assignment_changed_not_sub = true
|
||||
@event = Auditors::GradeChange.record(submission: @submission)
|
||||
|
||||
expect(@event.points_possible_before).to eq 10
|
||||
expect(@event.points_possible_after).to eq 15
|
||||
end
|
||||
|
||||
describe "options forwarding" do
|
||||
before do
|
||||
record = Auditors::GradeChange::Record.new(
|
||||
'submission' => @submission,
|
||||
'created_at' => 1.day.ago
|
||||
)
|
||||
@event2 = Auditors::GradeChange::Stream.insert(record)
|
||||
end
|
||||
|
||||
it "should recognize :oldest" do
|
||||
page = Auditors::GradeChange.for_assignment(@assignment, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
|
||||
page = Auditors::GradeChange.for_course(@course, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_student(@account, @student, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_grader(@account, @teacher, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
end
|
||||
|
||||
it "should recognize :newest" do
|
||||
page = Auditors::GradeChange.for_assignment(@assignment, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
|
||||
page = Auditors::GradeChange.for_course(@course, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_student(@account, @student, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_grader(@account, @teacher, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "inserts a record" do
|
||||
expect(Auditors::GradeChange::Stream).to receive(:insert).once
|
||||
Auditors::GradeChange.record(submission: @submission)
|
||||
end
|
||||
|
||||
it "does not insert a record if skip_insert is true" do
|
||||
expect(Auditors::GradeChange::Stream).not_to receive(:insert)
|
||||
Auditors::GradeChange.record(submission: @submission, skip_insert: true)
|
||||
end
|
||||
end
|
||||
|
||||
it "reports excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
@event = Auditors::GradeChange.record(submission: @excused)
|
||||
|
||||
for_assignment = Auditors::GradeChange.for_assignment(@assignment)
|
||||
for_course = Auditors::GradeChange.for_course(@course)
|
||||
for_root_account_student = Auditors::GradeChange.for_root_account_student(@account, @student)
|
||||
expect(for_assignment.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_course.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_root_account_student.paginate(per_page: 5)).to include(@event)
|
||||
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).paginate(per_page: 5)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "reports formerly excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
Auditors::GradeChange.record(submission: @excused)
|
||||
@unexcused = @assignment.grade_student(@student, grader: @teacher, excused: false).first
|
||||
@event = Auditors::GradeChange.record(submission: @unexcused)
|
||||
|
||||
for_assignment = Auditors::GradeChange.for_assignment(@assignment)
|
||||
for_course = Auditors::GradeChange.for_course(@course)
|
||||
for_root_account_student = Auditors::GradeChange.for_root_account_student(@account, @student)
|
||||
|
||||
expect(for_assignment.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_course.paginate(per_page: 5)).to include(@event)
|
||||
expect(for_root_account_student.paginate(per_page: 5)).to include(@event)
|
||||
test_course_and_other_contexts do |contexts|
|
||||
expect(Auditors::GradeChange.for_course_and_other_arguments(@course, contexts).paginate(per_page: 5)).
|
||||
to include(@event)
|
||||
end
|
||||
end
|
||||
|
||||
it "records excused_before and excused_after as booleans on initial grading" do
|
||||
expect(@event.excused_before).to eql(false)
|
||||
expect(@event.excused_after).to eql(false)
|
||||
end
|
||||
|
||||
it "records excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
@event = Auditors::GradeChange.record(submission: @excused)
|
||||
|
||||
expect(@event.grade_before).to eql(@submission.grade)
|
||||
expect(@event.grade_after).to be_nil
|
||||
expect(@event.excused_before).to eql(false)
|
||||
expect(@event.excused_after).to eql(true)
|
||||
end
|
||||
|
||||
it "records formerly excused submissions" do
|
||||
@excused = @assignment.grade_student(@student, grader: @teacher, excused: true).first
|
||||
Auditors::GradeChange.record(submission: @excused)
|
||||
@unexcused = @assignment.grade_student(@student, grader: @teacher, excused: false).first
|
||||
@event = Auditors::GradeChange.record(submission: @unexcused)
|
||||
|
||||
expect(@event.grade_before).to be_nil
|
||||
expect(@event.grade_after).to be_nil
|
||||
expect(@event.excused_before).to eql(true)
|
||||
expect(@event.excused_after).to eql(false)
|
||||
end
|
||||
|
||||
it "records regraded submissions" do
|
||||
@submission.score = 5
|
||||
@submission.with_versioning(:explicit => true, &:save!)
|
||||
@event = Auditors::GradeChange.record(submission: @submission)
|
||||
|
||||
expect(@event.score_before).to eq 8
|
||||
expect(@event.score_after).to eq 5
|
||||
end
|
||||
|
||||
it "records grades affected by assignment update" do
|
||||
@assignment.points_possible = 15
|
||||
@assignment.save!
|
||||
@submission.assignment_changed_not_sub = true
|
||||
@event = Auditors::GradeChange.record(submission: @submission)
|
||||
|
||||
expect(@event.points_possible_before).to eq 10
|
||||
expect(@event.points_possible_after).to eq 15
|
||||
end
|
||||
|
||||
describe "options forwarding" do
|
||||
describe "with dual writing enabled to postgres" do
|
||||
before do
|
||||
record = Auditors::GradeChange::Record.new(
|
||||
'submission' => @submission,
|
||||
'created_at' => 1.day.ago
|
||||
)
|
||||
@event2 = Auditors::GradeChange::Stream.insert(record)
|
||||
allow(Auditors).to receive(:config).and_return({'write_paths' => ['cassandra', 'active_record'], 'read_path' => 'cassandra'})
|
||||
end
|
||||
|
||||
it "should recognize :oldest" do
|
||||
page = Auditors::GradeChange.for_assignment(@assignment, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
|
||||
page = Auditors::GradeChange.for_course(@course, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_student(@account, @student, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_grader(@account, @teacher, oldest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event)
|
||||
expect(page).not_to include(@event2)
|
||||
it "writes to cassandra" do
|
||||
event = Auditors::GradeChange.record(submission: @submission)
|
||||
expect(Auditors.write_to_cassandra?).to eq(true)
|
||||
expect(Auditors::GradeChange.for_assignment(@assignment).paginate(:per_page => 5)).to include(event)
|
||||
end
|
||||
|
||||
it "should recognize :newest" do
|
||||
page = Auditors::GradeChange.for_assignment(@assignment, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
|
||||
page = Auditors::GradeChange.for_course(@course, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_student(@account, @student, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
|
||||
page = Auditors::GradeChange.for_root_account_grader(@account, @teacher, newest: 12.hours.ago).paginate(:per_page => 2)
|
||||
expect(page).to include(@event2)
|
||||
expect(page).not_to include(@event)
|
||||
it "writes to postgres" do
|
||||
event = Auditors::GradeChange.record(submission: @submission)
|
||||
expect(Auditors.write_to_postgres?).to eq(true)
|
||||
pg_record = Auditors::ActiveRecord::GradeChangeRecord.where(uuid: event.id).first
|
||||
expect(pg_record).to_not be_nil
|
||||
expect(pg_record.submission_id).to eq(@submission.id)
|
||||
end
|
||||
end
|
||||
|
||||
it "inserts a record" do
|
||||
expect(Auditors::GradeChange::Stream).to receive(:insert).once
|
||||
Auditors::GradeChange.record(submission: @submission)
|
||||
end
|
||||
|
||||
it "does not insert a record if skip_insert is true" do
|
||||
expect(Auditors::GradeChange::Stream).not_to receive(:insert)
|
||||
Auditors::GradeChange.record(submission: @submission, skip_insert: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
#
|
||||
# Copyright (C) 2020 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Auditors do
|
||||
|
||||
after(:each) do
|
||||
Canvas::DynamicSettings.config = nil
|
||||
Canvas::DynamicSettings.reset_cache!
|
||||
Canvas::DynamicSettings.fallback_data = nil
|
||||
end
|
||||
|
||||
def inject_auditors_settings(yaml_string)
|
||||
Canvas::DynamicSettings.fallback_data = {
|
||||
"private": {
|
||||
"canvas": {
|
||||
"auditors.yml": yaml_string
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
describe "settings parsing" do
|
||||
it "parses pre-change write paths" do
|
||||
inject_auditors_settings("write_paths:\n - cassandra\nread_path: cassandra")
|
||||
expect(Auditors.write_to_cassandra?).to eq(true)
|
||||
expect(Auditors.write_to_postgres?).to eq(false)
|
||||
expect(Auditors.read_from_cassandra?).to eq(true)
|
||||
expect(Auditors.read_from_postgres?).to eq(false)
|
||||
end
|
||||
|
||||
it "understands dual write path" do
|
||||
inject_auditors_settings("write_paths:\n - cassandra\n - active_record\nread_path: cassandra")
|
||||
expect(Auditors.write_to_cassandra?).to eq(true)
|
||||
expect(Auditors.write_to_postgres?).to eq(true)
|
||||
expect(Auditors.read_from_cassandra?).to eq(true)
|
||||
expect(Auditors.read_from_postgres?).to eq(false)
|
||||
end
|
||||
|
||||
it "understands postgres reading path" do
|
||||
inject_auditors_settings("write_paths:\n - cassandra\n - active_record\nread_path: active_record")
|
||||
expect(Auditors.write_to_cassandra?).to eq(true)
|
||||
expect(Auditors.write_to_postgres?).to eq(true)
|
||||
expect(Auditors.read_from_cassandra?).to eq(false)
|
||||
expect(Auditors.read_from_postgres?).to eq(true)
|
||||
end
|
||||
|
||||
it "understands full cutover" do
|
||||
inject_auditors_settings("write_paths:\n - active_record\nread_path: active_record")
|
||||
expect(Auditors.write_to_cassandra?).to eq(false)
|
||||
expect(Auditors.write_to_postgres?).to eq(true)
|
||||
expect(Auditors.read_from_cassandra?).to eq(false)
|
||||
expect(Auditors.read_from_postgres?).to eq(true)
|
||||
end
|
||||
|
||||
it "defaults to cassandra read/write" do
|
||||
expect(Auditors.write_to_cassandra?).to eq(true)
|
||||
expect(Auditors.write_to_postgres?).to eq(false)
|
||||
expect(Auditors.read_from_cassandra?).to eq(true)
|
||||
expect(Auditors.read_from_postgres?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
Loading…
Reference in New Issue