diff --git a/package.js b/package.js index 86110fd..c1054d8 100755 --- a/package.js +++ b/package.js @@ -20,7 +20,8 @@ Package.onTest(function (api) { api.addFiles([ "tests/basic.js", + "tests/configSchemaValidation.js", "tests/internals.js", "tests/replication.js" - ]) + ]); }); diff --git a/server/api.js b/server/api.js index ac70737..8ae32e0 100755 --- a/server/api.js +++ b/server/api.js @@ -6,6 +6,49 @@ import './imports/startup' const Jobs = {} +// Validated Jobs.run configuration schema + +const JobRunConfigSchema = { + in: Match.Maybe({ + millisecond: Match.Maybe(Number), + milliseconds: Match.Maybe(Number), + second: Match.Maybe(Number), + seconds: Match.Maybe(Number), + minute: Match.Maybe(Number), + minutes: Match.Maybe(Number), + hour: Match.Maybe(Number), + hours: Match.Maybe(Number), + day: Match.Maybe(Number), + days: Match.Maybe(Number), + month: Match.Maybe(Number), + months: Match.Maybe(Number), + year: Match.Maybe(Number), + years: Match.Maybe(Number), + }), + on: Match.Maybe({ + millisecond: Match.Maybe(Number), + milliseconds: Match.Maybe(Number), + second: Match.Maybe(Number), + seconds: Match.Maybe(Number), + minute: Match.Maybe(Number), + minutes: Match.Maybe(Number), + hour: Match.Maybe(Number), + hours: Match.Maybe(Number), + day: Match.Maybe(Number), + days: Match.Maybe(Number), + month: Match.Maybe(Number), + months: Match.Maybe(Number), + year: Match.Maybe(Number), + years: Match.Maybe(Number), + }), + date: Match.Maybe(Date), + priority: Match.Maybe(Number), + unique: Match.Maybe(Boolean), + singular: Match.Maybe(Boolean), + callback: Match.Maybe(Function), + data: Match.Maybe(Object), +}; + // Configure the package (optional) Jobs.configure = function (config) { @@ -56,6 +99,10 @@ Jobs.run = async function () { check(arguments[0], String) const lastArg = arguments[arguments.length - 1]; + const config = (typeof lastArg === "object" && !Array.isArray(lastArg) && !lastArg.remote) ? lastArg : {}; + + check(config, Match.Optional(JobRunConfigSchema)); + const remote = typeof lastArg === "object" && lastArg.remote; if (Utilities.registry.data[arguments[0]] || remote) { @@ -112,7 +159,7 @@ Jobs.execute = async function (jobId, callback, force) { check(force, Match.Optional(Boolean)) // 1. Get the job - const doc = await Utilities.collection.findOneAsync({ + const doc = await Utilities.collection.findOneAsync({ _id: jobId, state: { $nin: ["success", "cancelled"] diff --git a/tests/configSchemaValidation.js b/tests/configSchemaValidation.js new file mode 100644 index 0000000..de917ef --- /dev/null +++ b/tests/configSchemaValidation.js @@ -0,0 +1,124 @@ +import { Jobs, JobsInternal } from 'meteor/msavin:sjobs'; +import { Tinytest } from 'meteor/tinytest'; + +Tinytest.addAsync('Config Schema Validations', async function (test) { + // 0 - Clear the collection + console.log("--- 0 ---"); + var clear = await Jobs.clear("*"); + + // 1 - Register a Job + console.log("--- 1 ---"); + Jobs.register({ + "sendReminder": function (email, message) { + console.log("Sending reminder to " + email + " with message: " + message); + this.success(); + }, + "simpleJob": function () { + console.log("Running simple job with no arguments."); + this.success(); + } + }); + + // 2 - Schedule a job with valid config + console.log("--- 2 ---"); + let jobId1; + try { + jobId1 = await Jobs.run("sendReminder", "jony@apple.com", "The future is here!", { + in: { days: 3 }, + priority: 999, + singular: true, + callback: function () { + console.log("Callback executed successfully for jobId1"); + } + }); + console.log("JobId1 created successfully:", jobId1._id); + } catch (e) { + console.error("Error scheduling job with valid config:", e.message); + } + + // Verify that the job was created + test.isNotNull(jobId1, "JobId1 should be created successfully"); + + // 3 - Schedule a job with only the job name (no config or additional arguments) + console.log("--- 3 ---"); + let jobId2; + try { + jobId2 = await Jobs.run("simpleJob"); + console.log("JobId2 created successfully:", jobId2._id); + } catch (e) { + console.error("Error scheduling simple job:", e.message); + } + + // Verify that the job was created + test.isNotNull(jobId2, "JobId2 should be created successfully"); + + // 4 - Schedule a job with invalid config (typo in 'priority') + console.log("--- 4 ---"); + try { + await Jobs.run("sendReminder", "jony@apple.com", "Hello again!", { + in: { days: 1 }, + prioirty: 1000, // Intentional typo: should be 'priority' + singular: true, + callback: function () { + console.log("This callback should not be executed for jobId3"); + } + }); + test.fail("JobId3 should not be created due to invalid config."); + } catch (e) { + console.log("Caught expected error for invalid config:", e.message); + test.ok("Caught expected error for invalid config"); + } + + // 5 - Schedule a job with an unsupported config key + console.log("--- 5 ---"); + try { + await Jobs.run("sendReminder", "jony@apple.com", "Reminder!", { + in: { hours: 2 }, + singular: true, + unsupportedKey: "this should fail" // Unsupported key + }); + test.fail("JobId4 should not be created due to unsupported config key."); + } catch (e) { + console.log("Caught expected error for unsupported config key:", e.message); + test.ok("Caught expected error for unsupported config key"); + } + + // 6 - Log whatever is in the collection + console.log("--- 6 ---"); + var allJobDocs = await JobsInternal.Utilities.collection.find().fetchAsync(); + console.log(allJobDocs); + + // 7 - Cancel the successfully created job (jobId1) + console.log("--- 7 ---"); + if (jobId1 && jobId1._id) { + var cancel = await Jobs.cancel(jobId1._id); + console.log("Cancelled jobId1:", cancel); + + // 8 - Verify the job was cancelled + var jobDoc = await Jobs.get(jobId1._id); + console.log("--- 8 ---"); + console.log("Job doc after cancel:"); + console.log(jobDoc); + + test.equal(jobDoc.state, "cancelled", "Job should be successfully cancelled"); + } else { + test.fail("jobId1 was not created successfully, skipping cancel test."); + } + + // 9 - Try to cancel a job that was created without any config or arguments (jobId2) + console.log("--- 9 ---"); + if (jobId2 && jobId2._id) { + var cancel2 = await Jobs.cancel(jobId2._id); + console.log("Cancelled jobId2:", cancel2); + + // 10 - Verify the job was cancelled + var jobDoc2 = await Jobs.get(jobId2._id); + console.log("--- 10 ---"); + console.log("Job doc after cancel:"); + console.log(jobDoc2); + + test.equal(jobDoc2.state, "cancelled", "Job should be successfully cancelled"); + } else { + test.fail("jobId2 was not created successfully, skipping cancel test."); + } +}); \ No newline at end of file