odac 1.4.1 → 1.4.3
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.
- package/.agent/rules/memory.md +5 -0
- package/.releaserc.js +9 -2
- package/CHANGELOG.md +64 -0
- package/README.md +1 -1
- package/bin/odac.js +3 -2
- package/client/odac.js +124 -28
- package/docs/ai/skills/backend/database.md +19 -0
- package/docs/ai/skills/backend/forms.md +107 -13
- package/docs/ai/skills/backend/migrations.md +8 -2
- package/docs/ai/skills/backend/validation.md +132 -32
- package/docs/ai/skills/frontend/forms.md +43 -15
- package/docs/backend/08-database/02-basics.md +49 -9
- package/docs/backend/08-database/04-migrations.md +1 -0
- package/package.json +1 -1
- package/src/Auth.js +15 -2
- package/src/Database/ConnectionFactory.js +1 -0
- package/src/Database/Migration.js +26 -1
- package/src/Database/nanoid.js +30 -0
- package/src/Database.js +122 -11
- package/src/Ipc.js +37 -0
- package/src/Odac.js +1 -1
- package/src/Route/Cron.js +11 -0
- package/src/Route.js +49 -30
- package/src/Server.js +77 -23
- package/src/Storage.js +15 -1
- package/src/Validator.js +22 -20
- package/test/{Auth.test.js → Auth/check.test.js} +91 -5
- package/test/Client/data.test.js +91 -0
- package/test/Client/get.test.js +90 -0
- package/test/Client/storage.test.js +87 -0
- package/test/Client/token.test.js +82 -0
- package/test/Client/ws.test.js +118 -0
- package/test/Config/deepMerge.test.js +14 -0
- package/test/Config/init.test.js +66 -0
- package/test/Config/interpolate.test.js +35 -0
- package/test/Database/ConnectionFactory/buildConnectionConfig.test.js +13 -0
- package/test/Database/ConnectionFactory/buildConnections.test.js +31 -0
- package/test/Database/ConnectionFactory/resolveClient.test.js +12 -0
- package/test/Database/Migration/migrate_column.test.js +52 -0
- package/test/Database/Migration/migrate_files.test.js +70 -0
- package/test/Database/Migration/migrate_index.test.js +89 -0
- package/test/Database/Migration/migrate_nanoid.test.js +160 -0
- package/test/Database/Migration/migrate_seed.test.js +77 -0
- package/test/Database/Migration/migrate_table.test.js +88 -0
- package/test/Database/Migration/rollback.test.js +61 -0
- package/test/Database/Migration/snapshot.test.js +38 -0
- package/test/Database/Migration/status.test.js +41 -0
- package/test/Database/autoNanoid.test.js +215 -0
- package/test/Database/nanoid.test.js +19 -0
- package/test/Lang/constructor.test.js +25 -0
- package/test/Lang/get.test.js +65 -0
- package/test/Lang/set.test.js +49 -0
- package/test/Odac/init.test.js +42 -0
- package/test/Odac/instance.test.js +58 -0
- package/test/Route/{Middleware.test.js → Middleware/chaining.test.js} +5 -29
- package/test/Route/Middleware/use.test.js +35 -0
- package/test/{Route.test.js → Route/check.test.js} +100 -50
- package/test/Route/set.test.js +52 -0
- package/test/Route/ws.test.js +23 -0
- package/test/View/EarlyHints/cache.test.js +32 -0
- package/test/View/EarlyHints/extractFromHtml.test.js +143 -0
- package/test/View/EarlyHints/formatLinkHeader.test.js +33 -0
- package/test/View/EarlyHints/send.test.js +99 -0
- package/test/View/{Form.test.js → Form/generateFieldHtml.test.js} +2 -2
- package/test/View/constructor.test.js +22 -0
- package/test/View/print.test.js +19 -0
- package/test/WebSocket/Client/limits.test.js +55 -0
- package/test/WebSocket/Server/broadcast.test.js +33 -0
- package/test/WebSocket/Server/route.test.js +37 -0
- package/test/Client.test.js +0 -197
- package/test/Config.test.js +0 -119
- package/test/Database/ConnectionFactory.test.js +0 -80
- package/test/Lang.test.js +0 -92
- package/test/Migration.test.js +0 -943
- package/test/Odac.test.js +0 -88
- package/test/View/EarlyHints.test.js +0 -282
- package/test/WebSocket.test.js +0 -238
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tests the Database.js proxy's auto-nanoid insert interception and schema metadata loading.
|
|
9
|
+
* Why: Validates that columns with type 'nanoid' are auto-populated on insert
|
|
10
|
+
* without requiring manual Odac.DB.nanoid() calls — core to ODAC's zero-config philosophy.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let tmpDir, knexLib, db
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'odac-db-autonanoid-'))
|
|
17
|
+
knexLib = require('knex')
|
|
18
|
+
db = knexLib({client: 'sqlite3', connection: {filename: ':memory:'}, useNullAsDefault: true})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await db.destroy()
|
|
23
|
+
fs.rmSync(tmpDir, {recursive: true, force: true})
|
|
24
|
+
jest.resetModules()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function writeSchema(name, content) {
|
|
28
|
+
const dir = path.join(tmpDir, 'schema')
|
|
29
|
+
fs.mkdirSync(dir, {recursive: true})
|
|
30
|
+
fs.writeFileSync(path.join(dir, `${name}.js`), `module.exports = ${JSON.stringify(content, null, 2)}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('Database.js - Auto NanoID Insert', () => {
|
|
34
|
+
it('should auto-generate nanoid on single-row insert', async () => {
|
|
35
|
+
await db.schema.createTable('posts', table => {
|
|
36
|
+
table.string('id', 21).primary()
|
|
37
|
+
table.string('title', 255)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const DB = require('../../src/Database')
|
|
41
|
+
DB.connections = {default: db}
|
|
42
|
+
db._odacConnectionKey = 'default'
|
|
43
|
+
DB._nanoidColumns = {default: {posts: [{column: 'id', size: 21}]}}
|
|
44
|
+
|
|
45
|
+
// Access through the proxy — Odac.DB.posts.insert()
|
|
46
|
+
await DB.posts.insert({title: 'Hello World'})
|
|
47
|
+
|
|
48
|
+
const rows = await db('posts').select()
|
|
49
|
+
expect(rows).toHaveLength(1)
|
|
50
|
+
expect(rows[0].title).toBe('Hello World')
|
|
51
|
+
expect(typeof rows[0].id).toBe('string')
|
|
52
|
+
expect(rows[0].id.length).toBe(21)
|
|
53
|
+
expect(rows[0].id).toMatch(/^[a-zA-Z0-9]+$/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should auto-generate unique nanoid for bulk inserts', async () => {
|
|
57
|
+
await db.schema.createTable('items', table => {
|
|
58
|
+
table.string('id', 21).primary()
|
|
59
|
+
table.string('name', 100)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const DB = require('../../src/Database')
|
|
63
|
+
DB.connections = {default: db}
|
|
64
|
+
db._odacConnectionKey = 'default'
|
|
65
|
+
DB._nanoidColumns = {default: {items: [{column: 'id', size: 21}]}}
|
|
66
|
+
|
|
67
|
+
await DB.items.insert([{name: 'Item A'}, {name: 'Item B'}, {name: 'Item C'}])
|
|
68
|
+
|
|
69
|
+
const rows = await db('items').select()
|
|
70
|
+
expect(rows).toHaveLength(3)
|
|
71
|
+
|
|
72
|
+
const ids = rows.map(r => r.id)
|
|
73
|
+
expect(new Set(ids).size).toBe(3) // all unique
|
|
74
|
+
|
|
75
|
+
for (const id of ids) {
|
|
76
|
+
expect(id.length).toBe(21)
|
|
77
|
+
expect(id).toMatch(/^[a-zA-Z0-9]+$/)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should NOT overwrite user-provided id', async () => {
|
|
82
|
+
await db.schema.createTable('docs', table => {
|
|
83
|
+
table.string('id', 21).primary()
|
|
84
|
+
table.string('content', 500)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const DB = require('../../src/Database')
|
|
88
|
+
DB.connections = {default: db}
|
|
89
|
+
db._odacConnectionKey = 'default'
|
|
90
|
+
DB._nanoidColumns = {default: {docs: [{column: 'id', size: 21}]}}
|
|
91
|
+
|
|
92
|
+
await DB.docs.insert({id: 'MY_CUSTOM_ID_1234567', content: 'Test'})
|
|
93
|
+
|
|
94
|
+
const rows = await db('docs').select()
|
|
95
|
+
expect(rows[0].id).toBe('MY_CUSTOM_ID_1234567')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should support custom nanoid length per column', async () => {
|
|
99
|
+
await db.schema.createTable('tokens', table => {
|
|
100
|
+
table.string('code', 8).primary()
|
|
101
|
+
table.string('label', 100)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const DB = require('../../src/Database')
|
|
105
|
+
DB.connections = {default: db}
|
|
106
|
+
db._odacConnectionKey = 'default'
|
|
107
|
+
DB._nanoidColumns = {default: {tokens: [{column: 'code', size: 8}]}}
|
|
108
|
+
|
|
109
|
+
await DB.tokens.insert({label: 'discount'})
|
|
110
|
+
|
|
111
|
+
const rows = await db('tokens').select()
|
|
112
|
+
expect(rows[0].code.length).toBe(8)
|
|
113
|
+
expect(rows[0].code).toMatch(/^[a-zA-Z0-9]+$/)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should NOT interfere with tables without nanoid columns', async () => {
|
|
117
|
+
await db.schema.createTable('logs', table => {
|
|
118
|
+
table.increments('id')
|
|
119
|
+
table.string('message', 500)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const DB = require('../../src/Database')
|
|
123
|
+
DB.connections = {default: db}
|
|
124
|
+
db._odacConnectionKey = 'default'
|
|
125
|
+
DB._nanoidColumns = {} // no nanoid metadata
|
|
126
|
+
|
|
127
|
+
await DB.logs.insert({message: 'test log'})
|
|
128
|
+
|
|
129
|
+
const rows = await db('logs').select()
|
|
130
|
+
expect(rows).toHaveLength(1)
|
|
131
|
+
expect(rows[0].message).toBe('test log')
|
|
132
|
+
expect(rows[0].id).toBe(1) // auto-increment
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('Database._loadNanoidMeta()', () => {
|
|
137
|
+
it('should detect nanoid columns from schema files', () => {
|
|
138
|
+
writeSchema('users', {
|
|
139
|
+
columns: {
|
|
140
|
+
id: {type: 'nanoid', primary: true},
|
|
141
|
+
name: {type: 'string', length: 255}
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const DB = require('../../src/Database')
|
|
146
|
+
global.__dir = tmpDir
|
|
147
|
+
DB._nanoidColumns = {}
|
|
148
|
+
DB._loadNanoidMeta()
|
|
149
|
+
|
|
150
|
+
expect(DB._nanoidColumns).toHaveProperty('default')
|
|
151
|
+
expect(DB._nanoidColumns.default).toHaveProperty('users')
|
|
152
|
+
expect(DB._nanoidColumns.default.users).toEqual([{column: 'id', size: 21}])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should detect multiple nanoid columns in a single table', () => {
|
|
156
|
+
writeSchema('events', {
|
|
157
|
+
columns: {
|
|
158
|
+
id: {type: 'nanoid', primary: true},
|
|
159
|
+
public_id: {type: 'nanoid', length: 12},
|
|
160
|
+
name: {type: 'string'}
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const DB = require('../../src/Database')
|
|
165
|
+
global.__dir = tmpDir
|
|
166
|
+
DB._nanoidColumns = {}
|
|
167
|
+
DB._loadNanoidMeta()
|
|
168
|
+
|
|
169
|
+
expect(DB._nanoidColumns.default.events).toEqual([
|
|
170
|
+
{column: 'id', size: 21},
|
|
171
|
+
{column: 'public_id', size: 12}
|
|
172
|
+
])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should skip tables without nanoid columns', () => {
|
|
176
|
+
writeSchema('logs', {
|
|
177
|
+
columns: {
|
|
178
|
+
id: {type: 'increments'},
|
|
179
|
+
message: {type: 'text'}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const DB = require('../../src/Database')
|
|
184
|
+
global.__dir = tmpDir
|
|
185
|
+
DB._nanoidColumns = {}
|
|
186
|
+
DB._loadNanoidMeta()
|
|
187
|
+
|
|
188
|
+
expect(DB._nanoidColumns.default).not.toHaveProperty('logs')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should handle missing schema directory gracefully', () => {
|
|
192
|
+
const DB = require('../../src/Database')
|
|
193
|
+
global.__dir = tmpDir
|
|
194
|
+
DB._nanoidColumns = {}
|
|
195
|
+
DB._loadNanoidMeta()
|
|
196
|
+
|
|
197
|
+
expect(DB._nanoidColumns).toEqual({})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should detect nanoid in subdirectory schemas (named connections)', () => {
|
|
201
|
+
const subDir = path.join(tmpDir, 'schema', 'analytics')
|
|
202
|
+
fs.mkdirSync(subDir, {recursive: true})
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
path.join(subDir, 'metrics.js'),
|
|
205
|
+
`module.exports = ${JSON.stringify({columns: {id: {type: 'nanoid', length: 16}, value: {type: 'integer'}}})}`
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const DB = require('../../src/Database')
|
|
209
|
+
global.__dir = tmpDir
|
|
210
|
+
DB._nanoidColumns = {}
|
|
211
|
+
DB._loadNanoidMeta()
|
|
212
|
+
|
|
213
|
+
expect(DB._nanoidColumns.analytics.metrics).toEqual([{column: 'id', size: 16}])
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const DB = require('../../src/Database')
|
|
2
|
+
|
|
3
|
+
describe('Database.nanoid()', () => {
|
|
4
|
+
it('should generate a string of default length (21)', () => {
|
|
5
|
+
const id = DB.nanoid()
|
|
6
|
+
expect(typeof id).toBe('string')
|
|
7
|
+
expect(id.length).toBe(21)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should generate a string of specified length', () => {
|
|
11
|
+
const id = DB.nanoid(10)
|
|
12
|
+
expect(id.length).toBe(10)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should generate only alphanumeric characters', () => {
|
|
16
|
+
const id = DB.nanoid(100)
|
|
17
|
+
expect(id).toMatch(/^[a-zA-Z0-9]+$/)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const Lang = require('../../src/Lang')
|
|
2
|
+
|
|
3
|
+
describe('Lang.constructor()', () => {
|
|
4
|
+
let mockOdac
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
mockOdac = {
|
|
8
|
+
Config: {lang: {default: 'en'}},
|
|
9
|
+
Var: jest.fn(() => ({is: () => true})),
|
|
10
|
+
Request: {header: jest.fn()}
|
|
11
|
+
}
|
|
12
|
+
global.__dir = '/mock'
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should initialize successfully', () => {
|
|
16
|
+
const lang = new Lang(mockOdac)
|
|
17
|
+
expect(lang).toBeDefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should call Var if lang is provided', () => {
|
|
21
|
+
const lang = new Lang(mockOdac)
|
|
22
|
+
lang.set('en')
|
|
23
|
+
expect(mockOdac.Var).toHaveBeenCalledWith('en')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const Lang = require('../../src/Lang')
|
|
3
|
+
|
|
4
|
+
jest.mock('fs')
|
|
5
|
+
|
|
6
|
+
describe('Lang.get()', () => {
|
|
7
|
+
let mockOdac
|
|
8
|
+
let lang
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks()
|
|
12
|
+
global.__dir = '/mock'
|
|
13
|
+
|
|
14
|
+
mockOdac = {
|
|
15
|
+
Config: {lang: {default: 'en'}},
|
|
16
|
+
Var: jest.fn(val => ({
|
|
17
|
+
is: jest.fn(type => type === 'alpha' && /^[a-zA-Z]+$/.test(val))
|
|
18
|
+
})),
|
|
19
|
+
Request: {
|
|
20
|
+
header: jest.fn()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fs.existsSync.mockReturnValue(false)
|
|
25
|
+
fs.mkdirSync.mockImplementation(() => {})
|
|
26
|
+
fs.writeFileSync.mockImplementation(() => {})
|
|
27
|
+
fs.readFileSync.mockImplementation(() => {
|
|
28
|
+
throw new Error('ENOENT')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
lang = new Lang(mockOdac)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should return matching string and support placeholders', () => {
|
|
35
|
+
fs.existsSync.mockImplementation(path => path.includes('/tr.json'))
|
|
36
|
+
fs.readFileSync.mockReturnValue(
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
welcome: 'Merhaba %s!'
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
lang.set('tr')
|
|
43
|
+
expect(lang.get('welcome', 'Emre')).toBe('Merhaba Emre!')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should support numbered placeholders', () => {
|
|
47
|
+
fs.existsSync.mockImplementation(path => path.includes('/en.json'))
|
|
48
|
+
fs.readFileSync.mockReturnValue(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
order: 'First: %s1, Second: %s2'
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
lang.set('en')
|
|
55
|
+
expect(lang.get('order', 'A', 'B')).toBe('First: A, Second: B')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should auto-save new keys', () => {
|
|
59
|
+
lang.set('en')
|
|
60
|
+
const result = lang.get('new_key')
|
|
61
|
+
|
|
62
|
+
expect(result).toBe('new_key')
|
|
63
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/en.json'), expect.stringContaining('"new_key": "new_key"'))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const Lang = require('../../src/Lang')
|
|
3
|
+
|
|
4
|
+
jest.mock('fs')
|
|
5
|
+
|
|
6
|
+
describe('Lang.set()', () => {
|
|
7
|
+
let mockOdac
|
|
8
|
+
let lang
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks()
|
|
12
|
+
global.__dir = '/mock'
|
|
13
|
+
|
|
14
|
+
mockOdac = {
|
|
15
|
+
Config: {lang: {default: 'en'}},
|
|
16
|
+
Var: jest.fn(val => ({
|
|
17
|
+
is: jest.fn(type => type === 'alpha' && /^[a-zA-Z]+$/.test(val))
|
|
18
|
+
})),
|
|
19
|
+
Request: {
|
|
20
|
+
header: jest.fn()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fs.existsSync.mockReturnValue(false)
|
|
25
|
+
fs.readFileSync.mockImplementation(() => {
|
|
26
|
+
throw new Error('ENOENT')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should default to en when no config or header', () => {
|
|
31
|
+
lang = new Lang(mockOdac)
|
|
32
|
+
lang.set()
|
|
33
|
+
// No Var call here because lang is falsy
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should use lang from header if available', () => {
|
|
37
|
+
mockOdac.Request.header.mockReturnValue('tr-TR')
|
|
38
|
+
lang = new Lang(mockOdac)
|
|
39
|
+
lang.set()
|
|
40
|
+
// Verifying it uses 'tr' can be tricky since it's private, but constructor calls set()
|
|
41
|
+
expect(mockOdac.Request.header).toHaveBeenCalledWith('ACCEPT-LANGUAGE')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should use explicit lang in set()', () => {
|
|
45
|
+
lang = new Lang(mockOdac)
|
|
46
|
+
lang.set('fr')
|
|
47
|
+
expect(mockOdac.Var).toHaveBeenCalledWith('fr')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const Odac = require('../../src/Odac')
|
|
2
|
+
|
|
3
|
+
// Mock all dependencies
|
|
4
|
+
jest.mock('../../src/Storage', () => ({init: jest.fn()}))
|
|
5
|
+
jest.mock('../../src/Env', () => ({init: jest.fn(), get: jest.fn()}))
|
|
6
|
+
jest.mock('../../src/Config', () => ({
|
|
7
|
+
init: jest.fn(),
|
|
8
|
+
request: {timeout: 10000},
|
|
9
|
+
lang: {default: 'en'}
|
|
10
|
+
}))
|
|
11
|
+
jest.mock('../../src/Database', () => ({init: jest.fn()}))
|
|
12
|
+
jest.mock('../../src/Ipc', () => ({init: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn()}))
|
|
13
|
+
jest.mock('../../src/Route', () => {
|
|
14
|
+
return jest.fn().mockImplementation(() => ({
|
|
15
|
+
init: jest.fn(),
|
|
16
|
+
routes: {
|
|
17
|
+
www: {}
|
|
18
|
+
}
|
|
19
|
+
}))
|
|
20
|
+
})
|
|
21
|
+
jest.mock('../../src/Server', () => ({init: jest.fn()}))
|
|
22
|
+
|
|
23
|
+
describe('Odac.init()', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks()
|
|
26
|
+
global.__dir = '/mock/project'
|
|
27
|
+
delete global.Odac
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should initialize all components and set global.Odac', async () => {
|
|
31
|
+
await Odac.init()
|
|
32
|
+
expect(global.Odac).toBeDefined()
|
|
33
|
+
expect(global.Odac.Storage).toBeDefined()
|
|
34
|
+
expect(global.Odac.Config).toBeDefined()
|
|
35
|
+
expect(global.Odac.Env).toBeDefined()
|
|
36
|
+
expect(global.Odac.Database).toBeDefined()
|
|
37
|
+
expect(global.Odac.Ipc).toBeDefined()
|
|
38
|
+
expect(global.Odac.Route).toBeDefined()
|
|
39
|
+
expect(global.Odac.Server).toBeDefined()
|
|
40
|
+
expect(typeof global.__).toBe('function')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const Odac = require('../../src/Odac')
|
|
2
|
+
|
|
3
|
+
describe('Odac.instance()', () => {
|
|
4
|
+
let mockOdac
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
mockOdac = {
|
|
8
|
+
Config: {request: {timeout: 1000}},
|
|
9
|
+
DB: {init: jest.fn(), close: jest.fn()},
|
|
10
|
+
Auth: {constructor: jest.fn()},
|
|
11
|
+
Route: {
|
|
12
|
+
init: jest.fn(),
|
|
13
|
+
routes: {www: {}}
|
|
14
|
+
},
|
|
15
|
+
Storage: {get: jest.fn(), put: jest.fn()},
|
|
16
|
+
Env: {get: jest.fn()}
|
|
17
|
+
}
|
|
18
|
+
global.Odac = mockOdac
|
|
19
|
+
global.__dir = '/mock'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
delete global.Odac
|
|
24
|
+
delete global.__dir
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should create a context object with req/res', () => {
|
|
28
|
+
const mockReq = {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
url: '/',
|
|
31
|
+
headers: {host: 'www.example.com'},
|
|
32
|
+
connection: {remoteAddress: '127.0.0.1'},
|
|
33
|
+
on: jest.fn()
|
|
34
|
+
}
|
|
35
|
+
const mockRes = {on: jest.fn(), writeHead: jest.fn(), end: jest.fn()}
|
|
36
|
+
|
|
37
|
+
const ctx = Odac.instance('id', mockReq, mockRes)
|
|
38
|
+
|
|
39
|
+
expect(ctx.Request).toBeDefined()
|
|
40
|
+
expect(ctx.Request.id).toBe('id')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should provide helper methods on the context', () => {
|
|
44
|
+
const mockReq = {
|
|
45
|
+
method: 'GET',
|
|
46
|
+
url: '/',
|
|
47
|
+
headers: {host: 'www.example.com'},
|
|
48
|
+
connection: {remoteAddress: '127.0.0.1'},
|
|
49
|
+
on: jest.fn()
|
|
50
|
+
}
|
|
51
|
+
const mockRes = {on: jest.fn()}
|
|
52
|
+
const ctx = Odac.instance('id', mockReq, mockRes)
|
|
53
|
+
|
|
54
|
+
expect(typeof ctx.abort).toBe('function')
|
|
55
|
+
expect(typeof ctx.cookie).toBe('function')
|
|
56
|
+
expect(typeof ctx.env).toBe('function')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const Route = require('
|
|
1
|
+
const Route = require('../../../src/Route.js')
|
|
2
2
|
|
|
3
|
-
describe('Middleware
|
|
3
|
+
describe('Middleware Chaining Integration', () => {
|
|
4
4
|
let route
|
|
5
5
|
|
|
6
6
|
beforeEach(() => {
|
|
@@ -9,38 +9,21 @@ describe('Middleware System', () => {
|
|
|
9
9
|
global.__dir = __dirname
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
test('
|
|
13
|
-
const chain = route.use('auth', 'logger')
|
|
14
|
-
expect(chain).not.toBe(route)
|
|
15
|
-
expect(chain._middlewares).toEqual(['auth', 'logger'])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('use() should support chaining with more middlewares', () => {
|
|
19
|
-
const chain = route.use('auth').use('logger')
|
|
20
|
-
expect(chain._middlewares).toEqual(['auth', 'logger'])
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('page() should return this for chaining', () => {
|
|
12
|
+
test('page() should return this for chaining when not on a chain', () => {
|
|
24
13
|
const result = route.page('/', 'index')
|
|
25
14
|
expect(result).toBe(route)
|
|
26
15
|
})
|
|
27
16
|
|
|
28
|
-
test('post() should return this for chaining', () => {
|
|
17
|
+
test('post() should return this for chaining when not on a chain', () => {
|
|
29
18
|
const result = route.post('/api', 'api')
|
|
30
19
|
expect(result).toBe(route)
|
|
31
20
|
})
|
|
32
21
|
|
|
33
|
-
test('get() should return this for chaining', () => {
|
|
22
|
+
test('get() should return this for chaining when not on a chain', () => {
|
|
34
23
|
const result = route.get('/api', 'api')
|
|
35
24
|
expect(result).toBe(route)
|
|
36
25
|
})
|
|
37
26
|
|
|
38
|
-
test('auth.use() should return MiddlewareChain', () => {
|
|
39
|
-
const chain = route.auth.use('admin')
|
|
40
|
-
expect(chain).not.toBe(route)
|
|
41
|
-
expect(chain._middlewares).toEqual(['admin'])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
27
|
test('chaining should work: use().page().page()', () => {
|
|
45
28
|
route
|
|
46
29
|
.use('auth')
|
|
@@ -75,11 +58,4 @@ describe('Middleware System', () => {
|
|
|
75
58
|
route.use('cors', 'rateLimit').post('/api/upload', () => {})
|
|
76
59
|
expect(route.routes.test.post['/api/upload'].middlewares).toEqual(['cors', 'rateLimit'])
|
|
77
60
|
})
|
|
78
|
-
|
|
79
|
-
test('separate use() chains should be independent', () => {
|
|
80
|
-
route.use('auth').page('/profile', () => {})
|
|
81
|
-
route.use('cors').page('/api', () => {})
|
|
82
|
-
expect(route.routes.test.page['/profile'].middlewares).toEqual(['auth'])
|
|
83
|
-
expect(route.routes.test.page['/api'].middlewares).toEqual(['cors'])
|
|
84
|
-
})
|
|
85
61
|
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const Route = require('../../../src/Route.js')
|
|
2
|
+
|
|
3
|
+
describe('MiddlewareChain.use()', () => {
|
|
4
|
+
let route
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
route = new Route()
|
|
8
|
+
global.Odac = {Route: {buff: 'test'}}
|
|
9
|
+
global.__dir = __dirname
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('use() should return MiddlewareChain', () => {
|
|
13
|
+
const chain = route.use('auth', 'logger')
|
|
14
|
+
expect(chain).not.toBe(route)
|
|
15
|
+
expect(chain._middlewares).toEqual(['auth', 'logger'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('use() should support chaining with more middlewares', () => {
|
|
19
|
+
const chain = route.use('auth').use('logger')
|
|
20
|
+
expect(chain._middlewares).toEqual(['auth', 'logger'])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('auth.use() should return MiddlewareChain', () => {
|
|
24
|
+
const chain = route.auth.use('admin')
|
|
25
|
+
expect(chain).not.toBe(route)
|
|
26
|
+
expect(chain._middlewares).toEqual(['admin'])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('separate use() chains should be independent', () => {
|
|
30
|
+
route.use('auth').page('/profile', () => {})
|
|
31
|
+
route.use('cors').page('/api', () => {})
|
|
32
|
+
expect(route.routes.test.page['/profile'].middlewares).toEqual(['auth'])
|
|
33
|
+
expect(route.routes.test.page['/api'].middlewares).toEqual(['cors'])
|
|
34
|
+
})
|
|
35
|
+
})
|