diff --git a/.idea/dictionaries/VLE2FE.xml b/.idea/dictionaries/VLE2FE.xml index 5e9cd2d..62d7fb4 100644 --- a/.idea/dictionaries/VLE2FE.xml +++ b/.idea/dictionaries/VLE2FE.xml @@ -37,6 +37,9 @@ lati lyucy materialnumber + modela + modelb + modelx nvmrc oldpass opblock diff --git a/api/model.yaml b/api/model.yaml index f9c3d72..b701df4 100644 --- a/api/model.yaml +++ b/api/model.yaml @@ -2,7 +2,7 @@ parameters: - $ref: 'api.yaml#/components/parameters/Name' get: - summary: TODO get model data by name + summary: get model data by name description: 'Auth: all, levels: dev, admin' tags: - /model @@ -22,14 +22,14 @@ $ref: 'api.yaml#/components/responses/404' 500: $ref: 'api.yaml#/components/responses/500' - put: - summary: TODO add/replace model data by name + post: + summary: add/replace model data by name description: 'Auth: all, levels: dev, admin' tags: - /model requestBody: required: true - description: binary model data + description: binary model data, Content-Type header must be set to application/octet-stream content: application/json: schema: @@ -38,18 +38,14 @@ responses: 200: $ref: 'api.yaml#/components/responses/Ok' - 400: - $ref: 'api.yaml#/components/responses/400' 401: $ref: 'api.yaml#/components/responses/401' 403: $ref: 'api.yaml#/components/responses/403' - 404: - $ref: 'api.yaml#/components/responses/404' 500: $ref: 'api.yaml#/components/responses/500' delete: - summary: TODO delete model data + summary: delete model data description: 'Auth: basic, levels: dev, admin' tags: - /model diff --git a/src/index.ts b/src/index.ts index 1e763b7..5b4ed8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -111,9 +111,10 @@ if (process.env.NODE_ENV !== 'production') { app.use('/', require('./routes/root')); app.use('/', require('./routes/sample')); app.use('/', require('./routes/material')); -app.use('/', require('./routes/template')); -app.use('/', require('./routes/user')); app.use('/', require('./routes/measurement')); +app.use('/', require('./routes/template')); +app.use('/', require('./routes/model')); +app.use('/', require('./routes/user')); // static files app.use('/static', express.static('static')); diff --git a/src/models/model.ts b/src/models/model.ts new file mode 100644 index 0000000..925601f --- /dev/null +++ b/src/models/model.ts @@ -0,0 +1,8 @@ +import mongoose from 'mongoose'; + +const ModelSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + data: Buffer +}); + +export default mongoose.model>('model', ModelSchema); \ No newline at end of file diff --git a/src/routes/model.spec.ts b/src/routes/model.spec.ts new file mode 100644 index 0000000..382efd4 --- /dev/null +++ b/src/routes/model.spec.ts @@ -0,0 +1,197 @@ +import should from 'should/as-function'; +import ModelModel from '../models/model'; +import TestHelper from "../test/helper"; + + +describe('/model', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); + + describe('GET /model/{name}', (() => { + it('returns the binary data', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/modela', + auth: {basic: 'admin'}, + httpStatus: 200, + contentType: 'application/octet-stream; charset=utf-8', + }).end((err, res) => { + if (err) return done (err); + should(res.body.toString()).be.eql('binary data'); + done(); + }); + }); + it('returns the binary data for an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/modela', + auth: {key: 'admin'}, + httpStatus: 200, + contentType: 'application/octet-stream; charset=utf-8', + }).end((err, res) => { + if (err) return done (err); + should(res.body.toString()).be.eql('binary data'); + done(); + }); + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/modelx', + auth: {basic: 'admin'}, + httpStatus: 404 + }) + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/modela', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }) + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/modela', + httpStatus: 401 + }) + }); + })); + + describe('POST /model/{name}', () => { + it('stores the data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/modelb', + auth: {basic: 'admin'}, + httpStatus: 200, + reqContentType: 'application/octet-stream', + req: 'another binary data' + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.find({name: 'modelb'}).lean().exec((err, data) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); + should(data[0]).have.property('name', 'modelb'); + should(data[0].data.buffer.toString()).be.eql('another binary data'); + done(); + }); + }); + }); + it('stores the data with an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/modelb', + auth: {key: 'admin'}, + httpStatus: 200, + reqContentType: 'application/octet-stream', + req: 'another binary data' + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.find({name: 'modelb'}).lean().exec((err, data) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); + should(data[0]).have.property('name', 'modelb'); + should(data[0].data.buffer.toString()).be.eql('another binary data'); + done(); + }); + }); + }); + it('overwrites existing data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/modela', + auth: {basic: 'admin'}, + httpStatus: 200, + reqContentType: 'application/octet-stream', + req: 'another binary data' + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.find({name: 'modela'}).lean().exec((err, data) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); + should(data[0]).have.property('name', 'modela'); + should(data[0].data.buffer.toString()).be.eql('another binary data'); + done(); + }); + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/modelb', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: 'another binary data' + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/modelb', + httpStatus: 401, + req: 'another binary data' + }); + }); + }); + + describe('DELETE /model/{name}', () => { + it('deletes the data', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/modela', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.find({name: 'modela'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/modelx', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/modela', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/modela', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/modela', + httpStatus: 401 + }); + }); + }); +}); \ No newline at end of file diff --git a/src/routes/model.ts b/src/routes/model.ts new file mode 100644 index 0000000..882de58 --- /dev/null +++ b/src/routes/model.ts @@ -0,0 +1,47 @@ +import express from 'express'; +import bodyParser from 'body-parser'; + +import ModelModel from '../models/model'; + +const router = express.Router(); + +router.get('/model/:name', (req, res, next) => { + if (!req.auth(res, ['dev', 'admin'], 'all')) return; + + ModelModel.findOne({name: req.params.name}).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.set('Content-Type', 'application/octet-stream'); + res.send(data.data.buffer); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.post('/model/:name', bodyParser.raw({limit: '500kb'}), (req, res, next) => { + if (!req.auth(res, ['dev', 'admin'], 'all')) return; + + ModelModel.replaceOne({name: req.params.name}, {name: req.params.name, data: req.body}).setOptions({upsert: true}) + .lean().exec(err => { + if (err) return next(err); + res.json({status: 'OK'}); + }); +}); + +router.delete('/model/:name', (req, res, next) => { + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + + ModelModel.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/test/db.json b/src/test/db.json index 83fba42..468c43d 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -678,6 +678,13 @@ "__v": 0 } ], + "models": [ + { + "_id": {"$oid":"140000000000000000000001"}, + "name": "modela", + "data": {"buffer": "binary data"} + } + ], "users": [ { "_id": {"$oid":"000000000000000000000001"}, diff --git a/src/test/helper.ts b/src/test/helper.ts index d4b11e4..ff337a3 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -69,6 +69,9 @@ export default class TestHelper { if (options.hasOwnProperty('req')) { // request body st = st.send(options.req); } + if (options.hasOwnProperty('reqContentType')) { // request body + st = st.set('Content-Type', options.reqContentType); + } if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) { // resolve basic auth if (this.auth.hasOwnProperty(options.auth.basic)) { st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass)