hieraviz 0.1.1 → 0.1.2

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.
@@ -35,7 +35,6 @@ ready( () => {
35
35
  }
36
36
  }
37
37
 
38
-
39
38
  function build_line(top, file, key, value, overriden) {
40
39
  if (overriden === true) {
41
40
  rowclass = "row overriden";
@@ -84,7 +83,6 @@ ready( () => {
84
83
  wrapper.className = 'rows info';
85
84
  top.appendChild(wrapper);
86
85
  Array.prototype.forEach.call(Object.keys(hash), (item, k) => {
87
- console.log(item);
88
86
  addTo(wrapper, "<div class=\"row\">" +
89
87
  "<span class=\"infokey\">" + item + "</span>" +
90
88
  "<span class=\"infovalue\">" + JSON.stringify(hash[item], null, 2) + "</span>" +
@@ -98,6 +96,163 @@ ready( () => {
98
96
  window.location.hash = '#' + title +'/info';
99
97
  }
100
98
 
99
+ function update_facts(node, facts) {
100
+ var req = new Request(
101
+ '/v1/' + base + '/node/' + node + '/facts',
102
+ {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Accept': 'application/json',
106
+ 'Content-Type': 'application/json'
107
+ },
108
+ body: JSON.stringify(facts)
109
+ }
110
+ );
111
+ fetch(req, auth_header()).
112
+ then(res => res.json()).
113
+ then(j => {
114
+ if (j.error != undefined) {
115
+ show_error(hierachy, j['error']);
116
+ } else {
117
+ location.reload();
118
+ }
119
+ });
120
+ }
121
+
122
+ function get_input() {
123
+ var input = document.querySelectorAll('input.userinput');
124
+ var output = {};
125
+ Array.prototype.forEach.call(Object.keys(input), (item, k) => {
126
+ output[input[item].name] = input[item].value;
127
+ });
128
+ return output;
129
+ }
130
+
131
+ function clear_input(node) {
132
+ var req = new Request(
133
+ '/v1/' + base + '/node/' + node + '/facts',
134
+ { method: 'DELETE' }
135
+ );
136
+ fetch(req, auth_header()).
137
+ then(res => res.json()).
138
+ then(j => {
139
+ if (j.error != undefined) {
140
+ show_error(hierachy, j['error']);
141
+ } else {
142
+ location.reload();
143
+ }
144
+ });
145
+ }
146
+
147
+ function build_hierarchy(top, node) {
148
+ var hierarchy = document.createElement('div');
149
+ hierarchy.className = 'hierarchy';
150
+ top.insertBefore(hierarchy, top.firstChild);
151
+ fetch('/v1/' + base + '/node/' + node + '/hierarchy', auth_header()).
152
+ then(res => res.json()).
153
+ then(j => {
154
+ var hierachy = document.querySelector('div.hierarchy');
155
+ if (j.error != undefined) {
156
+ show_error(hierachy, j['error']);
157
+ } else {
158
+ console.debug(j);
159
+ // --------------------
160
+ addTo(hierarchy, "<h3>From hiera config</h3>");
161
+ var hierafiles = document.createElement('div');
162
+ hierafiles.className = "hierafiles";
163
+ hierarchy.appendChild(hierafiles);
164
+ Array.prototype.forEach.call(Object.keys(j.hiera), (item, k) => {
165
+ addTo(hierafiles, "<div>"+j.hiera[item]+"</div>");
166
+ });
167
+ // --------------------
168
+ addTo(hierarchy, "<h3>From Node Data</h3>");
169
+ var nodeinfo = document.createElement('div');
170
+ nodeinfo.className = "nodeinfo";
171
+ hierarchy.appendChild(nodeinfo);
172
+ Array.prototype.forEach.call(Object.keys(j.info), (item, k) => {
173
+ var index = j.vars.indexOf(item);
174
+ if (index > -1 && (j.facts == null || j.facts[item] == undefined)) {
175
+ addTo(nodeinfo, "<div class=\"var\"><div class=\"label\">"+item+"</div>" +
176
+ "<div><input type=\"text\" name=\""+item+"\" value=\""+j.info[item]+"\" disabled></div></div>");
177
+ j.vars.splice(index, 1);
178
+ }
179
+ });
180
+ // --------------------
181
+ addTo(hierarchy, "<h3>From Facts</h3>");
182
+ var factinfo = document.createElement('div');
183
+ factinfo.className = "factinfo";
184
+ hierarchy.appendChild(factinfo);
185
+ if (j.facts == null) {
186
+ Array.prototype.forEach.call(Object.keys(j.vars), (item, k) => {
187
+ if (j.defaults != null && j.defaults[j.vars[item]] != undefined) {
188
+ addTo(factinfo, "<div class=\"var\"><div class=\"label\">"+j.vars[item]+"</div>" +
189
+ "<div><input type=\"text\" class=\"userinput\" name=\"" +
190
+ j.vars[item]+"\" value=\"" +
191
+ j.defaults[j.vars[item]]+"\"></div></div>");
192
+ }
193
+ });
194
+ Array.prototype.forEach.call(Object.keys(j.vars), (item, k) => {
195
+ if (j.defaults == null || j.defaults[j.vars[item]] == undefined) {
196
+ addTo(factinfo, "<div class=\"var\"><div class=\"label\">"+j.vars[item]+"</div>" +
197
+ "<div><input type=\"text\" class=\"userinput\" name=\"" +
198
+ j.vars[item]+"\" value=\"\"></div></div>");
199
+ }
200
+ });
201
+ } else {
202
+ Array.prototype.forEach.call(Object.keys(j.facts), (item, k) => {
203
+ var override = '';
204
+ if (j.defaults != null && j.defaults[item] != undefined) {
205
+ override = " <i>("+j.defaults[item]+")</i>";
206
+ }
207
+ addTo(factinfo, "<div class=\"var\"><div class=\"label\">" +
208
+ item + override + "</div>" +
209
+ "<div><input type=\"text\" class=\"userinput\" name=\"" +
210
+ item + "\" value=\"" +
211
+ j.facts[item]+"\"></div></div>");
212
+
213
+ });
214
+ }
215
+ var updatediv = document.createElement('div');
216
+ updatediv.className = "updateinfo";
217
+ hierarchy.appendChild(updatediv);
218
+ // . . . . . . . . . . .
219
+ var updateinfo = document.createElement('button');
220
+ updateinfo.id = 'updateinfo';
221
+ updateinfo.innerText = 'Update';
222
+ updatediv.appendChild(updateinfo);
223
+ updateinfo.addEventListener('click', (ev) => {
224
+ var fields = get_input();
225
+ update_facts(node, fields);
226
+ });
227
+ // // . . . . . . . . . . .
228
+ // var checkinfo = document.createElement('button');
229
+ // checkinfo.id = 'checkinfo';
230
+ // checkinfo.innerText = 'Check';
231
+ // updatediv.appendChild(checkinfo);
232
+ // checkinfo.addEventListener('click', (ev) => {
233
+
234
+ // });
235
+ // . . . . . . . . . . .
236
+ var restoreinfo = document.createElement('button');
237
+ restoreinfo.id = 'restoreinfo';
238
+ restoreinfo.innerText = 'Restore Defaults';
239
+ updatediv.appendChild(restoreinfo);
240
+ restoreinfo.addEventListener('click', (ev) => {
241
+ clear_input(node);
242
+ location.reload();
243
+ });
244
+ // --------------------
245
+ addTo(hierarchy, "<h3>Resulting files</h3>");
246
+ var nodefiles = document.createElement('div');
247
+ nodefiles.className = "nodefiles";
248
+ hierarchy.appendChild(nodefiles);
249
+ Array.prototype.forEach.call(Object.keys(j.files), (item, k) => {
250
+ addTo(nodefiles, "<div>"+j.files[item]+"</div>");
251
+ });
252
+ }
253
+ });
254
+ }
255
+
101
256
  function build_params(top, title, hash) {
102
257
  if (Object.keys(hash).length > 0) {
103
258
  var wrapper = document.createElement('div');
@@ -108,6 +263,7 @@ ready( () => {
108
263
  });
109
264
  var rows = document.querySelectorAll('div.row');
110
265
  filterBox(".paramfilter input", rows);
266
+ build_hierarchy(top, title);
111
267
  } else {
112
268
  addTo(top, "<div>There is no params in this node.</div>\n");
113
269
  }
@@ -136,7 +292,7 @@ ready( () => {
136
292
  fetch('/v1/' + base + '/node/' + node, auth_header()).
137
293
  then(res => res.json()).
138
294
  then(j => {
139
- console.log(auth_header().headers.getAll('x-auth'));
295
+ // console.log(auth_header().headers.getAll('x-auth'));
140
296
  build_top(node);
141
297
  if (j.error != undefined) {
142
298
  show_error(meat, j['error']);
@@ -197,6 +353,7 @@ ready( () => {
197
353
  });
198
354
  });
199
355
 
356
+ update_footer('/v1/' + base + '/nodes');
200
357
  restore_url(nodes);
201
358
 
202
359
  });
data/app/views/farms.erb CHANGED
@@ -7,8 +7,8 @@
7
7
  <input type="text" name="filter" />
8
8
  </form>
9
9
  <ul>
10
- <% @farms.each do |farm| %>
11
- <li class="farm" data-env="<%= @base %>" data-item="<%= farm %>"><%= farm %></li>
10
+ <% @farms.each do |farm, count| %>
11
+ <li class="farm<%= ' none' if count == 0 %>" data-env="<%= @base %>" data-item="<%= farm %>"><%= farm %> <span class="count"><%= count %></span></li>
12
12
  <% end %>
13
13
  </ul>
14
14
  </div>
data/app/views/user.erb CHANGED
@@ -18,5 +18,11 @@ ready( () => {
18
18
  <% end -%>
19
19
  <% end -%>
20
20
  <tr><td>Session Key</td><td><%= session['access_token'] %></td></tr>
21
+ <% if settings.configdata['auth_method'] == 'gitlab' -%>
22
+ <tr><td>Manage authorization</td><td>
23
+ <a href="<%= settings.configdata['gitlab_auth']['host'] %>/profile/applications">
24
+ <%= settings.configdata['gitlab_auth']['host'] %>/profile/applications
25
+ </a></td></tr>
26
+ <% end -%>
21
27
  </table>
22
28
  </div>
data/app/web.rb CHANGED
@@ -29,6 +29,24 @@ module HieravizApp
29
29
  end
30
30
 
31
31
  case settings.configdata['auth_method']
32
+ when 'dummy'
33
+
34
+ get '/logout' do
35
+ session.delete :access_token
36
+ erb :logout
37
+ end
38
+
39
+ get '/login' do
40
+ session[:access_token] = '0000'
41
+ redirect '/'
42
+ end
43
+
44
+ helpers do
45
+ def check_authorization
46
+ 'dummy'
47
+ end
48
+ end
49
+
32
50
  when 'http'
33
51
 
34
52
  use Rack::Auth::Basic, "Puppet Private Access" do |username, password|
@@ -37,18 +55,15 @@ module HieravizApp
37
55
  end
38
56
 
39
57
  get '/logout' do
40
- erb :logout, layout: :_layout
58
+ erb :logout
41
59
  end
42
60
 
43
61
  helpers do
44
- def get_username
45
- settings.configdata['http_auth']['username']
46
- end
47
- def get_userinfo
48
- { 'username' => settings.configdata['http_auth']['username'] }
49
- end
50
62
  def check_authorization
51
- true
63
+ if !session['access_token']
64
+ session[:access_token] = settings.configdata['http_auth']['access_token']
65
+ end
66
+ settings.configdata['http_auth']['username']
52
67
  end
53
68
  end
54
69
 
@@ -57,20 +72,6 @@ module HieravizApp
57
72
  set :oauth, Hieraviz::AuthGitlab.new(settings.configdata['gitlab_auth'])
58
73
 
59
74
  helpers do
60
- def get_username
61
- if session['access_token']
62
- session_info = Hieraviz::Store.get(session['access_token'], settings.configdata['session_renew'])
63
- if session_info
64
- session_info['username']
65
- else
66
- ''
67
- end
68
- end
69
- end
70
-
71
- def get_userinfo
72
- Hieraviz::Store.get(session['access_token'], settings.configdata['session_renew'])
73
- end
74
75
 
75
76
  def check_authorization
76
77
  if !session['access_token']
@@ -131,7 +132,7 @@ module HieravizApp
131
132
  get %r{^/?([-_\.a-zA-Z0-9]+)?/farms} do |base|
132
133
  @username = check_authorization
133
134
  hieracles_config = prepare_config(base)
134
- @farms = Hieracles::Registry.farms(hieracles_config)
135
+ @farms = Hieracles::Registry.farms_counted(hieracles_config, base)
135
136
  erb :farms
136
137
  end
137
138
 
@@ -43,9 +43,7 @@ module Hieraviz
43
43
  if resp['error'] ||
44
44
  (resp[@settings['required_response_key']] &&
45
45
  resp[@settings['required_response_key']] != resp[@settings['required_response_value']])
46
- false
47
- else
48
- true
46
+ return false
49
47
  end
50
48
  end
51
49
  true
@@ -3,7 +3,7 @@ module Hieraviz
3
3
  extend self
4
4
 
5
5
  def load
6
- @_config ||= YAML.load_file(configfile)
6
+ @_config = YAML.load_file(configfile)
7
7
  end
8
8
 
9
9
  def configfile
@@ -0,0 +1,27 @@
1
+ module Hieraviz
2
+ class Facts
3
+
4
+ def initialize(tmpdir, base, node, user)
5
+ @filename = File.join(tmpdir, "#{base}__#{node}__#{user}")
6
+ end
7
+
8
+ def exist?
9
+ File.exist? @filename
10
+ end
11
+
12
+ def read
13
+ if exist?
14
+ Marshal.load(File.binread(@filename))
15
+ end
16
+ end
17
+
18
+ def write(data)
19
+ File.open(@filename, 'wb') {|f| f.write(Marshal.dump(data)) }
20
+ end
21
+
22
+ def remove
23
+ File.unlink @filename
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module Hieraviz
2
+ class Puppetdb
3
+
4
+ def initialize(config)
5
+ @request = Hieracles::Puppetdb::Request.new config
6
+ end
7
+
8
+ def events
9
+ @request.events
10
+ end
11
+
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module Hieraviz
2
- VERSION = "0.1.0"
2
+ VERSION = File.read(File.expand_path('../../../CHANGELOG.md', __FILE__))[/([0-9]+\.[0-9]+\.[0-9]+)/]
3
3
  end
data/lib/hieraviz.rb CHANGED
@@ -2,7 +2,9 @@ require 'hieracles'
2
2
  require "hieraviz/version"
3
3
  require "hieraviz/config"
4
4
  require "hieraviz/store"
5
+ require "hieraviz/facts"
5
6
  require "hieraviz/auth_gitlab"
7
+ require "hieraviz/puppetdb"
6
8
 
7
9
  module Hieraviz
8
10
  # Your code goes here...
@@ -1,11 +1,10 @@
1
- require 'spec_helper'
2
1
  require 'sinatra_helper'
3
2
 
4
3
  describe HieravizApp::ApiV1 do
5
4
 
6
5
  context "page not found" do
7
6
  describe "GET /v1/blahblah" do
8
- let(:expected) { { 'error' => "data not found" } }
7
+ let(:expected) { { 'error' => "endpoint not found" } }
9
8
  before do
10
9
  get '/blahblah'
11
10
  end
@@ -171,13 +170,21 @@ describe HieravizApp::ApiV1 do
171
170
  it { expect(JSON.parse last_response.body).to eq expected }
172
171
  end
173
172
  describe "GET /v1/farms" do
174
- let(:expected) { ['farm1'] }
173
+ let(:expected) { { "farm1" => 0 } }
175
174
  before do
176
175
  get '/farms'
177
176
  end
178
177
  it { expect(last_response).to be_ok }
179
178
  it { expect(JSON.parse last_response.body).to eq expected }
180
179
  end
180
+ describe "GET /v1/vars" do
181
+ let(:expected) { ['fqdn', 'farm'] }
182
+ before do
183
+ get '/vars'
184
+ end
185
+ it { expect(last_response).to be_ok }
186
+ it { expect(JSON.parse last_response.body).to eq expected }
187
+ end
181
188
  end
182
189
 
183
190
  end
data/spec/app/web_spec.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'spec_helper'
2
1
  require 'sinatra_helper'
3
2
 
4
3
  describe HieravizApp::Web do
@@ -44,7 +43,10 @@ describe HieravizApp::Web do
44
43
  it { expect(last_response.status).to eq 404 }
45
44
  it { expect(last_response.body).to include 'Page not found' }
46
45
  end
46
+ describe "GET /user" do
47
+ before { get '/user' }
48
+ it { expect(last_response.body).to include 'toto' }
49
+ end
47
50
  end
48
51
 
49
-
50
52
  end
@@ -16,6 +16,7 @@ auth_method: http
16
16
  http_auth:
17
17
  username: 'toto'
18
18
  password: 'toto'
19
+ access_token: '123456'
19
20
  gitlab_auth:
20
21
  host: https://gitlab.example.com
21
22
  application_id: xxxid
@@ -0,0 +1,27 @@
1
+ ---
2
+ app_name: HieraViz
3
+ basepath: "spec/files/puppet"
4
+ classpath: "farm_modules/%s/manifests/init.pp"
5
+ hierafile: "hiera.yml"
6
+ session_seed: "toto"
7
+ tmpdir: "spec/files/tmp"
8
+ session_renew: 3600
9
+ usedb: false
10
+ debug: false
11
+ puppetdb:
12
+ usessl: false
13
+ host: puppetdb.example.com
14
+ port: 8080
15
+
16
+ auth_method: dummy
17
+ http_auth:
18
+ username: 'toto'
19
+ password: 'toto'
20
+ access_token: '123456'
21
+ gitlab_auth:
22
+ host: https://gitlab.example.com
23
+ application_id: xxxid
24
+ secret: xxxsecret
25
+ resource_required: '/api/v3/projects/group%2Fpuppet'
26
+ required_response_key: 'id'
27
+ required_response_value: '42'
@@ -16,6 +16,7 @@ auth_method: gitlab
16
16
  http_auth:
17
17
  username: 'toto'
18
18
  password: 'toto'
19
+ access_token: '123456'
19
20
  gitlab_auth:
20
21
  host: https://gitlab.example.com
21
22
  application_id: xxxid
@@ -0,0 +1,27 @@
1
+ ---
2
+ app_name: HieraViz
3
+ basepath: "spec/files/puppet"
4
+ basepath_dir: "spec/files/puppet*"
5
+ classpath: "farm_modules/%s/manifests/init.pp"
6
+ hierafile: "hiera.yml"
7
+ session_seed: "toto"
8
+ tmpdir: "spec/files/tmp"
9
+ session_renew: 3600
10
+ usedb: false
11
+ debug: false
12
+ puppetdb:
13
+ usessl: false
14
+ host: puppetdb.example.com
15
+ port: 8080
16
+ auth_method: http
17
+ http_auth:
18
+ username: 'toto'
19
+ password: 'toto'
20
+ access_token: '123456'
21
+ gitlab_auth:
22
+ host: https://gitlab.example.com
23
+ application_id: xxxid
24
+ secret: xxxsecret
25
+ resource_required: '/api/v3/projects/group%2Fpuppet'
26
+ required_response_key: 'id'
27
+ required_response_value: '42'
@@ -0,0 +1,7 @@
1
+ ---
2
+ classes:
3
+ - farm1
4
+ parameters:
5
+ country: fr
6
+ datacenter: equinix
7
+ farm: dev
@@ -0,0 +1,3 @@
1
+ class farm1 {
2
+ include module1
3
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ :backends:
3
+ - yaml
4
+ :hierarchy:
5
+ - "nodes/%{fqdn}"
6
+ - "farm/%{farm}"
7
+ - "common/common"
8
+ :yaml:
9
+ :datadir: "params/"
10
+ :merge_behavior: deeper
@@ -0,0 +1,2 @@
1
+ class module1 {
2
+ }
@@ -0,0 +1,4 @@
1
+ param1:
2
+ subparam1: value common
3
+ param2:
4
+ subparam2: another value
@@ -0,0 +1,2 @@
1
+ param1:
2
+ subparam1: value1
@@ -2,23 +2,62 @@ require 'spec_helper'
2
2
 
3
3
  describe Hieraviz::Config do
4
4
 
5
- before do
6
- ENV['HIERAVIZ_CONFIG_FILE'] = File.expand_path '../../files/config.yml', __FILE__
7
- end
5
+ context 'with a single puppet dir' do
6
+ before do
7
+ ENV['HIERAVIZ_CONFIG_FILE'] = File.expand_path '../../files/config.yml', __FILE__
8
+ Hieraviz::Config.load
9
+ end
8
10
 
9
- describe '.load' do
10
- let(:expected) { "spec/files/puppet" }
11
- it { expect(Hieraviz::Config.load['basepath']).to eq expected }
12
- end
11
+ describe '.load' do
12
+ let(:expected) { "spec/files/puppet" }
13
+ it { expect(Hieraviz::Config.load['basepath']).to eq expected }
14
+ end
15
+
16
+ describe '.configfile' do
17
+ let(:expected) { File.expand_path('../../files/config.yml', __FILE__) }
18
+ it { expect(Hieraviz::Config.configfile).to eq expected }
19
+ end
20
+
21
+ describe '.basepaths' do
22
+ let(:expected) { nil }
23
+ it { expect(Hieraviz::Config.basepaths).to eq expected }
24
+ end
13
25
 
14
- describe '.configfile' do
15
- let(:expected) { File.expand_path('../../files/config.yml', __FILE__) }
16
- it { expect(Hieraviz::Config.configfile).to eq expected }
26
+ describe '.root' do
27
+ let(:expected) { File.expand_path('../../../', __FILE__) }
28
+ it { expect(Hieraviz::Config.root).to eq expected }
29
+ end
30
+
31
+ describe '.root_path' do
32
+ context 'when path starts with a slash' do
33
+ let(:path) { '/some/path' }
34
+ let(:expected) { path }
35
+ it { expect(Hieraviz::Config.root_path path).to eq expected }
36
+ end
37
+ context 'when path don\'t start with a slash' do
38
+ let(:path) { 'relative/path' }
39
+ let(:expected) { File.expand_path(File.join('../../../', path), __FILE__) }
40
+ it { expect(Hieraviz::Config.root_path path).to eq expected }
41
+ end
42
+ end
17
43
  end
18
44
 
19
- describe '.root' do
20
- let(:expected) { File.expand_path('../../../', __FILE__) }
21
- it { expect(Hieraviz::Config.root).to eq expected }
45
+ context 'with multiple puppet dirs' do
46
+ before do
47
+ ENV['HIERAVIZ_CONFIG_FILE'] = File.expand_path '../../files/config_multi.yml', __FILE__
48
+ Hieraviz::Config.load
49
+ end
50
+
51
+ describe '.basepaths' do
52
+ let(:expected) {
53
+ [
54
+ File.expand_path('../../../spec/files/puppet', __FILE__),
55
+ File.expand_path('../../../spec/files/puppet2', __FILE__)
56
+ ]
57
+ }
58
+ it { expect(Hieraviz::Config.basepaths).to match_array(expected) }
59
+ end
60
+
22
61
  end
23
62
 
24
63
  end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hieraviz::Facts do
4
+
5
+ let(:tmpdir) { "spec/files/tmp" }
6
+ let(:base) { "base" }
7
+ let(:node) { "node" }
8
+ let(:user) { "dummy" }
9
+ let(:facts) { Hieraviz::Facts.new tmpdir, base, node, user }
10
+ let(:expected) { "spec/files/tmp/base__node__dummy" }
11
+
12
+ describe '.new' do
13
+ it { expect(facts.instance_variable_get(:@filename)).to eq expected }
14
+ end
15
+
16
+ describe '.exist?' do
17
+ context 'when file exists' do
18
+ before { FileUtils.touch expected }
19
+ after { File.unlink expected }
20
+ it { expect(facts.exist?).to be_truthy }
21
+ end
22
+ context 'when file does not exist' do
23
+ it { expect(facts.exist?).to be_falsey }
24
+ end
25
+ end
26
+
27
+ describe '.write' do
28
+ let(:data) { Object.new }
29
+ before { facts.write(data) }
30
+ after { File.unlink expected }
31
+ it { expect(File).to exist(expected) }
32
+ end
33
+
34
+ describe '.read' do
35
+ let(:data) { { a: 'b'} }
36
+ before { facts.write(data) }
37
+ after { File.unlink expected }
38
+ it { expect(facts.read).to eq data }
39
+ end
40
+
41
+ end