marty 3.1.0 → 4.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -7
  3. data/.rubocop.yml +0 -1
  4. data/.rubocop_todo.yml +15 -2
  5. data/Dockerfile.dummy +0 -3
  6. data/Gemfile +15 -21
  7. data/Gemfile.lock +289 -0
  8. data/app/components/marty/data_grid_view.rb +18 -68
  9. data/app/components/marty/extras/layout.rb +1 -1
  10. data/app/components/marty/grid.rb +1 -1
  11. data/app/components/marty/import_view.rb +3 -3
  12. data/app/components/marty/main_auth_app.rb +1 -11
  13. data/app/components/marty/report_form.rb +6 -6
  14. data/app/components/marty/script_form.rb +5 -5
  15. data/app/components/marty/script_tester.rb +2 -2
  16. data/app/components/marty/user_view.rb +9 -3
  17. data/app/models/marty/data_grid.rb +11 -16
  18. data/app/models/marty/event.rb +2 -2
  19. data/app/models/marty/promise.rb +4 -4
  20. data/app/models/marty/role_type.rb +1 -14
  21. data/app/services/marty/data_grid_view/save_grid.rb +2 -2
  22. data/app/services/marty/promises/delorean/create.rb +2 -2
  23. data/app/services/marty/promises/ruby/create.rb +2 -2
  24. data/config/locales/en.yml +2 -11
  25. data/docker-compose.dummy.yml +0 -1
  26. data/lib/marty/content_handler.rb +2 -2
  27. data/lib/marty/data_change.rb +1 -1
  28. data/lib/marty/data_conversion.rb +4 -3
  29. data/lib/marty/data_importer.rb +4 -4
  30. data/lib/marty/mcfly_model.rb +10 -7
  31. data/lib/marty/migrations.rb +1 -1
  32. data/lib/marty/monkey.rb +2 -2
  33. data/lib/marty/promise_job.rb +5 -5
  34. data/lib/marty/promise_proxy.rb +2 -2
  35. data/lib/marty/promise_ruby_job.rb +4 -4
  36. data/lib/marty/version.rb +1 -1
  37. data/marty.gemspec +18 -13
  38. data/other/marty/diagnostic/aws/ec2_instance.rb +2 -17
  39. data/other/marty/diagnostic/database.rb +2 -2
  40. data/other/marty/diagnostic/delayed_job_version.rb +1 -0
  41. data/spec/dummy/app/models/gemini/fannie_bup.rb +20 -13
  42. data/spec/dummy/config/application.rb +0 -1
  43. data/spec/dummy/config/initializers/secret_token.rb +1 -1
  44. data/spec/features/data_grid_spec.rb +46 -109
  45. data/spec/features/reporting_spec.rb +4 -4
  46. data/spec/features/rule_spec.rb +1 -1
  47. data/spec/features/scripting_spec.rb +3 -3
  48. data/spec/features/user_view_spec.rb +8 -17
  49. data/spec/lib/data_importer_spec.rb +8 -8
  50. data/spec/lib/mcfly_model_spec.rb +6 -6
  51. data/spec/models/data_grid_spec.rb +4 -19
  52. data/spec/spec_helper.rb +2 -2
  53. data/spec/support/netzke.rb +3 -4
  54. metadata +53 -50
  55. data/.ssh-docker/.keep +0 -0
  56. data/app/components/marty/data_grid_user_view.rb +0 -39
  57. data/db/migrate/108_add_data_grid_perms.rb +0 -16
  58. data/other/marty/diagnostic/aws/error.rb +0 -8
@@ -41,7 +41,7 @@ module Marty::Migrations
41
41
  SQL
42
42
 
43
43
  db_values = res.first['enum_range'].gsub(/[{"}]/, '').split(',')
44
- ex_values = klass::VALUES.map(&:to_s) - db_values
44
+ ex_values = klass::VALUES - db_values
45
45
 
46
46
  return if ex_values.empty?
47
47
 
@@ -54,8 +54,8 @@ class Delorean::BaseModule::NodeCall
54
54
  begin
55
55
  # make sure params is serialzable before starting a Job
56
56
  JSON.dump(params)
57
- rescue StandardError => e
58
- raise "non-serializable parameters: #{params} #{e}"
57
+ rescue StandardError => exc
58
+ raise "non-serializable parameters: #{params} #{exc}"
59
59
  end
60
60
 
61
61
  Marty::Promises::Delorean::Create.call(
@@ -41,9 +41,9 @@ class Marty::PromiseJob < Struct.new(:promise,
41
41
  end
42
42
 
43
43
  # log "DONE #{Process.pid} #{promise.id} #{Time.now.to_f} #{res}"
44
- rescue StandardError => e
45
- res = Delorean::Engine.grok_runtime_exception(e)
46
- # log "ERR- #{Process.pid} #{promise.id} #{Time.now.to_f} #{e}"
44
+ rescue StandardError => exc
45
+ res = Delorean::Engine.grok_runtime_exception(exc)
46
+ # log "ERR- #{Process.pid} #{promise.id} #{Time.now.to_f} #{exc}"
47
47
  end
48
48
  promise.set_result(res)
49
49
  process_hook(res)
@@ -53,8 +53,8 @@ class Marty::PromiseJob < Struct.new(:promise,
53
53
  return unless hook
54
54
 
55
55
  hook.run(params: params, result: res)
56
- rescue StandardError => e
57
- Marty::Util.logger.error "promise hook failed: #{e}"
56
+ rescue StandardError => exc
57
+ Marty::Util.logger.error "promise hook failed: #{exc}"
58
58
  end
59
59
 
60
60
  def max_attempts
@@ -55,8 +55,8 @@ class Marty::PromiseProxy < BasicObject
55
55
  begin
56
56
  @result = @promise.wait_for_result(@timeout)
57
57
  @result = @result[@attr] if @attr && !@result['error']
58
- rescue ::Exception => e
59
- @result = ::Delorean::Engine.grok_runtime_exception(e)
58
+ rescue ::Exception => exc
59
+ @result = ::Delorean::Engine.grok_runtime_exception(exc)
60
60
  end
61
61
  end
62
62
  end
@@ -25,8 +25,8 @@ class Marty::PromiseRubyJob < Struct.new(:promise,
25
25
 
26
26
  mod = module_name.constantize
27
27
  res = { 'result' => mod.send(method_name, *method_args) }
28
- rescue StandardError => e
29
- res = ::Marty::Promise.exception_to_result(promise: promise, exception: e)
28
+ rescue StandardError => exc
29
+ res = ::Marty::Promise.exception_to_result(promise: promise, exception: exc)
30
30
  end
31
31
 
32
32
  promise.set_result(res)
@@ -37,8 +37,8 @@ class Marty::PromiseRubyJob < Struct.new(:promise,
37
37
  return unless hook
38
38
 
39
39
  hook.run(params: method_args, result: res)
40
- rescue StandardError => e
41
- Marty::Util.logger.error "promise hook failed: #{e}"
40
+ rescue StandardError => exc
41
+ Marty::Util.logger.error "promise hook failed: #{exc}"
42
42
  end
43
43
 
44
44
  def max_attempts
@@ -1,3 +1,3 @@
1
1
  module Marty
2
- VERSION = '3.1.0'
2
+ VERSION = '4.0.0.rc2'
3
3
  end
@@ -26,21 +26,26 @@ Gem::Specification.new do |s|
26
26
  'Marty is a framework for viewing and reporting on versioned data.'
27
27
  s.files = `git ls-files`.split($\)
28
28
  s.licenses = ['MIT']
29
- # used for signing aws ec2 requests
30
- s.add_dependency 'aws-sigv4'
31
- # Only pinning this because there's no other way around it for Axlsx.
32
- # DO NOT unpin this.
29
+
30
+ s.add_dependency 'pg', '~> 0.21'
31
+
32
+ s.add_dependency 'netzke', '6.5.0.0'
33
+
33
34
  s.add_dependency 'axlsx', '3.0.0pre'
35
+
36
+ s.add_dependency 'delorean_lang', '~> 1.0'
37
+ s.add_dependency 'mcfly', '~> 0.0.20'
38
+
34
39
  s.add_dependency 'coderay'
35
- s.add_dependency 'daemons'
36
- s.add_dependency 'delayed_cron_job'
37
- s.add_dependency 'delayed_job_active_record'
38
- s.add_dependency 'delorean_lang'
39
40
  s.add_dependency 'json-schema'
40
- s.add_dependency 'mcfly'
41
- s.add_dependency 'net-ldap'
42
- s.add_dependency 'netzke'
43
- s.add_dependency 'pg'
41
+ s.add_dependency 'net-ldap', '~> 0.16.1'
44
42
  s.add_dependency 'rubyzip'
45
- s.add_dependency 'zip-zip'
43
+ s.add_dependency 'sqlite3'
44
+
45
+ # used for signing aws ec2 requests
46
+ s.add_dependency 'aws-sigv4', '~> 1.0', '>= 1.0.2'
47
+
48
+ s.add_dependency 'daemons', '~> 1.3.1'
49
+ s.add_dependency 'delayed_cron_job'
50
+ s.add_dependency 'delayed_job_active_record'
46
51
  end
@@ -35,30 +35,15 @@ class Marty::Diagnostic::Aws::Ec2Instance < Marty::Aws::Request
35
35
 
36
36
  def ec2_request action, params = {}
37
37
  resp = request({ action: action }, params)
38
- parsed = Hash.from_xml(resp)
39
-
40
- # check AWS response for errors
41
- error = parsed.dig('Response', 'Errors', 'Error')
42
- raise Marty::Diagnostic::Aws::Error.new(action, error) if error
43
-
44
- action_resp = parsed["#{action}Response"]
45
- raise Marty::Diagnostic::Aws::Error.new(action, parsed) unless action_resp
46
-
47
- action_resp
38
+ Hash.from_xml(resp)["#{action}Response"]
48
39
  end
49
40
 
50
41
  def get_tag
51
- action = 'DescribeTags'
52
42
  params = { 'Filter.1.Name' => 'resource-id',
53
43
  'Filter.1.Value.1' => get_instance_id,
54
44
  'Filter.2.Name' => 'key',
55
45
  'Filter.2.Value.1' => 'Name' }
56
-
57
- action_resp = ec2_request(action, params)
58
- tag = action_resp.dig('tagSet', 'item', 'value')
59
- raise Marty::Diagnostic::Aws::Error.new(action, action_resp) unless tag
60
-
61
- tag
46
+ ec2_request('DescribeTags', params)['tagSet']['item']['value']
62
47
  end
63
48
 
64
49
  def get_instances
@@ -20,9 +20,9 @@ module Marty::Diagnostic::Database
20
20
  end
21
21
 
22
22
  def self.db_schema
23
- current = ActiveRecord::Migration.current_version
23
+ current = ActiveRecord::Migrator.current_version
24
24
  raise "Migration is needed.\nCurrent Version: #{current}" if
25
- ActiveRecord::Base.connection.migration_context.needs_migration?
25
+ ActiveRecord::Migrator.needs_migration?
26
26
 
27
27
  current.to_s
28
28
  end
@@ -31,6 +31,7 @@ module Marty::Diagnostic; class DelayedJobVersion < Base
31
31
  hash[r[0]] ||= []
32
32
  hash[r[0]] << r[1]
33
33
  end.map do |node, result|
34
+
34
35
  versions = result.uniq
35
36
  status = versions.count == 1 && versions[0] == ENV['DELAYED_VER']
36
37
 
@@ -29,63 +29,70 @@ module Gemini
29
29
  gen_mcfly_lookup :lookup, {
30
30
  entity: true,
31
31
  note_rate: false
32
- }
32
+ }, to_hash: true
33
+
33
34
  gen_mcfly_lookup :lookup_p, {
34
35
  entity: true,
35
36
  note_rate: false
36
- }, private: true
37
+ }, to_hash: false
38
+
37
39
  gen_mcfly_lookup :clookup, {
38
40
  entity: true,
39
41
  note_rate: false
40
- }, cache: true
42
+ }, cache: true, to_hash: true
43
+
41
44
  gen_mcfly_lookup :clookup_p, {
42
45
  entity: true,
43
46
  note_rate: false
44
- }, cache: true, private: true
47
+ }, cache: true, to_hash: false
48
+
45
49
  gen_mcfly_lookup :lookupn, {
46
50
  entity: true,
47
51
  note_rate: false
48
- }, mode: nil
52
+ }, mode: nil, to_hash: true
53
+
49
54
  gen_mcfly_lookup :lookupn_p, {
50
55
  entity: true,
51
56
  note_rate: false
52
- }, private: true, mode: nil
57
+ }, to_hash: false, mode: nil
58
+
53
59
  gen_mcfly_lookup :clookupn, {
54
60
  entity: true,
55
61
  note_rate: false
56
- }, cache: true, mode: nil
62
+ }, cache: true, mode: nil, to_hash: true
63
+
57
64
  gen_mcfly_lookup :clookupn_p, {
58
65
  entity: true,
59
66
  note_rate: false
60
- }, cache: true, private: true, mode: nil
67
+ }, cache: true, to_hash: false, mode: nil
61
68
 
62
- mcfly_lookup :a_func, sig: 3 do
69
+ mcfly_lookup :a_func, sig: 3, to_hash: true do
63
70
  |pt, e_id, bc_id|
64
71
  where(entity_id: e_id, bud_category_id: bc_id).
65
72
  order(:settlement_mm)
66
73
  end
67
74
 
68
- mcfly_lookup :b_func, sig: [3, 4] do
75
+ mcfly_lookup :b_func, sig: [3, 4], to_hash: true do
69
76
  |pt, e_id, bc_id, mm = nil|
70
77
  q = where(entity_id: e_id, bud_category_id: bc_id)
71
78
  q = q.where(settlement_mm: mm) if mm
72
79
  q.order(:settlement_mm).first
73
80
  end
74
81
 
75
- mcfly_lookup :a_func_p, sig: 3, private: true do
82
+ mcfly_lookup :a_func_p, sig: 3, to_hash: false do
76
83
  |pt, e_id, bc_id|
77
84
  where(entity_id: e_id, bud_category_id: bc_id).
78
85
  order(:settlement_mm)
79
86
  end
80
87
 
81
- mcfly_lookup :b_func_p, sig: [3, 4], private: true do
88
+ mcfly_lookup :b_func_p, sig: [3, 4], to_hash: false do
82
89
  |pt, e_id, bc_id, mm = nil|
83
90
  q = where(entity_id: e_id, bud_category_id: bc_id)
84
91
  q = q.where(settlement_mm: mm) if mm
85
92
  q.order(:settlement_mm)
86
93
  end
87
94
 
88
- cached_mcfly_lookup :ca_func, sig: 3 do
95
+ cached_mcfly_lookup :ca_func, sig: 3, to_hash: true do
89
96
  |pt, e_id, bc_id|
90
97
  where(entity_id: e_id, bud_category_id: bc_id).
91
98
  order(:settlement_mm)
@@ -79,7 +79,6 @@ module Dummy
79
79
  :dev,
80
80
  :viewer,
81
81
  :user_manager,
82
- :data_grid_editor,
83
82
  ]
84
83
  #config.marty.default_posting_type = 'BASE'
85
84
  config.secret_key_base = "SECRET_KEY_BASE"
@@ -4,4 +4,4 @@
4
4
  # If you change this key, all old signed cookies will become invalid!
5
5
  # Make sure the secret is at least 30 characters and all random,
6
6
  # no regular words or you'll be exposed to dictionary attacks.
7
- Dummy::Application.config.secret_key_base = 'a087c113817a728e1552d63682abbd7f19fd7481ea3e24154889af53ab56d114f8901f63909a5e8310260c7187f6fd203cddc8743f6486fead0a4043cd7976c2'
7
+ Dummy::Application.config.secret_token = 'a087c113817a728e1552d63682abbd7f19fd7481ea3e24154889af53ab56d114f8901f63909a5e8310260c7187f6fd203cddc8743f6486fead0a4043cd7976c2'
@@ -2,10 +2,6 @@ require 'spec_helper'
2
2
  require 'marty_rspec'
3
3
 
4
4
  feature 'data grid view', js: true do
5
- before(:all) do
6
- self.use_transactional_tests = true
7
- end
8
-
9
5
  before(:each) do
10
6
  marty_whodunnit
11
7
  Marty::Script.load_scripts
@@ -15,22 +11,12 @@ feature 'data grid view', js: true do
15
11
  n = File.basename(path, '.txt').camelize
16
12
  Marty::DataGrid.create_from_import(n, File.read(path), dt)
17
13
  end
18
- u = Marty::User.create!(login: 'grid_user',
19
- firstname: 'grid',
20
- lastname: 'user',
21
- active: true)
22
- Marty::UserRole.create!(user_id: u.id, role: 'data_grid_editor')
23
- @no_perm = { 'view' => [],
24
- 'edit_data' => [],
25
- 'edit_all' => [] }
26
14
  end
27
15
 
28
- def go_to_data_grids(admin: true)
29
- log_in_as(admin ? 'marty' : 'grid_user')
16
+ def go_to_data_grids
30
17
  press('Applications')
31
- dest = 'Data Grids' + (admin ? ' Admin' : '')
32
- press(dest)
33
- expect(page).to have_content dest
18
+ press('Data Grids')
19
+ expect(page).to have_content 'Data Grids'
34
20
  end
35
21
 
36
22
  # setup the info for the ext grid
@@ -38,7 +24,7 @@ feature 'data grid view', js: true do
38
24
  widths, rows = get_grid_info
39
25
  @grid = page.all(:xpath,
40
26
  ".//div[contains(@class, 'x-grid-item-container')]").
41
- reject { |e| e.text.include?('DataGrid1') }.first
27
+ reject { |e| e.text.include?('2016-12-31') }.first
42
28
  @gridx = @grid.native.rect.x
43
29
  @gridy = @grid.native.rect.y
44
30
  height = @grid.native.size.height
@@ -292,96 +278,18 @@ feature 'data grid view', js: true do
292
278
  expect(grid_meta).to eq(exp_meta)
293
279
  end
294
280
 
295
- it 'dg perms' do
296
- log_in_as('marty')
297
- go_to_data_grids
298
- dgv = netzke_find('data_grid_view')
299
- grids = dgv.get_col_vals('name', 5)
300
- set_one = lambda do |grid, perms|
301
- pos = grids.index(grid) + 1
302
- dgv.select_row(pos)
303
- press('Edit')
304
- wait_for_ajax
305
- view_combo = netzke_find('Can View', 'combobox')
306
- edit_data_combo = netzke_find('Can Edit Data', 'combobox')
307
- edit_all_combo = netzke_find('Can Edit All', 'combobox')
308
- view_combo.select_values(perms['view'].join(', '))
309
- edit_data_combo.select_values(perms['edit_data'].join(', '))
310
- edit_all_combo.select_values(perms['edit_all'].join(', '))
311
- press('OK')
312
- wait_for_ajax
313
- end
314
- dg1p = { 'view' => ['Data Grid Editor'],
315
- 'edit_data' => ['Data Grid Editor'],
316
- 'edit_all' => ['Admin'] }
317
- dg2p = { 'view' => ['Data Grid Editor', 'User Manager'],
318
- 'edit_data' => ['Data Grid Editor', 'User Manager'],
319
- 'edit_all' => ['Data Grid Editor', 'Admin'] }
320
- set_one.call('DataGrid1', dg1p)
321
- set_one.call('DataGrid2', dg2p)
322
- dg1 = Marty::DataGrid.mcfly_pt('infinity').find_by(name: 'DataGrid1')
323
- dg2 = Marty::DataGrid.mcfly_pt('infinity').find_by(name: 'DataGrid2')
324
- fixexp = lambda do |orig_h|
325
- orig_h.each_with_object({}) do |(k, v), h|
326
- h[k] = v.map do |vals|
327
- vals.split(' ').map(&:downcase).join('_')
328
- end.sort
329
- end
330
- end
331
- exp1 = fixexp.call(dg1p)
332
- exp2 = fixexp.call(dg2p)
333
- fixgot = ->(orig_h) { Hash[orig_h.map { |k, v| [k, v.sort] }] }
334
- got1 = fixgot.call(dg1.permissions)
335
- got2 = fixgot.call(dg2.permissions)
336
- cmp1 = struct_compare(got1, exp1)
337
- cmp2 = struct_compare(got2, exp2)
338
- expect(cmp1).to be_falsey
339
- expect(cmp2).to be_falsey
340
-
341
- set_one.call('DataGrid1', @no_perm)
342
- set_one.call('DataGrid2', @no_perm)
343
- dg1 = Marty::DataGrid.mcfly_pt('infinity').find_by(name: 'DataGrid1')
344
- dg2 = Marty::DataGrid.mcfly_pt('infinity').find_by(name: 'DataGrid2')
345
- expect(dg1.permissions).to eq(@no_perm)
346
- expect(dg2.permissions).to eq(@no_perm)
347
- end
348
-
349
- it 'show grid' do
281
+ it 'dg editor' do
350
282
  log_in_as('marty')
351
283
  go_to_data_grids
352
284
  dgv = netzke_find('data_grid_view')
353
285
  grids = dgv.get_col_vals('name', 5)
354
- pos = grids.index('DataGrid1') + 1
355
- dgv.select_row(pos)
356
- press('Show Grid')
357
- expect(page).to have_content('Antwerp')
358
- end
359
-
360
- def set_perm(perm)
361
- p = @no_perm + (perm ? { perm => ['data_grid_editor'] } : {})
362
- Marty::DataGrid.mcfly_pt('infinity').each do |dg|
363
- dg.permissions = p
364
- dg.save!
365
- end
366
- end
367
-
368
- it 'dg context menus' do
369
- log_in_as('grid_user')
370
- go_to_data_grids(admin: false)
371
- dgv = netzke_find('data_grid_user_view')
372
- context_test_all = ENV['DG_FEATURE_FULL'] == 'true'
373
- [[nil, nil],
374
- ['edit_all', context_test_all],
286
+ context_test_all = ENV['DG_FEATURE_QUICK'] != 'true'
287
+ [['edit_all', context_test_all],
375
288
  ['edit_data', false],
376
289
  ['view', false]].each do |perm, all_cells|
377
- set_perm(perm)
378
- press('Refresh')
379
- grids = dgv.get_col_vals('name', 5)
380
- if perm.nil?
381
- expect(grids).to be_nil
382
- next
383
- end
384
290
  grids.each do |grid|
291
+ Marty::Config['grid_edit_edit_perm'] = perm
292
+ Marty::Config['grid_edit_save_perm'] = perm
385
293
  pos = grids.index(grid) + 1
386
294
  dgv.select_row(pos)
387
295
  press('Edit Grid')
@@ -392,14 +300,8 @@ feature 'data grid view', js: true do
392
300
  wait_for_ajax
393
301
  end
394
302
  end
395
- end
396
-
397
- it 'dg editor' do
398
- set_perm('edit_all')
399
- log_in_as('grid_user')
400
- go_to_data_grids(admin: false)
401
- dgv = netzke_find('data_grid_user_view')
402
- grids = dgv.get_col_vals('name', 5)
303
+ Marty::Config['grid_edit_edit_perm'] = 'edit_all'
304
+ Marty::Config['grid_edit_save_perm'] = 'edit_all'
403
305
 
404
306
  # now test some editing, saving, and cancel logic
405
307
  get_latest = lambda do
@@ -558,5 +460,40 @@ feature 'data grid view', js: true do
558
460
  press('Save')
559
461
  sleep 1
560
462
  validate_grid('1')
463
+
464
+ Marty::Config['grid_edit_save_perm'] = 'view'
465
+ pos = grids.index('DataGrid5') + 1
466
+ dgv.select_row(pos)
467
+ count = 1
468
+ begin
469
+ press('Edit Grid')
470
+ wait_for_ajax
471
+ grid_setup
472
+ cell_edit(2, 3, '123.456')
473
+ rescue StandardError => e
474
+ if count > 0
475
+ sleep 1
476
+ retry
477
+ end
478
+ count -= 1
479
+ end
480
+ press('Save')
481
+ errexp = 'error: save_grid: entered with view permissions'
482
+ expect(page).to have_content(errexp)
483
+ press('OK')
484
+ wait_for_ajax
485
+ context_click(2, 4, 4, click: true)
486
+ Marty::Config['grid_edit_save_perm'] = 'edit_data'
487
+ press('Save')
488
+ errexp = 'error: save_grid: grid modification not allowed'
489
+ expect(page).to have_content(errexp)
490
+ exp_log = JSON.parse(File.read('spec/fixtures/misc/grid_log_errs.json'))
491
+ got = Marty::Log.all.select(:message, :details).attributes.map do |l|
492
+ newl = l.except('id')
493
+ newl['details'].delete('rec_id')
494
+ newl
495
+ end
496
+ cmp = struct_compare(got, exp_log)
497
+ expect(cmp).to be_falsey
561
498
  end
562
499
  end