diff --git a/src/routes/projectReports/getEmbedReport.js b/src/routes/projectReports/getEmbedReport.js index a600aa26..dc19770c 100644 --- a/src/routes/projectReports/getEmbedReport.js +++ b/src/routes/projectReports/getEmbedReport.js @@ -14,7 +14,7 @@ module.exports = [ permissions('projectReporting.view'), async (req, res, next) => { const projectId = Number(req.params.projectId); - const mockReport = config.lookerConfig.USE_MOCK === 'true'; + const mockReport = config.get('lookerConfig.USE_MOCK') === 'true'; let reportName = mockReport ? 'mock' : req.query.reportName; const authUser = req.authUser; let REPORTS = null; @@ -22,7 +22,7 @@ module.exports = [ try { allowedUsers = JSON.parse(_.get(config, 'lookerConfig.ALLOWED_USERS', '[]')); req.log.trace(allowedUsers, 'allowedUsers'); - REPORTS = JSON.parse(config.lookerConfig.EMBED_REPORTS_MAPPING); + REPORTS = JSON.parse(config.get('lookerConfig.EMBED_REPORTS_MAPPING')); } catch (error) { req.log.error(error); req.log.debug('Invalid reports mapping. Should be a valid JSON.'); @@ -35,14 +35,39 @@ module.exports = [ if (!mockReport) { const project = await models.Project.findOne({ where: { id: projectId }, - attributes: ['id', 'templateId'], + attributes: ['id', 'templateId', 'details'], raw: true, }); + + // we would use Project Template or Product Template category to format report name + let category = ''; + + // try to get project template of the project to generate the report name const projectTemplate = project.templateId ? await models.ProjectTemplate.findByPk(project.templateId, { attributes: ['category'], raw: true }) : null; - const projectCategory = _.get(projectTemplate, 'category', ''); - reportName = `${reportName}-${projectCategory}`; + if (projectTemplate) { + category = _.get(projectTemplate, 'category', ''); + + // if no project template found, try to find product template (for old project v2) + } else { + const productTemplate = _.get(project, 'details.products[0]') + ? await models.ProductTemplate.findOne( + { + where: { + productKey: _.get(project, 'details.products[0]'), + }, + }, + { + attributes: ['category'], + raw: true, + }, + ) : null; + + category = _.get(productTemplate, 'category', ''); + } + + reportName = `${reportName}-${category}`; } // check if auth user has acecss to this project const members = req.context.currentProjectMembers; diff --git a/src/routes/projectReports/getEmbedReport.spec.js b/src/routes/projectReports/getEmbedReport.spec.js new file mode 100644 index 00000000..c0d4f174 --- /dev/null +++ b/src/routes/projectReports/getEmbedReport.spec.js @@ -0,0 +1,359 @@ +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; +import config from 'config'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; +import util from '../../util'; +import lookerSerivce from '../../services/lookerService'; + +const should = chai.should(); + +describe('GET embed report', () => { + let project0; + let project1; + let project3; + let productTemplate0; + let projectTemplate0; + beforeEach((done) => { + testUtil.clearDb() + .then(() => models.Project.create({ + type: 'generic', + directProjectId: 0, + billingAccountId: 0, + name: 'test0', + description: 'test project0', + status: 'reviewed', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + })) + .then((p0) => { + project0 = p0; + return models.ProjectMember.create({ + userId: 40051331, + projectId: project0.id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + }) + .then(() => models.ProjectTemplate.create({ + name: 'template 2', + key: 'key 2', + category: 'concrete', + icon: 'http://example.com/icon1.ico', + question: 'question 2', + info: 'info 2', + aliases: ['key-2', 'key_2'], + scope: {}, + phases: {}, + createdBy: 1, + updatedBy: 2, + }).then((projectTemplate) => { projectTemplate0 = projectTemplate; })) + .then(() => models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'reviewed', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + templateId: projectTemplate0.id, + lastActivityUserId: '1', + })) + .then((p) => { + project1 = p; + // create members + return models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + }) + .then(() => models.ProjectMember.create({ + userId: 40051334, + projectId: project1.id, + role: 'manager', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + })) + .then(() => models.ProjectMember.create({ + userId: 40051331, + projectId: project1.id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + })) + .then(() => models.ProductTemplate.create({ + name: 'product template 1', + productKey: 'product-key', + category: 'prodCut', + subCategory: 'prodSubCut', + icon: 'http://example.com/product-icon.ico', + brief: 'product brief', + details: 'product details', + aliases: ['product-key', 'product_key'], + createdBy: 1, + updatedBy: 2, + }).then((productTemplate) => { productTemplate0 = productTemplate; })) + .then(() => models.Project.create({ + type: 'generic', + directProjectId: 3, + billingAccountId: 3, + name: 'product test', + description: 'product test description', + status: 'reviewed', + details: { + products: [productTemplate0.productKey], + }, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((project) => { project3 = project; })) + .then(() => { + done(); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('GET /projects/{id}/reports/embed', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return 403 if user does not have permissions', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/reports/embed`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .expect('Content-Type', /json/) + .expect(403, done); + }); + + it('should return 403 if project not exist', (done) => { + request(server) + .get('/v5/projects/100100/reports/embed') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(403, done); + }); + + it('should return 404 when report name not mock and not in EMBED_REPORTS_MAPPING', (done) => { + const cfg = sinon.stub(config, 'get'); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + request(server) + .get(`/v5/projects/${project1.id}/reports/embed?reportName=random`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(404, (err) => { + cfg.restore(); + done(err); + }); + }); + + it('should return 500 when get admin user error', (done) => { + const cfg = sinon.stub(config, 'get'); + const gem = sinon.stub(lookerSerivce, 'generateEmbedUrl', () => 'generatedUrl'); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + cfg.withArgs('lookerConfig.EMBED_REPORTS_MAPPING').returns('{"mock-concrete-customer": "/embed/looks/2"}'); + request(server) + .get(`/v5/projects/${project1.id}/reports/embed?reportName=mock`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(500, (err) => { + gem.restore(); + cfg.restore(); + done(err); + }); + }); + + it('should return 404 when the project template or product template is not found', (done) => { + const cfg = sinon.stub(config, 'get'); + const gem = sinon.stub(lookerSerivce, 'generateEmbedUrl', () => 'generatedUrl'); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + cfg.withArgs('lookerConfig.EMBED_REPORTS_MAPPING').returns('{"mock-concrete-customer": "/embed/looks/2"}'); + request(server) + .get(`/v5/projects/${project0.id}/reports/embed?reportName=mock`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(404, (err) => { + gem.restore(); + cfg.restore(); + done(err); + }); + }); + + it('should return customer url', (done) => { + const cfg = sinon.stub(config, 'get'); + const gem = sinon.stub(lookerSerivce, 'generateEmbedUrl', () => 'generatedUrl'); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + cfg.withArgs('lookerConfig.EMBED_REPORTS_MAPPING') + .returns('{"mock-concrete-customer": "/customer/embed/looks/2"}'); + request(server) + .get(`/v5/projects/${project1.id}/reports/embed?reportName=mock`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + gem.restore(); + cfg.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.equal('generatedUrl'); + const [user, project, member, embedUrl] = gem.lastCall.args; + user.userId.should.equal(40051331); + project.should.deep.equal({ id: project1.id }); + member.userId.should.equal(40051331); + member.role.should.equal('customer'); + embedUrl.should.equal('/customer/embed/looks/2'); + done(); + } + }); + }); + + it('should return admin url', (done) => { + const cfg = sinon.stub(config, 'get'); + const gem = sinon.stub(lookerSerivce, 'generateEmbedUrl', () => 'generatedUrl'); + const getAdmin = sinon.stub(util, 'getTopcoderUser', () => ({ + firstName: 'fn', + lastName: 'ln', + userId: 40051333, + })); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + cfg.withArgs('lookerConfig.EMBED_REPORTS_MAPPING').returns('{"mock-concrete-topcoder": "/admin/embed/looks/2"}'); + request(server) + .get(`/v5/projects/${project1.id}/reports/embed?reportName=mock`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + getAdmin.restore(); + gem.restore(); + cfg.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.equal('generatedUrl'); + const [user, project, member, embedUrl] = gem.lastCall.args; + user.userId.should.equal(40051333); + project.should.deep.equal({ id: project1.id }); + member.userId.should.equal(40051333); + member.firstName.should.equal('fn'); + member.lastName.should.equal('ln'); + member.role.should.equal(''); + embedUrl.should.equal('/admin/embed/looks/2'); + done(); + } + }); + }); + + it('should return copilot url', (done) => { + const cfg = sinon.stub(config, 'get'); + const gem = sinon.stub(lookerSerivce, 'generateEmbedUrl', () => 'generatedUrl'); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + cfg.withArgs('lookerConfig.EMBED_REPORTS_MAPPING').returns('{"mock-concrete-copilot": "/copilot/embed/looks/2"}'); + request(server) + .get(`/v5/projects/${project1.id}/reports/embed?reportName=mock`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + gem.restore(); + cfg.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.equal('generatedUrl'); + const [user, project, member, embedUrl] = gem.lastCall.args; + user.userId.should.equal(40051332); + project.should.deep.equal({ id: project1.id }); + member.userId.should.equal(40051332); + member.role.should.equal('copilot'); + embedUrl.should.equal('/copilot/embed/looks/2'); + done(); + } + }); + }); + + it('should return admin url for project with product template', (done) => { + const cfg = sinon.stub(config, 'get'); + const gem = sinon.stub(lookerSerivce, 'generateEmbedUrl', () => 'generatedUrl'); + const getAdmin = sinon.stub(util, 'getTopcoderUser', () => ({ + firstName: 'fn', + lastName: 'ln', + userId: 40051333, + })); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + cfg.withArgs('lookerConfig.EMBED_REPORTS_MAPPING') + .returns('{"mock-prodCut-topcoder": "/admin/embed/looks/3"}'); + request(server) + .get(`/v5/projects/${project3.id}/reports/embed?reportName=mock`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + getAdmin.restore(); + gem.restore(); + cfg.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.equal('generatedUrl'); + const [user, project, member, embedUrl] = gem.lastCall.args; + user.userId.should.equal(40051333); + project.should.deep.equal({ id: project3.id }); + member.userId.should.equal(40051333); + member.firstName.should.equal('fn'); + member.lastName.should.equal('ln'); + member.role.should.equal(''); + embedUrl.should.equal('/admin/embed/looks/3'); + done(); + } + }); + }); + }); +}); diff --git a/src/routes/projectReports/getReport.js b/src/routes/projectReports/getReport.js index fe842fd6..d306b09b 100644 --- a/src/routes/projectReports/getReport.js +++ b/src/routes/projectReports/getReport.js @@ -17,7 +17,7 @@ module.exports = [ const projectId = Number(req.params.projectId); const reportName = req.query.reportName; - if (config.lookerConfig.USE_MOCK === 'true') { + if (config.get('lookerConfig.USE_MOCK') === 'true') { req.log.info('using mock'); // using mock return mock(projectId, reportName, req, res); diff --git a/src/routes/projectReports/getReport.spec.js b/src/routes/projectReports/getReport.spec.js new file mode 100644 index 00000000..9bdf5d3c --- /dev/null +++ b/src/routes/projectReports/getReport.spec.js @@ -0,0 +1,308 @@ +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; +import config from 'config'; +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const summaryJson = require('./mockFiles/summary.json'); +const projectBudget = require('./mockFiles/projectBudget.json'); +const axios = require('axios'); + +const should = chai.should(); + +describe('GET report', () => { + let project1; + beforeEach((done) => { + testUtil.clearDb() + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'reviewed', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + project1 = p; + // create members + return models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }).then(() => models.ProjectMember.create({ + userId: 40051334, + projectId: project1.id, + role: 'manager', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }).then(() => models.ProjectMember.create({ + userId: 40051331, + projectId: project1.id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }).then(() => { + done(); + }), + ), + ); + }); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('GET /projects/{id}/reports', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return 403 if user does not have permissions', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/reports/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member2}`, + }) + .expect('Content-Type', /json/) + .expect(403, done); + }); + + it('should return 403 if project not exist', (done) => { + request(server) + .get('/v5/projects/100100/reports/') + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(403, done); + }); + + it('should return 400 if report not exist and lookerConfig.USE_MOCK is true', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/reports/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(400, done); + }); + + it('should return mock summary report when lookerConfig.USE_MOCK is true', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/reports?reportName=summary`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.deep.equal(summaryJson); + done(); + } + }); + }); + + it('should return mock projectBudget report when lookerConfig.USE_MOCK is true', (done) => { + request(server) + .get(`/v5/projects/${project1.id}/reports?reportName=projectBudget`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.deep.equal(projectBudget); + done(); + } + }); + }); + + it('should return 404 when report name illegal', (done) => { + const cfg = sinon.stub(config, 'get'); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + request(server) + .get(`/v5/projects/${project1.id}/reports?reportName=random`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(404, (err) => { + cfg.restore(); + done(err); + }); + }); + + it('should return summary report when a customer get summary report', (done) => { + const cfg = sinon.stub(config, 'get'); + const ast = sinon.stub(axios, 'post', () => Promise.resolve({ data: { report: 'summary' } })); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + request(server) + .get(`/v5/projects/${project1.id}/reports?reportName=summary`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + cfg.restore(); + ast.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.deep.equal({ report: 'summary' }); + const accessArgs = ast.lastCall.args; + accessArgs[0].should.equal('/queries/run/json'); + accessArgs[1].id.should.equal(1234); + accessArgs[1].model.should.equal('topcoder_model_main'); + accessArgs[1].view.should.equal('challenge'); + accessArgs[1].filters.should.deep.equal({ 'connect_project.id': 1 }); + accessArgs[1].fields[0].should.equal('connect_project.id'); + accessArgs[1].fields[1].should.equal('challenge.track'); + accessArgs[1].fields[2].should.equal('challenge.num_registrations'); + accessArgs[1].fields[3].should.equal('challenge.num_submissions'); + accessArgs[1].limit.should.equal(10); + accessArgs[1].query_timezon.should.equal('America/Los_Angeles'); + done(); + } + }); + }); + + it('should return projectBudget report when a customer get projectBudget report', (done) => { + const cfg = sinon.stub(config, 'get'); + const ast = sinon.stub(axios, 'post', () => Promise.resolve({ data: { report: 'projectBudget' } })); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + request(server) + .get(`/v5/projects/${project1.id}/reports?reportName=projectBudget`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + cfg.restore(); + ast.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.deep.equal({ report: 'projectBudget' }); + const accessArgs = ast.lastCall.args; + accessArgs[0].should.equal('/queries/run/json'); + accessArgs[1].id.should.equal(123); + accessArgs[1].model.should.equal('topcoder_model_main'); + accessArgs[1].view.should.equal('project_stream'); + accessArgs[1].filters.should.deep.equal({ 'project_stream.tc_connect_project_id': 1 }); + accessArgs[1].fields[0].should.equal('project_stream.tc_connect_project_id'); + accessArgs[1].fields[1].should.equal('project_stream.total_invoiced_amount'); + accessArgs[1].fields[2].should.equal('project_stream.remaining_invoiced_budget'); + accessArgs[1].limit.should.equal(10); + accessArgs[1].query_timezon.should.equal('America/Los_Angeles'); + done(); + } + }); + }); + + it('should return projectBudget report when a copilot get projectBudget report', (done) => { + const cfg = sinon.stub(config, 'get'); + const ast = sinon.stub(axios, 'post', () => Promise.resolve({ data: { report: 'projectBudget' } })); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + request(server) + .get(`/v5/projects/${project1.id}/reports?reportName=projectBudget`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + cfg.restore(); + ast.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.deep.equal({ report: 'projectBudget' }); + const accessArgs = ast.lastCall.args; + accessArgs[0].should.equal('/queries/run/json'); + accessArgs[1].id.should.equal(123); + accessArgs[1].model.should.equal('topcoder_model_main'); + accessArgs[1].view.should.equal('project_stream'); + accessArgs[1].filters.should.deep.equal({ 'project_stream.tc_connect_project_id': 1 }); + accessArgs[1].fields[0].should.equal('project_stream.tc_connect_project_id'); + accessArgs[1].fields[1].should.equal('project_stream.total_actual_member_payment'); + accessArgs[1].limit.should.equal(10); + accessArgs[1].query_timezon.should.equal('America/Los_Angeles'); + done(); + } + }); + }); + + it('should return projectBudget report when an admin get projectBudget report', (done) => { + const cfg = sinon.stub(config, 'get'); + const ast = sinon.stub(axios, 'post', () => Promise.resolve({ data: { report: 'projectBudget' } })); + cfg.withArgs('lookerConfig.USE_MOCK').returns(false); + request(server) + .get(`/v5/projects/${project1.id}/reports?reportName=projectBudget`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + cfg.restore(); + ast.restore(); + if (err) { + done(err); + } else { + const resJson = res.body; + should.exist(resJson); + resJson.should.deep.equal({ report: 'projectBudget' }); + const accessArgs = ast.lastCall.args; + accessArgs[0].should.equal('/queries/run/json'); + accessArgs[1].id.should.equal(123); + accessArgs[1].model.should.equal('topcoder_model_main'); + accessArgs[1].view.should.equal('project_stream'); + accessArgs[1].filters.should.deep.equal({ 'project_stream.tc_connect_project_id': 1 }); + accessArgs[1].fields[0].should.equal('project_stream.tc_connect_project_id'); + accessArgs[1].fields[1].should.equal('project_stream.total_actual_challenge_fee'); + accessArgs[1].fields[2].should.equal('project_stream.total_actual_member_payment'); + accessArgs[1].fields[3].should.equal('project_stream.total_invoiced_amount'); + accessArgs[1].fields[4].should.equal('project_stream.remaining_invoiced_budget'); + accessArgs[1].limit.should.equal(10); + accessArgs[1].query_timezon.should.equal('America/Los_Angeles'); + done(); + } + }); + }); + }); +});