From bab4a795ade863bf944b91eccc44409b5b0c6ae6 Mon Sep 17 00:00:00 2001 From: Pikalot Date: Fri, 1 Aug 2025 16:50:40 -0700 Subject: [PATCH 1/6] Added create audit log test in printer.js --- api/main_endpoints/routes/Printer.js | 91 +++++++++++++--------- api/main_endpoints/util/auditLogHelpers.js | 38 +++++++++ test/api/Printer.js | 51 +++++++++++- 3 files changed, 144 insertions(+), 36 deletions(-) create mode 100644 api/main_endpoints/util/auditLogHelpers.js diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index 73a5c97f7..33b4bfe67 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -21,10 +21,11 @@ const { const { PRINTING = {} } = require('../../config/config.json'); +const AuditLogActions = require('../util/auditLogActions.js'); +const { createAuditLog } = require('../util/auditLogHelpers.js'); // see https://github.com/SCE-Development/Quasar/tree/dev/docker-compose.dev.yml#L11 -let PRINTER_URL = process.env.PRINTER_URL - || 'http://localhost:14000'; +let PRINTER_URL = process.env.PRINTER_URL || 'http://localhost:14000'; const router = express.Router(); @@ -36,7 +37,7 @@ const storage = multer.diskStorage({ filename: function(req, file, cb) { const uniqueSuffix = Date.now(); cb(null, uniqueSuffix + '_' + file.originalname); - } + }, }); const upload = multer({ storage }); @@ -58,7 +59,10 @@ router.get('/healthCheck', async (req, res) => { * https://github.com/SCE-Development/Quasar/wiki/How-do-Health-Checks-Work%3F */ if (!PRINTING.ENABLED) { - logger.warn('Printing is disabled, returning 200 to mock the printing server'); + logger.warn( + 'Printing is disabled, returning 200 to mock the printing server' + ); + return res.sendStatus(OK); } await axios @@ -74,29 +78,40 @@ router.get('/healthCheck', async (req, res) => { }); router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { + let totalFileSize = 0; + const { copies, sides, id } = req.body; + const action = AuditLogActions.PRINT_PAGE; + if (!checkIfTokenSent(req)) { logger.warn('/sendPrintRequest was requested without a token'); return res.sendStatus(UNAUTHORIZED); } - if (!await decodeToken(req)) { + const user = decodeToken(req); + if (!user) { logger.warn('/sendPrintRequest was requested with an invalid token'); return res.sendStatus(UNAUTHORIZED); } - if (!PRINTING.ENABLED) { - logger.warn('Printing is disabled, returning 200 and dummy print id to mock the printing server'); - return res.status(OK).send({ printId: null }); - } - const dir = path.join(__dirname, 'printing'); - const { totalChunks, chunkIdx } = req.body; + const details = { + copies: parseInt(copies), + sides: sides, + fileSize: totalFileSize, + userEmail: user.email, + printedAt: new Date(), + printJobId: id + }; - // reassemble pdf on last chunk received - if (Number(chunkIdx) < totalChunks - 1) { + if (!PRINTING.ENABLED) { + // create audit log on print + await createAuditLog({ + user, + action, + details + }); + logger.warn('Printing is disabled, returning 200 to mock the printing server'); return res.sendStatus(OK); } - const { copies, sides, id } = req.body; - const chunks = await fs.promises.readdir(dir); const assembledPdfFromChunks = path.join(dir, id + '.pdf'); @@ -106,6 +121,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { try { const chunkData = await fs.promises.readFile(path.join(dir, chunk)); + totalFileSize += chunkData.length; fs.appendFileSync(assembledPdfFromChunks, chunkData); } catch (err) { logger.error('/sendPrintRequest encountered an error while assembling pdf: ' + err); @@ -116,31 +132,36 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { const stream = await fs.createReadStream(assembledPdfFromChunks); const data = new FormData(); - data.append('file', stream, {filename: id, type: 'application/pdf'}); + data.append('file', fs.createReadStream(file.path), { filename: file.originalname }); data.append('copies', copies); data.append('sides', sides); - - try { - // full pdf can be sent to quasar no problem - const printRes = await axios.post(PRINTER_URL + '/print', data, { + axios.post(PRINTER_URL + '/print', + data, + { headers: { ...data.getHeaders(), - }, - maxContentLength: 1024 * 1024 * 150, // 150 mb - maxBodyLength: Infinity + } + }) + .then( async () => { + + // create audit log on print + await createAuditLog({ + user, + action, + details + }); + + // delete file from temp folder after printing + fs.unlink(file.path, (err) => { + if (err) { + logger.error(`Unable to delete file at path ${file.path}:`, err); + } + }); + res.sendStatus(OK); + }).catch((err) => { + logger.error('/sendPrintRequest had an error: ', err); + res.sendStatus(SERVER_ERROR); }); - - // { print_id: null | string } - const printId = printRes.data; - - await cleanUpChunks(dir, id); - res.status(OK).send(printId); - } catch (err) { - logger.error('/sendPrintRequest had an error: ', err); - - await cleanUpChunks(dir, id); - res.sendStatus(SERVER_ERROR); - } }); module.exports = router; diff --git a/api/main_endpoints/util/auditLogHelpers.js b/api/main_endpoints/util/auditLogHelpers.js new file mode 100644 index 000000000..48bd7a289 --- /dev/null +++ b/api/main_endpoints/util/auditLogHelpers.js @@ -0,0 +1,38 @@ +const AuditLog = require('../models/AuditLog'); +const logger = require('../../util/logger'); +const { + SERVER_ERROR, +} = require('../../util/constants.js').STATUS_CODES; + +/** + * Creates an audit log entry. + * @param {Object} params - The parameters for the audit log. + * @param {Object} params.user - User object containing at least `_id`. + * @param {string} params.action - Action type from `AuditLogActions`. + * @param {Object} params.details - Additional details for the log. + * @returns {Object|undefined} - Returns error object if failed, otherwise undefined. + */ +const createAuditLog = async ({ user, action, details }) => { + try { + await AuditLog.create({ + userId: user._id, + action, + details + }); + } catch(err) { + logger.error('auditLogHelpers had an error', err); + if (err.response && err.response.data) { + return { + status: err.response.status, + error: err.response.data + }; + } else { + return { + status: SERVER_ERROR, + error: 'Failed to create audit log in cleezyHelpers' + }; + } + } + +}; +module.exports = { createAuditLog }; diff --git a/test/api/Printer.js b/test/api/Printer.js index 58135cc4a..d45fddb03 100644 --- a/test/api/Printer.js +++ b/test/api/Printer.js @@ -1,8 +1,9 @@ process.env.NODE_ENV = 'test'; - +const mongoose = require('mongoose'); const chai = require('chai'); const chaiHttp = require('chai-http'); const fs = require('fs'); +const User = require('../../api/main_endpoints/models/User.js'); const { OK, @@ -25,6 +26,12 @@ const tools = require('../util/tools/tools.js'); const crypto = require('crypto'); const token = ''; const printerUtil = require('../../api/main_endpoints/util/Printer.js'); +const { MEMBERSHIP_STATE } = require('../../api/util/constants'); + + +const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions.js'); +const AuditLog = require('../../api/main_endpoints/models/AuditLog.js'); + let app = null; let test = null; @@ -58,6 +65,7 @@ describe('Printer', () => { resetTokenMock(); }); + const url = '/api/Printer/sendPrintRequest'; describe('cleanUpExpiredChunks', () => { const CHUNK_DIRECTORY = __dirname + '/../../api/main_endpoints/routes/printing'; const MY_BIRTH_DATE = new Date('December 4, 2005 07:53:00'); @@ -126,6 +134,7 @@ describe('Printer', () => { expect(result).to.have.status(UNAUTHORIZED); }); + it(`Should successfully process all ${TOTAL_CHUNKS} chunks sent (with valid token)`, async () => { let chunksProcessed = 0; setTokenStatus(true); @@ -154,5 +163,45 @@ describe('Printer', () => { expect(chunksProcessed).to.equal(TOTAL_CHUNKS); }); + + describe('Successfully send a print request and create an audit log', () => { + before( async () => { + await User.deleteMany({}); + await AuditLog.deleteMany({}); + }); + + it('Should return 200 when invalid token is sent', async () => { + const userId = new mongoose.Types.ObjectId(); + const user = new User({ + _id: userId, + firstName: 'first_name', + lastName: 'last_name', + email: 'print_user@b.c', + password: 'Passw0rd123', + emailVerified: true, + accessLevel: MEMBERSHIP_STATE.MEMBER, + }); + await user.save(); + + setTokenStatus(true, { + _id: user._id, + email: user.email, + accessLevel: user.accessLevel, + }); + + const result = await test.sendPostRequestWithToken(token, url, { DUMMY_CHUNK }); + expect(result).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ + action: AuditLogActions.PRINT_PAGE, + }).lean(); + expect(auditEntry).to.exist; + }); + + after(() => { + User.deleteMany({}); + AuditLog.deleteMany({}); + }); + }); }); }); From d84ee1c1edc78b4c7d23128118118d62d1cdd815 Mon Sep 17 00:00:00 2001 From: Pikalot Date: Fri, 1 Aug 2025 17:27:55 -0700 Subject: [PATCH 2/6] Added print status to create audit log entry --- api/main_endpoints/routes/Printer.js | 10 +++-- test/api/Printer.js | 62 +++++++++++++--------------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index 33b4bfe67..326ec81d9 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -98,10 +98,12 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { fileSize: totalFileSize, userEmail: user.email, printedAt: new Date(), - printJobId: id + printJobId: id, + status: 'success' || 'fail' }; if (!PRINTING.ENABLED) { + details.status = 'mocked'; // create audit log on print await createAuditLog({ user, @@ -143,7 +145,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } }) .then( async () => { - + details.status = 'success'; // create audit log on print await createAuditLog({ user, @@ -158,8 +160,10 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } }); res.sendStatus(OK); - }).catch((err) => { + }).catch(async (err) => { logger.error('/sendPrintRequest had an error: ', err); + details.status = 'fail'; + await createAuditLog({ user, action, details }); res.sendStatus(SERVER_ERROR); }); }); diff --git a/test/api/Printer.js b/test/api/Printer.js index d45fddb03..cc2741586 100644 --- a/test/api/Printer.js +++ b/test/api/Printer.js @@ -164,44 +164,38 @@ describe('Printer', () => { expect(chunksProcessed).to.equal(TOTAL_CHUNKS); }); - describe('Successfully send a print request and create an audit log', () => { - before( async () => { - await User.deleteMany({}); - await AuditLog.deleteMany({}); + it('Should create an audit log when the response status is 200', async () => { + await User.deleteMany({}); + await AuditLog.deleteMany({}); + + const userId = new mongoose.Types.ObjectId(); + const user = new User({ + _id: userId, + firstName: 'first_name', + lastName: 'last_name', + email: 'print_user@b.c', + password: 'Passw0rd123', + emailVerified: true, + accessLevel: MEMBERSHIP_STATE.MEMBER, }); + await user.save(); - it('Should return 200 when invalid token is sent', async () => { - const userId = new mongoose.Types.ObjectId(); - const user = new User({ - _id: userId, - firstName: 'first_name', - lastName: 'last_name', - email: 'print_user@b.c', - password: 'Passw0rd123', - emailVerified: true, - accessLevel: MEMBERSHIP_STATE.MEMBER, - }); - await user.save(); - - setTokenStatus(true, { - _id: user._id, - email: user.email, - accessLevel: user.accessLevel, - }); - - const result = await test.sendPostRequestWithToken(token, url, { DUMMY_CHUNK }); - expect(result).to.have.status(OK); - - const auditEntry = await AuditLog.findOne({ - action: AuditLogActions.PRINT_PAGE, - }).lean(); - expect(auditEntry).to.exist; + setTokenStatus(true, { + _id: user._id, + email: user.email, + accessLevel: user.accessLevel, }); - after(() => { - User.deleteMany({}); - AuditLog.deleteMany({}); - }); + const result = await test.sendPostRequestWithToken(token, url, { DUMMY_CHUNK }); + expect(result).to.have.status(OK); + + const auditEntry = await AuditLog.findOne({ + action: AuditLogActions.PRINT_PAGE, + }).lean(); + expect(auditEntry).to.exist; }); + + User.deleteMany({}); + AuditLog.deleteMany({}); }); }); From f47bd7756b1d501a7f324eca10cf57f0d67f51a0 Mon Sep 17 00:00:00 2001 From: Pikalot Date: Sat, 2 Aug 2025 15:27:38 -0700 Subject: [PATCH 3/6] test/api/Printer.js tests if there is only one audit log exist --- test/api/Printer.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/api/Printer.js b/test/api/Printer.js index cc2741586..03f6f6113 100644 --- a/test/api/Printer.js +++ b/test/api/Printer.js @@ -28,7 +28,6 @@ const token = ''; const printerUtil = require('../../api/main_endpoints/util/Printer.js'); const { MEMBERSHIP_STATE } = require('../../api/util/constants'); - const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions.js'); const AuditLog = require('../../api/main_endpoints/models/AuditLog.js'); @@ -187,15 +186,21 @@ describe('Printer', () => { }); const result = await test.sendPostRequestWithToken(token, url, { DUMMY_CHUNK }); - expect(result).to.have.status(OK); - const auditEntry = await AuditLog.findOne({ action: AuditLogActions.PRINT_PAGE, }).lean(); + + expect(result).to.have.status(OK); expect(auditEntry).to.exist; }); - User.deleteMany({}); - AuditLog.deleteMany({}); + it('Should exist only one audit log', async () => { + expect(await AuditLog.count()).to.equal(1); + }); + + after(async () => { + await User.deleteMany({}); + await AuditLog.deleteMany({}); + }); }); }); From 12ef5f869ac07993a3acd2c3fbbb1bcc9852d4ca Mon Sep 17 00:00:00 2001 From: Pikalot Date: Sun, 3 Aug 2025 00:18:54 -0700 Subject: [PATCH 4/6] Delete old audit logs before and after running the Printer.js tests. Clean up all test data after completing User.js and ShortcutSearch.js tests. --- test/api/Printer.js | 25 +++++++++---------------- test/api/ShortcutSearch.js | 4 ++-- test/api/User.js | 5 +++-- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/test/api/Printer.js b/test/api/Printer.js index 03f6f6113..1d89380b3 100644 --- a/test/api/Printer.js +++ b/test/api/Printer.js @@ -31,7 +31,6 @@ const { MEMBERSHIP_STATE } = require('../../api/util/constants'); const AuditLogActions = require('../../api/main_endpoints/util/auditLogActions.js'); const AuditLog = require('../../api/main_endpoints/models/AuditLog.js'); - let app = null; let test = null; let sandbox = sinon.createSandbox(); @@ -47,6 +46,7 @@ describe('Printer', () => { __dirname + '/../../api/main_endpoints/routes/Printer.js', ]); test = new SceApiTester(app); + done(); }); @@ -56,11 +56,15 @@ describe('Printer', () => { tools.terminateServer(done); }); - beforeEach(() => { + beforeEach(async () => { + await User.deleteMany({}); + await AuditLog.deleteMany({}); setTokenStatus(false); }); - afterEach(() => { + afterEach(async () => { + await User.deleteMany({}); + await AuditLog.deleteMany({}); resetTokenMock(); }); @@ -163,10 +167,7 @@ describe('Printer', () => { expect(chunksProcessed).to.equal(TOTAL_CHUNKS); }); - it('Should create an audit log when the response status is 200', async () => { - await User.deleteMany({}); - await AuditLog.deleteMany({}); - + it('Should create only one audit log in the database when the response status is 200', async () => { const userId = new mongoose.Types.ObjectId(); const user = new User({ _id: userId, @@ -192,15 +193,7 @@ describe('Printer', () => { expect(result).to.have.status(OK); expect(auditEntry).to.exist; - }); - - it('Should exist only one audit log', async () => { - expect(await AuditLog.count()).to.equal(1); - }); - - after(async () => { - await User.deleteMany({}); - await AuditLog.deleteMany({}); + expect(await AuditLog.countDocuments()).to.equal(1); }); }); }); diff --git a/test/api/ShortcutSearch.js b/test/api/ShortcutSearch.js index faa61dcde..b5544d9bf 100644 --- a/test/api/ShortcutSearch.js +++ b/test/api/ShortcutSearch.js @@ -263,8 +263,8 @@ describe('ShortcutSearch', () => { }); }); - after(() => { - User.deleteMany({}); + after(async () => { + await User.deleteMany({}); }); }); }); diff --git a/test/api/User.js b/test/api/User.js index cd97047cb..0a5b1a9c3 100644 --- a/test/api/User.js +++ b/test/api/User.js @@ -835,9 +835,10 @@ describe('User', () => { expect(result.body.newAnnualMembers).to.equal(2); }); - after(() => { + after(async () => { revertClock(); - User.deleteMany({}); + await User.deleteMany({}); + await AuditLog.deleteMany({}); }); }); }); From 0da2dfa9c5136263abf933abd9bd7160af5fa5b2 Mon Sep 17 00:00:00 2001 From: Tuan-Anh Ho Date: Sun, 3 Aug 2025 17:01:48 -0700 Subject: [PATCH 5/6] Update api/main_endpoints/routes/Printer.js Co-authored-by: adarsh <110150037+adarshm11@users.noreply.github.com> --- api/main_endpoints/routes/Printer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index 326ec81d9..b06492f4d 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -94,7 +94,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { const details = { copies: parseInt(copies), - sides: sides, + sides, fileSize: totalFileSize, userEmail: user.email, printedAt: new Date(), From d539165069e767f4ea523e3ebc58bf5653c0a5bc Mon Sep 17 00:00:00 2001 From: Pikalot Date: Sun, 3 Aug 2025 23:38:33 -0700 Subject: [PATCH 6/6] Removed auditLogHelper.js and Printer.js would not create an audit log and skip the test if config/json.PRINTING is false. --- api/main_endpoints/routes/Printer.js | 48 ++++++++-------------- api/main_endpoints/util/auditLogHelpers.js | 38 ----------------- test/api/Printer.js | 6 +++ 3 files changed, 23 insertions(+), 69 deletions(-) delete mode 100644 api/main_endpoints/util/auditLogHelpers.js diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index b06492f4d..0ecb1b574 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -22,7 +22,7 @@ const { PRINTING = {} } = require('../../config/config.json'); const AuditLogActions = require('../util/auditLogActions.js'); -const { createAuditLog } = require('../util/auditLogHelpers.js'); +const AuditLog = require('../models/AuditLog.js'); // see https://github.com/SCE-Development/Quasar/tree/dev/docker-compose.dev.yml#L11 let PRINTER_URL = process.env.PRINTER_URL || 'http://localhost:14000'; @@ -62,7 +62,6 @@ router.get('/healthCheck', async (req, res) => { logger.warn( 'Printing is disabled, returning 200 to mock the printing server' ); - return res.sendStatus(OK); } await axios @@ -79,8 +78,6 @@ router.get('/healthCheck', async (req, res) => { router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { let totalFileSize = 0; - const { copies, sides, id } = req.body; - const action = AuditLogActions.PRINT_PAGE; if (!checkIfTokenSent(req)) { logger.warn('/sendPrintRequest was requested without a token'); @@ -91,29 +88,13 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { logger.warn('/sendPrintRequest was requested with an invalid token'); return res.sendStatus(UNAUTHORIZED); } - - const details = { - copies: parseInt(copies), - sides, - fileSize: totalFileSize, - userEmail: user.email, - printedAt: new Date(), - printJobId: id, - status: 'success' || 'fail' - }; - if (!PRINTING.ENABLED) { - details.status = 'mocked'; - // create audit log on print - await createAuditLog({ - user, - action, - details - }); logger.warn('Printing is disabled, returning 200 to mock the printing server'); return res.sendStatus(OK); } + const { copies, sides, id } = req.body; + const chunks = await fs.promises.readdir(dir); const assembledPdfFromChunks = path.join(dir, id + '.pdf'); @@ -145,13 +126,20 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } }) .then( async () => { - details.status = 'success'; + // create audit log on print - await createAuditLog({ - user, - action, - details - }); + await AuditLog.create({ + userId: user._id, + action: AuditLogActions.PRINT_PAGE, + details: { + copies: parseInt(copies), + sides, + fileSize: totalFileSize, + userEmail: user.email, + printedAt: new Date(), + printJobId: id + } + }).catch(logger.error); // delete file from temp folder after printing fs.unlink(file.path, (err) => { @@ -160,10 +148,8 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } }); res.sendStatus(OK); - }).catch(async (err) => { + }).catch((err) => { logger.error('/sendPrintRequest had an error: ', err); - details.status = 'fail'; - await createAuditLog({ user, action, details }); res.sendStatus(SERVER_ERROR); }); }); diff --git a/api/main_endpoints/util/auditLogHelpers.js b/api/main_endpoints/util/auditLogHelpers.js deleted file mode 100644 index 48bd7a289..000000000 --- a/api/main_endpoints/util/auditLogHelpers.js +++ /dev/null @@ -1,38 +0,0 @@ -const AuditLog = require('../models/AuditLog'); -const logger = require('../../util/logger'); -const { - SERVER_ERROR, -} = require('../../util/constants.js').STATUS_CODES; - -/** - * Creates an audit log entry. - * @param {Object} params - The parameters for the audit log. - * @param {Object} params.user - User object containing at least `_id`. - * @param {string} params.action - Action type from `AuditLogActions`. - * @param {Object} params.details - Additional details for the log. - * @returns {Object|undefined} - Returns error object if failed, otherwise undefined. - */ -const createAuditLog = async ({ user, action, details }) => { - try { - await AuditLog.create({ - userId: user._id, - action, - details - }); - } catch(err) { - logger.error('auditLogHelpers had an error', err); - if (err.response && err.response.data) { - return { - status: err.response.status, - error: err.response.data - }; - } else { - return { - status: SERVER_ERROR, - error: 'Failed to create audit log in cleezyHelpers' - }; - } - } - -}; -module.exports = { createAuditLog }; diff --git a/test/api/Printer.js b/test/api/Printer.js index 1d89380b3..1b04de0b1 100644 --- a/test/api/Printer.js +++ b/test/api/Printer.js @@ -1,4 +1,5 @@ process.env.NODE_ENV = 'test'; +const { PRINTING = {} } = require('../../api/config/config.json'); const mongoose = require('mongoose'); const chai = require('chai'); const chaiHttp = require('chai-http'); @@ -168,6 +169,11 @@ describe('Printer', () => { }); it('Should create only one audit log in the database when the response status is 200', async () => { + // Skip tests if printing is disabled + if (!PRINTING.ENABLED) { + return; + } + const userId = new mongoose.Types.ObjectId(); const user = new User({ _id: userId,