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:
Ethan Vizitei 2020-04-15 12:05:15 -05:00
parent 17ae63a596
commit 432419fbcd
27 changed files with 1354 additions and 584 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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={})

View File

@ -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={})

View File

@ -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={})

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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