intranet-core 1.0.0

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