odac 1.4.15 → 1.4.16
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/CHANGELOG.md +17 -0
- package/client/odac.js +38 -1
- package/docs/ai/skills/backend/forms.md +54 -3
- package/docs/ai/skills/backend/validation.md +30 -2
- package/docs/backend/05-forms/01-custom-forms.md +52 -0
- package/docs/backend/09-validation/01-the-validator-service.md +40 -0
- package/docs/frontend/03-forms/01-form-handling.md +30 -13
- package/package.json +2 -1
- package/src/Auth.js +252 -24
- package/src/Config.js +5 -1
- package/src/Odac.js +3 -0
- package/src/Request.js +235 -34
- package/src/Route/Internal.js +32 -3
- package/src/Validator.js +143 -2
- package/src/View/Form.js +56 -0
- package/src/View/Image.js +15 -6
- package/src/View.js +2 -2
- package/test/Auth/check.test.js +219 -18
- package/test/Auth/verifyMagicLink.test.js +8 -1
- package/test/Odac/cache.test.js +5 -2
- package/test/Odac/image.test.js +8 -5
- package/test/Odac/instance.test.js +5 -2
- package/test/Request/cache.test.js +1 -0
- package/test/Request/multipart.test.js +164 -0
- package/test/Validator/file.test.js +172 -0
- package/test/View/Form/generateFieldHtml.test.js +56 -0
- package/test/View/Image/serve.test.js +9 -4
- package/test/View/Image/url.test.js +10 -4
- package/test/View/addNavigateAttribute.test.js +10 -1
- package/test/View/print.test.js +10 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const {PassThrough} = require('stream')
|
|
5
|
+
const OdacRequest = require('../../src/Request')
|
|
6
|
+
|
|
7
|
+
const BOUNDARY = '----odacmultiparttest'
|
|
8
|
+
|
|
9
|
+
// Build a raw multipart/form-data body as a Buffer so binary parts (with \r\n
|
|
10
|
+
// and null bytes inside) survive intact — the regression case the old
|
|
11
|
+
// string-splitting parser corrupted.
|
|
12
|
+
function buildBody(parts) {
|
|
13
|
+
const chunks = []
|
|
14
|
+
for (const p of parts) {
|
|
15
|
+
chunks.push(Buffer.from(`--${BOUNDARY}\r\n`))
|
|
16
|
+
if (p.filename !== undefined) {
|
|
17
|
+
chunks.push(Buffer.from(`Content-Disposition: form-data; name="${p.name}"; filename="${p.filename}"\r\n`))
|
|
18
|
+
chunks.push(Buffer.from(`Content-Type: ${p.contentType || 'application/octet-stream'}\r\n\r\n`))
|
|
19
|
+
chunks.push(p.data)
|
|
20
|
+
chunks.push(Buffer.from('\r\n'))
|
|
21
|
+
} else {
|
|
22
|
+
chunks.push(Buffer.from(`Content-Disposition: form-data; name="${p.name}"\r\n\r\n`))
|
|
23
|
+
chunks.push(Buffer.from(`${p.value}\r\n`))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
chunks.push(Buffer.from(`--${BOUNDARY}--\r\n`))
|
|
27
|
+
return Buffer.concat(chunks)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeReq() {
|
|
31
|
+
const req = new PassThrough()
|
|
32
|
+
req.method = 'POST'
|
|
33
|
+
req.url = '/upload'
|
|
34
|
+
req.headers = {
|
|
35
|
+
host: 'www.example.com',
|
|
36
|
+
'content-type': `multipart/form-data; boundary=${BOUNDARY}`
|
|
37
|
+
}
|
|
38
|
+
req.connection = {remoteAddress: '127.0.0.1', destroy: jest.fn()}
|
|
39
|
+
return req
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeRes() {
|
|
43
|
+
return {writeHead: jest.fn(), end: jest.fn(), finished: false, on: jest.fn()}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Temp-file cleanup in Request.js uses fire-and-forget fs.unlink(), which
|
|
47
|
+
// completes via libuv's thread pool. A single event-loop tick isn't a
|
|
48
|
+
// reliable signal that it finished, especially under CI I/O contention, so
|
|
49
|
+
// poll for the expected end-state instead of assuming a fixed number of ticks.
|
|
50
|
+
async function waitUntil(conditionFn, {timeoutMs = 2000, intervalMs = 10} = {}) {
|
|
51
|
+
const start = Date.now()
|
|
52
|
+
while (!conditionFn()) {
|
|
53
|
+
if (Date.now() - start > timeoutMs) return false
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs))
|
|
55
|
+
}
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('Request multipart parsing', () => {
|
|
60
|
+
let tmpDir, request
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'odac-mp-'))
|
|
64
|
+
global.Odac = {
|
|
65
|
+
Config: {request: {timeout: 5000, maxBodySize: 1e6, maxFileSize: 5 * 1024 * 1024, maxFiles: 10, uploadDir: tmpDir}},
|
|
66
|
+
Route: {routes: {www: {}}},
|
|
67
|
+
Request: {}
|
|
68
|
+
}
|
|
69
|
+
global.__dir = tmpDir
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
if (request) request.clearTimeout()
|
|
74
|
+
request = null
|
|
75
|
+
delete global.Odac
|
|
76
|
+
delete global.__dir
|
|
77
|
+
fs.rmSync(tmpDir, {recursive: true, force: true})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
function run(body) {
|
|
81
|
+
const req = makeReq()
|
|
82
|
+
const res = makeRes()
|
|
83
|
+
request = new OdacRequest('id', req, res, {setTimeout: (fn, ms) => setTimeout(fn, ms)})
|
|
84
|
+
req.end(body)
|
|
85
|
+
return request
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
it('parses text fields into data.post', async () => {
|
|
89
|
+
const r = run(buildBody([{name: 'title', value: 'hello world'}]))
|
|
90
|
+
const val = await r.request('title')
|
|
91
|
+
expect(val).toBe('hello world')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('round-trips binary file content byte-for-byte', async () => {
|
|
95
|
+
// Contains a PNG header, an embedded CRLF, and null bytes — exactly what the
|
|
96
|
+
// old toString()-based parser mangled.
|
|
97
|
+
const binary = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0xff, 0x0d, 0x0a, 0x42])
|
|
98
|
+
const r = run(buildBody([{name: 'avatar', filename: 'photo.png', contentType: 'image/png', data: binary}]))
|
|
99
|
+
|
|
100
|
+
const file = await r.file('avatar')
|
|
101
|
+
expect(file).toBeTruthy()
|
|
102
|
+
expect(file.name).toBe('photo.png')
|
|
103
|
+
expect(file.ext).toBe('png')
|
|
104
|
+
expect(file.mimetype).toBe('image/png')
|
|
105
|
+
expect(file.size).toBe(binary.length)
|
|
106
|
+
expect(file.truncated).toBe(false)
|
|
107
|
+
|
|
108
|
+
const written = fs.readFileSync(file.path)
|
|
109
|
+
expect(Buffer.compare(written, binary)).toBe(0)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('mixes text fields and a file in one request', async () => {
|
|
113
|
+
const r = run(
|
|
114
|
+
buildBody([
|
|
115
|
+
{name: 'name', value: 'Ada'},
|
|
116
|
+
{name: 'doc', filename: 'a.txt', contentType: 'text/plain', data: Buffer.from('content')}
|
|
117
|
+
])
|
|
118
|
+
)
|
|
119
|
+
expect(await r.request('name')).toBe('Ada')
|
|
120
|
+
const file = await r.file('doc')
|
|
121
|
+
expect(file.name).toBe('a.txt')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('returns null for an untouched (empty filename) file input', async () => {
|
|
125
|
+
const r = run(buildBody([{name: 'avatar', filename: '', contentType: 'application/octet-stream', data: Buffer.alloc(0)}]))
|
|
126
|
+
expect(await r.file('avatar')).toBeNull()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns an array when the same field has multiple files', async () => {
|
|
130
|
+
const r = run(
|
|
131
|
+
buildBody([
|
|
132
|
+
{name: 'docs', filename: '1.txt', contentType: 'text/plain', data: Buffer.from('one')},
|
|
133
|
+
{name: 'docs', filename: '2.txt', contentType: 'text/plain', data: Buffer.from('two')}
|
|
134
|
+
])
|
|
135
|
+
)
|
|
136
|
+
const files = await r.file('docs')
|
|
137
|
+
expect(Array.isArray(files)).toBe(true)
|
|
138
|
+
expect(files).toHaveLength(2)
|
|
139
|
+
expect(files.map(f => f.name).sort()).toEqual(['1.txt', '2.txt'])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('flags an oversize file as truncated with no leftover temp file', async () => {
|
|
143
|
+
global.Odac.Config.request.maxFileSize = 10 // bytes
|
|
144
|
+
const big = Buffer.alloc(1000, 0x41)
|
|
145
|
+
const r = run(buildBody([{name: 'avatar', filename: 'big.bin', contentType: 'application/octet-stream', data: big}]))
|
|
146
|
+
|
|
147
|
+
const file = await r.file('avatar')
|
|
148
|
+
expect(file.truncated).toBe(true)
|
|
149
|
+
expect(file.path).toBeNull()
|
|
150
|
+
// No orphaned odac-* temp file remains in the upload dir.
|
|
151
|
+
const noLeftovers = await waitUntil(() => fs.readdirSync(tmpDir).filter(n => n.startsWith('odac-')).length === 0)
|
|
152
|
+
expect(noLeftovers).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('cleans up unstored temp files after the response ends', async () => {
|
|
156
|
+
const r = run(buildBody([{name: 'avatar', filename: 'x.bin', contentType: 'application/octet-stream', data: Buffer.from('abc')}]))
|
|
157
|
+
const file = await r.file('avatar')
|
|
158
|
+
expect(fs.existsSync(file.path)).toBe(true)
|
|
159
|
+
|
|
160
|
+
r.end('ok')
|
|
161
|
+
const removed = await waitUntil(() => !fs.existsSync(file.path))
|
|
162
|
+
expect(removed).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const Validator = require('../../src/Validator')
|
|
5
|
+
|
|
6
|
+
// Minimal Odac stub — file rules never touch Var()/Auth(), but other rules do,
|
|
7
|
+
// so provide a small shim to keep the constructor happy.
|
|
8
|
+
const odacStub = {
|
|
9
|
+
Var: () => ({is: () => false})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function mockRequest(filesByField) {
|
|
13
|
+
return {
|
|
14
|
+
file: async name => {
|
|
15
|
+
const arr = filesByField[name]
|
|
16
|
+
if (!arr) return null
|
|
17
|
+
return arr.length === 1 ? arr[0] : arr
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fileObj(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
field: 'upload',
|
|
25
|
+
name: 'file.bin',
|
|
26
|
+
ext: 'bin',
|
|
27
|
+
mimetype: 'application/octet-stream',
|
|
28
|
+
size: 1000,
|
|
29
|
+
path: null,
|
|
30
|
+
truncated: false,
|
|
31
|
+
stored: false,
|
|
32
|
+
...overrides
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('Validator file rules', () => {
|
|
37
|
+
let tmpDir
|
|
38
|
+
|
|
39
|
+
beforeAll(() => {
|
|
40
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'odac-file-test-'))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterAll(() => {
|
|
44
|
+
fs.rmSync(tmpDir, {recursive: true, force: true})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function writeTmp(name, buf) {
|
|
48
|
+
const p = path.join(tmpDir, name)
|
|
49
|
+
fs.writeFileSync(p, buf)
|
|
50
|
+
return p
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('required', () => {
|
|
54
|
+
it('fails when no file is present', async () => {
|
|
55
|
+
const v = new Validator(mockRequest({}), odacStub)
|
|
56
|
+
v.file('avatar').check('required').message('Avatar required')
|
|
57
|
+
expect(await v.error()).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('passes when a file is present', async () => {
|
|
61
|
+
const v = new Validator(mockRequest({avatar: [fileObj()]}), odacStub)
|
|
62
|
+
v.file('avatar').check('required').message('Avatar required')
|
|
63
|
+
expect(await v.error()).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('maxsize / minsize', () => {
|
|
68
|
+
it('rejects a file above maxsize', async () => {
|
|
69
|
+
const v = new Validator(mockRequest({doc: [fileObj({size: 3 * 1024 * 1024})]}), odacStub)
|
|
70
|
+
v.file('doc').check('maxsize:2MB').message('Too large')
|
|
71
|
+
expect(await v.error()).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('accepts a file at/below maxsize', async () => {
|
|
75
|
+
const v = new Validator(mockRequest({doc: [fileObj({size: 1024 * 1024})]}), odacStub)
|
|
76
|
+
v.file('doc').check('maxsize:2MB').message('Too large')
|
|
77
|
+
expect(await v.error()).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('rejects a file below minsize', async () => {
|
|
81
|
+
const v = new Validator(mockRequest({doc: [fileObj({size: 500})]}), odacStub)
|
|
82
|
+
v.file('doc').check('minsize:1KB').message('Too small')
|
|
83
|
+
expect(await v.error()).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('ext', () => {
|
|
88
|
+
it('rejects a disallowed extension', async () => {
|
|
89
|
+
const v = new Validator(mockRequest({img: [fileObj({ext: 'gif'})]}), odacStub)
|
|
90
|
+
v.file('img').check('ext:jpg,png').message('Bad ext')
|
|
91
|
+
expect(await v.error()).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('accepts an allowed extension', async () => {
|
|
95
|
+
const v = new Validator(mockRequest({img: [fileObj({ext: 'png'})]}), odacStub)
|
|
96
|
+
v.file('img').check('ext:jpg,png').message('Bad ext')
|
|
97
|
+
expect(await v.error()).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('maxfiles', () => {
|
|
102
|
+
it('rejects too many files', async () => {
|
|
103
|
+
const files = [fileObj(), fileObj(), fileObj()]
|
|
104
|
+
const v = new Validator(mockRequest({docs: files}), odacStub)
|
|
105
|
+
v.file('docs').check('maxfiles:2').message('Too many')
|
|
106
|
+
expect(await v.error()).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('accepts within the limit', async () => {
|
|
110
|
+
const files = [fileObj(), fileObj()]
|
|
111
|
+
const v = new Validator(mockRequest({docs: files}), odacStub)
|
|
112
|
+
v.file('docs').check('maxfiles:2').message('Too many')
|
|
113
|
+
expect(await v.error()).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('mimetype', () => {
|
|
118
|
+
const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0])
|
|
119
|
+
|
|
120
|
+
it('accepts a genuine PNG matching the whitelist', async () => {
|
|
121
|
+
const p = writeTmp('real.png', PNG)
|
|
122
|
+
const file = fileObj({ext: 'png', mimetype: 'image/png', path: p})
|
|
123
|
+
const v = new Validator(mockRequest({avatar: [file]}), odacStub)
|
|
124
|
+
v.file('avatar').check('mimetype:image/png,image/jpeg').message('Bad type')
|
|
125
|
+
expect(await v.error()).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('rejects a spoofed .png whose bytes are not PNG', async () => {
|
|
129
|
+
const p = writeTmp('fake.png', Buffer.from('<script>alert(1)</script>'))
|
|
130
|
+
const file = fileObj({ext: 'png', mimetype: 'image/png', path: p})
|
|
131
|
+
const v = new Validator(mockRequest({avatar: [file]}), odacStub)
|
|
132
|
+
v.file('avatar').check('mimetype:image/png,image/jpeg').message('Bad type')
|
|
133
|
+
expect(await v.error()).toBe(true)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('rejects a type outside the whitelist', async () => {
|
|
137
|
+
const file = fileObj({ext: 'pdf', mimetype: 'application/pdf'})
|
|
138
|
+
const v = new Validator(mockRequest({avatar: [file]}), odacStub)
|
|
139
|
+
v.file('avatar').check('mimetype:image/png,image/jpeg').message('Bad type')
|
|
140
|
+
expect(await v.error()).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('supports image/* wildcard', async () => {
|
|
144
|
+
const p = writeTmp('w.png', PNG)
|
|
145
|
+
const file = fileObj({ext: 'png', mimetype: 'image/png', path: p})
|
|
146
|
+
const v = new Validator(mockRequest({avatar: [file]}), odacStub)
|
|
147
|
+
v.file('avatar').check('accept:image/*').message('Bad type')
|
|
148
|
+
expect(await v.error()).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('truncated files', () => {
|
|
153
|
+
it('always fails, even with only a size rule that it would pass', async () => {
|
|
154
|
+
const file = fileObj({truncated: true, path: null, size: 0})
|
|
155
|
+
const v = new Validator(mockRequest({big: [file]}), odacStub)
|
|
156
|
+
v.file('big').check('maxsize:2MB').message('File too large')
|
|
157
|
+
expect(await v.error()).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('parameterized-rule split regression', () => {
|
|
162
|
+
it('parses maxsize suffix correctly via split(/:(.+)/)', async () => {
|
|
163
|
+
const under = new Validator(mockRequest({d: [fileObj({size: 1024})]}), odacStub)
|
|
164
|
+
under.file('d').check('maxsize:2KB').message('x')
|
|
165
|
+
expect(await under.error()).toBe(false)
|
|
166
|
+
|
|
167
|
+
const over = new Validator(mockRequest({d: [fileObj({size: 4096})]}), odacStub)
|
|
168
|
+
over.file('d').check('maxsize:2KB').message('x')
|
|
169
|
+
expect(await over.error()).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
})
|
|
@@ -34,4 +34,60 @@ describe('Form.generateFieldHtml()', () => {
|
|
|
34
34
|
expect(html).toContain('value="a&b"c<d>e'f"')
|
|
35
35
|
expect(html).not.toContain('value="a&b"c<d>e\'f"')
|
|
36
36
|
})
|
|
37
|
+
|
|
38
|
+
test('should render a file input without value/placeholder attributes', () => {
|
|
39
|
+
const html = Form.generateFieldHtml({
|
|
40
|
+
name: 'avatar',
|
|
41
|
+
type: 'file',
|
|
42
|
+
placeholder: 'ignored',
|
|
43
|
+
label: null,
|
|
44
|
+
class: '',
|
|
45
|
+
id: null,
|
|
46
|
+
value: null,
|
|
47
|
+
validations: [],
|
|
48
|
+
extraAttributes: {}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(html).toContain('type="file"')
|
|
52
|
+
expect(html).toContain('name="avatar"')
|
|
53
|
+
expect(html).not.toContain('value=')
|
|
54
|
+
expect(html).not.toContain('placeholder=')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('should map file validation rules to accept/multiple/data-* attributes', () => {
|
|
58
|
+
const html = Form.generateFieldHtml({
|
|
59
|
+
name: 'photos',
|
|
60
|
+
type: 'file',
|
|
61
|
+
placeholder: '',
|
|
62
|
+
label: null,
|
|
63
|
+
class: '',
|
|
64
|
+
id: null,
|
|
65
|
+
value: null,
|
|
66
|
+
validations: [{rule: 'required|maxsize:2MB|mimetype:image/png,image/jpeg|maxfiles:3', message: 'Bad file'}],
|
|
67
|
+
extraAttributes: {}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(html).toContain('required')
|
|
71
|
+
expect(html).toContain('accept="image/png,image/jpeg"')
|
|
72
|
+
expect(html).toContain(`data-maxsize="${2 * 1024 * 1024}"`)
|
|
73
|
+
expect(html).toContain('multiple')
|
|
74
|
+
expect(html).toContain('data-maxfiles="3"')
|
|
75
|
+
expect(html).toContain('data-error-maxsize="Bad file"')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('should convert ext rule to a dot-prefixed accept list', () => {
|
|
79
|
+
const html = Form.generateFieldHtml({
|
|
80
|
+
name: 'doc',
|
|
81
|
+
type: 'file',
|
|
82
|
+
placeholder: '',
|
|
83
|
+
label: null,
|
|
84
|
+
class: '',
|
|
85
|
+
id: null,
|
|
86
|
+
value: null,
|
|
87
|
+
validations: [{rule: 'ext:pdf,docx', message: null}],
|
|
88
|
+
extraAttributes: {}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(html).toContain('accept=".pdf,.docx"')
|
|
92
|
+
})
|
|
37
93
|
})
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const fsPromises = fs.promises
|
|
3
|
+
const os = require('os')
|
|
3
4
|
const path = require('path')
|
|
4
5
|
const Image = require('../../../src/View/Image')
|
|
5
6
|
|
|
6
|
-
const IMG_CACHE_DIR = './storage/.cache/img'
|
|
7
|
-
|
|
8
7
|
describe('Image.serve()', () => {
|
|
9
8
|
const testFilename = 'testserve1234567.webp'
|
|
10
|
-
|
|
9
|
+
let tmpDir, IMG_CACHE_DIR, testFilePath
|
|
11
10
|
|
|
12
11
|
beforeAll(async () => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'odac-image-serve-'))
|
|
13
|
+
global.__dir = tmpDir
|
|
14
|
+
IMG_CACHE_DIR = path.join(tmpDir, 'storage/.cache/img')
|
|
15
|
+
testFilePath = path.join(IMG_CACHE_DIR, testFilename)
|
|
16
|
+
|
|
13
17
|
await fsPromises.mkdir(IMG_CACHE_DIR, {recursive: true})
|
|
14
18
|
await fsPromises.writeFile(testFilePath, Buffer.from('fake-image-data'))
|
|
15
19
|
})
|
|
16
20
|
|
|
17
21
|
afterAll(async () => {
|
|
18
|
-
|
|
22
|
+
delete global.__dir
|
|
23
|
+
await fsPromises.rm(tmpDir, {recursive: true, force: true})
|
|
19
24
|
})
|
|
20
25
|
|
|
21
26
|
test('should return stream, type, and size for a cached file', async () => {
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const fsPromises = fs.promises
|
|
3
|
+
const os = require('os')
|
|
3
4
|
const path = require('path')
|
|
4
5
|
const Image = require('../../../src/View/Image')
|
|
5
6
|
|
|
6
7
|
describe('Image.url()', () => {
|
|
7
|
-
|
|
8
|
-
const testImageDir = path.join(publicDir, 'images')
|
|
9
|
-
const testImagePath = path.join(testImageDir, 'url-test.jpg')
|
|
8
|
+
let tmpDir, testImagePath
|
|
10
9
|
|
|
11
10
|
beforeAll(async () => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'odac-image-url-'))
|
|
12
|
+
global.__dir = tmpDir
|
|
13
|
+
|
|
14
|
+
const testImageDir = path.join(tmpDir, 'public', 'images')
|
|
15
|
+
testImagePath = path.join(testImageDir, 'url-test.jpg')
|
|
16
|
+
|
|
12
17
|
await fsPromises.mkdir(testImageDir, {recursive: true})
|
|
13
18
|
await fsPromises.writeFile(testImagePath, Buffer.from('fake-jpg-data'))
|
|
14
19
|
})
|
|
15
20
|
|
|
16
21
|
afterAll(async () => {
|
|
17
|
-
|
|
22
|
+
delete global.__dir
|
|
23
|
+
await fsPromises.rm(tmpDir, {recursive: true, force: true})
|
|
18
24
|
})
|
|
19
25
|
|
|
20
26
|
test('should return empty string for empty src', async () => {
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
const fs = require('fs').promises
|
|
2
|
+
const fsSync = require('fs')
|
|
3
|
+
const os = require('os')
|
|
2
4
|
const path = require('path')
|
|
3
5
|
const View = require('../../src/View')
|
|
4
6
|
|
|
5
7
|
describe('View.#addNavigateAttribute()', () => {
|
|
6
8
|
let view
|
|
7
9
|
let mockOdac
|
|
10
|
+
let tmpDir
|
|
11
|
+
let originalCwd
|
|
8
12
|
|
|
9
13
|
beforeEach(() => {
|
|
10
|
-
|
|
14
|
+
tmpDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'odac-view-navattr-'))
|
|
15
|
+
global.__dir = tmpDir
|
|
16
|
+
originalCwd = process.cwd()
|
|
17
|
+
process.chdir(tmpDir)
|
|
11
18
|
mockOdac = {
|
|
12
19
|
Config: {view: {earlyHints: {enabled: false}}},
|
|
13
20
|
View: {},
|
|
@@ -28,6 +35,8 @@ describe('View.#addNavigateAttribute()', () => {
|
|
|
28
35
|
afterEach(() => {
|
|
29
36
|
jest.restoreAllMocks()
|
|
30
37
|
delete global.__dir
|
|
38
|
+
process.chdir(originalCwd)
|
|
39
|
+
fsSync.rmSync(tmpDir, {recursive: true, force: true})
|
|
31
40
|
})
|
|
32
41
|
|
|
33
42
|
it('should not wrap placeholder in display:contents if it is the first child of an element', async () => {
|
package/test/View/print.test.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
const fs = require('fs').promises
|
|
2
|
+
const fsSync = require('fs')
|
|
3
|
+
const os = require('os')
|
|
2
4
|
const path = require('path')
|
|
3
5
|
const View = require('../../src/View')
|
|
4
6
|
|
|
5
7
|
describe('View.print()', () => {
|
|
6
8
|
let view
|
|
7
9
|
let mockOdac
|
|
10
|
+
let tmpDir
|
|
11
|
+
let originalCwd
|
|
8
12
|
|
|
9
13
|
beforeEach(() => {
|
|
10
|
-
|
|
14
|
+
tmpDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'odac-view-print-'))
|
|
15
|
+
global.__dir = tmpDir
|
|
16
|
+
originalCwd = process.cwd()
|
|
17
|
+
process.chdir(tmpDir)
|
|
11
18
|
mockOdac = {
|
|
12
19
|
Config: {view: {earlyHints: {enabled: false}}},
|
|
13
20
|
View: {},
|
|
@@ -28,6 +35,8 @@ describe('View.print()', () => {
|
|
|
28
35
|
afterEach(() => {
|
|
29
36
|
jest.restoreAllMocks()
|
|
30
37
|
delete global.__dir
|
|
38
|
+
process.chdir(originalCwd)
|
|
39
|
+
fsSync.rmSync(tmpDir, {recursive: true, force: true})
|
|
31
40
|
})
|
|
32
41
|
|
|
33
42
|
it('should be a function', () => {
|