patronus_fati 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 664042f58354d590c6310e4e91d2ae507b274c3e
4
- data.tar.gz: 0b5d0718f1d2dbf95b8cd2881afa8fc1795902c2
3
+ metadata.gz: c9a7acfd177a783e5557a8e81a9dfe4eee777437
4
+ data.tar.gz: 5a8a3889d839f81386008de1f76e4bd68662e62a
5
5
  SHA512:
6
- metadata.gz: 595d620cd4546db4e09a1f46b7626c49db070599066adf72f8c804b900aef332afd700086b87d1051d0247f295439334f49e937f206c02b5bb9fb53d292ff442
7
- data.tar.gz: 834f2b21212f2c46cfe923f562891f88fdcf3c3025ece35de17443f8a6c4e7ec4761549ded6e470a2c17bdc7e9d895f1fb352dd0fb9a7baf309df6bba7b3e0a6
6
+ metadata.gz: 81a6b3e81d3634847fafaa5da8db3b07c3bcb41e02de0746b74e2d9b55194854302cd12d341b02d828d5a61bb1276d25632373d9147250cef9289473e995d432
7
+ data.tar.gz: c69f576ed4fdec47eb42560f9137cca80488b6ae0f79855b41b01800f47efdbe8a7361644693fcf9ef70c91636e88f1262016f9e150d1e32d439bb3ff19845f1
data/bin/patronus_fati CHANGED
@@ -39,8 +39,16 @@ exception_logger('process') do
39
39
  connection = PatronusFati.setup(options['server'], options['port'])
40
40
  connection.connect
41
41
 
42
- PatronusFati.event_handler.on(:any) do |asset_type, event_type, msg, _|
43
- PatronusFati.logger.info(JSON.generate({asset_type: asset_type, event_type: event_type, data: msg, timestamp: Time.now}))
42
+ PatronusFati.event_handler.on(:any) do |asset_type, event_type, msg, diagnostics|
43
+ PatronusFati.logger.info(JSON.generate(
44
+ {
45
+ asset_type: asset_type,
46
+ event_type: event_type,
47
+ data: msg,
48
+ diagnostics: diagnostics,
49
+ timestamp: Time.now.to_i
50
+ }
51
+ ))
44
52
  end
45
53
 
46
54
  while (line = connection.read_queue.pop)
@@ -3,25 +3,16 @@ module PatronusFati
3
3
  class AccessPoint
4
4
  include CommonState
5
5
 
6
- attr_accessor :client_macs, :local_attributes, :presence, :ssids,
7
- :sync_status
6
+ attr_accessor :client_macs, :local_attributes, :ssids
8
7
 
9
8
  LOCAL_ATTRIBUTE_KEYS = [ :bssid, :channel, :type ].freeze
10
9
 
11
- def self.[](bssid)
12
- instances[bssid] ||= new(bssid)
13
- end
14
-
15
10
  def self.current_expiration_threshold
16
11
  Time.now.to_i - AP_EXPIRATION
17
12
  end
18
13
 
19
- def self.instances
20
- @instances ||= {}
21
- end
22
-
23
14
  def active_ssids
24
- ssids.select { |_, v| v.active? }.values
15
+ ssids.select { |_, v| v.active? } if ssids
25
16
  end
26
17
 
27
18
  def add_client(mac)
@@ -37,14 +28,16 @@ module PatronusFati
37
28
  PatronusFati.event_handler.event(
38
29
  :access_point,
39
30
  status,
40
- full_state
31
+ full_state,
32
+ diagnostic_data
41
33
  )
42
34
  else
43
35
  PatronusFati.event_handler.event(
44
36
  :access_point, :offline, {
45
37
  'bssid' => local_attributes[:bssid],
46
38
  'uptime' => presence.visible_time
47
- }
39
+ },
40
+ diagnostic_data
48
41
  )
49
42
 
50
43
  # We need to reset the first seen so we get fresh duration
@@ -61,34 +54,40 @@ module PatronusFati
61
54
  end
62
55
 
63
56
  def cleanup_ssids
64
- return if ssids.select { |_, v| v.presence.dead? }.empty?
57
+ return if ssids.nil? || ssids.select { |_, v| v.presence.dead? }.empty?
65
58
 
66
- # When an AP is offline we don't care about it's SSIDs
67
- # expiring
68
- set_sync_flag(:dirtyChildren) if active? && !status_dirty?
69
- ssids.reject { |_, v| v.presence.dead? }
59
+ # When an AP is offline we don't care about announcing that it's SSIDs
60
+ # have expired, but we do want to remove them.
61
+ set_sync_flag(:dirtyChildren) if active?
62
+
63
+ ssids.reject! { |_, v| v.presence.dead? }
64
+ end
65
+
66
+ def diagnostic_data
67
+ dd = super
68
+ dd.merge!(ssids: Hash[ssids.map { |k, s| [k, s.diagnostic_data] }]) if ssids
69
+ dd
70
70
  end
71
71
 
72
72
  def full_state
73
- local_attributes.merge({
73
+ state = local_attributes.merge({
74
74
  active: active?,
75
75
  connected_clients: client_macs,
76
- ssids: active_ssids.map(&:local_attributes),
77
76
  vendor: vendor
78
77
  })
78
+ state[:ssids] = active_ssids.values.map(&:local_attributes) if ssids
79
+ state
79
80
  end
80
81
 
81
82
  def initialize(bssid)
83
+ super
82
84
  self.local_attributes = { bssid: bssid }
83
85
  self.client_macs = []
84
- self.presence = Presence.new
85
- self.ssids = {}
86
- self.sync_status = 0
87
86
  end
88
87
 
89
88
  def mark_synced
90
89
  super
91
- ssids.each { |_, v| v.mark_synced }
90
+ ssids.each { |_, v| v.mark_synced } if ssids
92
91
  end
93
92
 
94
93
  def remove_client(mac)
@@ -96,6 +95,7 @@ module PatronusFati
96
95
  end
97
96
 
98
97
  def track_ssid(ssid_data)
98
+ self.ssids ||= {}
99
99
  ssids[ssid_data[:essid]] ||= DataModels::Ssid.new(ssid_data[:essid])
100
100
 
101
101
  ssid = ssids[ssid_data[:essid]]
@@ -3,25 +3,16 @@ module PatronusFati
3
3
  class Client
4
4
  include CommonState
5
5
 
6
- attr_accessor :access_point_bssids, :local_attributes, :presence,
7
- :probes, :sync_status
6
+ attr_accessor :access_point_bssids, :local_attributes, :probes
8
7
 
9
8
  LOCAL_ATTRIBUTE_KEYS = [ :mac, :channel ].freeze
10
9
 
11
- def self.[](mac)
12
- instances[mac] ||= new(mac)
13
- end
14
-
15
10
  def self.current_expiration_threshold
16
11
  Time.now.to_i - CLIENT_EXPIRATION
17
12
  end
18
13
 
19
- def self.exists?(mac)
20
- instances.key?(mac)
21
- end
22
-
23
- def self.instances
24
- @instances ||= {}
14
+ def add_access_point(bssid)
15
+ access_point_bssids << bssid unless access_point_bssids.include?(bssid)
25
16
  end
26
17
 
27
18
  def announce_changes
@@ -29,13 +20,14 @@ module PatronusFati
29
20
 
30
21
  if active?
31
22
  status = new? ? :new : :changed
32
- PatronusFati.event_handler.event(:client, status, full_state)
23
+ PatronusFati.event_handler.event(:client, status, full_state, diagnostic_data)
33
24
  else
34
25
  PatronusFati.event_handler.event(
35
26
  :client, :offline, {
36
27
  'bssid' => local_attributes[:mac],
37
28
  'uptime' => presence.visible_time
38
- }
29
+ },
30
+ diagnostic_data
39
31
  )
40
32
 
41
33
  # We need to reset the first seen so we get fresh duration information
@@ -50,15 +42,13 @@ module PatronusFati
50
42
  mark_synced
51
43
  end
52
44
 
53
- def add_access_point(bssid)
54
- access_point_bssids << bssid unless access_point_bssids.include?(bssid)
55
- end
56
-
45
+ # Probes don't have an explicit visibility window so this will only
46
+ # remove probes that haven't been seen in the entire duration of the time
47
+ # we track any visibility.
57
48
  def cleanup_probes
58
- return if probes.select { |_, v| v.presence.dead? }.empty?
59
-
49
+ return if probes.select { |_, pres| pres.dead? }.empty?
60
50
  set_sync_flag(:dirtyChildren)
61
- probes.reject { |_, v| v.presence.dead? }
51
+ probes.reject! { |_, pres| pres.dead? }
62
52
  end
63
53
 
64
54
  def full_state
@@ -73,11 +63,10 @@ module PatronusFati
73
63
  end
74
64
 
75
65
  def initialize(mac)
66
+ super
76
67
  self.access_point_bssids = []
77
68
  self.local_attributes = { mac: mac }
78
- self.presence = Presence.new
79
69
  self.probes = {}
80
- self.sync_status = 0
81
70
  end
82
71
 
83
72
  def remove_access_point(bssid)
@@ -1,6 +1,27 @@
1
1
  module PatronusFati
2
2
  module DataModels
3
3
  module CommonState
4
+ module KlassMethods
5
+ def [](key)
6
+ instances[key] ||= new(key)
7
+ end
8
+
9
+ def exists?(mac)
10
+ instances.key?(mac)
11
+ end
12
+
13
+ def instances
14
+ @instances ||= {}
15
+ end
16
+ end
17
+
18
+ def self.included(klass)
19
+ klass.extend(KlassMethods)
20
+ klass.class_eval do
21
+ attr_accessor :presence, :sync_status
22
+ end
23
+ end
24
+
4
25
  def active?
5
26
  presence.visible_since?(self.class.current_expiration_threshold)
6
27
  end
@@ -9,12 +30,22 @@ module PatronusFati
9
30
  sync_flag?(:dirtyAttributes) || sync_flag?(:dirtyChildren)
10
31
  end
11
32
 
33
+ def diagnostic_data
34
+ {
35
+ sync_status: sync_status,
36
+ presence_bit_offset: presence.current_bit_offset,
37
+ current_presence: presence.current_presence.bits,
38
+ last_presence: presence.last_presence.bits
39
+ }
40
+ end
41
+
12
42
  def dirty?
13
43
  new? || data_dirty? || status_dirty?
14
44
  end
15
45
 
16
- def new?
17
- sync_status == SYNC_FLAGS[:unsynced]
46
+ def initialize(*_args)
47
+ self.presence = Presence.new
48
+ self.sync_status = 0
18
49
  end
19
50
 
20
51
  def mark_synced
@@ -22,6 +53,10 @@ module PatronusFati
22
53
  self.sync_status = SYNC_FLAGS[flag]
23
54
  end
24
55
 
56
+ def new?
57
+ !(sync_flag?(:syncedOnline) || sync_flag?(:syncedOffline))
58
+ end
59
+
25
60
  def set_sync_flag(flag)
26
61
  self.sync_status |= SYNC_FLAGS[flag]
27
62
  end
@@ -3,7 +3,7 @@ module PatronusFati
3
3
  class Connection
4
4
  include CommonState
5
5
 
6
- attr_accessor :bssid, :link_lost, :mac, :presence, :sync_status
6
+ attr_accessor :bssid, :link_lost, :mac
7
7
 
8
8
  def self.[](key)
9
9
  bssid, mac = key.split('^')
@@ -14,16 +14,12 @@ module PatronusFati
14
14
  Time.now.to_i - CONNECTION_EXPIRATION
15
15
  end
16
16
 
17
- def self.instances
18
- @instances ||= {}
19
- end
20
-
21
17
  def announce_changes
22
18
  return unless dirty? && DataModels::AccessPoint[bssid].valid? &&
23
19
  DataModels::Client[mac].valid?
24
20
 
25
21
  state = active? ? :connect : :disconnect
26
- PatronusFati.event_handler.event(:connection, state, full_state)
22
+ PatronusFati.event_handler.event(:connection, state, full_state, diagnostic_data)
27
23
 
28
24
  # We need to reset the first seen so we get fresh duration information
29
25
  presence.first_seen = nil
@@ -41,11 +37,10 @@ module PatronusFati
41
37
  end
42
38
 
43
39
  def initialize(bssid, mac)
40
+ super
44
41
  self.bssid = bssid
45
42
  self.link_lost = false
46
43
  self.mac = mac
47
- self.presence = Presence.new
48
- self.sync_status = 0
49
44
  end
50
45
 
51
46
  def full_state
@@ -3,7 +3,7 @@ module PatronusFati
3
3
  class Ssid
4
4
  include CommonState
5
5
 
6
- attr_accessor :local_attributes, :presence, :sync_status
6
+ attr_accessor :local_attributes
7
7
 
8
8
  LOCAL_ATTRIBUTE_KEYS = [
9
9
  :beacon_info, :beacon_rate, :cloaked, :crypt_set, :essid, :max_rate
@@ -14,9 +14,11 @@ module PatronusFati
14
14
  end
15
15
 
16
16
  def initialize(essid)
17
- self.local_attributes = { essid: essid }
18
- self.presence = PatronusFati::Presence.new
19
- self.sync_status = 0
17
+ super
18
+ self.local_attributes = {
19
+ cloaked: essid.nil? || essid.empty?,
20
+ essid: essid
21
+ }
20
22
  end
21
23
 
22
24
  def update(attrs)
@@ -34,6 +34,7 @@ module PatronusFati
34
34
 
35
35
  def self.offline_clients
36
36
  DataModels::Client.instances.each do |_, client|
37
+ client.cleanup_probes
37
38
  client.announce_changes
38
39
  end
39
40
 
@@ -106,13 +107,13 @@ module PatronusFati
106
107
 
107
108
  PatronusFati::DataModels::AccessPoint.instances.each do |bssid, access_point|
108
109
  next unless access_point.active?
109
- PatronusFati.event_handler.event(:access_point, :sync, access_point.full_state)
110
+ PatronusFati.event_handler.event(:access_point, :sync, access_point.full_state, access_point.diagnostic_data)
110
111
  access_points << bssid
111
112
  end
112
113
 
113
114
  PatronusFati::DataModels::Client.instances.each do |mac, client|
114
115
  next unless client.active?
115
- PatronusFati.event_handler.event(:client, :sync, client.full_state)
116
+ PatronusFati.event_handler.event(:client, :sync, client.full_state, client.diagnostic_data)
116
117
  clients << mac
117
118
  end
118
119
 
@@ -116,9 +116,13 @@ module PatronusFati
116
116
  end
117
117
 
118
118
  # Returns the duration in seconds of how long the specific object was
119
- # absolutely seen.
119
+ # absolutely seen. One additional interval duration is added to this length
120
+ # as we consider to have seen the tracked object for the entire duration of
121
+ # the interval not the length from the start of one interval to the start
122
+ # of the last interval, which makes logical sense (1 bit set is 1 interval
123
+ # duration, not zero seconds).
120
124
  def visible_time
121
- last_visible - first_seen if first_seen
125
+ (last_visible + INTERVAL_DURATION) - first_seen if first_seen
122
126
  end
123
127
  end
124
128
  end
@@ -1,3 +1,3 @@
1
1
  module PatronusFati
2
- VERSION = '1.1.2'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -0,0 +1,282 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe(PatronusFati::DataModels::AccessPoint) do
4
+ subject { described_class.new('66:77:88:99:aa:bb') }
5
+
6
+ it_behaves_like 'a common stateful model'
7
+
8
+ context '#active_ssids' do
9
+ it 'should return a hash when ssids have been populated' do
10
+ subject.track_ssid(essid: 'test')
11
+ expect(subject.active_ssids).to be_kind_of(Hash)
12
+ end
13
+
14
+ it 'should not include inactive SSIDs' do
15
+ ssid = double(PatronusFati::DataModels::Ssid)
16
+ expect(ssid).to receive(:active?).and_return(false)
17
+
18
+ subject.ssids = { tmp: ssid }
19
+ expect(subject.active_ssids.values).to_not include(ssid)
20
+ end
21
+
22
+ it 'should include active SSIDs' do
23
+ ssid = double(PatronusFati::DataModels::Ssid)
24
+ expect(ssid).to receive(:active?).and_return(true)
25
+
26
+ subject.ssids = { tmp: ssid }
27
+ expect(subject.active_ssids.values).to include(ssid)
28
+ end
29
+ end
30
+
31
+ context '#add_client' do
32
+ it 'should not add a client more than once' do
33
+ sample_mac = 'cc:bb:cc:bb:cc:bb'
34
+ subject.client_macs = [ sample_mac ]
35
+
36
+ expect { subject.add_client(sample_mac) }
37
+ .to_not change { subject.client_macs }
38
+ end
39
+
40
+ it 'should add a client if it\'s not presently in the list' do
41
+ sample_mac = 'ac:db:fc:4b:8c:0b'
42
+ subject.client_macs = []
43
+
44
+ expect { subject.add_client(sample_mac) }
45
+ .to change { subject.client_macs }.from([]).to([sample_mac])
46
+ end
47
+ end
48
+
49
+ # TODO:
50
+ context '#announce_changes'
51
+
52
+ context '#cleanup_ssids' do
53
+ it 'should not set the dirty children flag if there is nothing to change' do
54
+ subject.track_ssid(essid: 'test')
55
+
56
+ expect { subject.cleanup_ssids }.to_not change { subject.sync_status }
57
+ end
58
+
59
+ it 'should not set the dirty children flag when the AP is offline' do
60
+ allow(subject).to receive(:active?).and_return(false)
61
+
62
+ # Create a 'dead' SSID
63
+ subject.track_ssid(essid: 'test')
64
+ subject.mark_synced
65
+ subject.ssids['test'].presence = PatronusFati::Presence.new
66
+
67
+ expect { subject.cleanup_ssids }.to_not change { subject.sync_status }
68
+ end
69
+
70
+ it 'should remove dead SSIDs from the SSID list' do
71
+ allow(subject).to receive(:active?).and_return(true)
72
+
73
+ # Create a 'dead' SSID
74
+ subject.track_ssid(essid: 'test')
75
+ subject.mark_synced
76
+ subject.ssids['test'].presence = PatronusFati::Presence.new
77
+
78
+ expect(subject.ssids).to_not be_empty
79
+ subject.cleanup_ssids
80
+ expect(subject.ssids).to be_empty
81
+ end
82
+
83
+ it 'should set the dirty children flag when SSIDs have been removed while the AP is active' do
84
+ allow(subject).to receive(:active?).and_return(true)
85
+
86
+ # Create a 'dead' SSID
87
+ subject.track_ssid(essid: 'test')
88
+ subject.mark_synced
89
+ subject.ssids['test'].presence = PatronusFati::Presence.new
90
+
91
+ expect { subject.cleanup_ssids }.to change { subject.sync_status }
92
+ end
93
+ end
94
+
95
+ context '#diagnostic_data' do
96
+ it 'should include all SSID diagnostic data' do
97
+ ssid_dbl = double(PatronusFati::DataModels::Ssid)
98
+ expect(ssid_dbl).to receive(:diagnostic_data).and_return(:datum)
99
+ subject.ssids = { tmp: ssid_dbl }
100
+ expect(subject.diagnostic_data[:ssids]).to eql({tmp: :datum})
101
+ end
102
+ end
103
+
104
+ context '#full_state' do
105
+ it 'should return a hash' do
106
+ expect(subject.full_state).to be_kind_of(Hash)
107
+ end
108
+
109
+ it 'should include the keys expected by pulse' do
110
+ subject.track_ssid(essid: 'test')
111
+ subject.update(channel: 45, type: 'adhoc')
112
+
113
+ [:active, :bssid, :channel, :connected_clients, :ssids, :type, :vendor].each do |k|
114
+ expect(subject.full_state.key?(k)).to be_truthy
115
+ end
116
+ end
117
+
118
+ it 'should include the attributes of active ssids' do
119
+ subject.ssids = {}
120
+ ssid_dbl = double(PatronusFati::DataModels::Ssid)
121
+ expect(subject).to receive(:active_ssids).and_return({ pnt: ssid_dbl })
122
+ expect(ssid_dbl).to receive(:local_attributes).and_return('data')
123
+ expect(subject.full_state[:ssids]).to eql(['data'])
124
+ end
125
+ end
126
+
127
+ context '#initialize' do
128
+ it 'should set the local attributes to an appropriate hash' do
129
+ subject = described_class.new('12:23:34:45:56:67')
130
+ expect(subject.local_attributes).to eql(bssid: '12:23:34:45:56:67')
131
+ end
132
+
133
+ it 'should initialize client_macs to an empty array' do
134
+ expect(subject.client_macs).to be_kind_of(Array)
135
+ expect(subject.client_macs).to be_empty
136
+ end
137
+ end
138
+
139
+ context '#mark_synced' do
140
+ it 'should clear dirty flags' do
141
+ subject.update(channel: 8)
142
+ expect(subject.data_dirty?).to be_truthy
143
+ expect { subject.mark_synced }
144
+ .to change { subject.data_dirty? }.from(true).to(false)
145
+ end
146
+
147
+ it 'should call mark_synced on each SSID as well' do
148
+ dbl = double(PatronusFati::DataModels::Ssid)
149
+ subject.ssids = { test: dbl }
150
+
151
+ expect(dbl).to receive(:mark_synced)
152
+ subject.mark_synced
153
+ end
154
+ end
155
+
156
+ context '#remove_client' do
157
+ it 'should make no changes if the provided mac isn\'t present' do
158
+ subject.client_macs = [ 'one' ]
159
+ expect { subject.remove_client('test') }.to_not change { subject.client_macs }
160
+ end
161
+
162
+ it 'should remove only the provided mac if other macs are present' do
163
+ subject.client_macs = [ 'a', 'b', 'c' ]
164
+ subject.remove_client('b')
165
+
166
+ expect(subject.client_macs).to eql(['a', 'c'])
167
+ end
168
+ end
169
+
170
+ context '#track_ssid' do
171
+ let(:valid_ssid_data) { { essid: 'test' } }
172
+
173
+ it 'should create a new SSID instance if one doesn\'t already exist' do
174
+ expect(subject.ssids).to be_nil
175
+ expect { subject.track_ssid(valid_ssid_data) }.to change { subject.ssids }
176
+ end
177
+
178
+ it 'should mark the SSID as visible' do
179
+ ssid_dbl = double(PatronusFati::DataModels::Ssid)
180
+ pres_dbl = double(PatronusFati::Presence)
181
+
182
+ subject.ssids = { 'test' => ssid_dbl }
183
+
184
+ allow(ssid_dbl).to receive(:dirty?)
185
+ allow(ssid_dbl).to receive(:update)
186
+ expect(ssid_dbl).to receive(:presence).and_return(pres_dbl)
187
+ expect(pres_dbl).to receive(:mark_visible)
188
+
189
+ subject.track_ssid(valid_ssid_data)
190
+ end
191
+
192
+ it 'should update the SSID with the attributes provided' do
193
+ subject.track_ssid(valid_ssid_data)
194
+ expect(subject.ssids['test'].presence).to receive(:mark_visible)
195
+ subject.track_ssid(valid_ssid_data)
196
+ end
197
+
198
+ it 'should set the dirty children attribute if the SSID changed' do
199
+ expect(subject.sync_flag?(:dirtyChildren)).to be_falsey
200
+ subject.track_ssid(max_rate: 100)
201
+ expect(subject.sync_flag?(:dirtyChildren)).to be_truthy
202
+ end
203
+
204
+ it 'should not set the dirty children attribute if the SSID info didn\'t change' do
205
+ subject.track_ssid(valid_ssid_data)
206
+ subject.mark_synced
207
+
208
+ expect(subject.sync_flag?(:dirtyChildren)).to be_falsey
209
+ subject.track_ssid(valid_ssid_data)
210
+ expect(subject.sync_flag?(:dirtyChildren)).to be_falsey
211
+ end
212
+ end
213
+
214
+ context '#update' do
215
+ it 'should not set invalid keys' do
216
+ expect { subject.update(bad: 'key') }
217
+ .to_not change { subject.local_attributes }
218
+ end
219
+
220
+ it 'shouldn\'t modify the sync flags on invalid keys' do
221
+ expect { subject.update(other: 'key') }
222
+ .to_not change { subject.sync_status }
223
+ end
224
+
225
+ it 'shouldn\'t modify the sync flags if the values haven\'t changed' do
226
+ expect { subject.update(subject.local_attributes) }
227
+ .to_not change { subject.sync_status }
228
+ end
229
+
230
+ it 'should set the dirty attribute flag when a value has changed' do
231
+ expect { subject.update(channel: 9) }
232
+ .to change { subject.sync_status }
233
+ expect(subject.sync_flag?(:dirtyAttributes)).to be_truthy
234
+ end
235
+ end
236
+
237
+ context '#valid?' do
238
+ it 'should be true when all required attributes are set' do
239
+ subject.local_attributes = { bssid: 'something', channel: 4, type: 'adhoc' }
240
+ expect(subject).to be_valid
241
+ end
242
+
243
+ it 'should not be valid when the channel is 0' do
244
+ subject.local_attributes = { bssid: 'something', channel: 0, type: 'adhoc' }
245
+ expect(subject).to_not be_valid
246
+ end
247
+
248
+ it 'should be false when missing a required attribute' do
249
+ subject.local_attributes = { bssid: 'something', channel: 4 }
250
+ expect(subject).to_not be_valid
251
+ end
252
+ end
253
+
254
+ context '#vendor' do
255
+ it 'should short circuit if no BSSID is available' do
256
+ expect(Louis).to_not receive(:lookup)
257
+
258
+ subject.update(bssid: nil)
259
+ subject.vendor
260
+ end
261
+
262
+ it 'should use the Louis gem to perform it\'s lookup' do
263
+ inst = 'test string'
264
+ subject.update(bssid: inst)
265
+
266
+ expect(Louis).to receive(:lookup).with(inst).and_return({})
267
+ subject.vendor
268
+ end
269
+
270
+ it 'should default the long vendor name if it\'s available' do
271
+ result = { 'long_vendor' => 'correct', 'short_vendor' => 'bad' }
272
+ expect(Louis).to receive(:lookup).and_return(result)
273
+ expect(subject.vendor).to eql('correct')
274
+ end
275
+
276
+ it 'should fallback on the short vendor name if long isn\'t available' do
277
+ result = { 'short_vendor' => 'short' }
278
+ expect(Louis).to receive(:lookup).and_return(result)
279
+ expect(subject.vendor).to eql('short')
280
+ end
281
+ end
282
+ end