From 4dcb0c7e2d961f645404912b4f55496d93545def Mon Sep 17 00:00:00 2001 From: AntoineDao Date: Wed, 1 Jan 2020 08:56:37 +1000 Subject: [PATCH 1/5] test(objects): add initial series of tests for speckle object calls --- test/api/object.test.js | 654 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 test/api/object.test.js diff --git a/test/api/object.test.js b/test/api/object.test.js new file mode 100644 index 00000000..2003dea7 --- /dev/null +++ b/test/api/object.test.js @@ -0,0 +1,654 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-undef */ +const chai = require( 'chai' ); +const chaiHttp = require( 'chai-http' ); +const chaiSubset = require( 'chai-subset' ); +const testUtils = require( '../testUtils' ); + +const SpeckleObject = require( '../../models/SpeckleObject' ); + +const app = testUtils.newAPIServer( '/api', null ) +const should = chai.should(); +const expect = chai.expect; + +chai.use( chaiSubset ); +chai.use( chaiHttp ); + +describe( 'objects', () => { + + const routeBase = '/api/objects' + + let testUser1; + let testUser2; + let unauthorizedUser; + let adminUser; + + let object1; + let object2; + let object3; + + before( async () => { + testUser1 = await testUtils.createTestUser( 'test1@test.com', 'not-admin' ) + testUser2 = await testUtils.createTestUser( 'test2@test.com', 'not-admin' ) + unauthorizedUser = await testUtils.createTestUser( 'unauthorized@test.com', 'not-admin' ) + adminUser = await testUtils.createTestUser( 'admin@test.com', 'admin' ) + } ) + + after( async () => { + await testUser1.remove() + await testUser2.remove() + await unauthorizedUser.remove() + await adminUser.remove() + } ) + + beforeEach( async () => { + + object1 = new SpeckleObject( { + owner: testUser1._id, + private: true, + name: 'Test SpeckleObject 1', + type: 'test-object', + geometryHash: 'hash', + hash: 'hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } ) + + await object1.save() + + object2 = new SpeckleObject( { + owner: testUser2._id, + private: false, + name: 'Test SpeckleObject 2', + type: 'test-object', + geometryHash: 'hash-hash', + hash: 'hash-hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } ) + + await object2.save() + + object3 = new SpeckleObject( { + owner: testUser2._id, + private: true, + canRead: [ testUser1._id ], + name: 'Test SpeckleObject 3', + type: 'test-object', + geometryHash: 'hash-hash-hashh', + hash: 'hash-hash-hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } ) + await object3.save() + + } ) + + afterEach( async () => { + await SpeckleObject.collection.drop(); + } ) + + + describe( '/POST /objects', () => { + + let postPayload = { + name: 'Test SpeckleObject', + type: 'test-object', + geometryHash: 'hashy-hash', + hash: 'hashy-hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .post( routeBase ) + .send( postPayload ) + .end( ( err, res ) => { + res.should.have.status( 401 ); + done() + } ) + } ) + + it( 'should create a mongodb object matching the input payload', ( done ) => { + chai.request( app ) + .post( routeBase ) + .set( 'Authorization', testUser1.apiToken ) + .send( postPayload ) + .end( ( err, res ) => { + const _id = res.body.resources[0]._id; + SpeckleObject.findOne( { _id } ).then( object => { + object.should.containSubset( postPayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + } ) + + describe( '/GET /objects/{id}', () => { + + it( 'should not require authentication', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${object2._id}` ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + done() + } ) + } ) + + it( 'should require user to have some form of access', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${object1._id}` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should return a resource if user is owner', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${object1._id}` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + SpeckleObject.findOne( { _id: object1._id } ).then( result => { + res.body.resource.should.containSubset( JSON.parse( JSON.stringify( result ) ) ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should return a resource if user has read access', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${object3._id}` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + SpeckleObject.findOne( { _id: object3._id } ).then( result => { + res.body.resource.should.containSubset( JSON.parse( JSON.stringify( result ) ) ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should return a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${object3._id}` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + SpeckleObject.findOne( { _id: object3._id } ).then( result => { + res.body.resource.should.containSubset( JSON.parse( JSON.stringify( result ) ) ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ); + + + describe( '/PUT /objects/{id}', () => { + + let updatePayload = { + name: 'Test SpeckleObject update', + type: 'test-object update', + geometryHash: 'hashy-hash update', + hash: 'hashy-hash update', + applicationId: 'test update', + properties: { + foo: 'bar update' + } + } + + + beforeEach( async () => { + + object1 = await SpeckleObject.findOne( {_id: object1._id} ); + object1.canWrite = [ testUser2._id ] + + await object1.save() + } ) + + afterEach( async () => { + object1 = await SpeckleObject.findOne( {_id: object1._id} ); + object1.canWrite = [ ] + + await object1.save() + } ) + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}` ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users without write access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users with read access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object3._id}` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should modify a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}` ) + .set( 'Authorization', adminUser.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + res.should.containSubset( updatePayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should modify a resource if user is non-owner with write access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}` ) + .set( 'Authorization', testUser2.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + res.should.containSubset( updatePayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should modify a resource if user is owner', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}` ) + .set( 'Authorization', testUser1.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + res.should.containSubset( updatePayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ) + + describe( '/PUT /objects/{id}/properties', () => { + + + let updatePayload = { + foo: 'bar update', + new: 'property' + } + + + beforeEach( async () => { + + object1 = await SpeckleObject.findOne( {_id: object1._id} ); + object1.canWrite = [ testUser2._id ] + + await object1.save() + } ) + + afterEach( async () => { + object1 = await SpeckleObject.findOne( {_id: object1._id} ); + object1.canWrite = [ ] + + await object1.save() + } ) + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}/properties` ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users without write access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}/properties` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users with read access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object3._id}/properties` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should modify a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}/properties` ) + .set( 'Authorization', adminUser.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + res.properties.should.containSubset( updatePayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should modify a resource if user is non-owner with write access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}/properties` ) + .set( 'Authorization', testUser2.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + res.properties.should.containSubset( updatePayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should modify a resource if user is owner', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${object1._id}/properties` ) + .set( 'Authorization', testUser1.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + res.properties.should.containSubset( updatePayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ) + + describe( '/POST /objects/derive', () => { + + + let derivePayload; + let object1Copy; + + beforeEach( async () => { + + derivePayload = [ + { _id: object1._id }, + { _id: object2._id }, + { _id: object3._id }, + ] + + object1Copy = { + private: object1.private, + name: object1.name, + type: object1.type, + geometryHash: object1.geometryHash, + applicationId: object1.applicationId, + properties: object1.properties, + } + + object1 = await SpeckleObject.findOne( {_id: object1._id} ); + object1.canRead = [ unauthorizedUser._id ] + + await object1.save() + + } ) + + afterEach( async () => { + object1 = await SpeckleObject.findOne( {_id: object1._id} ); + object1.canRead = [ ] + + await object1.save() + } ) + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .post( `${routeBase}/derive` ) + .send( derivePayload ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should require user to have some form of access', ( done ) => { + chai.request( app ) + .post( `${routeBase}/derive` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .send( derivePayload ) + .end( ( err, res ) => { + res.body.resources.should.have.lengthOf( '3' ) + + // res.body.resources[0].type.should.be.equal( 'String' ) + // res.body.resources[0].value.should.be.equal( 'You do not have permissions to view this object' ) + // res.body.resources[1].type.should.be.equal( 'Placeholder' ) + // res.body.resources[2].type.should.be.equal( 'String' ) + // res.body.resources[2].value.should.be.equal( 'You do not have permissions to view this object' ) + + SpeckleObject.find( { owner: unauthorizedUser._id } ).then( + objects => objects.length.should.equal( 1 ) + ).catch( err => done( err ) ) + + done() + } ) + } ) + + it( 'should require a payload of object ID objects', ( done ) => { + chai.request( app ) + .post( `${routeBase}/derive` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should derive a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .post( `${routeBase}/derive` ) + .set( 'Authorization', adminUser.apiToken ) + .send( [ {_id: object1._id} ] ) + .end( ( err, res ) => { + res.body.resources.should.have.lengthOf( '1' ) + SpeckleObject.findOne( { + _id: res.body.resources[0]._id + } ).then( res => { + res.should.containSubset( object1Copy ); + res.hash.should.not.equal( object1.hash ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ) + + + describe( '/POST /objects/getBulk', () => { + + + let derivePayload; + let object1Copy; + + beforeEach( async () => { + + getBulkPayload = [ + object1._id , + object2._id , + object3._id , + ] + + object1Copy = { + private: object1.private, + name: object1.name, + type: object1.type, + geometryHash: object1.geometryHash, + applicationId: object1.applicationId, + properties: object1.properties, + } + + } ) + + it( 'should not require authentication and only return public objects', ( done ) => { + chai.request( app ) + .post( `${routeBase}/getBulk` ) + .send( getBulkPayload ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + done() + } ) + } ) + + it( 'should return full public objects and unauthorized message for private objects if user is not authenticated', ( done ) => { + chai.request( app ) + .post( `${routeBase}/getBulk` ) + .send( getBulkPayload ) + .end( ( err, res ) => { + res.body.resources.should.have.lengthOf( '3' ) + res.body.resources[0].value.should.be.equal( 'You do not have permissions to view this object' ) + res.body.resources[1].name.should.be.equal( 'Test SpeckleObject 2' ) + res.body.resources[2].value.should.be.equal( 'You do not have permissions to view this object' ) + done() + } ) + } ) + + it( 'should return all resources if user is admin regardless of access', ( done ) => { + chai.request( app ) + .post( `${routeBase}/getBulk` ) + .set( 'Authorization', adminUser.apiToken ) + .send( getBulkPayload ) + .end( ( err, res ) => { + res.body.resources.should.have.lengthOf( '3' ) + res.body.resources[0].name.should.be.equal( 'Test SpeckleObject 1' ) + res.body.resources[1].name.should.be.equal( 'Test SpeckleObject 2' ) + res.body.resources[2].name.should.be.equal( 'Test SpeckleObject 3' ) + done() + } ) + } ) + + } ) + + + describe( '/DELETE /objects/{id}', () => { + + + + beforeEach( async () => { + + object1 = await SpeckleObject.findOne( {_id: object1._id} ); + object1.canWrite = [ testUser2._id ] + + await object1.save() + } ) + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${object1._id}` ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users without write access', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${object1._id}` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 404 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users with read access', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${object3._id}` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 404 ) + done() + } ) + } ) + + it( 'should delete a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${object1._id}` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + expect( res ).to.be.a( 'null' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should not delete a resource if user is non-owner with write access', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${object1._id}` ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status ( 404 ) + done() + } ) + } ) + + it( 'should delete a resource if user is owner', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${object1._id}` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + SpeckleObject.findOne( { + _id: object1._id + } ).then( res => { + expect( res ).to.be.a( 'null' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ) + + } +) \ No newline at end of file From 7c55d2ff497c97f337774808fe602c0efe84bcfd Mon Sep 17 00:00:00 2001 From: AntoineDao Date: Wed, 15 Jan 2020 08:15:55 +1000 Subject: [PATCH 2/5] style(eslint): make eslint command autofix what it can `npm run lint` now auto lints what is can so those pesky spaces between everything get applied easily --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a5bdd63..b3ddd5a4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node server.js", "dev": "nodemon server.js", - "lint": "eslint .", + "lint": "eslint . --fix", "test": "NODE_ENV=test mocha --timeout 120000 --exit --recursive" }, "author": "Dimitrie Stefanescu & Project Contributors", From 81f29817e1c742c0221bfa6d2801b887f9ba789c Mon Sep 17 00:00:00 2001 From: AntoineDao Date: Wed, 15 Jan 2020 08:17:02 +1000 Subject: [PATCH 3/5] test(streams): implement most endpoint tests for streams Missing GET /diff, /objects and /clients --- test/api/streams.test.js | 864 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 864 insertions(+) create mode 100644 test/api/streams.test.js diff --git a/test/api/streams.test.js b/test/api/streams.test.js new file mode 100644 index 00000000..9171d63e --- /dev/null +++ b/test/api/streams.test.js @@ -0,0 +1,864 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-undef */ +const chai = require( 'chai' ); +const chaiHttp = require( 'chai-http' ); +const chaiSubset = require( 'chai-subset' ); +const testUtils = require( '../testUtils' ); + +const SpeckleObject = require( '../../models/SpeckleObject' ); +const DataStream = require( '../../models/DataStream' ); + +const app = testUtils.newAPIServer( '/api', null ) +const should = chai.should(); +const expect = chai.expect; + +chai.use( chaiSubset ); +chai.use( chaiHttp ); + + + +describe( 'streams', () => { + + const routeBase = '/api/streams' + + let testUser1; + let testUser2; + let unauthorizedUser; + let adminUser; + + let stream1; + let stream2; + let stream3; + let stream4; + + + let object1; + let object2; + let object3; + + before( async () => { + testUser1 = await testUtils.createTestUser( 'test1@test.com', 'not-admin' ) + testUser2 = await testUtils.createTestUser( 'test2@test.com', 'not-admin' ) + unauthorizedUser = await testUtils.createTestUser( 'unauthorized@test.com', 'not-admin' ) + adminUser = await testUtils.createTestUser( 'admin@test.com', 'admin' ) + } ) + + after( async () => { + await testUser1.remove() + await testUser2.remove() + await unauthorizedUser.remove() + await adminUser.remove() + } ) + + beforeEach( async () => { + + // Save objects first so their _id can be assigned to a stream + object1 = new SpeckleObject( { + owner: testUser1._id, + private: true, + name: 'Test SpeckleObject 1', + type: 'test-object', + geometryHash: 'hash', + hash: 'hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } ) + + await object1.save() + + object2 = new SpeckleObject( { + owner: testUser1._id, + private: false, + name: 'Test SpeckleObject 2', + type: 'test-object', + geometryHash: 'hash-hash', + hash: 'hash-hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } ) + + await object2.save() + + object3 = new SpeckleObject( { + owner: testUser1._id, + private: true, + canRead: [ testUser1._id ], + name: 'Test SpeckleObject 3', + type: 'test-object', + geometryHash: 'hash-hash-hashh', + hash: 'hash-hash-hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } ) + await object3.save() + + + stream1 = new DataStream( { + owner: testUser1._id, + private: true, + streamId: 'stream1', + name: 'Test Stream 1', + description: 'A test stream for testing purposes', + objects: [ + object1._id, + object2._id, + object3._id + ] + } ) + + await stream1.save(); + + stream2 = new DataStream( { + owner: testUser1._id, + private: false, + streamId: 'stream2', + name: 'Test Stream 2', + description: 'A test stream for testing purposes', + objects: [ + object1._id, + object2._id, + object3._id + ] + } ) + + await stream2.save(); + + stream3 = new DataStream( { + owner: testUser1._id, + private: true, + streamId: 'stream3', + name: 'Test Stream 3', + description: 'A test stream for testing purposes', + objects: [ + object1._id, + ] + } ) + + await stream3.save(); + + stream4 = new DataStream( { + owner: testUser2._id, + private: true, + streamId: 'stream4', + name: 'Test Stream 4', + description: 'A test stream for testing purposes', + objects: [ + object1._id, + object2._id, + object3._id + ] + } ) + + await stream4.save(); + + + } ) + + afterEach( async () => { + await DataStream.collection.drop(); + await SpeckleObject.collection.drop(); + } ) + + describe( '/POST /streams', () => { + + let postPayload; + + beforeEach( () => { + postPayload = { + private: true, + name: 'Test Stream', + description: 'A test stream for posting purposes', + objects: [ + object1, + object2, + object3 + ] + } + } ) + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .post( routeBase ) + .send( postPayload ) + .end( ( err, res ) => { + res.should.have.status( 401 ); + done() + } ) + } ) + + it( 'should create a mongodb object matching the input payload', ( done ) => { + chai.request( app ) + .post( routeBase ) + .set( 'Authorization', testUser1.apiToken ) + .send( postPayload ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + const _id = res.body.resource._id; + DataStream.findOne( { _id } ).then( stream => { + postPayload.objects = postPayload.objects.map( o => o._id ) + stream.should.containSubset( postPayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + // TODO: Should probably not allow this... + it( 'should allow users to create streams with objects they do not have access to', ( done ) => { + chai.request( app ) + .post( routeBase ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .send( postPayload ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + const _id = res.body.resource._id; + DataStream.findOne( { _id } ).then( stream => { + postPayload.objects = postPayload.objects.map( o => o._id ) + stream.should.containSubset( postPayload ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + + } ); + + + describe( '/GET /streams', () => { + + beforeEach( async () => { + stream3.canRead = [ testUser2._id ]; + await stream3.save() + } ); + + afterEach( async () => { + stream3.canRead = [ ]; + await stream3.save() + } ); + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .get( routeBase ) + .end( ( err, res ) => { + res.should.have.status( 401 ); + done() + } ) + } ) + + it( 'should return streams where user is owner', ( done ) => { + chai.request( app ) + .get( routeBase ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ); + res.body.resources.should.have.lengthOf( '3' ) + done() + } ) + } ) + + it( 'should return streams where user is not owner but has read access', ( done ) => { + chai.request( app ) + .get( routeBase ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ); + res.body.resources.should.have.lengthOf( '3' ) + done() + } ) + } ) + + it( 'should return all public streams if user has read access to or owns none', ( done ) => { + chai.request( app ) + .get( routeBase ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ); + res.body.resources.should.have.lengthOf( '1' ) + done() + } ) + } ) + } ); + + describe( '/GET /streams/admin', () => { + it( 'should require authentication', ( done ) => { + chai.request( app ) + .get( `${routeBase}/admin` ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should require admin user', ( done ) => { + chai.request( app ) + .get( `${routeBase}/admin` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should GET all the streams regardless of ownership or write/read access', ( done ) => { + chai.request( app ) + .get( `${routeBase}/admin` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ); + res.body.should.have.property( 'resources' ) + res.body.resources.should.have.lengthOf( '4' ) + done(); + } ); + } ); + } ); + + describe( '/GET /streams/{id}', () => { + + let defaultLayer = { + name: "Default Generated Speckle Layer", + objectCount: 3, + orderIndex: 0, + properties: { + color: { + a: 1, + hex: "Black", + } + }, + startIndex: 0, + topology: "0;0-3 ", + } + + let streamSubset = { + private: true, + streamId: 'stream1', + name: 'Test Stream 1', + description: 'A test stream for testing purposes', + } + + beforeEach( async () => { + stream3.canRead = [ testUser2._id ]; + await stream3.save() + } ); + + afterEach( async () => { + stream3.canRead = []; + await stream3.save() + } ); + + + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${stream1.streamId}` ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should require user to have some form of access', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should return a resource if user is owner', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.resource.should.containSubset( streamSubset ); + res.body.resource.objects.should.have.lengthOf( '3' ) + done() + } ) + } ) + + + it( 'should add a default layer if none exists', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.resource.layers[0].should.containSubset( defaultLayer ); + done() + } ) + } ) + + it( 'should return a resource if user has read access', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${stream3.streamId}` ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + done() + } ) + } ) + + it( 'should return a resource if it is not private', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${stream2.streamId}` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + done() + } ) + } ) + + + it( 'should return a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .get( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.resource.should.containSubset( streamSubset ); + res.body.resource.objects.should.have.lengthOf( '3' ) + done() + } ) + } ) + + + } ); + + describe( '/PUT /streams/{id}', () => { + + let updatePayload = { + name: 'Updated Stream', + objects: [ + { + private: true, + name: 'updated Stream object', + type: 'test-object', + geometryHash: 'updated-hash', + hash: 'updated-hash', + applicationId: 'test', + properties: { + foo: 'bar' + } + } + ] + } + + let numberOfObjects; + + beforeEach( async () => { + stream2.canRead = [ testUser2._id ]; + await stream2.save() + + stream3.canWrite = [ testUser2._id ]; + await stream3.save() + + numberOfObjects = await SpeckleObject.countDocuments(); + } ); + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream1.streamId}` ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users without access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser2.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should not accept request from non owner users with read only access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream2.streamId}` ) + .set( 'Authorization', testUser2.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should modify a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', adminUser.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( { streamId: stream1.streamId } ).then( stream => { + stream.name.should.equal( updatePayload.name ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should modify a resource if user is non-owner with write access', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream3.streamId}` ) + .set( 'Authorization', testUser2.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( { streamId: stream3.streamId } ).then( stream => { + stream.name.should.equal( updatePayload.name ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should modify a resource if user is owner', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser1.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( { streamId: stream1.streamId } ).then( stream => { + stream.name.should.equal( updatePayload.name ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + // TODO: I don't think it's expected to work this way though... + it( 'should replace all objects if present in update payload (no upsert behaviour)', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser1.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( { streamId: stream1.streamId } ).then( stream => { + stream.objects.should.have.lengthOf( '1' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should create object if it is not of type "Placeholder"', ( done ) => { + chai.request( app ) + .put( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser1.apiToken ) + .send( updatePayload ) + .end( ( err, res ) => { + SpeckleObject.countDocuments().then( res => { + res.should.equal( numberOfObjects + 1 ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ); + + describe( '/DELETE /streams/{id}', () => { + let numberOfStreams; + + beforeEach( async () => { + stream1.children = [ + stream2.streamId, + stream3.streamId + ] + await stream1.save() + + stream3.canWrite = [ testUser2._id ]; + await stream3.save() + numberOfStreams = await DataStream.countDocuments(); + + } ) + + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${stream1.streamId}` ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + DataStream.findOne( { streamId: stream3.streamId } ).then( stream => { + should.not.equal( stream, null ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should not accept request from non owner users without write access', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 404 ) + DataStream.findOne( { streamId: stream3.streamId } ).then( stream => { + should.not.equal( stream, null ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + + it( 'should delete a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( { streamId: stream1.streamId } ).then( stream => { + should.equal( stream, null ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should not delete a resource if user is non-owner with write access', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${stream3.streamId}` ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 404 ) + DataStream.findOne( { streamId: stream3.streamId } ).then( stream => { + should.not.equal( stream, null ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should delete a resource if user is owner', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( { streamId: stream1.streamId } ).then( stream => { + should.equal( stream, null ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should delete all children streams', ( done ) => { + chai.request( app ) + .delete( `${routeBase}/${stream1.streamId}` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.countDocuments().then( res => { + res.should.equal( numberOfStreams - 3 ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ); + + describe( '/POST /streams/{id}/clone', () => { + + let defaultLayer = { + name: "Default Generated Speckle Layer", + objectCount: 3, + orderIndex: 0, + properties: { + color: { + a: 1, + hex: "Black", + } + }, + startIndex: 0, + topology: "0;0-3 ", + } + + let streamSubset = { + private: true, + name: 'Test Stream 1 (clone)', + description: 'A test stream for testing purposes', + } + + let stream2Subset = { + private: false, + name: 'Test Stream 2 (clone)', + description: 'A test stream for testing purposes', + } + + let stream3Subset = { + private: true, + name: 'Test Stream 3 (clone)', + description: 'A test stream for testing purposes', + } + + beforeEach( async () => { + stream1.children = [ 'test' ]; + await stream1.save(); + + stream3.canRead = [ testUser2._id ]; + await stream3.save() + } ); + + + it( 'should require authentication', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream1.streamId}/clone` ) + .end( ( err, res ) => { + res.should.have.status( 401 ) + done() + } ) + } ) + + it( 'should require user to have some form of access', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream1.streamId}/clone` ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 400 ) + done() + } ) + } ) + + it( 'should clone a stream if user is owner', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream1.streamId}/clone` ) + .set( 'Authorization', testUser1.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.clone.should.containSubset( streamSubset ); + DataStream.findOne( {streamId: res.body.clone.streamId} ).then( stream => { + should.not.equal( stream, null ) + stream.objects.should.have.lengthOf( '3' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + + it( 'should return a resource if user has read access', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream3.streamId}/clone` ) + .set( 'Authorization', testUser2.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.clone.should.containSubset( stream3Subset ); + DataStream.findOne( {streamId: res.body.clone.streamId} ).then( stream => { + should.not.equal( stream, null ) + stream.objects.should.have.lengthOf( '1' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should return a resource if it is not private', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream2.streamId}/clone` ) + .set( 'Authorization', unauthorizedUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.clone.should.containSubset( stream2Subset ); + DataStream.findOne( {streamId: res.body.clone.streamId} ).then( stream => { + should.not.equal( stream, null ) + stream.objects.should.have.lengthOf( '3' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + + it( 'should return a resource if user is admin regardless of access', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream1.streamId}/clone` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.clone.should.containSubset( streamSubset ); + DataStream.findOne( {streamId: res.body.clone.streamId} ).then( stream => { + should.not.equal( stream, null ) + stream.objects.should.have.lengthOf( '3' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + + + it( 'should accept a new name for the clone in the post payload', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream1.streamId}/clone` ) + .send( {name: 'new name'} ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + res.body.clone.name.should.equal( 'new name' ); + DataStream.findOne( {streamId: res.body.clone.streamId} ).then( stream => { + should.not.equal( stream, null ) + stream.name.should.equal( 'new name' ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + + it( 'should add stream cloned from as a parent to the new stream', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream1.streamId}/clone` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( {streamId: res.body.clone.streamId} ).then( stream => { + stream.parent.should.equal( stream1.streamId ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + it( 'should new stream to list of parent stream children', ( done ) => { + chai.request( app ) + .post( `${routeBase}/${stream1.streamId}/clone` ) + .set( 'Authorization', adminUser.apiToken ) + .end( ( err, res ) => { + res.should.have.status( 200 ) + DataStream.findOne( {streamId: stream1.streamId} ).then( stream => { + stream.children.should.deep.equal( [ 'test', res.body.clone.streamId ] ) + done() + } ).catch( err => done( err ) ) + } ) + } ) + + } ); + + + + describe( '/GET /streams/{id}/diff/{otherId}', () => { + + // Not implemented! + // TODO: Implement!!! + + } ); + + + + describe( '/GET /streams/{id}/objects', () => { + + // Not implemented! + // TODO: Implement!!! + + } ); + + + describe( '/GET /streams/{id}/clients', () => { + + // Not implemented! + // TODO: Implement!!! + + } ); + + } +) \ No newline at end of file From e93d6ce898ecfff0db4dbb60579df6550644a728 Mon Sep 17 00:00:00 2001 From: AntoineDao Date: Wed, 15 Jan 2020 08:18:55 +1000 Subject: [PATCH 4/5] fix(stream): fix deletion bug and list get bug Fix delete so it now properly deletes all children of a stream when it is deleted. Fix get all so it also exposes public streams. --- app/api/streams/StreamDelete.js | 3 +-- app/api/streams/StreamGetAll.js | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/streams/StreamDelete.js b/app/api/streams/StreamDelete.js index 786e4bf3..43c1052c 100644 --- a/app/api/streams/StreamDelete.js +++ b/app/api/streams/StreamDelete.js @@ -14,8 +14,7 @@ module.exports = ( req, res ) => { .then( stream => PermissionCheck( req.user, 'delete', stream ) ) .then( stream => { myStream = stream - DataStream.remove( { streamId: { $in: [ ...myStream.children, req.params.streamId ] } } ) - return stream.remove( ) + return DataStream.deleteMany( { streamId: { $in: [ ...myStream.children, req.params.streamId ] } } ) } ) .then( ( ) => { return res.send( { success: true, message: `Stream ${req.params.streamId} and its children have been deleted.`, deletedStreams: [ ...myStream.children, req.params.streamId ] } ) diff --git a/app/api/streams/StreamGetAll.js b/app/api/streams/StreamGetAll.js index 3463bb65..eb2992e7 100644 --- a/app/api/streams/StreamGetAll.js +++ b/app/api/streams/StreamGetAll.js @@ -28,7 +28,8 @@ module.exports = ( req, res ) => { finalCriteria.$or = [ { owner: req.user._id }, { 'canWrite': mongoose.Types.ObjectId( req.user._id ) }, - { 'canRead': mongoose.Types.ObjectId( req.user._id ) } + { 'canRead': mongoose.Types.ObjectId( req.user._id ) }, + { 'private': false } ] DataStream.find( finalCriteria, query.options.fields, { sort: query.options.sort, skip: query.options.skip, limit: query.options.limit } ) From abdcc7da8e60d5b6a351fa063554148681f846d8 Mon Sep 17 00:00:00 2001 From: AntoineDao Date: Fri, 17 Jan 2020 17:58:24 +1000 Subject: [PATCH 5/5] fix(object): deriving object silently drops unauthorized objects --- app/api/objects/ObjectDerive.js | 23 ++++++++++++++--------- test/api/object.test.js | 14 ++++++-------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/api/objects/ObjectDerive.js b/app/api/objects/ObjectDerive.js index edab6167..16d0e64c 100644 --- a/app/api/objects/ObjectDerive.js +++ b/app/api/objects/ObjectDerive.js @@ -3,6 +3,7 @@ const { merge } = require( 'lodash' ) const SpeckleObject = require( '../../../models/SpeckleObject' ) const BulkObjectSave = require( '../middleware/BulkObjectSave' ) +const PermissionCheck = require( '../middleware/PermissionCheck' ) // Derives an object from an existing object module.exports = ( req, res ) => { @@ -14,21 +15,25 @@ module.exports = ( req, res ) => { let objects = req.body SpeckleObject.find( { _id: { $in: objects.map( obj => obj._id ) } } ).lean() + .then( objects => Promise.all( objects.map( o => PermissionCheck( req.user, 'read', o ) ).map( prom => prom.catch( e => e ) ) ) ) .then( existingObjects => { let toSave = [ ] + for ( let original of existingObjects ) { - let found = objects.find( o => o._id === original._id.toString() ) - let mod = {} + if ( original._id ) { + let found = objects.find( o => o._id === original._id.toString() ) + let mod = {} - merge( mod, original, found ) + merge( mod, original, found ) - // delete hash to prepare for rehashing in bulk save - delete mod.hash - delete mod._id - delete mod.createdAt - toSave.push( mod ) + // delete hash to prepare for rehashing in bulk save + delete mod.hash + delete mod._id + delete mod.createdAt + toSave.push( mod ) + } } - return BulkObjectSave( toSave, req.user ) + return BulkObjectSave( toSave, req.user ); } ) .then( newObjects => { res.send( { success: true, message: 'Saved objects to database.', resources: newObjects.map( o => { return { type: 'Placeholder', _id: o._id } } ) } ) diff --git a/test/api/object.test.js b/test/api/object.test.js index 2003dea7..56a364af 100644 --- a/test/api/object.test.js +++ b/test/api/object.test.js @@ -458,16 +458,14 @@ describe( 'objects', () => { .set( 'Authorization', unauthorizedUser.apiToken ) .send( derivePayload ) .end( ( err, res ) => { - res.body.resources.should.have.lengthOf( '3' ) - - // res.body.resources[0].type.should.be.equal( 'String' ) - // res.body.resources[0].value.should.be.equal( 'You do not have permissions to view this object' ) - // res.body.resources[1].type.should.be.equal( 'Placeholder' ) - // res.body.resources[2].type.should.be.equal( 'String' ) - // res.body.resources[2].value.should.be.equal( 'You do not have permissions to view this object' ) + res.body.resources.should.have.lengthOf( '2' ) SpeckleObject.find( { owner: unauthorizedUser._id } ).then( - objects => objects.length.should.equal( 1 ) + objects => { + objects.length.should.equal( 2 ); + objects[0].name = object1.name; + objects[1].name = object2.name; + } ).catch( err => done( err ) ) done()