diff --git a/src/api.v2/controllers/metadataTrackController.js b/src/api.v2/controllers/metadataTrackController.js new file mode 100644 index 000000000..7af28de30 --- /dev/null +++ b/src/api.v2/controllers/metadataTrackController.js @@ -0,0 +1,80 @@ +const MetadataTrack = require('../model/MetadataTrack'); + +const getLogger = require('../../utils/getLogger'); +const { OK, NotFoundError } = require('../../utils/responses'); + +const logger = getLogger('[MetadataTrackController] - '); + +const createMetadataTrack = async (req, res) => { + const { + params: { experimentId, metadataTrackKey }, + } = req; + + logger.log(`Creating metadata track ${metadataTrackKey} in experiment ${experimentId}`); + + await new MetadataTrack().createNewMetadataTrack(experimentId, metadataTrackKey); + + logger.log(`Finished creating metadata track ${metadataTrackKey} in experiment ${experimentId}`); + + res.json(OK()); +}; + +const patchMetadataTrack = async (req, res) => { + const { + params: { experimentId, metadataTrackKey: oldKey }, + body: { key }, + } = req; + + logger.log(`Patching metadata track ${oldKey} in experiment ${experimentId}`); + + const result = await new MetadataTrack() + .update({ experiment_id: experimentId, key: oldKey }, { key }); + + if (result.length === 0) { + throw new NotFoundError(`Metadata track ${oldKey} not found`); + } + + logger.log(`Finished patching metadata track ${oldKey} in experiment ${experimentId}, changed to ${key}`); + res.json(OK()); +}; + +const deleteMetadataTrack = async (req, res) => { + const { + params: { experimentId, metadataTrackKey }, + } = req; + + logger.log(`Creating metadata track ${metadataTrackKey} in experiment ${experimentId}`); + + const result = await new MetadataTrack().delete( + { experiment_id: experimentId, key: metadataTrackKey }, + ); + + if (result.length === 0) { + throw new NotFoundError(`Metadata track ${metadataTrackKey} not found`); + } + + logger.log(`Finished creating metadata track ${metadataTrackKey} in experiment ${experimentId}`); + + res.json(OK()); +}; + +const patchValueForSample = async (req, res) => { + const { + params: { experimentId, sampleId, metadataTrackKey }, + body: { value }, + } = req; + + logger.log(`Patching value of metadata track ${metadataTrackKey} in sample ${sampleId} in experiment ${experimentId}`); + + await new MetadataTrack().patchValueForSample(experimentId, sampleId, metadataTrackKey, value); + + logger.log(`Finished patching value of metadata track ${metadataTrackKey} in sample ${sampleId} in experiment ${experimentId}, changed to ${value}`); + res.json(OK()); +}; + +module.exports = { + createMetadataTrack, + patchMetadataTrack, + deleteMetadataTrack, + patchValueForSample, +}; diff --git a/src/api.v2/controllers/sampleController.js b/src/api.v2/controllers/sampleController.js index 916d941dd..4be01d1d1 100644 --- a/src/api.v2/controllers/sampleController.js +++ b/src/api.v2/controllers/sampleController.js @@ -52,7 +52,7 @@ const deleteSample = async (req, res) => { logger.log(`Deleting sample ${sampleId} from experiment ${experimentId}`); await sqlClient.get().transaction(async (trx) => { - await new Sample(trx).destroy(sampleId); + await new Sample(trx).deleteById(sampleId); await new Experiment(trx).deleteSample(experimentId, sampleId); }); diff --git a/src/api.v2/model/BasicModel.js b/src/api.v2/model/BasicModel.js index ea0804918..e685c58b1 100644 --- a/src/api.v2/model/BasicModel.js +++ b/src/api.v2/model/BasicModel.js @@ -68,10 +68,19 @@ class BasicModel { .timeout(this.timeout); } - destroy(id) { + deleteById(id) { return this.sql.del() .from(this.tableName) .where({ id }) + .returning(this.selectableProps) + .timeout(this.timeout); + } + + delete(filters) { + return this.sql.del() + .from(this.tableName) + .where(filters) + .returning(this.selectableProps) .timeout(this.timeout); } } diff --git a/src/api.v2/model/MetadataTrack.js b/src/api.v2/model/MetadataTrack.js index 3a30ca996..968e96483 100644 --- a/src/api.v2/model/MetadataTrack.js +++ b/src/api.v2/model/MetadataTrack.js @@ -3,14 +3,12 @@ const BasicModel = require('./BasicModel'); const sqlClient = require('../../sql/sqlClient'); const tableNames = require('./tableNames'); +const { NotFoundError } = require('../../utils/responses'); const sampleFields = [ 'id', 'experiment_id', - 'name', - 'sample_technology', - 'created_at', - 'updated_at', + 'key', ]; class MetadataTrack extends BasicModel { @@ -18,6 +16,35 @@ class MetadataTrack extends BasicModel { super(sql, tableNames.METADATA_TRACK, sampleFields); } + async createNewMetadataTrack(experimentId, key) { + const sampleIds = await this.sql.select(['id']) + .from(tableNames.SAMPLE) + .where({ experiment_id: experimentId }); + + await this.sql.transaction(async (trx) => { + const response = await trx + .insert({ + experiment_id: experimentId, + key, + }) + .returning(['id']) + .into(this.tableName); + + if (sampleIds.length === 0) { + return; + } + + const [{ id: metadataTrackId }] = response; + + const valuesToInsert = sampleIds.map(({ id: sampleId }) => ({ + metadata_track_id: metadataTrackId, + sample_id: sampleId, + })); + + await trx(tableNames.SAMPLE_IN_METADATA_TRACK_MAP).insert(valuesToInsert); + }); + } + async createNewSampleValues(experimentId, sampleId) { const tracks = await this.sql.select(['id']) .from(tableNames.METADATA_TRACK) @@ -30,11 +57,22 @@ class MetadataTrack extends BasicModel { const valuesToInsert = tracks.map(({ id }) => ({ metadata_track_id: id, sample_id: sampleId, - value: 'N.A.', })); - await this.sql(tableNames.SAMPLE_IN_METADATA_TRACK_MAP) - .insert(valuesToInsert); + await this.sql(tableNames.SAMPLE_IN_METADATA_TRACK_MAP).insert(valuesToInsert); + } + + async patchValueForSample(experimentId, sampleId, key, value) { + const [{ id }] = await this.find({ experiment_id: experimentId, key }); + + const result = await this.sql(tableNames.SAMPLE_IN_METADATA_TRACK_MAP) + .update({ value }) + .where({ metadata_track_id: id, sample_id: sampleId }) + .returning(['metadata_track_id']); + + if (result.length === 0) { + throw new NotFoundError(`Metadata track ${key} or sample ${sampleId} don't exist`); + } } } diff --git a/src/api.v2/model/__mocks__/BasicModel.js b/src/api.v2/model/__mocks__/BasicModel.js index 802212abd..3e966e764 100644 --- a/src/api.v2/model/__mocks__/BasicModel.js +++ b/src/api.v2/model/__mocks__/BasicModel.js @@ -6,7 +6,8 @@ const stub = { findById: jest.fn(), update: jest.fn(), updateById: jest.fn(), - destroy: jest.fn(), + delete: jest.fn(), + deleteById: jest.fn(), }; const BasicModel = jest.fn().mockImplementation(() => stub); diff --git a/src/api.v2/model/__mocks__/MetadataTrack.js b/src/api.v2/model/__mocks__/MetadataTrack.js index 7f459c38c..ad4dc4b01 100644 --- a/src/api.v2/model/__mocks__/MetadataTrack.js +++ b/src/api.v2/model/__mocks__/MetadataTrack.js @@ -1,7 +1,9 @@ const BasicModel = require('./BasicModel')(); const stub = { + createNewMetadataTrack: jest.fn(), createNewSampleValues: jest.fn(), + patchValueForSample: jest.fn(), ...BasicModel, }; diff --git a/src/api.v2/routes/metadataTrack.js b/src/api.v2/routes/metadataTrack.js new file mode 100644 index 000000000..370b32065 --- /dev/null +++ b/src/api.v2/routes/metadataTrack.js @@ -0,0 +1,27 @@ +const { + createMetadataTrack, + patchMetadataTrack, + patchValueForSample, + deleteMetadataTrack, +} = require('../controllers/metadataTrackController'); + +const { expressAuthorizationMiddleware } = require('../middlewares/authMiddlewares'); + +module.exports = { + 'metadataTrack#createMetadataTrack': [ + expressAuthorizationMiddleware, + (req, res, next) => createMetadataTrack(req, res).catch(next), + ], + 'metadataTrack#patchMetadataTrack': [ + expressAuthorizationMiddleware, + (req, res, next) => patchMetadataTrack(req, res).catch(next), + ], + 'metadataTrack#patchSampleInMetadataTrackValue': [ + expressAuthorizationMiddleware, + (req, res, next) => patchValueForSample(req, res).catch(next), + ], + 'metadataTrack#deleteMetadataTrack': [ + expressAuthorizationMiddleware, + (req, res, next) => deleteMetadataTrack(req, res).catch(next), + ], +}; diff --git a/src/specs/api.v2.yaml b/src/specs/api.v2.yaml index 7f8fc55e0..f534fefc0 100644 --- a/src/specs/api.v2.yaml +++ b/src/specs/api.v2.yaml @@ -86,7 +86,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/HTTPError' + $ref: '#/components/schemas/HTTPError' + '403': + description: Forbidden request for this user. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' '404': description: Not found error. content: @@ -119,6 +125,12 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPError' + '403': + description: Forbidden request for this user. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' '404': description: Not found error. content: @@ -433,6 +445,7 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPError' + '/experiments/{experimentId}/samples/position': put: summary: Update position for a sample @@ -485,6 +498,179 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPError' + + '/experiments/{experimentId}/metadataTracks/{metadataTrackKey}': + post: + summary: Creates a new metadata track + description: Creates a new metadata track, no requestBody required + operationId: createMetadataTrack + x-eov-operation-id: metadataTrack#createMetadataTrack + x-eov-operation-handler: routes/metadataTrack + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPSuccess' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '401': + description: The request lacks authentication credentials. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '403': + description: Forbidden request for this user. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '404': + description: Not found error. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + patch: + summary: Updates a metadata tracks properties + description: Updates a metadata tracks properties + operationId: patchMetadataTrack + x-eov-operation-id: metadataTrack#patchMetadataTrack + x-eov-operation-handler: routes/metadataTrack + requestBody: + content: + application/json: + schema: + type: object + properties: + key: + type: string + required: + - key + additionalProperties: false + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPSuccess' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '401': + description: The request lacks authentication credentials. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '403': + description: Forbidden request for this user. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '404': + description: Not found error. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + delete: + summary: Deletes a metadata track + description: Deletes a metadata track, no requestBody required + operationId: deleteMetadataTrack + x-eov-operation-id: metadataTrack#deleteMetadataTrack + x-eov-operation-handler: routes/metadataTrack + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPSuccess' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '401': + description: The request lacks authentication credentials. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '403': + description: Forbidden request for this user. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '404': + description: Not found error. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '/experiments/{experimentId}/samples/{sampleId}/metadataTracks/{metadataTrackKey}': + patch: + summary: Updates the value that a sample has in a metadata track in particular + description: Updates a metadata tracks properties + operationId: patchMetadataTrack + x-eov-operation-id: metadataTrack#patchSampleInMetadataTrackValue + x-eov-operation-handler: routes/metadataTrack + requestBody: + content: + application/json: + schema: + type: object + properties: + value: + type: string + required: + - value + additionalProperties: false + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPSuccess' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '401': + description: The request lacks authentication credentials. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '403': + description: Forbidden request for this user. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' + '404': + description: Not found error. + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPError' '/experiments/{experimentId}/samples/{sampleId}/sampleFiles/{sampleFileType}': post: summary: Create a new file for a sample diff --git a/src/sql/migrations/20220304184711_schema.js b/src/sql/migrations/20220304184711_schema.js index 404500a17..500638414 100644 --- a/src/sql/migrations/20220304184711_schema.js +++ b/src/sql/migrations/20220304184711_schema.js @@ -112,6 +112,8 @@ exports.up = async (knex) => { table.increments('id', { primaryKey: true }); table.uuid('experiment_id').references('experiment.id').onDelete('CASCADE').notNullable(); table.string('key'); + + table.unique(['experiment_id', 'key']); }); await knex.schema @@ -128,7 +130,7 @@ exports.up = async (knex) => { .createTable('sample_in_metadata_track_map', (table) => { table.integer('metadata_track_id').references('metadata_track.id').onDelete('CASCADE').notNullable(); table.uuid('sample_id').references('sample.id').onDelete('CASCADE').notNullable(); - table.string('value').notNullable(); + table.string('value').defaultTo('N.A.'); table.primary(['metadata_track_id', 'sample_id']); }); diff --git a/tests/api.v2/controllers/metadataTrackController.test.js b/tests/api.v2/controllers/metadataTrackController.test.js new file mode 100644 index 000000000..fc5794b1b --- /dev/null +++ b/tests/api.v2/controllers/metadataTrackController.test.js @@ -0,0 +1,148 @@ +// @ts-nocheck +const metadataTrackController = require('../../../src/api.v2/controllers/metadataTrackController'); +const { OK, NotFoundError } = require('../../../src/utils/responses'); +const MetadataTrack = require('../../../src/api.v2/model/MetadataTrack'); + +const metadataTrackInstance = new MetadataTrack(); + +jest.mock('../../../src/api.v2/model/MetadataTrack'); + +const mockRes = { + json: jest.fn(), +}; + +describe('metadataTrackController', () => { + beforeEach(async () => { + jest.clearAllMocks(); + }); + + it('createMetadataTrack works correctly', async () => { + const experimentId = 'experimentId'; + const metadataTrackKey = 'metadataTrackKey'; + + const mockReq = { + params: { experimentId, metadataTrackKey }, + }; + + await metadataTrackController.createMetadataTrack(mockReq, mockRes); + + expect( + metadataTrackInstance.createNewMetadataTrack, + ).toHaveBeenCalledWith(experimentId, metadataTrackKey); + + // Response is ok + expect(mockRes.json).toHaveBeenCalledWith(OK()); + }); + + it('patchMetadataTrack works correctly', async () => { + const experimentId = 'experimentId'; + const oldMetadataTrackKey = 'oldKey'; + const newMetadataTrackKey = 'newKey'; + + const mockReq = { + params: { experimentId, metadataTrackKey: oldMetadataTrackKey }, + body: { key: newMetadataTrackKey }, + }; + + metadataTrackInstance.update.mockImplementationOnce( + () => [{ id: 1, experimentId, key: newMetadataTrackKey }], + ); + + await metadataTrackController.patchMetadataTrack(mockReq, mockRes); + + expect(metadataTrackInstance.update).toHaveBeenCalledWith( + { experiment_id: experimentId, key: oldMetadataTrackKey }, + { key: newMetadataTrackKey }, + ); + + // Response is ok + expect(mockRes.json).toHaveBeenCalledWith(OK()); + }); + + it('patchMetadataTrack throws if it didnt find a row to update', async () => { + const experimentId = 'experimentId'; + const oldMetadataTrackKey = 'oldKey'; + const newMetadataTrackKey = 'newKey'; + + const mockReq = { + params: { experimentId, metadataTrackKey: oldMetadataTrackKey }, + body: { key: newMetadataTrackKey }, + }; + + metadataTrackInstance.update.mockImplementationOnce(() => []); + + await expect( + metadataTrackController.patchMetadataTrack(mockReq, mockRes), + ).rejects.toThrow( + new NotFoundError(`Metadata track ${oldMetadataTrackKey} not found`), + ); + + // Response is not generated in controller + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('deleteMetadataTrack works correctly', async () => { + const experimentId = 'experimentId'; + const metadataTrackKey = 'key'; + + const mockReq = { params: { experimentId, metadataTrackKey } }; + + metadataTrackInstance.delete.mockImplementationOnce( + () => [{ id: 1, experimentId, key: metadataTrackKey }], + ); + + await metadataTrackController.deleteMetadataTrack(mockReq, mockRes); + + expect(metadataTrackInstance.delete).toHaveBeenCalledWith( + { experiment_id: experimentId, key: metadataTrackKey }, + ); + + // Response is ok + expect(mockRes.json).toHaveBeenCalledWith(OK()); + }); + + it('deleteMetadataTrack throws if it didnt find a row to delete', async () => { + const experimentId = 'experimentId'; + const metadataTrackKey = 'key'; + + const mockReq = { params: { experimentId, metadataTrackKey } }; + + metadataTrackInstance.delete.mockImplementationOnce(() => []); + + await expect( + metadataTrackController.deleteMetadataTrack(mockReq, mockRes), + ).rejects.toThrow( + new NotFoundError(`Metadata track ${metadataTrackKey} not found`), + ); + + expect(metadataTrackInstance.delete).toHaveBeenCalledWith( + { experiment_id: experimentId, key: metadataTrackKey }, + ); + + // Response is not generated in controller + expect(mockRes.json).not.toHaveBeenCalledWith(OK()); + }); + + it('patchSampleInMetadataTrackValue works correctly', async () => { + const experimentId = 'experimentId'; + const metadataTrackKey = 'key'; + const sampleId = 'sampleId'; + const value = 'value'; + + const mockReq = { + params: { experimentId, sampleId, metadataTrackKey }, + body: { value }, + }; + + metadataTrackInstance.patchValueForSample.mockImplementationOnce(() => Promise.resolve()); + + await metadataTrackController.patchValueForSample(mockReq, mockRes); + + expect(metadataTrackInstance.patchValueForSample).toHaveBeenCalledWith( + experimentId, sampleId, metadataTrackKey, value, + ); + + // Response is ok + expect(mockRes.json).toHaveBeenCalledWith(OK()); + }); +}); diff --git a/tests/api.v2/controllers/sampleController.test.js b/tests/api.v2/controllers/sampleController.test.js index 76782ff86..bd12ce578 100644 --- a/tests/api.v2/controllers/sampleController.test.js +++ b/tests/api.v2/controllers/sampleController.test.js @@ -91,7 +91,7 @@ describe('sampleController', () => { it('deleteSample works correctly', async () => { const mockReq = { params: { experimentId: mockExperimentId, sampleId: mockSampleId } }; - sampleInstance.destroy.mockImplementationOnce(() => Promise.resolve()); + sampleInstance.deleteById.mockImplementationOnce(() => Promise.resolve()); experimentInstance.deleteSample.mockImplementationOnce(() => Promise.resolve()); await sampleController.deleteSample(mockReq, mockRes); @@ -106,7 +106,7 @@ describe('sampleController', () => { expect(Experiment).not.toHaveBeenCalledWith(mockSqlClient); expect(Sample).not.toHaveBeenCalledWith(mockSqlClient); - expect(sampleInstance.destroy).toHaveBeenCalledWith(mockSampleId); + expect(sampleInstance.deleteById).toHaveBeenCalledWith(mockSampleId); expect(experimentInstance.deleteSample).toHaveBeenCalledWith(mockExperimentId, mockSampleId); expect(mockRes.json).toHaveBeenCalledWith(OK()); diff --git a/tests/api.v2/model/BasicModel.test.js b/tests/api.v2/model/BasicModel.test.js index 813a61b07..28fe5c8c8 100644 --- a/tests/api.v2/model/BasicModel.test.js +++ b/tests/api.v2/model/BasicModel.test.js @@ -95,12 +95,27 @@ describe('model/BasicModel', () => { expect(mockSqlClient.timeout).toHaveBeenCalledWith(4000); }); - it('destroy works correctly', async () => { - await new BasicModel(mockSqlClient, mockTableName, ['id', 'name']).destroy('mockId'); + it('deleteById works correctly', async () => { + await new BasicModel(mockSqlClient, mockTableName, ['id', 'name']).deleteById('mockId'); expect(mockSqlClient.del).toHaveBeenCalled(); expect(mockSqlClient.from).toHaveBeenCalledWith(mockTableName); expect(mockSqlClient.where).toHaveBeenCalledWith({ id: 'mockId' }); expect(mockSqlClient.timeout).toHaveBeenCalledWith(4000); }); + + it('delete works correctly', async () => { + const mockKey = 'aKey'; + const mockExperimentId = 'anExperimentId'; + + await new BasicModel(mockSqlClient, mockTableName, ['key', 'experiment_id', 'name']) + .delete({ key: mockKey, experiment_id: mockExperimentId }); + + expect(mockSqlClient.del).toHaveBeenCalled(); + expect(mockSqlClient.from).toHaveBeenCalledWith(mockTableName); + expect(mockSqlClient.where).toHaveBeenCalledWith( + { key: mockKey, experiment_id: mockExperimentId }, + ); + expect(mockSqlClient.timeout).toHaveBeenCalledWith(4000); + }); }); diff --git a/tests/api.v2/model/MetadataTrack.test.js b/tests/api.v2/model/MetadataTrack.test.js index f547f1e33..209482b64 100644 --- a/tests/api.v2/model/MetadataTrack.test.js +++ b/tests/api.v2/model/MetadataTrack.test.js @@ -1,17 +1,103 @@ // @ts-nocheck -const { mockSqlClient } = require('../mocks/getMockSqlClient')(); +const { mockSqlClient, mockTrx } = require('../mocks/getMockSqlClient')(); jest.mock('../../../src/sql/sqlClient', () => ({ get: jest.fn(() => mockSqlClient), })); const MetadataTrack = require('../../../src/api.v2/model/MetadataTrack'); +const BasicModel = require('../../../src/api.v2/model/BasicModel'); + +const tableNames = require('../../../src/api.v2/model/tableNames'); describe('model/userAccess', () => { beforeEach(() => { jest.clearAllMocks(); }); + it('createNewMetadataTrack works correctly when there are samples', async () => { + const experimentId = 'mockExperimentId'; + const key = 'mockKey'; + + mockSqlClient.where.mockReturnValueOnce([{ id: 'sampleId1' }, { id: 'sampleId2' }, { id: 'sampleId3' }, { id: 'sampleId4' }]); + mockTrx.into.mockReturnValueOnce([{ id: 'metadataTrackId1' }, { id: 'metadataTrackId2' }]); + + await new MetadataTrack().createNewMetadataTrack(experimentId, key); + + expect(mockSqlClient.transaction).toHaveBeenCalled(); + + expect(mockSqlClient.select.mock.calls).toMatchSnapshot('selectParams'); + expect(mockSqlClient.from.mock.calls).toMatchSnapshot('fromParams'); + expect(mockSqlClient.where.mock.calls).toMatchSnapshot('whereParams'); + + expect(mockTrx.insert.mock.calls).toMatchSnapshot('insertParams'); + expect(mockTrx.returning.mock.calls).toMatchSnapshot('returningParams'); + expect(mockTrx.into.mock.calls).toMatchSnapshot('intoParams'); + expect(mockTrx.insert.mock.calls).toMatchSnapshot('insertParams'); + expect(mockTrx).toHaveBeenCalledWith(tableNames.SAMPLE_IN_METADATA_TRACK_MAP); + }); + + it('createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples', async () => { + const experimentId = 'mockExperimentId'; + const key = 'mockKey'; + + mockTrx.into.mockReturnValueOnce([]); + + await new MetadataTrack().createNewMetadataTrack(experimentId, key); + + expect(mockSqlClient.transaction).toHaveBeenCalled(); + + expect(mockSqlClient.select.mock.calls).toMatchSnapshot('selectParams'); + expect(mockSqlClient.from.mock.calls).toMatchSnapshot('fromParams'); + expect(mockSqlClient.where.mock.calls).toMatchSnapshot('whereParams'); + + expect(mockTrx.insert.mock.calls).toMatchSnapshot('insertParams'); + expect(mockTrx.returning.mock.calls).toMatchSnapshot('returningParams'); + expect(mockTrx.into.mock.calls).toMatchSnapshot('intoParams'); + expect(mockTrx.insert.mock.calls).toMatchSnapshot('insertParams'); + expect(mockTrx).not.toHaveBeenCalledWith(tableNames.SAMPLE_IN_METADATA_TRACK_MAP); + }); + + it('patchValueForSample works correctly', async () => { + const experimentId = 'mockExperimentId'; + const key = 'mockKey'; + const sampleId = 'mockSampleId'; + const value = 'mockValue'; + + const metadataTrackId = 'mockMetadataTrackId'; + + const mockFind = jest.spyOn(BasicModel.prototype, 'find') + .mockImplementationOnce(() => Promise.resolve([{ id: metadataTrackId }])); + + + + await new MetadataTrack().patchValueForSample(experimentId, sampleId, key, value); + + expect(mockFind).toHaveBeenCalledWith({ experiment_id: experimentId, key }); + + expect(mockSqlClient.update).toHaveBeenCalledWith({ value }); + expect(mockSqlClient.where).toHaveBeenCalledWith( + { metadata_track_id: metadataTrackId, sample_id: sampleId }, + ); + expect(mockSqlClient.returning).toHaveBeenCalledWith(['metadata_track_id']); + }); + + it('patchValueForSample throws if the track-sample map doesn\'t exist', async () => { + const experimentId = 'mockExperimentId'; + const key = 'mockKey'; + const sampleId = 'mockSampleId'; + const value = 'mockValue'; + + const mockFind = jest.spyOn(BasicModel.prototype, 'find') + .mockImplementationOnce(() => Promise.resolve([])); + + await expect( + new MetadataTrack().patchValueForSample(experimentId, sampleId, key, value), + ).rejects.toThrow(); + + expect(mockFind).toHaveBeenCalledWith({ experiment_id: experimentId, key }); + }); + it('createNewExperimentPermissions works correctly when experiment has metadata tracks', async () => { const mockExperimentId = 'mockExperimentId'; const mockSampleId = 'mockSampleId'; @@ -21,8 +107,8 @@ describe('model/userAccess', () => { await new MetadataTrack().createNewSampleValues(mockExperimentId, mockSampleId); expect(mockSqlClient.insert).toHaveBeenCalledWith([ - { metadata_track_id: 'track1Id', sample_id: 'mockSampleId', value: 'N.A.' }, - { metadata_track_id: 'track2Id', sample_id: 'mockSampleId', value: 'N.A.' }, + { metadata_track_id: 'track1Id', sample_id: 'mockSampleId' }, + { metadata_track_id: 'track2Id', sample_id: 'mockSampleId' }, ]); }); diff --git a/tests/api.v2/model/__snapshots__/MetadataTrack.test.js.snap b/tests/api.v2/model/__snapshots__/MetadataTrack.test.js.snap new file mode 100644 index 000000000..a750f67e1 --- /dev/null +++ b/tests/api.v2/model/__snapshots__/MetadataTrack.test.js.snap @@ -0,0 +1,177 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`model/userAccess createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples: fromParams 1`] = ` +Array [ + Array [ + "sample", + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples: insertParams 1`] = ` +Array [ + Array [ + Object { + "experiment_id": "mockExperimentId", + "key": "mockKey", + }, + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples: insertParams 2`] = ` +Array [ + Array [ + Object { + "experiment_id": "mockExperimentId", + "key": "mockKey", + }, + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples: intoParams 1`] = ` +Array [ + Array [ + "metadata_track", + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples: returningParams 1`] = ` +Array [ + Array [ + Array [ + "id", + ], + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples: selectParams 1`] = ` +Array [ + Array [ + Array [ + "id", + ], + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack skips insert into SAMPLE_IN_METADATA_TRACK_MAP when there are no samples: whereParams 1`] = ` +Array [ + Array [ + Object { + "experiment_id": "mockExperimentId", + }, + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack works correctly when there are samples: fromParams 1`] = ` +Array [ + Array [ + "sample", + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack works correctly when there are samples: insertParams 1`] = ` +Array [ + Array [ + Object { + "experiment_id": "mockExperimentId", + "key": "mockKey", + }, + ], + Array [ + Array [ + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId1", + }, + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId2", + }, + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId3", + }, + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId4", + }, + ], + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack works correctly when there are samples: insertParams 2`] = ` +Array [ + Array [ + Object { + "experiment_id": "mockExperimentId", + "key": "mockKey", + }, + ], + Array [ + Array [ + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId1", + }, + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId2", + }, + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId3", + }, + Object { + "metadata_track_id": "metadataTrackId1", + "sample_id": "sampleId4", + }, + ], + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack works correctly when there are samples: intoParams 1`] = ` +Array [ + Array [ + "metadata_track", + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack works correctly when there are samples: returningParams 1`] = ` +Array [ + Array [ + Array [ + "id", + ], + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack works correctly when there are samples: selectParams 1`] = ` +Array [ + Array [ + Array [ + "id", + ], + ], +] +`; + +exports[`model/userAccess createNewMetadataTrack works correctly when there are samples: whereParams 1`] = ` +Array [ + Array [ + Object { + "experiment_id": "mockExperimentId", + }, + ], +] +`; diff --git a/tests/api.v2/routes/metadataTrack.test.js b/tests/api.v2/routes/metadataTrack.test.js new file mode 100644 index 000000000..6b78da7b5 --- /dev/null +++ b/tests/api.v2/routes/metadataTrack.test.js @@ -0,0 +1,158 @@ +// @ts-nocheck +const express = require('express'); +const request = require('supertest'); +const expressLoader = require('../../../src/loaders/express'); + +const { OK } = require('../../../src/utils/responses'); + +const metadataTrackController = require('../../../src/api.v2/controllers/metadataTrackController'); + +jest.mock('../../../src/api.v2/controllers/metadataTrackController', () => ({ + createMetadataTrack: jest.fn(), + patchMetadataTrack: jest.fn(), + deleteMetadataTrack: jest.fn(), + patchValueForSample: jest.fn(), +})); + +jest.mock('../../../src/api.v2/middlewares/authMiddlewares'); + +const experimentId = 'experimentId'; +const sampleId = 'sampleId'; +const metadataTrackKey = 'metadataTrackKey'; + +describe('tests for metadata track routes', () => { + let app = null; + + beforeEach(async () => { + const mockApp = await expressLoader(express()); + app = mockApp.app; + }); + + afterEach(() => { + /** + * Most important since b'coz of caching, the mocked implementations sometimes does not reset + */ + jest.resetModules(); + jest.restoreAllMocks(); + }); + + it('Creating a new metadata track works', async (done) => { + metadataTrackController.createMetadataTrack.mockImplementationOnce((req, res) => { + res.json(OK()); + return Promise.resolve(); + }); + + request(app) + .post(`/v2/experiments/${experimentId}/metadataTracks/${metadataTrackKey}`) + .send() + .expect(200) + .end((err) => { + if (err) { + return done(err); + } + // there is no point testing for the values of the response body + // - if something is wrong, the schema validator will catch it + return done(); + }); + }); + + it('Patching a metadata track works', async (done) => { + metadataTrackController.patchMetadataTrack.mockImplementationOnce((req, res) => { + res.json(OK()); + return Promise.resolve(); + }); + + request(app) + .patch(`/v2/experiments/${experimentId}/metadataTracks/${metadataTrackKey}`) + .send({ key: 'newMetadataTrackKey' }) + .expect(200) + .end((err) => { + if (err) { + return done(err); + } + // there is no point testing for the values of the response body + // - if something is wrong, the schema validator will catch it + return done(); + }); + }); + + it('Patching a metadata track with invalid body fails', async (done) => { + metadataTrackController.patchMetadataTrack.mockImplementationOnce((req, res) => { + res.json(OK()); + return Promise.resolve(); + }); + + request(app) + .patch(`/v2/experiments/${experimentId}/metadataTracks/${metadataTrackKey}`) + .send({ invalidKey: 'newMetadataTrackKey' }) + .expect(400) + .end((err) => { + if (err) { + return done(err); + } + // there is no point testing for the values of the response body + // - if something is wrong, the schema validator will catch it + return done(); + }); + }); + + it('Deleting a metadata track works', async (done) => { + metadataTrackController.deleteMetadataTrack.mockImplementationOnce((req, res) => { + res.json(OK()); + return Promise.resolve(); + }); + + request(app) + .delete(`/v2/experiments/${experimentId}/metadataTracks/${metadataTrackKey}`) + .send() + .expect(200) + .end((err) => { + if (err) { + return done(err); + } + // there is no point testing for the values of the response body + // - if something is wrong, the schema validator will catch it + return done(); + }); + }); + + it('Patching the value a sample has in a metadata track works', async (done) => { + metadataTrackController.patchValueForSample.mockImplementationOnce((req, res) => { + res.json(OK()); + return Promise.resolve(); + }); + + request(app) + .patch(`/v2/experiments/${experimentId}/samples/${sampleId}/metadataTracks/${metadataTrackKey}`) + .send({ value: 'mockNewValue' }) + .expect(200) + .end((err) => { + if (err) { + return done(err); + } + // there is no point testing for the values of the response body + // - if something is wrong, the schema validator will catch it + return done(); + }); + }); + + it('Patching the value a sample has in a metadata track with invalid request body fails', async (done) => { + metadataTrackController.patchValueForSample.mockImplementationOnce((req, res) => { + res.json(OK()); + return Promise.resolve(); + }); + + request(app) + .patch(`/v2/experiments/${experimentId}/samples/${sampleId}/metadataTracks/${metadataTrackKey}`) + .send({ invalidValue: 'mockNewValue' }) + .expect(400) + .end((err) => { + if (err) { + return done(err); + } + // there is no point testing for the values of the response body + // - if something is wrong, the schema validator will catch it + return done(); + }); + }); +});