odac 1.1.0 → 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.
Files changed (113) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +33 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/release.yml +42 -1
  6. package/.github/workflows/test-coverage.yml +6 -5
  7. package/.github/workflows/test-publish.yml +36 -0
  8. package/.husky/pre-commit +10 -0
  9. package/.husky/pre-push +13 -0
  10. package/.releaserc.js +3 -3
  11. package/CHANGELOG.md +67 -0
  12. package/README.md +16 -0
  13. package/bin/odac.js +182 -40
  14. package/client/odac.js +10 -4
  15. package/docs/backend/01-overview/03-development-server.md +38 -45
  16. package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
  17. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  18. package/docs/backend/03-config/01-database-connection.md +2 -2
  19. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  20. package/docs/backend/03-config/03-request-timeout.md +1 -1
  21. package/docs/backend/03-config/04-environment-variables.md +4 -4
  22. package/docs/backend/03-config/05-early-hints.md +2 -2
  23. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  24. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  25. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  26. package/docs/backend/05-controllers/03-controller-classes.md +40 -20
  27. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  28. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  29. package/docs/backend/08-database/01-getting-started.md +2 -2
  30. package/docs/backend/10-authentication/03-register.md +1 -1
  31. package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
  32. package/docs/backend/10-authentication/05-session-management.md +15 -1
  33. package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
  34. package/docs/backend/10-authentication/07-magic-links.md +1 -1
  35. package/docs/index.json +5 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +9 -5
  38. package/src/Auth.js +58 -23
  39. package/src/Config.js +7 -7
  40. package/src/Env.js +3 -1
  41. package/src/Ipc.js +7 -0
  42. package/src/Lang.js +9 -2
  43. package/src/Odac.js +44 -35
  44. package/src/Request.js +1 -1
  45. package/src/Route/Cron.js +58 -17
  46. package/src/Route/Internal.js +1 -1
  47. package/src/Route.js +282 -99
  48. package/src/Server.js +40 -3
  49. package/src/Storage.js +4 -0
  50. package/src/Token.js +6 -4
  51. package/src/Validator.js +1 -1
  52. package/src/Var.js +22 -6
  53. package/src/View/EarlyHints.js +43 -33
  54. package/src/View/Form.js +17 -11
  55. package/src/View.js +62 -6
  56. package/template/package.json +3 -1
  57. package/template/view/content/home.html +3 -3
  58. package/template/view/head/main.html +2 -2
  59. package/test/Client.test.js +168 -0
  60. package/test/Config.test.js +112 -0
  61. package/test/Lang.test.js +92 -0
  62. package/test/Odac.test.js +86 -0
  63. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  64. package/test/{framework/Route.test.js → Route.test.js} +1 -1
  65. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  66. package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
  67. package/test/scripts/check-coverage.js +4 -4
  68. package/test/cli/Cli.test.js +0 -36
  69. package/test/core/Commands.test.js +0 -538
  70. package/test/core/Config.test.js +0 -1432
  71. package/test/core/Lang.test.js +0 -250
  72. package/test/core/Odac.test.js +0 -234
  73. package/test/core/Process.test.js +0 -156
  74. package/test/server/Api.test.js +0 -647
  75. package/test/server/DNS.test.js +0 -2050
  76. package/test/server/DNS.test.js.bak +0 -2084
  77. package/test/server/Hub.test.js +0 -497
  78. package/test/server/Log.test.js +0 -73
  79. package/test/server/Mail.account.test_.js +0 -460
  80. package/test/server/Mail.init.test_.js +0 -411
  81. package/test/server/Mail.test_.js +0 -1340
  82. package/test/server/SSL.test_.js +0 -1491
  83. package/test/server/Server.test.js +0 -765
  84. package/test/server/Service.test_.js +0 -1127
  85. package/test/server/Subdomain.test.js +0 -440
  86. package/test/server/Web/Firewall.test.js +0 -175
  87. package/test/server/Web/Proxy.test.js +0 -397
  88. package/test/server/Web.test.js +0 -1494
  89. package/test/server/__mocks__/acme-client.js +0 -17
  90. package/test/server/__mocks__/bcrypt.js +0 -50
  91. package/test/server/__mocks__/child_process.js +0 -389
  92. package/test/server/__mocks__/crypto.js +0 -432
  93. package/test/server/__mocks__/fs.js +0 -450
  94. package/test/server/__mocks__/globalOdac.js +0 -227
  95. package/test/server/__mocks__/http.js +0 -575
  96. package/test/server/__mocks__/https.js +0 -272
  97. package/test/server/__mocks__/index.js +0 -249
  98. package/test/server/__mocks__/mail/server.js +0 -100
  99. package/test/server/__mocks__/mail/smtp.js +0 -31
  100. package/test/server/__mocks__/mailparser.js +0 -81
  101. package/test/server/__mocks__/net.js +0 -369
  102. package/test/server/__mocks__/node-forge.js +0 -328
  103. package/test/server/__mocks__/os.js +0 -320
  104. package/test/server/__mocks__/path.js +0 -291
  105. package/test/server/__mocks__/selfsigned.js +0 -8
  106. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  107. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  108. package/test/server/__mocks__/smtp-server.js +0 -106
  109. package/test/server/__mocks__/sqlite3.js +0 -394
  110. package/test/server/__mocks__/testFactories.js +0 -299
  111. package/test/server/__mocks__/testHelpers.js +0 -363
  112. package/test/server/__mocks__/tls.js +0 -229
  113. /package/template/{config.json → odac.json} +0 -0
@@ -1,497 +0,0 @@
1
- const mockLog = jest.fn()
2
- const mockError = jest.fn()
3
-
4
- const {mockOdac} = require('./__mocks__/globalOdac')
5
-
6
- mockOdac.setMock('core', 'Log', {
7
- init: jest.fn().mockReturnValue({
8
- log: mockLog,
9
- error: mockError
10
- })
11
- })
12
-
13
- global.Odac = mockOdac
14
-
15
- jest.mock('axios')
16
- const axios = require('axios')
17
-
18
- jest.mock('ws')
19
-
20
- jest.mock('os')
21
- const os = require('os')
22
-
23
- jest.mock('fs')
24
- const fs = require('fs')
25
-
26
- jest.useFakeTimers()
27
-
28
- describe('Hub', () => {
29
- let Hub
30
-
31
- beforeEach(() => {
32
- jest.clearAllMocks()
33
- jest.clearAllTimers()
34
-
35
- mockOdac.setMock('core', 'Config', {
36
- config: {
37
- hub: null,
38
- server: {started: Date.now()},
39
- websites: {},
40
- services: [],
41
- mail: {accounts: {}}
42
- }
43
- })
44
-
45
- mockOdac.setMock('server', 'Api', {
46
- result: jest.fn((success, message) => ({success, message}))
47
- })
48
-
49
- os.hostname.mockReturnValue('test-host')
50
- os.platform.mockReturnValue('linux')
51
- os.arch.mockReturnValue('x64')
52
- os.totalmem.mockReturnValue(8589934592)
53
- os.freemem.mockReturnValue(4294967296)
54
- os.cpus.mockReturnValue([
55
- {times: {user: 1000, nice: 0, sys: 500, idle: 8500, irq: 0}},
56
- {times: {user: 1000, nice: 0, sys: 500, idle: 8500, irq: 0}}
57
- ])
58
-
59
- jest.isolateModules(() => {
60
- Hub = require('../../server/src/Hub')
61
- })
62
- })
63
-
64
- afterEach(() => {
65
- if (Hub.websocket) {
66
- Hub.websocket = null
67
- }
68
- Hub.checkCounter = 0
69
- })
70
-
71
- describe('initialization', () => {
72
- it('should initialize with default values', () => {
73
- expect(Hub.websocket).toBeNull()
74
- expect(Hub.websocketReconnectAttempts).toBe(0)
75
- expect(Hub.maxReconnectAttempts).toBe(5)
76
- expect(Hub.checkCounter).toBe(0)
77
- })
78
- })
79
-
80
- describe('check counter', () => {
81
- it('should increment counter on each check', () => {
82
- expect(Hub.checkCounter).toBe(0)
83
- Hub.check()
84
- expect(Hub.checkCounter).toBe(1)
85
- Hub.check()
86
- expect(Hub.checkCounter).toBe(2)
87
- })
88
-
89
- it('should reset counter after reaching 60', () => {
90
- Hub.checkCounter = 59
91
- Hub.check()
92
- expect(Hub.checkCounter).toBe(0)
93
- })
94
-
95
- it('should skip API call when counter is not 0', async () => {
96
- mockOdac.setMock('core', 'Config', {
97
- config: {
98
- hub: {token: 'test-token', secret: 'test-secret'},
99
- server: {started: Date.now()}
100
- }
101
- })
102
-
103
- Hub.checkCounter = 1
104
- await Hub.check()
105
- expect(axios.post).not.toHaveBeenCalled()
106
- })
107
-
108
- it('should skip API call when websocket is connected', async () => {
109
- mockOdac.setMock('core', 'Config', {
110
- config: {
111
- hub: {token: 'test-token', secret: 'test-secret'},
112
- server: {started: Date.now()}
113
- }
114
- })
115
-
116
- Hub.websocket = {readyState: 1}
117
- Hub.checkCounter = 0
118
- await Hub.check()
119
- expect(axios.post).not.toHaveBeenCalled()
120
- })
121
- })
122
-
123
- describe('check', () => {
124
- beforeEach(() => {
125
- Hub.checkCounter = 0
126
- })
127
-
128
- it('should return early if no hub config', async () => {
129
- mockOdac.setMock('core', 'Config', {
130
- config: {hub: null}
131
- })
132
-
133
- await Hub.check()
134
- expect(axios.post).not.toHaveBeenCalled()
135
- })
136
-
137
- it('should return early if no token', async () => {
138
- mockOdac.setMock('core', 'Config', {
139
- config: {hub: {}}
140
- })
141
-
142
- await Hub.check()
143
- expect(axios.post).not.toHaveBeenCalled()
144
- })
145
-
146
- it('should send status to hub when counter is 0', async () => {
147
- jest.useRealTimers()
148
-
149
- mockOdac.setMock('core', 'Config', {
150
- config: {
151
- hub: {token: 'test-token', secret: 'test-secret'},
152
- server: {started: Date.now()}
153
- }
154
- })
155
-
156
- axios.post.mockResolvedValue({
157
- data: {
158
- result: {success: true},
159
- data: {authenticated: true}
160
- }
161
- })
162
-
163
- Hub.checkCounter = 59
164
- Hub.check()
165
-
166
- await new Promise(resolve => setImmediate(resolve))
167
- expect(axios.post).toHaveBeenCalled()
168
-
169
- jest.useFakeTimers()
170
- })
171
-
172
- it('should handle authentication failure', async () => {
173
- jest.useRealTimers()
174
-
175
- mockOdac.setMock('core', 'Config', {
176
- config: {
177
- hub: {token: 'invalid-token', secret: 'test-secret'},
178
- server: {started: Date.now()}
179
- }
180
- })
181
-
182
- axios.post.mockResolvedValue({
183
- data: {
184
- result: {success: true},
185
- data: {authenticated: false, reason: 'token_invalid'}
186
- }
187
- })
188
-
189
- Hub.checkCounter = 59
190
- Hub.check()
191
-
192
- await new Promise(resolve => setImmediate(resolve))
193
- expect(mockLog).toHaveBeenCalledWith('Server not authenticated: %s', 'token_invalid')
194
-
195
- jest.useFakeTimers()
196
- })
197
-
198
- it('should clear config on invalid token', async () => {
199
- jest.useRealTimers()
200
-
201
- const config = {
202
- hub: {token: 'invalid-token', secret: 'test-secret'},
203
- server: {started: Date.now()}
204
- }
205
- mockOdac.setMock('core', 'Config', {config})
206
-
207
- axios.post.mockResolvedValue({
208
- data: {
209
- result: {success: true},
210
- data: {authenticated: false, reason: 'token_invalid'}
211
- }
212
- })
213
-
214
- Hub.checkCounter = 59
215
- Hub.check()
216
-
217
- await new Promise(resolve => setImmediate(resolve))
218
- expect(config.hub).toBeUndefined()
219
-
220
- jest.useFakeTimers()
221
- })
222
-
223
- it('should handle check errors gracefully', async () => {
224
- jest.useRealTimers()
225
-
226
- mockOdac.setMock('core', 'Config', {
227
- config: {
228
- hub: {token: 'test-token', secret: 'test-secret'},
229
- server: {started: Date.now()}
230
- }
231
- })
232
-
233
- axios.post.mockRejectedValue(new Error('Network error'))
234
-
235
- Hub.checkCounter = 59
236
- Hub.check()
237
-
238
- await new Promise(resolve => setImmediate(resolve))
239
- expect(mockLog).toHaveBeenCalledWith('Failed to report status: %s', 'Network error')
240
-
241
- jest.useFakeTimers()
242
- })
243
- })
244
-
245
- describe('authentication', () => {
246
- it('should authenticate with valid code', async () => {
247
- const mockResponse = {
248
- data: {
249
- result: {success: true},
250
- data: {
251
- token: 'new-token',
252
- secret: 'new-secret'
253
- }
254
- }
255
- }
256
-
257
- const mockApiResult = {success: true, message: 'Authentication successful'}
258
- mockOdac.setMock('server', 'Api', {
259
- result: jest.fn(() => mockApiResult)
260
- })
261
-
262
- axios.post.mockResolvedValue(mockResponse)
263
-
264
- const result = await Hub.auth('valid-code')
265
-
266
- expect(axios.post).toHaveBeenCalled()
267
- expect(result).toEqual(mockApiResult)
268
- expect(mockOdac.core('Config').config.hub).toEqual({
269
- token: 'new-token',
270
- secret: 'new-secret'
271
- })
272
- })
273
-
274
- it('should handle authentication failure', async () => {
275
- const mockApiResult = {success: false, message: 'Authentication failed'}
276
- mockOdac.setMock('server', 'Api', {
277
- result: jest.fn(() => mockApiResult)
278
- })
279
-
280
- axios.post.mockRejectedValue(new Error('Invalid code'))
281
-
282
- const result = await Hub.auth('invalid-code')
283
-
284
- expect(result).toEqual(mockApiResult)
285
- expect(mockLog).toHaveBeenCalledWith('Authentication failed: %s', 'Invalid code')
286
- })
287
-
288
- it('should include distro info on Linux', async () => {
289
- os.platform.mockReturnValue('linux')
290
- fs.readFileSync.mockReturnValue('NAME="Ubuntu"\nVERSION_ID="20.04"\nID=ubuntu')
291
-
292
- axios.post.mockResolvedValue({
293
- data: {
294
- result: {success: true},
295
- data: {token: 'token', secret: 'secret'}
296
- }
297
- })
298
-
299
- await Hub.auth('code')
300
-
301
- const callArgs = axios.post.mock.calls[0][1]
302
- expect(callArgs.distro).toBeDefined()
303
- expect(callArgs.distro.name).toBe('Ubuntu')
304
- })
305
- })
306
-
307
- describe('system status', () => {
308
- it('should get system status', () => {
309
- const status = Hub.getSystemStatus()
310
-
311
- expect(status).toHaveProperty('cpu')
312
- expect(status).toHaveProperty('memory')
313
- expect(status).toHaveProperty('disk')
314
- expect(status).toHaveProperty('network')
315
- expect(status).toHaveProperty('services')
316
- expect(status).toHaveProperty('uptime')
317
- expect(status.hostname).toBe('test-host')
318
- expect(status.platform).toBe('linux')
319
- expect(status.arch).toBe('x64')
320
- })
321
-
322
- it('should get services info', () => {
323
- mockOdac.setMock('core', 'Config', {
324
- config: {
325
- websites: {
326
- 'example.com': {},
327
- 'test.com': {}
328
- },
329
- services: ['web', 'mail'],
330
- mail: {
331
- accounts: {
332
- 'user@example.com': {},
333
- 'admin@test.com': {}
334
- }
335
- }
336
- }
337
- })
338
-
339
- const services = Hub.getServicesInfo()
340
-
341
- expect(services.websites).toBe(2)
342
- expect(services.services).toBe(2)
343
- expect(services.mail).toBe(2)
344
- })
345
-
346
- it('should handle missing services config', () => {
347
- mockOdac.setMock('core', 'Config', {config: {}})
348
-
349
- const services = Hub.getServicesInfo()
350
-
351
- expect(services.websites).toBe(0)
352
- expect(services.services).toBe(0)
353
- expect(services.mail).toBe(0)
354
- })
355
- })
356
-
357
- describe('memory usage', () => {
358
- it('should get memory usage on Linux', () => {
359
- os.platform.mockReturnValue('linux')
360
- os.totalmem.mockReturnValue(8589934592)
361
- os.freemem.mockReturnValue(4294967296)
362
-
363
- const memory = Hub.getMemoryUsage()
364
-
365
- expect(memory.total).toBe(8589934592)
366
- expect(memory.used).toBe(4294967296)
367
- })
368
- })
369
-
370
- describe('CPU usage', () => {
371
- it('should return 0 on first call', () => {
372
- const usage = Hub.getCpuUsage()
373
- expect(usage).toBe(0)
374
- })
375
-
376
- it('should calculate CPU usage on subsequent calls', () => {
377
- Hub.getCpuUsage()
378
-
379
- os.cpus.mockReturnValue([
380
- {times: {user: 2000, nice: 0, sys: 1000, idle: 7000, irq: 0}},
381
- {times: {user: 2000, nice: 0, sys: 1000, idle: 7000, irq: 0}}
382
- ])
383
-
384
- const usage = Hub.getCpuUsage()
385
- expect(usage).toBeGreaterThanOrEqual(0)
386
- expect(usage).toBeLessThanOrEqual(100)
387
- })
388
- })
389
-
390
- describe('request signing', () => {
391
- it('should sign request with secret', () => {
392
- mockOdac.setMock('core', 'Config', {
393
- config: {
394
- hub: {token: 'token', secret: 'test-secret'}
395
- }
396
- })
397
-
398
- const data = {test: 'data'}
399
- const signature = Hub.signRequest(data)
400
-
401
- expect(signature).toBeTruthy()
402
- expect(typeof signature).toBe('string')
403
- })
404
-
405
- it('should return null without secret', () => {
406
- mockOdac.setMock('core', 'Config', {
407
- config: {hub: {token: 'token'}}
408
- })
409
-
410
- const signature = Hub.signRequest({test: 'data'})
411
- expect(signature).toBeNull()
412
- })
413
- })
414
-
415
- describe('API calls', () => {
416
- it('should make successful API call', async () => {
417
- axios.post.mockResolvedValue({
418
- data: {
419
- result: {success: true},
420
- data: {response: 'data'}
421
- }
422
- })
423
-
424
- const result = await Hub.call('test-action', {param: 'value'})
425
-
426
- expect(result).toEqual({response: 'data'})
427
- expect(axios.post).toHaveBeenCalledWith('https://hub.odac.run/test-action', {param: 'value'}, expect.any(Object))
428
- })
429
-
430
- it('should include authorization header when token exists', async () => {
431
- mockOdac.setMock('core', 'Config', {
432
- config: {
433
- hub: {token: 'test-token', secret: 'test-secret'}
434
- }
435
- })
436
-
437
- axios.post.mockResolvedValue({
438
- data: {
439
- result: {success: true},
440
- data: {}
441
- }
442
- })
443
-
444
- await Hub.call('test', {})
445
-
446
- const callArgs = axios.post.mock.calls[0][2]
447
- expect(callArgs.headers.Authorization).toBe('Bearer test-token')
448
- })
449
-
450
- it('should handle API errors', async () => {
451
- axios.post.mockResolvedValue({
452
- data: {
453
- result: {success: false, message: 'API error'}
454
- }
455
- })
456
-
457
- await expect(Hub.call('test', {})).rejects.toBe('API error')
458
- })
459
-
460
- it('should handle network errors', async () => {
461
- axios.post.mockRejectedValue({
462
- response: {status: 500, data: 'Server error'}
463
- })
464
-
465
- await expect(Hub.call('test', {})).rejects.toBe('Server error')
466
- })
467
- })
468
-
469
- describe('Linux distro detection', () => {
470
- it('should return null on non-Linux platforms', () => {
471
- os.platform.mockReturnValue('darwin')
472
- const distro = Hub.getLinuxDistro()
473
- expect(distro).toBeNull()
474
- })
475
-
476
- it('should parse os-release file', () => {
477
- os.platform.mockReturnValue('linux')
478
- fs.readFileSync.mockReturnValue('NAME="Ubuntu"\nVERSION_ID="20.04"\nID=ubuntu')
479
-
480
- const distro = Hub.getLinuxDistro()
481
-
482
- expect(distro.name).toBe('Ubuntu')
483
- expect(distro.version).toBe('20.04')
484
- expect(distro.id).toBe('ubuntu')
485
- })
486
-
487
- it('should handle missing os-release file', () => {
488
- os.platform.mockReturnValue('linux')
489
- fs.readFileSync.mockImplementation(() => {
490
- throw new Error('File not found')
491
- })
492
-
493
- const distro = Hub.getLinuxDistro()
494
- expect(distro).toBeNull()
495
- })
496
- })
497
- })
@@ -1,73 +0,0 @@
1
- const Log = require('../../core/Log')
2
-
3
- describe('Log', () => {
4
- let consoleLogSpy
5
- let consoleErrorSpy
6
-
7
- beforeEach(() => {
8
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
9
- consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
10
- })
11
-
12
- afterEach(() => {
13
- consoleLogSpy.mockRestore()
14
- consoleErrorSpy.mockRestore()
15
- })
16
-
17
- it('should initialize with a module prefix', () => {
18
- const log = new Log()
19
- const logger = log.init('TestModule')
20
- logger.log('test message')
21
- expect(consoleLogSpy).toHaveBeenCalledWith('[TestModule] ', 'test message')
22
- })
23
-
24
- it('should handle multiple module prefixes', () => {
25
- const log = new Log()
26
- const logger = log.init('Module1', 'Module2')
27
- logger.log('test message')
28
- expect(consoleLogSpy).toHaveBeenCalledWith('[Module1][Module2] ', 'test message')
29
- })
30
-
31
- it('should log messages correctly', () => {
32
- const log = new Log()
33
- const logger = log.init('Test')
34
- logger.log('message1', 'message2')
35
- expect(consoleLogSpy).toHaveBeenCalledWith('[Test] ', 'message1', 'message2')
36
- })
37
-
38
- it('should log error messages correctly', () => {
39
- const log = new Log()
40
- const logger = log.init('Test')
41
- logger.error('error message')
42
- expect(consoleErrorSpy).toHaveBeenCalledWith('[Test] ', 'error message')
43
- })
44
-
45
- it('should handle %s format specifiers', () => {
46
- const log = new Log()
47
- const logger = log.init('FormatTest')
48
- logger.log('Hello, %s!', 'World')
49
- expect(consoleLogSpy).toHaveBeenCalledWith('[FormatTest] ', 'Hello, World!')
50
- })
51
-
52
- it('should handle multiple %s format specifiers', () => {
53
- const log = new Log()
54
- const logger = log.init('FormatTest')
55
- logger.log('Hello, %s! Welcome, %s.', 'John', 'Jane')
56
- expect(consoleLogSpy).toHaveBeenCalledWith('[FormatTest] ', 'Hello, John! Welcome, Jane.')
57
- })
58
-
59
- it('should handle missing arguments for %s', () => {
60
- const log = new Log()
61
- const logger = log.init('FormatTest')
62
- logger.log('Hello, %s!')
63
- expect(consoleLogSpy).toHaveBeenCalledWith('[FormatTest] ', 'Hello, !')
64
- })
65
-
66
- it('should not log anything if no arguments are provided to log()', () => {
67
- const log = new Log()
68
- const logger = log.init('Test')
69
- const result = logger.log()
70
- expect(consoleLogSpy).not.toHaveBeenCalled()
71
- expect(result).toBeInstanceOf(Log)
72
- })
73
- })