intranet-core 1.0.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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +38 -0
  3. data/lib/core_extensions.rb +12 -0
  4. data/lib/core_extensions/string.rb +43 -0
  5. data/lib/core_extensions/tree.rb +84 -0
  6. data/lib/core_extensions/webrick/httpresponse.rb +22 -0
  7. data/lib/intranet/abstract_responder.rb +34 -0
  8. data/lib/intranet/core.rb +125 -0
  9. data/lib/intranet/core/builder.rb +98 -0
  10. data/lib/intranet/core/haml_wrapper.rb +60 -0
  11. data/lib/intranet/core/locales.rb +47 -0
  12. data/lib/intranet/core/servlet.rb +42 -0
  13. data/lib/intranet/core/version.rb +8 -0
  14. data/lib/intranet/logger.rb +38 -0
  15. data/lib/intranet/resources/haml/http_error.haml +27 -0
  16. data/lib/intranet/resources/haml/skeleton.haml +52 -0
  17. data/lib/intranet/resources/haml/title_and_breadcrumb.haml +8 -0
  18. data/lib/intranet/resources/locales/en.yml +46 -0
  19. data/lib/intranet/resources/locales/fr.yml +46 -0
  20. data/lib/intranet/resources/www/background.jpg +0 -0
  21. data/lib/intranet/resources/www/error.png +0 -0
  22. data/lib/intranet/resources/www/favicon.ico +0 -0
  23. data/lib/intranet/resources/www/fonts/SourceSansPro.woff2 +0 -0
  24. data/lib/intranet/resources/www/nav.js +25 -0
  25. data/lib/intranet/resources/www/style.css +306 -0
  26. data/spec/core_extensions/string_spec.rb +135 -0
  27. data/spec/core_extensions/tree_spec.rb +208 -0
  28. data/spec/core_extensions/webrick/httpresponse_spec.rb +43 -0
  29. data/spec/intranet/core/fr.yml +5 -0
  30. data/spec/intranet/core/haml_wrapper_spec.rb +70 -0
  31. data/spec/intranet/core/locales_spec.rb +74 -0
  32. data/spec/intranet/core_spec.rb +403 -0
  33. data/spec/intranet/logger_spec.rb +129 -0
  34. data/spec/spec_helper.rb +50 -0
  35. data/spec/test_responder/responder.rb +42 -0
  36. data/spec/test_responder/www/style.css +5 -0
  37. metadata +218 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'intranet/core/locales'
4
+
5
+ RSpec.describe Intranet::Core::Locales do
6
+ describe '.initialize' do
7
+ before do
8
+ @load_path = File.absolute_path('../../../lib/intranet/resources/locales', __dir__)
9
+ described_class.initialize(@load_path)
10
+ end
11
+
12
+ it 'should define i18n load path' do
13
+ Dir[File.join(@load_path, '*.yml')].each do |file|
14
+ expect(I18n.load_path).to include(file)
15
+ end
16
+ end
17
+
18
+ it 'should define i18n available locales according to the existing translation files' do
19
+ Dir[File.join(@load_path, '*.yml')].each do |file|
20
+ expect(I18n.available_locales).to include(File.basename(file).delete('.yml').to_sym)
21
+ end
22
+ end
23
+
24
+ context 'if ENV[\'LANG\'] is not empty and contains an available locale' do
25
+ it 'should define i18n default locale from ENV[\'LANG\'] value' do
26
+ ENV['LANG'] = 'fr_FR.UTF-8'
27
+ described_class.initialize(@load_path)
28
+ expect(I18n.default_locale).to eql(:fr)
29
+
30
+ ENV['LANG'] = 'en_GB.UTF-8'
31
+ described_class.initialize(@load_path)
32
+ expect(I18n.default_locale).to eql(:en)
33
+
34
+ ENV['LANG'] = 'en_US.UTF-8'
35
+ described_class.initialize(@load_path)
36
+ expect(I18n.default_locale).to eql(:en)
37
+ end
38
+ end
39
+
40
+ context 'if ENV[\'LANG\'] is not defined or empty' do
41
+ it 'should define i18n default locale to :en' do
42
+ ENV['LANG'] = nil
43
+ described_class.initialize(@load_path)
44
+ expect(I18n.default_locale).to eql(:en)
45
+
46
+ ENV['LANG'] = ''
47
+ described_class.initialize(@load_path)
48
+ expect(I18n.default_locale).to eql(:en)
49
+ end
50
+ end
51
+
52
+ context 'if ENV[\'LANG\'] contains an unknown locale' do
53
+ it 'should define i18n default locale to :en' do
54
+ ENV['LANG'] = 'ja_JP.UTF-8'
55
+ described_class.initialize(@load_path)
56
+ expect(I18n.default_locale).to eql(:en)
57
+ end
58
+ end
59
+ end
60
+
61
+ describe '.add_path' do
62
+ before do
63
+ load_path = File.absolute_path('../../../lib/intranet/resources/locales', __dir__)
64
+ described_class.initialize(load_path)
65
+ described_class.add_path(__dir__)
66
+ end
67
+
68
+ it 'should prepend the given directory to the i18n load path' do
69
+ Dir[File.join(__dir__, '*.yml')].each do |file|
70
+ expect(I18n.load_path.first).to include(file)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'intranet/core'
4
+ require 'intranet/logger'
5
+ # Warning! We use buffered methods for reading/writing data to the socket. Mixing unbuffered
6
+ # methods (send, recv) and buffered methods (puts, gets, and other IO methods) does not work.
7
+ require 'socket'
8
+
9
+ require_relative '../test_responder/responder'
10
+
11
+ RSpec.describe Intranet::Core do
12
+ it 'should define its version' do
13
+ expect { described_class::VERSION }.not_to raise_error
14
+ end
15
+
16
+ describe '#start' do
17
+ it 'should start an HTTP server on an available port' do
18
+ begin
19
+ # make sure port 80 & 8080 are not available
20
+ @intranet8080 = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
21
+ thread = Thread.new do
22
+ @intranet8080.start
23
+ end
24
+ # Wait for the server on port 8080 to be running
25
+ while @intranet8080.instance_variable_get(:@server).status != :Running
26
+ end
27
+
28
+ intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
29
+ expect(intranet.port).to eql(
30
+ intranet.instance_variable_get(:@server).instance_variable_get(:@config)[:Port]
31
+ )
32
+ expect(intranet.port).not_to eql(8080)
33
+ ensure
34
+ intranet.stop if intranet.respond_to?(:stop)
35
+ Thread.kill(thread)
36
+ thread.join
37
+ end
38
+ end
39
+
40
+ it 'should start searching for an available port at the given +preferred_port+' do
41
+ intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL), 9090)
42
+ expect(intranet.port).to be >= 9090
43
+ end
44
+
45
+ it 'should serve the /design directory' do
46
+ begin
47
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
48
+ thread = Thread.new do
49
+ @intranet.start
50
+ end
51
+ while @intranet.instance_variable_get(:@server).status != :Running
52
+ end
53
+
54
+ socket = TCPSocket.new('localhost', @intranet.port)
55
+ socket.puts("GET /design/favicon.ico HTTP/1.1\r\nHost: localhost:#{@intranet.port}\r\n\r\n")
56
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
57
+ ensure
58
+ socket.close
59
+ Thread.kill(thread)
60
+ thread.join
61
+ end
62
+ end
63
+
64
+ context 'when no module is registered' do
65
+ it 'should return HTTP error 404 when requested for /index.html' do
66
+ begin
67
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
68
+ thread = Thread.new do
69
+ @intranet.start
70
+ end
71
+ while @intranet.instance_variable_get(:@server).status != :Running
72
+ end
73
+
74
+ socket = TCPSocket.new('localhost', @intranet.port)
75
+ socket.puts("GET /index.html HTTP/1.1\r\n" \
76
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
77
+ expect(socket.gets).to include('HTTP/1.1 404 Not Found')
78
+ ensure
79
+ socket.close
80
+ Thread.kill(thread)
81
+ thread.join
82
+ end
83
+ end
84
+ end
85
+
86
+ context 'registering a module when the server is running' do
87
+ it 'should fail' do
88
+ begin
89
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
90
+ thread = Thread.new do
91
+ @intranet.start
92
+ end
93
+ while @intranet.instance_variable_get(:@server).status != :Running
94
+ end
95
+
96
+ expect { @intranet.register_module(nil, [], '') }.to raise_error Errno::EALREADY
97
+ ensure
98
+ Thread.kill(thread)
99
+ thread.join
100
+ end
101
+ end
102
+ end
103
+
104
+ context 'registering a module with an invalid path' do
105
+ it 'should fail' do
106
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
107
+ expect { @intranet.register_module(nil, ['Invalid'], '') }.to raise_error ArgumentError
108
+ end
109
+ end
110
+
111
+ context 'registering an invalid module' do
112
+ it 'should succeed but accesses to the module URL should return HTTP error 404' do
113
+ begin
114
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
115
+ @intranet.register_module(nil, [], '')
116
+ thread = Thread.new do
117
+ @intranet.start
118
+ end
119
+ while @intranet.instance_variable_get(:@server).status != :Running
120
+ end
121
+
122
+ socket = TCPSocket.new('localhost', @intranet.port)
123
+ socket.puts("GET /index.html HTTP/1.1\r\n" \
124
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
125
+ expect(socket.gets).to include('HTTP/1.1 404 Not Found')
126
+ socket.close
127
+ ensure
128
+ socket.close
129
+ Thread.kill(thread)
130
+ thread.join
131
+ end
132
+ end
133
+ end
134
+
135
+ context 'when a valid module is registered with no path (home module)' do
136
+ it 'should be used to serve all URI' do
137
+ begin
138
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
139
+ responder = Intranet::TestResponder.new('/index.html' => [200, 'text/html', ''],
140
+ '/folder/index.html' => [200, 'text/html', ''])
141
+ @intranet.register_module(responder, [], responder.resources_dir)
142
+ thread = Thread.new do
143
+ @intranet.start
144
+ end
145
+ while @intranet.instance_variable_get(:@server).status != :Running
146
+ end
147
+
148
+ socket = TCPSocket.new('localhost', @intranet.port)
149
+ socket.puts("GET /index.html HTTP/1.1\r\n" \
150
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
151
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
152
+ socket.close
153
+
154
+ socket = TCPSocket.new('localhost', @intranet.port)
155
+ socket.puts("GET /folder/index.html HTTP/1.1\r\n" \
156
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
157
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
158
+ socket.close
159
+
160
+ socket = TCPSocket.new('localhost', @intranet.port)
161
+ socket.puts("GET /folder/subdir/index.html HTTP/1.1\r\n" \
162
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
163
+ expect(socket.gets).to include('HTTP/1.1 404 Not Found')
164
+ socket.close
165
+
166
+ socket = TCPSocket.new('localhost', @intranet.port)
167
+ socket.puts("GET /design/home/style.css HTTP/1.1\r\n" \
168
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
169
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
170
+ ensure
171
+ socket.close
172
+ Thread.kill(thread)
173
+ thread.join
174
+ end
175
+ end
176
+ end
177
+
178
+ context 'when a valid module is registered with a path' do
179
+ it 'should be used to serve URI relative to the module root' do
180
+ begin
181
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
182
+ responder = Intranet::TestResponder.new('/index.html' => [200, 'text/html', ''])
183
+ @intranet.register_module(responder, [], responder.resources_dir)
184
+ @intranet.register_module(responder, %w[responder], responder.resources_dir)
185
+ @intranet.register_module(responder, %w[resp onder], responder.resources_dir)
186
+ thread = Thread.new do
187
+ @intranet.start
188
+ end
189
+ while @intranet.instance_variable_get(:@server).status != :Running
190
+ end
191
+
192
+ socket = TCPSocket.new('localhost', @intranet.port)
193
+ socket.puts("GET /index.html HTTP/1.1\r\n" \
194
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
195
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
196
+ socket.close
197
+ socket = TCPSocket.new('localhost', @intranet.port)
198
+ socket.puts("GET /design/home/style.css HTTP/1.1\r\n" \
199
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
200
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
201
+ socket.close
202
+
203
+ socket = TCPSocket.new('localhost', @intranet.port)
204
+ socket.puts("GET /responder/index.html HTTP/1.1\r\n" \
205
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
206
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
207
+ socket.close
208
+ socket = TCPSocket.new('localhost', @intranet.port)
209
+ socket.puts("GET /design/responder/style.css HTTP/1.1\r\n" \
210
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
211
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
212
+ socket.close
213
+
214
+ socket = TCPSocket.new('localhost', @intranet.port)
215
+ socket.puts("GET /resp/onder/index.html HTTP/1.1\r\n" \
216
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
217
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
218
+ socket.close
219
+ socket = TCPSocket.new('localhost', @intranet.port)
220
+ socket.puts("GET /design/resp/onder/style.css HTTP/1.1\r\n" \
221
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
222
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
223
+ ensure
224
+ socket.close
225
+ Thread.kill(thread)
226
+ thread.join
227
+ end
228
+ end
229
+ end
230
+
231
+ context 'given a valid and registered module' do
232
+ it 'should be called with the URL path and query' do
233
+ begin
234
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
235
+ responder = Intranet::TestResponder.new('/index.html' => [200, 'text/html', ''])
236
+ @intranet.register_module(responder, [], responder.resources_dir)
237
+ thread = Thread.new do
238
+ @intranet.start
239
+ end
240
+ while @intranet.instance_variable_get(:@server).status != :Running
241
+ end
242
+
243
+ socket = TCPSocket.new('localhost', @intranet.port)
244
+ socket.puts("GET /query?var1=value1&var2=value2 HTTP/1.1\r\n" \
245
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
246
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
247
+ while (line = socket.gets.chomp) # consume HTTP response headers
248
+ break if line.empty?
249
+ end
250
+ line = socket.gets.chomp
251
+ expect(line).to eql({ 'var1' => 'value1', 'var2' => 'value2' }.to_s)
252
+ socket.close
253
+
254
+ socket = TCPSocket.new('localhost', @intranet.port)
255
+ socket.puts("GET /query?foo=bar&baz=boz HTTP/1.1\r\n" \
256
+ "Host: localhost:#{@intranet.port}\r\n\r\n")
257
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
258
+ while (line = socket.gets.chomp) # consume HTTP response headers
259
+ break if line.empty?
260
+ end
261
+ line = socket.gets.chomp
262
+ expect(line).to eql({ 'foo' => 'bar', 'baz' => 'boz' }.to_s)
263
+ ensure
264
+ socket.close
265
+ Thread.kill(thread)
266
+ thread.join
267
+ end
268
+ end
269
+ end
270
+
271
+ context 'given a module returning partial HTML content' do
272
+ it 'should be called to retrieve the body of the page' do
273
+ begin
274
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
275
+ responder = Intranet::TestResponder.new(
276
+ {
277
+ '/index.html' => [206, 'text/html', { content: 'PARTIAL_CONTENT', title: 'MyTitle' }]
278
+ },
279
+ ['/responder.css', 'nav.css'],
280
+ ['module.js', '/js/interactive.js']
281
+ )
282
+ @intranet.register_module(responder, [], responder.resources_dir)
283
+ thread = Thread.new do
284
+ @intranet.start
285
+ end
286
+ while @intranet.instance_variable_get(:@server).status != :Running
287
+ end
288
+
289
+ socket = TCPSocket.new('localhost', @intranet.port)
290
+ socket.puts("GET /index.html HTTP/1.1\r\nHost: localhost:#{@intranet.port}\r\n\r\n")
291
+
292
+ # Return code: HTTP error 200
293
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
294
+
295
+ while (line = socket.gets.chomp) # consume HTTP response headers
296
+ break if line.empty?
297
+ end
298
+ html = socket.readpartial(4096) # read rest of data
299
+
300
+ # Returned HTML document: includes the partial content and the title
301
+ expect(html).to match(%r{<main>.*PARTIAL_CONTENT.*</main>}m)
302
+ expect(html).to match(%r{<head>.*<title>.*MyTitle.*</title>.*</head>}m)
303
+
304
+ # Returned HTML document: includes the hostname in title, h1-title and footer
305
+ hostname = Socket.gethostname
306
+ expect(html).to match(%r{<head>.*<title>.*#{hostname.capitalize}.*</title>.*</head>}m)
307
+ expect(html).to match(%r{<body>.*<h1>.*#{hostname.capitalize}.*</h1>.*</body>}m)
308
+ expect(html).to match(%r{<footer>.*#{hostname}.*</footer>}m)
309
+
310
+ # Returned HTML document: includes all CSS dependencies
311
+ expect(html).to match(%r{<link href='/responder.css' rel='stylesheet' type='text/css'})
312
+ expect(html).to match(%r{<link href='nav.css' rel='stylesheet' type='text/css'})
313
+
314
+ # Returned HTML document: includes all JS dependencies
315
+ expect(html).to match(%r{<script defer='defer' src='module.js'></script>})
316
+ expect(html).to match(%r{<script defer='defer' src='/js/interactive.js'></script>})
317
+
318
+ # Returned HTML document: includes Intranet Version
319
+ expect(html).to match(%r{<footer>.*#{Intranet::Core::VERSION}.*</footer>}m)
320
+ expect(html).to \
321
+ match(%r{<footer>.*https://bitbucket.org/linuxtools/intranet.git.*</footer>}m)
322
+ ensure
323
+ socket.close
324
+ Thread.kill(thread)
325
+ thread.join
326
+ end
327
+ end
328
+ it 'should be called to update the main navigation menu' do
329
+ begin
330
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
331
+ responder = Intranet::TestResponder.new(
332
+ '/index.html' => [206, 'text/html', { content: 'PARTIAL_CONTENT', title: 'MyTitle' }]
333
+ )
334
+ @intranet.register_module(responder, [], responder.resources_dir)
335
+ @intranet.register_module(responder, %w[depth1], responder.resources_dir)
336
+ @intranet.register_module(responder, %w[depth2 resp1], responder.resources_dir)
337
+ @intranet.register_module(responder, %w[depth2 resp2], responder.resources_dir)
338
+ thread = Thread.new do
339
+ @intranet.start
340
+ end
341
+ while @intranet.instance_variable_get(:@server).status != :Running
342
+ end
343
+
344
+ socket = TCPSocket.new('localhost', @intranet.port)
345
+ socket.puts("GET /index.html HTTP/1.1\r\nHost: localhost:#{@intranet.port}\r\n\r\n")
346
+
347
+ # Return code: HTTP error 200
348
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
349
+
350
+ while (line = socket.gets.chomp)
351
+ break if line.empty?
352
+ end
353
+ html = socket.readpartial(4096) # read rest of data
354
+
355
+ # Returned HTML document main menu
356
+ expect(html).to match(%r{<a href='/depth1/index.html'>.*depth1.*</a>})
357
+ expect(html).to match(
358
+ %r{<a>.*depth2.*</a>.*<ul>.*<a href='/depth2/resp1/index.html'>.*resp1.*</a>.*</ul>}m
359
+ )
360
+ expect(html).to match(
361
+ %r{<a>.*depth2.*</a>.*<ul>.*<a href='/depth2/resp2/index.html'>.*resp2.*</a>.*</ul>}m
362
+ )
363
+ ensure
364
+ socket.close
365
+ Thread.kill(thread)
366
+ thread.join
367
+ end
368
+ end
369
+ end
370
+ end
371
+
372
+ describe '#stop' do
373
+ it 'should stop the web server and finalize all registered responders' do
374
+ begin
375
+ @intranet = described_class.new(Intranet::Logger.new(Intranet::Logger::FATAL))
376
+ responder = Intranet::TestResponder.new('/index.html' => [200, 'text/html', 'CONTENT'])
377
+ @intranet.register_module(responder, [], responder.resources_dir)
378
+ thread = Thread.new do
379
+ @intranet.start
380
+ end
381
+ while @intranet.instance_variable_get(:@server).status != :Running
382
+ end
383
+
384
+ socket = TCPSocket.new('localhost', @intranet.port)
385
+ socket.puts("GET /index.html HTTP/1.1\r\nHost: localhost:#{@intranet.port}\r\n\r\n")
386
+ expect(socket.gets).to include('HTTP/1.1 200 OK')
387
+ expect(responder.finalized).to be false
388
+ socket.close
389
+
390
+ @intranet.stop
391
+ while @intranet.instance_variable_get(:@server).status != :Stop
392
+ end
393
+
394
+ expect { TCPSocket.new('localhost', @intranet.port) }.to raise_error(Errno::ECONNREFUSED)
395
+ expect(responder.finalized).to be true
396
+ ensure
397
+ socket.close
398
+ Thread.kill(thread)
399
+ thread.join
400
+ end
401
+ end
402
+ end
403
+ end