patronus_fati 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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