Skip to content

Commit 70be9ad

Browse files
authored
Merge pull request #40 from kamorel/feature/showcase-2679
/object route validation & unit testing
2 parents 57e471a + 6e694c9 commit 70be9ad

File tree

6 files changed

+312
-9
lines changed

6 files changed

+312
-9
lines changed

app/src/routes/v1/object.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const routes = require('express').Router();
22

33
const { Permissions } = require('../../components/constants');
44
const { objectController } = require('../../controllers');
5+
const { objectValidator } = require('../../validators');
56
const { requireDb, requireSomeAuth } = require('../../middleware/featureToggle');
67
const { checkAppMode, currentObject, hasPermission } = require('../../middleware/authorization');
78

@@ -13,19 +14,19 @@ routes.post('/', requireSomeAuth, (req, res, next) => {
1314
});
1415

1516
/** Search for objects */
16-
routes.get('/', requireDb, requireSomeAuth, (req, res, next) => {
17+
routes.get('/', requireDb, requireSomeAuth, objectValidator.searchObjects, (req, res, next) => {
1718
// TODO: Add validation to reject unexpected query parameters
1819
objectController.searchObjects(req, res, next);
1920
});
2021

2122
/** Returns object headers */
22-
routes.head('/:objId', currentObject, hasPermission(Permissions.READ), (req, res, next) => {
23+
routes.head('/:objId', objectValidator.headObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => {
2324
// TODO: Add validation to reject unexpected query parameters
2425
objectController.headObject(req, res, next);
2526
});
2627

2728
/** Returns the object */
28-
routes.get('/:objId', currentObject, hasPermission(Permissions.READ), (req, res, next) => {
29+
routes.get('/:objId', objectValidator.readObject, currentObject, hasPermission(Permissions.READ), (req, res, next) => {
2930
// TODO: Add validation to reject unexpected query parameters
3031
objectController.readObject(req, res, next);
3132
});
@@ -36,17 +37,17 @@ routes.post('/:objId', currentObject, hasPermission(Permissions.UPDATE), (req, r
3637
});
3738

3839
/** Deletes the object */
39-
routes.delete('/:objId', currentObject, hasPermission(Permissions.DELETE), async (req, res, next) => {
40+
routes.delete('/:objId', objectValidator.deleteObject, currentObject, hasPermission(Permissions.DELETE), async (req, res, next) => {
4041
objectController.deleteObject(req, res, next);
4142
});
4243

4344
/** Returns the object version history */
44-
routes.get('/:objId/versions', currentObject, hasPermission(Permissions.READ), async (req, res, next) => {
45+
routes.get('/:objId/versions', objectValidator.listObjectVersion, currentObject, hasPermission(Permissions.READ), async (req, res, next) => {
4546
objectController.listObjectVersion(req, res, next);
4647
});
4748

4849
/** Sets the public flag of an object */
49-
routes.patch('/:objId/public', requireDb, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => {
50+
routes.patch('/:objId/public', objectValidator.togglePublic, requireDb, currentObject, hasPermission(Permissions.MANAGE), (req, res, next) => {
5051
// TODO: Add validation to reject unexpected query parameters
5152
objectController.togglePublic(req, res, next);
5253
});

app/src/validators/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
2+
objectValidator: require('./object'),
23
permissionValidator: require('./permission'),
34
userValidator: require('./user'),
45
};

app/src/validators/object.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const { validate, Joi } = require('express-validation');
2+
const { scheme, type } = require('./common');
3+
4+
const schema = {
5+
deleteObject: {
6+
params: Joi.object({
7+
objId: type.uuidv4
8+
})
9+
},
10+
11+
headObject: {
12+
params: Joi.object({
13+
objId: type.uuidv4
14+
}),
15+
query: Joi.object({
16+
versionId: type.alphanum
17+
})
18+
},
19+
20+
listObjectVersion: {
21+
params: Joi.object({
22+
objId: type.uuidv4
23+
})
24+
},
25+
26+
readObject: {
27+
params: Joi.object({
28+
objId: type.uuidv4
29+
}),
30+
query: Joi.object({
31+
versionId: type.alphanum,
32+
expiresIn: Joi.number(),
33+
download: type.truthy
34+
})
35+
},
36+
37+
searchObjects: {
38+
query: Joi.object({
39+
objId: scheme.guid,
40+
originalName: type.alphanum,
41+
path: Joi.string().max(1024),
42+
mimeType: Joi.string().max(255),
43+
public: type.truthy,
44+
active: type.truthy
45+
})
46+
},
47+
48+
togglePublic: {
49+
params: Joi.object({
50+
objId: type.uuidv4
51+
}),
52+
query: Joi.object({
53+
public: type.truthy
54+
})
55+
},
56+
};
57+
58+
const validator = {
59+
deleteObject: validate(schema.deleteObject, { statusCode: 422 }),
60+
headObject: validate(schema.headObject, { statusCode: 422 }),
61+
listObjectVersion: validate(schema.listObjectVersion, { statusCode: 422 }),
62+
readObject: validate(schema.readObject, { statusCode: 422 }),
63+
searchObjects: validate(schema.searchObjects, { statusCode: 422 }),
64+
togglePublic: validate(schema.togglePublic, { statusCode: 422 }),
65+
};
66+
67+
module.exports = validator;
68+
module.exports.schema = schema;

app/src/validators/permission.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ const schema = {
4141
userId: scheme.guid,
4242
permCode: scheme.permCode,
4343
})
44-
},
45-
44+
}
4645
};
4746

4847
const validator = {
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
const crypto = require('crypto');
2+
const { Joi } = require('express-validation');
3+
const jestJoi = require('jest-joi');
4+
expect.extend(jestJoi.matchers);
5+
6+
const schema = require('../../../src/validators/object').schema;
7+
const { scheme, type } = require('../../../src/validators/common');
8+
9+
describe('deleteObject', () => {
10+
11+
describe('params', () => {
12+
const params = schema.deleteObject.params.describe();
13+
14+
describe('objId', () => {
15+
const objId = params.keys.objId;
16+
17+
it('is the expected schema', () => {
18+
expect(objId).toEqual(type.uuidv4.describe());
19+
});
20+
});
21+
});
22+
});
23+
24+
describe('headObject', () => {
25+
26+
describe('params', () => {
27+
const params = schema.headObject.params.describe();
28+
29+
describe('objId', () => {
30+
const objId = params.keys.objId;
31+
32+
it('is the expected schema', () => {
33+
expect(objId).toEqual(type.uuidv4.describe());
34+
});
35+
});
36+
});
37+
38+
describe('query', () => {
39+
const query = schema.headObject.query.describe();
40+
41+
describe('versionId', () => {
42+
const versionId = query.keys.versionId;
43+
44+
it('is the expected schema', () => {
45+
expect(versionId).toEqual(type.alphanum.describe());
46+
});
47+
});
48+
});
49+
});
50+
51+
describe('listObjectVersion', () => {
52+
53+
describe('params', () => {
54+
const params = schema.listObjectVersion.params.describe();
55+
56+
describe('objId', () => {
57+
const objId = params.keys.objId;
58+
59+
it('is the expected schema', () => {
60+
expect(objId).toEqual(type.uuidv4.describe());
61+
});
62+
});
63+
});
64+
});
65+
66+
describe('readObject', () => {
67+
68+
describe('params', () => {
69+
const params = schema.readObject.params.describe();
70+
71+
describe('objId', () => {
72+
const objId = params.keys.objId;
73+
74+
it('is the expected schema', () => {
75+
expect(objId).toEqual(type.uuidv4.describe());
76+
});
77+
});
78+
});
79+
80+
describe('query', () => {
81+
const query = schema.readObject.query.describe();
82+
83+
describe('versionId', () => {
84+
const versionId = query.keys.versionId;
85+
86+
it('is the expected schema', () => {
87+
expect(versionId).toEqual(type.alphanum.describe());
88+
});
89+
});
90+
91+
describe('expiresIn', () => {
92+
const expiresIn = query.keys.expiresIn;
93+
94+
it('is the expected schema', () => {
95+
expect(expiresIn).toEqual(Joi.number().describe());
96+
});
97+
});
98+
99+
describe('download', () => {
100+
const download = query.keys.download;
101+
102+
it('is the expected schema', () => {
103+
expect(download).toEqual(type.truthy.describe());
104+
});
105+
});
106+
});
107+
});
108+
109+
describe('searchObjects', () => {
110+
111+
describe('query', () => {
112+
const query = schema.searchObjects.query.describe();
113+
114+
describe('objId', () => {
115+
const objId = query.keys.objId;
116+
117+
it('is the expected schema', () => {
118+
expect(objId).toEqual(scheme.guid.describe());
119+
});
120+
});
121+
122+
describe('originalName', () => {
123+
const originalName = query.keys.originalName;
124+
125+
it('is the expected schema', () => {
126+
expect(originalName).toEqual(type.alphanum.describe());
127+
});
128+
});
129+
130+
describe('path', () => {
131+
const path = query.keys.path;
132+
133+
it('is a string', () => {
134+
expect(path).toBeTruthy();
135+
expect(path.type).toEqual('string');
136+
});
137+
138+
it('has a max length of 1024', () => {
139+
expect(Array.isArray(path.rules)).toBeTruthy();
140+
expect(path.rules).toHaveLength(1);
141+
expect(path.rules).toEqual(expect.arrayContaining([
142+
expect.objectContaining({
143+
'args': {
144+
'limit': 1024
145+
},
146+
'name': 'max'
147+
}),
148+
]));
149+
});
150+
151+
it('matches the schema', () => {
152+
expect('some/path/to/object').toMatchSchema(Joi.string().max(1024));
153+
});
154+
155+
it('must be less than or equal to 1024 characters long', () => {
156+
const reallyLongStr = crypto.randomBytes(1025).toString('hex');
157+
expect(reallyLongStr).not.toMatchSchema(Joi.string().max(1024));
158+
});
159+
});
160+
161+
describe('mimeType', () => {
162+
const mimeType = query.keys.mimeType;
163+
164+
it('is a string', () => {
165+
expect(mimeType).toBeTruthy();
166+
expect(mimeType.type).toEqual('string');
167+
});
168+
169+
it('has a max length of 255', () => {
170+
expect(Array.isArray(mimeType.rules)).toBeTruthy();
171+
expect(mimeType.rules).toHaveLength(1);
172+
expect(mimeType.rules).toEqual(expect.arrayContaining([
173+
expect.objectContaining({
174+
'args': {
175+
'limit': 255
176+
},
177+
'name': 'max'
178+
}),
179+
]));
180+
});
181+
182+
it('matches the schema', () => {
183+
expect('image/jpeg').toMatchSchema(Joi.string().max(255));
184+
});
185+
186+
it('must be less than or equal to 255 characters long', () => {
187+
const longStr = crypto.randomBytes(256).toString('hex');
188+
expect(longStr).not.toMatchSchema(Joi.string().max(255));
189+
});
190+
});
191+
192+
describe('public', () => {
193+
const publicKey = query.keys.public;
194+
195+
it('is the expected schema', () => {
196+
expect(publicKey).toEqual(type.truthy.describe());
197+
});
198+
});
199+
200+
describe('active', () => {
201+
const active = query.keys.active;
202+
203+
it('is the expected schema', () => {
204+
expect(active).toEqual(type.truthy.describe());
205+
});
206+
});
207+
});
208+
});
209+
210+
describe('togglePublic', () => {
211+
212+
describe('params', () => {
213+
const params = schema.togglePublic.params.describe();
214+
215+
describe('objId', () => {
216+
const objId = params.keys.objId;
217+
218+
it('is the expected schema', () => {
219+
expect(objId).toEqual(type.uuidv4.describe());
220+
});
221+
});
222+
});
223+
224+
describe('query', () => {
225+
const query = schema.togglePublic.query.describe();
226+
227+
describe('public', () => {
228+
const publicKey = query.keys.public;
229+
230+
it('is the expected schema', () => {
231+
expect(publicKey).toEqual(type.truthy.describe());
232+
});
233+
});
234+
});
235+
});

app/tests/unit/validators/permission.spec.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const schema = require('../../../src/validators/permission').schema;
55
const { scheme, type } = require('../../../src/validators/common');
66
const { Permissions } = require('../../../src/components/constants');
77

8-
98
describe('searchPermissions', () => {
109

1110
describe('query', () => {

0 commit comments

Comments
 (0)