canvas_sync 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +178 -0
  3. data/Rakefile +23 -0
  4. data/lib/canvas_sync.rb +96 -0
  5. data/lib/canvas_sync/generators/install_generator.rb +54 -0
  6. data/lib/canvas_sync/generators/templates/course.rb +8 -0
  7. data/lib/canvas_sync/generators/templates/create_courses.rb +21 -0
  8. data/lib/canvas_sync/generators/templates/create_enrollments.rb +26 -0
  9. data/lib/canvas_sync/generators/templates/create_sections.rb +20 -0
  10. data/lib/canvas_sync/generators/templates/create_terms.rb +18 -0
  11. data/lib/canvas_sync/generators/templates/create_users.rb +18 -0
  12. data/lib/canvas_sync/generators/templates/enrollment.rb +8 -0
  13. data/lib/canvas_sync/generators/templates/section.rb +7 -0
  14. data/lib/canvas_sync/generators/templates/term.rb +28 -0
  15. data/lib/canvas_sync/generators/templates/user.rb +6 -0
  16. data/lib/canvas_sync/importers/bulk_importer.rb +54 -0
  17. data/lib/canvas_sync/jobs/application_job.rb +25 -0
  18. data/lib/canvas_sync/jobs/report_checker.rb +48 -0
  19. data/lib/canvas_sync/jobs/report_processor_job.rb +36 -0
  20. data/lib/canvas_sync/jobs/report_starter.rb +29 -0
  21. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +58 -0
  22. data/lib/canvas_sync/jobs/sync_terms_job.rb +23 -0
  23. data/lib/canvas_sync/jobs/sync_users_job.rb +32 -0
  24. data/lib/canvas_sync/processors/provisioning_report_processor.rb +118 -0
  25. data/lib/canvas_sync/version.rb +3 -0
  26. data/spec/canvas_sync/canvas_sync_spec.rb +60 -0
  27. data/spec/canvas_sync/jobs/report_checker_spec.rb +62 -0
  28. data/spec/canvas_sync/jobs/report_processor_job_spec.rb +30 -0
  29. data/spec/canvas_sync/jobs/report_starter_spec.rb +27 -0
  30. data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +81 -0
  31. data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +18 -0
  32. data/spec/canvas_sync/jobs/sync_users_job_spec.rb +18 -0
  33. data/spec/canvas_sync/models/course_spec.rb +30 -0
  34. data/spec/canvas_sync/models/enrollment_spec.rb +30 -0
  35. data/spec/canvas_sync/models/section_spec.rb +24 -0
  36. data/spec/canvas_sync/models/term_spec.rb +71 -0
  37. data/spec/canvas_sync/models/user_spec.rb +18 -0
  38. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +41 -0
  39. data/spec/dummy/README.rdoc +1 -0
  40. data/spec/dummy/Rakefile +6 -0
  41. data/spec/dummy/app/models/application_record.rb +3 -0
  42. data/spec/dummy/app/models/course.rb +14 -0
  43. data/spec/dummy/app/models/enrollment.rb +14 -0
  44. data/spec/dummy/app/models/section.rb +13 -0
  45. data/spec/dummy/app/models/term.rb +34 -0
  46. data/spec/dummy/app/models/user.rb +12 -0
  47. data/spec/dummy/bin/rails +4 -0
  48. data/spec/dummy/config.ru +4 -0
  49. data/spec/dummy/config/application.rb +26 -0
  50. data/spec/dummy/config/boot.rb +5 -0
  51. data/spec/dummy/config/database.yml +25 -0
  52. data/spec/dummy/config/environment.rb +5 -0
  53. data/spec/dummy/config/environments/development.rb +41 -0
  54. data/spec/dummy/config/environments/test.rb +42 -0
  55. data/spec/dummy/config/initializers/assets.rb +11 -0
  56. data/spec/dummy/config/initializers/session_store.rb +3 -0
  57. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  58. data/spec/dummy/config/routes.rb +2 -0
  59. data/spec/dummy/config/secrets.yml +22 -0
  60. data/spec/dummy/db/development.sqlite3 +0 -0
  61. data/spec/dummy/db/migrate/20170831220702_create_courses.rb +27 -0
  62. data/spec/dummy/db/migrate/20170831221129_create_users.rb +24 -0
  63. data/spec/dummy/db/migrate/20170905192509_create_enrollments.rb +32 -0
  64. data/spec/dummy/db/migrate/20170906193506_create_terms.rb +24 -0
  65. data/spec/dummy/db/migrate/20170906203438_create_sections.rb +26 -0
  66. data/spec/dummy/db/schema.rb +88 -0
  67. data/spec/dummy/db/test.sqlite3 +0 -0
  68. data/spec/dummy/log/development.log +828 -0
  69. data/spec/dummy/log/test.log +14582 -0
  70. data/spec/factories/course_factory.rb +10 -0
  71. data/spec/factories/enrollment_factory.rb +5 -0
  72. data/spec/factories/section_factory.rb +5 -0
  73. data/spec/factories/term_factory.rb +10 -0
  74. data/spec/factories/user_factory.rb +9 -0
  75. data/spec/spec_helper.rb +46 -0
  76. data/spec/support/fake_canvas.rb +22 -0
  77. data/spec/support/fixtures/canvas_responses/terms.json +64 -0
  78. data/spec/support/fixtures/reports/courses.csv +3 -0
  79. data/spec/support/fixtures/reports/enrollments.csv +3 -0
  80. data/spec/support/fixtures/reports/provisioning_csv +0 -0
  81. data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +3 -0
  82. data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +4 -0
  83. data/spec/support/fixtures/reports/sections.csv +3 -0
  84. data/spec/support/fixtures/reports/users.csv +4 -0
  85. metadata +423 -0
@@ -0,0 +1,3 @@
1
+ module CanvasSync
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CanvasSync do
4
+ describe '.provisioning_sync' do
5
+ let(:canvas_base_url) { 'https://test.instructure.com' }
6
+ let(:canvas_api_token) { 'cool-api-token' }
7
+
8
+ it 'invokes the first job in the queue and passes on the rest of the job chain' do
9
+ expected_job_chain = CanvasSync.default_provisioning_report_chain(['courses'], nil)
10
+ first_job = expected_job_chain[:jobs].shift
11
+
12
+ expect(CanvasSync::Jobs::SyncTermsJob).to receive(:perform_later)
13
+ .with(
14
+ canvas_base_url,
15
+ canvas_api_token,
16
+ expected_job_chain,
17
+ first_job[:options]
18
+ )
19
+
20
+ CanvasSync.provisioning_sync(canvas_base_url, canvas_api_token, ['courses'])
21
+ end
22
+
23
+ context 'an invalid model is passed in' do
24
+ it 'raises a helpful exception' do
25
+ expect {
26
+ CanvasSync.provisioning_sync(canvas_base_url, canvas_api_token, ['courses', 'cheeses'])
27
+ }.to raise_error("Invalid model(s) specified: cheeses. Only #{CanvasSync::SUPPORTED_MODELS.join(', ')} are supported.")
28
+ end
29
+ end
30
+
31
+ describe '.default_provisioning_report_chain' do
32
+ context 'we are syncing users with a term scope' do
33
+ it 'syncs the users in a separate job that runs first' do
34
+ chain = CanvasSync.default_provisioning_report_chain(['users', 'courses'], :active)
35
+ expect(chain).to eq({
36
+ jobs: [
37
+ { job: CanvasSync::Jobs::SyncTermsJob.to_s, options: {} },
38
+ { job: CanvasSync::Jobs::SyncUsersJob.to_s, options: {} },
39
+ { job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s, options: { term_scope: 'active', models: ['courses'] } }
40
+ ],
41
+ options: {}
42
+ })
43
+ end
44
+ end
45
+
46
+ context 'we are syncing users without a term scope' do
47
+ it 'syncs users along with the rest of the provisioning report' do
48
+ chain = CanvasSync.default_provisioning_report_chain(['users', 'courses'])
49
+ expect(chain).to eq({
50
+ jobs: [
51
+ { job: CanvasSync::Jobs::SyncTermsJob.to_s, options: {} },
52
+ { job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s, options: { term_scope: nil, models: ['users', 'courses'] } }
53
+ ],
54
+ options: {}
55
+ })
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CanvasSync::Jobs::ReportChecker do
4
+ let(:canvas_base_url) { 'https://test.instructure.com' }
5
+ let(:canvas_api_token) { 'cool-api-token' }
6
+ let(:client_double) { double('client') }
7
+ let(:report_id) { 1 }
8
+ let(:report_name) { 'provisioning_csv' }
9
+ let(:processor) { 'FakeProcessor' }
10
+
11
+ describe '#perform' do
12
+ def start_job
13
+ CanvasSync::Jobs::ReportChecker.perform_now(
14
+ canvas_base_url,
15
+ canvas_api_token,
16
+ [],
17
+ 'provisioning_csv',
18
+ report_id,
19
+ processor,
20
+ {}
21
+ )
22
+ end
23
+
24
+ context 'the report has finished' do
25
+ it 'enqueues the report processor job' do
26
+ expect_any_instance_of(Bearcat::Client).to receive(:report_status)
27
+ .with('self', report_name, report_id)
28
+ .and_return({ 'status' => 'complete', 'attachment' => { 'url' => 'blah' } })
29
+
30
+ expect(CanvasSync::Jobs::ReportProcessorJob).to receive(:perform_later)
31
+ .with(canvas_base_url, canvas_api_token, [], report_name, 'blah', processor, {})
32
+
33
+ start_job
34
+ end
35
+ end
36
+
37
+ context 'the report is still running' do
38
+ it 're-enqueues the report checker' do
39
+ expect_any_instance_of(Bearcat::Client).to receive(:report_status)
40
+ .with('self', report_name, report_id)
41
+ .and_return({ 'status' => 'running' })
42
+
43
+ expect(CanvasSync::Jobs::ReportChecker).to receive(:set).and_call_original
44
+
45
+ start_job
46
+ end
47
+ end
48
+
49
+ context 'the report has errored' do
50
+ it 'does not re-enqueue the job or enqueue the processor' do
51
+ expect_any_instance_of(Bearcat::Client).to receive(:report_status)
52
+ .with('self', report_name, report_id)
53
+ .and_return({ 'status' => 'error' })
54
+
55
+ expect(CanvasSync::Jobs::ReportChecker).to_not receive(:set)
56
+ expect(CanvasSync::Jobs::ReportProcessorJob).to_not receive(:perform_later)
57
+
58
+ expect { start_job }.to raise_error
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ class FakeProcessor; end;
4
+
5
+ RSpec.describe CanvasSync::Jobs::ReportProcessorJob do
6
+ let(:canvas_base_url) { 'https://test.instructure.com' }
7
+ let(:canvas_api_token) { 'cool-api-token' }
8
+ let(:report_name) { 'provisioning_csv' }
9
+ let(:report_url) { 'https://test.instructure.com/sample_report_download' }
10
+ let(:processor) { FakeProcessor.to_s }
11
+ let(:job_chain) { ['another-job'] }
12
+
13
+ describe '#perform' do
14
+ it 'downloads the report to a file and then calls the process method on the processor, and then invokes the next job' do
15
+ expect(IO).to receive(:copy_stream)
16
+ expect(FakeProcessor).to receive(:process)
17
+ expect(CanvasSync).to receive(:invoke_next).with(canvas_base_url, canvas_api_token, job_chain)
18
+
19
+ CanvasSync::Jobs::ReportProcessorJob.perform_now(
20
+ canvas_base_url,
21
+ canvas_api_token,
22
+ job_chain,
23
+ report_name,
24
+ report_url,
25
+ processor,
26
+ {}
27
+ )
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CanvasSync::Jobs::ReportStarter do
4
+ let(:canvas_base_url) { 'https://test.instructure.com' }
5
+ let(:canvas_api_token) { 'cool-api-token' }
6
+ let(:report_params) { { 'parameters[users]' => true } }
7
+ let(:report_name) { 'provisioning_csv' }
8
+ let(:processor) { 'CoolProcessor' }
9
+
10
+ describe '#perform' do
11
+ it 'tells Canvas to start the report and then enqueues a ReportChecker with the report id' do
12
+ expect_any_instance_of(Bearcat::Client).to receive(:start_report)
13
+ .with('self', report_name, report_params)
14
+ .and_return({ 'id' => 1 })
15
+
16
+ expect(CanvasSync::Jobs::ReportChecker).to receive(:set).and_call_original
17
+
18
+ CanvasSync::Jobs::ReportStarter.perform_now(canvas_base_url,
19
+ canvas_api_token,
20
+ [],
21
+ report_name,
22
+ report_params,
23
+ processor,
24
+ { models: ['users'] })
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CanvasSync::Jobs::SyncProvisioningReportJob do
4
+ let(:canvas_base_url) { 'https://test.instructure.com' }
5
+ let(:canvas_api_token) { 'cool-api-token' }
6
+
7
+ describe '#perform' do
8
+ context 'a term scope is specified' do
9
+ let!(:active_term_1) { FactoryGirl.create(:term) }
10
+ let!(:active_term_2) { FactoryGirl.create(:term) }
11
+ let!(:inactive_term_1) { FactoryGirl.create(:term, workflow_state: 'inactive') }
12
+
13
+ it 'enqueues a ReportStarter for a provisioning report for the specified models for each term' do
14
+ expect(CanvasSync::Jobs::ReportStarter).to receive(:perform_later)
15
+ .with(
16
+ canvas_base_url,
17
+ canvas_api_token,
18
+ [],
19
+ 'proservices_provisioning_csv',
20
+ {
21
+ "parameters[include_deleted]" => true,
22
+ "parameters[users]" => true,
23
+ "parameters[courses]" => true,
24
+ "parameters[enrollment_term_id]" => active_term_1.canvas_term_id
25
+ },
26
+ CanvasSync::Processors::ProvisioningReportProcessor.to_s,
27
+ { models: ['users', 'courses'], term_scope: 'active', canvas_term_id: active_term_1.canvas_term_id }
28
+ )
29
+
30
+ expect(CanvasSync::Jobs::ReportStarter).to receive(:perform_later)
31
+ .with(
32
+ canvas_base_url,
33
+ canvas_api_token,
34
+ [],
35
+ 'proservices_provisioning_csv',
36
+ {
37
+ "parameters[include_deleted]" => true,
38
+ "parameters[users]" => true,
39
+ "parameters[courses]" => true,
40
+ "parameters[enrollment_term_id]" => active_term_2.canvas_term_id
41
+ },
42
+ CanvasSync::Processors::ProvisioningReportProcessor.to_s,
43
+ { models: ['users', 'courses'], term_scope: 'active', canvas_term_id: active_term_2.canvas_term_id }
44
+ )
45
+
46
+ CanvasSync::Jobs::SyncProvisioningReportJob.perform_now(
47
+ canvas_base_url,
48
+ canvas_api_token,
49
+ [],
50
+ { models: ['users', 'courses'], term_scope: 'active' }
51
+ )
52
+ end
53
+ end
54
+
55
+ context 'a term scope is not specified' do
56
+ it 'enqueues a single ReportStarter for a provisioning report across all terms for the specified models' do
57
+ expect(CanvasSync::Jobs::ReportStarter).to receive(:perform_later)
58
+ .with(
59
+ canvas_base_url,
60
+ canvas_api_token,
61
+ [],
62
+ 'proservices_provisioning_csv',
63
+ {
64
+ "parameters[include_deleted]" => true,
65
+ "parameters[users]" => true,
66
+ "parameters[courses]" => true
67
+ },
68
+ CanvasSync::Processors::ProvisioningReportProcessor.to_s,
69
+ { models: ['users', 'courses'] }
70
+ )
71
+
72
+ CanvasSync::Jobs::SyncProvisioningReportJob.perform_now(
73
+ canvas_base_url,
74
+ canvas_api_token,
75
+ [],
76
+ { models: ['users', 'courses'] }
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CanvasSync::Jobs::SyncTermsJob do
4
+ let(:canvas_base_url) { 'https://test.instructure.com' }
5
+ let(:canvas_api_token) { 'cool-api-token' }
6
+
7
+ describe '#perform' do
8
+ let(:term_params) { open_canvas_fixture('terms')['enrollment_terms'] }
9
+
10
+ it 'retrieves all terms from the Canvas API and then invokes the next job' do
11
+ expect(CanvasSync).to receive(:invoke_next).with(canvas_base_url, canvas_api_token, [])
12
+
13
+ expect {
14
+ CanvasSync::Jobs::SyncTermsJob.perform_now(canvas_base_url, canvas_api_token, [], {})
15
+ }.to change { Term.count }.by(term_params.length)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CanvasSync::Jobs::SyncUsersJob do
4
+ let(:canvas_base_url) { 'https://test.instructure.com' }
5
+ let(:canvas_api_token) { 'cool-api-token' }
6
+
7
+ describe '#perform' do
8
+ it 'enqueues a ReportStarter for a provisioning report across all terms with just users' do
9
+ expect_any_instance_of(Bearcat::Client).to receive(:start_report)
10
+ .with('self', 'proservices_provisioning_csv', CanvasSync::Jobs::SyncUsersJob::REPORT_PARAMS)
11
+ .and_return({ 'id' => 1 })
12
+
13
+ expect(CanvasSync::Jobs::ReportChecker).to receive(:set).and_call_original
14
+
15
+ CanvasSync::Jobs::SyncUsersJob.perform_now(canvas_base_url, canvas_api_token, [], {})
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Course, type: :model do
4
+ let(:subject) { FactoryGirl.create(:course) }
5
+
6
+ describe 'validations' do
7
+ it { should validate_presence_of(:canvas_course_id) }
8
+ it { should validate_uniqueness_of(:canvas_course_id) }
9
+ end
10
+
11
+ describe 'associations' do
12
+ it do
13
+ should belong_to(:term)
14
+ .with_primary_key(:canvas_term_id)
15
+ .with_foreign_key(:canvas_term_id)
16
+ end
17
+
18
+ it do
19
+ should have_many(:enrollments)
20
+ .with_primary_key(:canvas_course_id)
21
+ .with_foreign_key(:canvas_course_id)
22
+ end
23
+
24
+ it do
25
+ should have_many(:sections)
26
+ .with_primary_key(:canvas_course_id)
27
+ .with_foreign_key(:canvas_course_id)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Enrollment, type: :model do
4
+ let(:subject) { FactoryGirl.create(:enrollment) }
5
+
6
+ describe 'validations' do
7
+ it { should validate_presence_of(:canvas_enrollment_id) }
8
+ it { should validate_uniqueness_of(:canvas_enrollment_id) }
9
+ end
10
+
11
+ describe 'associations' do
12
+ it do
13
+ should belong_to(:user)
14
+ .with_primary_key(:canvas_user_id)
15
+ .with_foreign_key(:canvas_user_id)
16
+ end
17
+
18
+ it do
19
+ should belong_to(:course)
20
+ .with_primary_key(:canvas_course_id)
21
+ .with_foreign_key(:canvas_course_id)
22
+ end
23
+
24
+ it do
25
+ should belong_to(:section)
26
+ .with_primary_key(:canvas_section_id)
27
+ .with_foreign_key(:canvas_section_id)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Section, type: :model do
4
+ let(:subject) { FactoryGirl.create(:section) }
5
+
6
+ describe 'validations' do
7
+ it { should validate_presence_of(:canvas_section_id) }
8
+ it { should validate_uniqueness_of(:canvas_section_id) }
9
+ end
10
+
11
+ describe 'associations' do
12
+ it do
13
+ should belong_to(:course)
14
+ .with_primary_key(:canvas_course_id)
15
+ .with_foreign_key(:canvas_course_id)
16
+ end
17
+
18
+ it do
19
+ should have_many(:enrollments)
20
+ .with_primary_key(:canvas_section_id)
21
+ .with_foreign_key(:canvas_section_id)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Term, type: :model do
4
+ let(:subject) { FactoryGirl.create(:term) }
5
+
6
+ describe 'validations' do
7
+ it { should validate_presence_of(:canvas_term_id) }
8
+ it { should validate_uniqueness_of(:canvas_term_id) }
9
+ end
10
+
11
+ describe 'associations' do
12
+ it do
13
+ should have_many(:courses)
14
+ .with_primary_key(:canvas_term_id)
15
+ .with_foreign_key(:canvas_term_id)
16
+ end
17
+ end
18
+
19
+ describe 'scopes' do
20
+ describe '.active' do
21
+ let!(:active_term) { FactoryGirl.create(:term, workflow_state: 'active', start_at: 2.days.ago, end_at: 3.days.from_now) }
22
+ let!(:inactive_term) { FactoryGirl.create(:term, workflow_state: 'inactive') }
23
+ let!(:completed_term) { FactoryGirl.create(:term, start_at: 2.months.ago, end_at: 1.month.ago) }
24
+ let!(:unstarted_term) { FactoryGirl.create(:term, start_at: 6.months.from_now, end_at: 1.year.from_now) }
25
+
26
+ it 'returns terms with an active workflow_state that have a start_date 15 days in the future or earlier and an end_date 15 days in the past or later' do
27
+ expect(Term.active).to match_array([active_term])
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '.create_or_update' do
33
+ let(:term_params) { open_canvas_fixture('terms')['enrollment_terms'][0] }
34
+
35
+ context 'the term does not already exist' do
36
+ it 'creates it' do
37
+ expect {
38
+ Term.create_or_update(term_params)
39
+ }.to change { Term.count }.by(1)
40
+
41
+ term = Term.last
42
+ expect(term.name).to eq(term_params['name'])
43
+ expect(term.start_at).to eq(term_params['start_at'])
44
+ expect(term.end_at).to eq(term_params['end_at'])
45
+ expect(term.canvas_term_id).to eq(term_params['id'])
46
+ expect(term.grading_period_group_id).to eq(term_params['grading_period_group_id'])
47
+ expect(term.sis_id).to eq(term_params['sis_term_id'])
48
+ expect(term.workflow_state).to eq(term_params['workflow_state'])
49
+ end
50
+ end
51
+
52
+ context 'the term already exists' do
53
+ let!(:existing_term) { FactoryGirl.create(:term, canvas_term_id: term_params['id']) }
54
+
55
+ it 'updates it' do
56
+ expect {
57
+ Term.create_or_update(term_params)
58
+ }.to_not change { Term.count }
59
+
60
+ existing_term.reload
61
+ expect(existing_term.name).to eq(term_params['name'])
62
+ expect(existing_term.start_at).to eq(term_params['start_at'])
63
+ expect(existing_term.end_at).to eq(term_params['end_at'])
64
+ expect(existing_term.canvas_term_id).to eq(term_params['id'])
65
+ expect(existing_term.grading_period_group_id).to eq(term_params['grading_period_group_id'])
66
+ expect(existing_term.sis_id).to eq(term_params['sis_term_id'])
67
+ expect(existing_term.workflow_state).to eq(term_params['workflow_state'])
68
+ end
69
+ end
70
+ end
71
+ end