From 2275fe3b73692fec1535f2904c9d41b691302308 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 25 Jul 2025 15:04:11 +0300 Subject: [PATCH 01/45] Rework test files into js from coffeescript --- tests/grouping_index_tests.coffee | 14 -- tests/grouping_index_tests.js | 16 ++ tests/grouping_tests.coffee | 249 ------------------ tests/grouping_tests.js | 315 +++++++++++++++++++++++ tests/hook_tests.coffee | 343 ------------------------- tests/hook_tests.js | 402 ++++++++++++++++++++++++++++++ 6 files changed, 733 insertions(+), 606 deletions(-) delete mode 100644 tests/grouping_index_tests.coffee create mode 100644 tests/grouping_index_tests.js delete mode 100644 tests/grouping_tests.coffee create mode 100644 tests/grouping_tests.js delete mode 100644 tests/hook_tests.coffee create mode 100644 tests/hook_tests.js diff --git a/tests/grouping_index_tests.coffee b/tests/grouping_index_tests.coffee deleted file mode 100644 index 0f5c738..0000000 --- a/tests/grouping_index_tests.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Tinytest.add "partitioner - indexing - no index specified", (test) -> - index = TestFuncs.getPartitionedIndex(undefined) - - test.length Object.keys(index), 1 - test.equal index._groupId, 1 - -Tinytest.add "partitioner - indexing - simple index object", (test) -> - input = {foo: 1} - index = TestFuncs.getPartitionedIndex(input) - - keyArr = Object.keys(index) - test.length keyArr, 2 - test.equal keyArr[0], "_groupId" - test.equal keyArr[1], "foo" diff --git a/tests/grouping_index_tests.js b/tests/grouping_index_tests.js new file mode 100644 index 0000000..b1affed --- /dev/null +++ b/tests/grouping_index_tests.js @@ -0,0 +1,16 @@ +Tinytest.add("partitioner - indexing - no index specified", (test) => { + const index = TestFuncs.getPartitionedIndex(undefined); + + test.length(Object.keys(index), 1); + test.equal(index._groupId, 1); +}); + +Tinytest.add("partitioner - indexing - simple index object", (test) => { + const input = {foo: 1}; + const index = TestFuncs.getPartitionedIndex(input); + + const keyArr = Object.keys(index); + test.length(keyArr, 2); + test.equal(keyArr[0], "_groupId"); + test.equal(keyArr[1], "foo"); +}); \ No newline at end of file diff --git a/tests/grouping_tests.coffee b/tests/grouping_tests.coffee deleted file mode 100644 index 36bbf5d..0000000 --- a/tests/grouping_tests.coffee +++ /dev/null @@ -1,249 +0,0 @@ -myGroup = "group1" -otherGroup = "group2" -treatmentName = "baz" - -basicInsertCollection = new Mongo.Collection("basicInsert") -twoGroupCollection = new Mongo.Collection("twoGroup") - -### - Set up server and client hooks -### - -if Meteor.isServer - groupingCollections = {} - - groupingCollections.basicInsert = basicInsertCollection - groupingCollections.twoGroup = twoGroupCollection - - hookCollection = (collection) -> - collection._insecure = true - - # Attach the hooks to the collection - Partitioner.partitionCollection(collection) - -if Meteor.isClient - hookCollection = (collection) -> Partitioner.partitionCollection(collection) - -### - Hook collections and run tests -### - -hookCollection basicInsertCollection -hookCollection twoGroupCollection - -if Meteor.isServer - - # We create the collections in the publisher (instead of using a method or - # something) because if we made them with a method, we'd need to follow the - # method with some subscribes, and it's possible that the method call would - # be delayed by a wait method and the subscribe messages would be sent before - # it and fail due to the collection not yet existing. So we are very hacky - # and use a publish. - Meteor.publish "groupingTests", -> - return unless @userId - - Partitioner.directOperation -> - basicInsertCollection.remove({}) - twoGroupCollection.remove({}) - - cursors = [ basicInsertCollection.find(), twoGroupCollection.find() ] - - Meteor._debug "grouping publication activated" - - Partitioner.directOperation -> - twoGroupCollection.insert - _groupId: myGroup - a: 1 - - twoGroupCollection.insert - _groupId: otherGroup - a: 1 - - Meteor._debug "collections configured" - - return cursors - - Meteor.methods - joinGroup: (myGroup) -> - userId = Meteor.userId() - throw new Error(403, "Not logged in") unless userId - Partitioner.clearUserGroup userId - Partitioner.setUserGroup(userId, myGroup) - serverInsert: (name, doc) -> - return groupingCollections[name].insert(doc) - serverUpdate: (name, selector, mutator) -> - return groupingCollections[name].update(selector, mutator) - serverRemove: (name, selector) -> - return groupingCollections[name].remove(selector) - getCollection: (name, selector) -> - return Partitioner.directOperation -> groupingCollections[name].find(selector || {}).fetch() - getMyCollection: (name, selector) -> - return groupingCollections[name].find(selector).fetch() - printCollection: (name) -> - console.log Partitioner.directOperation -> groupingCollections[name].find().fetch() - printMyCollection: (name) -> - console.log groupingCollections[name].find().fetch() - - Tinytest.add "partitioner - grouping - undefined default group", (test) -> - test.equal Partitioner.group(), undefined - - # The overriding is done separately for hooks - Tinytest.add "partitioner - grouping - override group environment variable", (test) -> - Partitioner.bindGroup "overridden", -> - test.equal Partitioner.group(), "overridden" - - Tinytest.add "partitioner - collections - disallow arbitrary insert", (test) -> - test.throws -> - basicInsertCollection.insert {foo: "bar"} - , (e) -> e.error is 403 and e.reason is ErrMsg.userIdErr - - Tinytest.add "partitioner - collections - insert with overridden group", (test) -> - Partitioner.bindGroup "overridden", -> - basicInsertCollection.insert { foo: "bar"} - test.ok() - -if Meteor.isClient - ### - These tests need to all async so they are in the right order - ### - - # Ensure we are logged in before running these tests - Tinytest.addAsync "partitioner - collections - verify login", (test, next) -> - InsecureLogin.ready next - - Tinytest.addAsync "partitioner - collections - join group", (test, next) -> - Meteor.call "joinGroup", myGroup, (err, res) -> - test.isFalse err - next() - - # Ensure that the group id has been recorded before subscribing - Tinytest.addAsync "partitioner - collections - received group id", (test, next) -> - Tracker.autorun (c) -> - groupId = Partitioner.group() - if groupId - c.stop() - test.equal groupId, myGroup - next() - - Tinytest.addAsync "partitioner - collections - test subscriptions ready", (test, next) -> - handle = Meteor.subscribe("groupingTests") - Tracker.autorun (c) -> - if handle.ready() - c.stop() - next() - - Tinytest.addAsync "partitioner - collections - local empty find", (test, next) -> - test.equal basicInsertCollection.find().count(), 0 - test.equal basicInsertCollection.find({}).count(), 0 - next() - - Tinytest.addAsync "partitioner - collections - remote empty find", (test, next) -> - Meteor.call "getMyCollection", "basicInsert", {a: 1}, (err, res) -> - test.isFalse err - test.equal res.length, 0 - next() - - testAsyncMulti "partitioner - collections - basic insert", [ - (test, expect) -> - id = basicInsertCollection.insert { a: 1 }, expect (err, res) -> - test.isFalse err, JSON.stringify(err) - test.equal res, id - , (test, expect) -> - test.equal basicInsertCollection.find({a: 1}).count(), 1 - test.isFalse basicInsertCollection.findOne(a: 1)._groupId? - ] - - testAsyncMulti "partitioner - collections - find from two groups", [ (test, expect) -> - test.equal twoGroupCollection.find().count(), 1 - - twoGroupCollection.find().forEach (el) -> - test.isFalse el._groupId? - - Meteor.call "getCollection", "twoGroup", expect (err, res) -> - test.isFalse err - test.equal res.length, 2 - ] - - testAsyncMulti "partitioner - collections - insert into two groups", [ - (test, expect) -> - twoGroupCollection.insert {a: 2}, expect (err) -> - test.isFalse err, JSON.stringify(err) - test.equal twoGroupCollection.find().count(), 2 - - twoGroupCollection.find().forEach (el) -> - test.isFalse el._groupId? - ### - twoGroup now contains - { _groupId: "myGroup", a: 1 } - { _groupId: "myGroup", a: 2 } - { _groupId: "otherGroup", a: 1 } - ### - , (test, expect) -> - Meteor.call "getMyCollection", "twoGroup", expect (err, res) -> - test.isFalse err - test.equal res.length, 2 - - # Method finds should also not return _groupId - _.each res, (el) -> - test.isFalse el._groupId? - - , (test, expect) -> # Ensure that the other half is still on the server - Meteor.call "getCollection", "twoGroup", expect (err, res) -> - test.isFalse err, JSON.stringify(err) - test.equal res.length, 3 - ] - - testAsyncMulti "partitioner - collections - server insert for client", [ - (test, expect) -> - Meteor.call "serverInsert", "twoGroup", {a: 3}, expect (err, res) -> - test.isFalse err - ### - twoGroup now contains - { _groupId: "myGroup", a: 1 } - { _groupId: "myGroup", a: 2 } - { _groupId: "myGroup", a: 3 } - { _groupId: "otherGroup", a: 1 } - ### - , (test, expect) -> - Meteor.call "getMyCollection", "twoGroup", {}, expect (err, res) -> - test.isFalse err - test.equal res.length, 3 - - _.each res, (el) -> - test.isFalse el._groupId? - ] - - testAsyncMulti "partitioner - collections - server update identical keys across groups", [ - (test, expect) -> - Meteor.call "serverUpdate", "twoGroup", - {a: 1}, - $set: { b: 1 }, expect (err, res) -> - test.isFalse err - ### - twoGroup now contains - { _groupId: "myGroup", a: 1, b: 1 } - { _groupId: "myGroup", a: 2 } - { _groupId: "myGroup", a: 3 } - { _groupId: "otherGroup", a: 1 } - ### - , (test, expect) -> # Make sure that the other group's record didn't get updated - Meteor.call "getCollection", "twoGroup", expect (err, res) -> - test.isFalse err - _.each res, (doc) -> - if doc.a is 1 and doc._groupId is myGroup - test.equal doc.b, 1 - else - test.isFalse doc.b - ] - - testAsyncMulti "partitioner - collections - server remove identical keys across groups", [ - (test, expect) -> - Meteor.call "serverRemove", "twoGroup", - {a: 1}, expect (err, res) -> - test.isFalse err - , (test, expect) -> # Make sure that the other group's record didn't get updated - Meteor.call "getCollection", "twoGroup", {a: 1}, expect (err, res) -> - test.isFalse err - test.equal res.length, 1 - test.equal res[0].a, 1 - ] diff --git a/tests/grouping_tests.js b/tests/grouping_tests.js new file mode 100644 index 0000000..b8865f4 --- /dev/null +++ b/tests/grouping_tests.js @@ -0,0 +1,315 @@ +const myGroup = "group1"; +const otherGroup = "group2"; +const treatmentName = "baz"; + +const basicInsertCollection = new Mongo.Collection("basicInsert"); +const twoGroupCollection = new Mongo.Collection("twoGroup"); + +/* + Set up server and client hooks +*/ + +if (Meteor.isServer) { + const groupingCollections = {}; + + groupingCollections.basicInsert = basicInsertCollection; + groupingCollections.twoGroup = twoGroupCollection; + + const hookCollection = (collection) => { + collection._insecure = true; + + // Attach the hooks to the collection + Partitioner.partitionCollection(collection); + }; +} + +if (Meteor.isClient) { + const hookCollection = (collection) => Partitioner.partitionCollection(collection); +} + +/* + Hook collections and run tests +*/ + +hookCollection(basicInsertCollection); +hookCollection(twoGroupCollection); + +if (Meteor.isServer) { + + // We create the collections in the publisher (instead of using a method or + // something) because if we made them with a method, we'd need to follow the + // method with some subscribes, and it's possible that the method call would + // be delayed by a wait method and the subscribe messages would be sent before + // it and fail due to the collection not yet existing. So we are very hacky + // and use a publish. + Meteor.publish("groupingTests", function() { + if (!this.userId) return; + + Partitioner.directOperation(() => { + basicInsertCollection.remove({}); + twoGroupCollection.remove({}); + }); + + const cursors = [basicInsertCollection.find(), twoGroupCollection.find()]; + + Meteor._debug("grouping publication activated"); + + Partitioner.directOperation(() => { + twoGroupCollection.insert({ + _groupId: myGroup, + a: 1 + }); + + twoGroupCollection.insert({ + _groupId: otherGroup, + a: 1 + }); + }); + + Meteor._debug("collections configured"); + + return cursors; + }); + + Meteor.methods({ + joinGroup: function(myGroup) { + const userId = Meteor.userId(); + if (!userId) throw new Error(403, "Not logged in"); + Partitioner.clearUserGroup(userId); + Partitioner.setUserGroup(userId, myGroup); + }, + serverInsert: function(name, doc) { + return groupingCollections[name].insert(doc); + }, + serverUpdate: function(name, selector, mutator) { + return groupingCollections[name].update(selector, mutator); + }, + serverRemove: function(name, selector) { + return groupingCollections[name].remove(selector); + }, + getCollection: function(name, selector) { + return Partitioner.directOperation(() => groupingCollections[name].find(selector || {}).fetch()); + }, + getMyCollection: function(name, selector) { + return groupingCollections[name].find(selector).fetch(); + }, + printCollection: function(name) { + console.log(Partitioner.directOperation(() => groupingCollections[name].find().fetch())); + }, + printMyCollection: function(name) { + console.log(groupingCollections[name].find().fetch()); + } + }); + + Tinytest.add("partitioner - grouping - undefined default group", (test) => { + test.equal(Partitioner.group(), undefined); + }); + + // The overriding is done separately for hooks + Tinytest.add("partitioner - grouping - override group environment variable", (test) => { + Partitioner.bindGroup("overridden", () => { + test.equal(Partitioner.group(), "overridden"); + }); + }); + + Tinytest.add("partitioner - collections - disallow arbitrary insert", (test) => { + test.throws(() => { + basicInsertCollection.insert({foo: "bar"}); + }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); + }); + + Tinytest.add("partitioner - collections - insert with overridden group", (test) => { + Partitioner.bindGroup("overridden", () => { + basicInsertCollection.insert({foo: "bar"}); + test.ok(); + }); + }); +} + +if (Meteor.isClient) { + /* + These tests need to all async so they are in the right order + */ + + // Ensure we are logged in before running these tests + Tinytest.addAsync("partitioner - collections - verify login", (test, next) => { + InsecureLogin.ready(next); + }); + + Tinytest.addAsync("partitioner - collections - join group", (test, next) => { + Meteor.call("joinGroup", myGroup, (err, res) => { + test.isFalse(err); + next(); + }); + }); + + // Ensure that the group id has been recorded before subscribing + Tinytest.addAsync("partitioner - collections - received group id", (test, next) => { + Tracker.autorun((c) => { + const groupId = Partitioner.group(); + if (groupId) { + c.stop(); + test.equal(groupId, myGroup); + next(); + } + }); + }); + + Tinytest.addAsync("partitioner - collections - test subscriptions ready", (test, next) => { + const handle = Meteor.subscribe("groupingTests"); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + next(); + } + }); + }); + + Tinytest.addAsync("partitioner - collections - local empty find", (test, next) => { + test.equal(basicInsertCollection.find().count(), 0); + test.equal(basicInsertCollection.find({}).count(), 0); + next(); + }); + + Tinytest.addAsync("partitioner - collections - remote empty find", (test, next) => { + Meteor.call("getMyCollection", "basicInsert", {a: 1}, (err, res) => { + test.isFalse(err); + test.equal(res.length, 0); + next(); + }); + }); + + testAsyncMulti("partitioner - collections - basic insert", [ + (test, expect) => { + const id = basicInsertCollection.insert({a: 1}, expect((err, res) => { + test.isFalse(err, JSON.stringify(err)); + test.equal(res, id); + })); + }, + (test, expect) => { + test.equal(basicInsertCollection.find({a: 1}).count(), 1); + test.isFalse(basicInsertCollection.findOne({a: 1})._groupId != null); + } + ]); + + testAsyncMulti("partitioner - collections - find from two groups", [ + (test, expect) => { + test.equal(twoGroupCollection.find().count(), 1); + + twoGroupCollection.find().forEach((el) => { + test.isFalse(el._groupId != null); + }); + + Meteor.call("getCollection", "twoGroup", expect((err, res) => { + test.isFalse(err); + test.equal(res.length, 2); + })); + } + ]); + + testAsyncMulti("partitioner - collections - insert into two groups", [ + (test, expect) => { + twoGroupCollection.insert({a: 2}, expect((err) => { + test.isFalse(err, JSON.stringify(err)); + test.equal(twoGroupCollection.find().count(), 2); + + twoGroupCollection.find().forEach((el) => { + test.isFalse(el._groupId != null); + }); + })); + /* + twoGroup now contains + { _groupId: "myGroup", a: 1 } + { _groupId: "myGroup", a: 2 } + { _groupId: "otherGroup", a: 1 } + */ + }, + (test, expect) => { + Meteor.call("getMyCollection", "twoGroup", expect((err, res) => { + test.isFalse(err); + test.equal(res.length, 2); + + // Method finds should also not return _groupId + _.each(res, (el) => { + test.isFalse(el._groupId != null); + }); + })); + }, + (test, expect) => { // Ensure that the other half is still on the server + Meteor.call("getCollection", "twoGroup", expect((err, res) => { + test.isFalse(err, JSON.stringify(err)); + test.equal(res.length, 3); + })); + } + ]); + + testAsyncMulti("partitioner - collections - server insert for client", [ + (test, expect) => { + Meteor.call("serverInsert", "twoGroup", {a: 3}, expect((err, res) => { + test.isFalse(err); + })); + /* + twoGroup now contains + { _groupId: "myGroup", a: 1 } + { _groupId: "myGroup", a: 2 } + { _groupId: "myGroup", a: 3 } + { _groupId: "otherGroup", a: 1 } + */ + }, + (test, expect) => { + Meteor.call("getMyCollection", "twoGroup", {}, expect((err, res) => { + test.isFalse(err); + test.equal(res.length, 3); + + _.each(res, (el) => { + test.isFalse(el._groupId != null); + }); + })); + } + ]); + + testAsyncMulti("partitioner - collections - server update identical keys across groups", [ + (test, expect) => { + Meteor.call("serverUpdate", "twoGroup", + {a: 1}, + {$set: {b: 1}}, expect((err, res) => { + test.isFalse(err); + })); + /* + twoGroup now contains + { _groupId: "myGroup", a: 1, b: 1 } + { _groupId: "myGroup", a: 2 } + { _groupId: "myGroup", a: 3 } + { _groupId: "otherGroup", a: 1 } + */ + }, + (test, expect) => { // Make sure that the other group's record didn't get updated + Meteor.call("getCollection", "twoGroup", expect((err, res) => { + test.isFalse(err); + _.each(res, (doc) => { + if (doc.a === 1 && doc._groupId === myGroup) { + test.equal(doc.b, 1); + } else { + test.isFalse(doc.b); + } + }); + })); + } + ]); + + testAsyncMulti("partitioner - collections - server remove identical keys across groups", [ + (test, expect) => { + Meteor.call("serverRemove", "twoGroup", + {a: 1}, expect((err, res) => { + test.isFalse(err); + })); + }, + (test, expect) => { // Make sure that the other group's record didn't get updated + Meteor.call("getCollection", "twoGroup", {a: 1}, expect((err, res) => { + test.isFalse(err); + test.equal(res.length, 1); + test.equal(res[0].a, 1); + })); + } + ]); +} \ No newline at end of file diff --git a/tests/hook_tests.coffee b/tests/hook_tests.coffee deleted file mode 100644 index a5e2360..0000000 --- a/tests/hook_tests.coffee +++ /dev/null @@ -1,343 +0,0 @@ -testUsername = "hooks_foo" -testGroupId = "hooks_bar" - -if Meteor.isClient - # XXX All async here to ensure ordering - - Tinytest.addAsync "partitioner - hooks - ensure logged in", (test, next) -> - InsecureLogin.ready next - - Tinytest.addAsync "partitioner - hooks - add client group", (test, next) -> - Meteor.call "joinGroup", testGroupId, (err, res) -> - test.isFalse err - next() - - Tinytest.addAsync "partitioner - hooks - vanilla client find", (test, next) -> - ctx = - args: [] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.length ctx.args, 0 - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Also nothing changed - test.length ctx.args, 0 - - next() - - Tinytest.addAsync "partitioner - hooks - set admin", (test, next) -> - Meteor.call "setAdmin", true, (err, res) -> - test.isFalse err - test.isTrue Meteor.user().admin - next() - - Tinytest.addAsync "partitioner - hooks - admin hidden in client find", (test, next) -> - ctx = - args: [] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.length ctx.args, 0 - - TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]) - # Admin removed from find - test.equal ctx.args[0].admin.$exists, false - next() - - Tinytest.addAsync "partitioner - hooks - admin hidden in selector find", (test, next) -> - ctx = - args: [ { foo: "bar" }] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.length ctx.args, 1 - test.equal ctx.args[0].foo, "bar" - - TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]) - # Admin removed from find - test.equal ctx.args[0].foo, "bar" - test.equal ctx.args[0].admin.$exists, false - next() - - # Need to remove admin to avoid fubars in other tests - Tinytest.addAsync "partitioner - hooks - unset admin", (test, next) -> - Meteor.call "setAdmin", false, (err, res) -> - test.isFalse err - test.isFalse Meteor.user().admin - next() - -if Meteor.isServer - Meteor.methods - setAdmin: (value) -> - userId = Meteor.userId() - throw new Meteor.Error(403, "not logged in") unless userId - if value - Meteor.users.update userId, $set: admin: true - else - Meteor.users.update userId, $unset: admin: null - - userId = null - ungroupedUserId = null - try - userId = Accounts.createUser - username: testUsername - catch - userId = Meteor.users.findOne(username: testUsername)._id - - try - ungroupedUserId = Accounts.createUser - username: "blahblah" - catch - ungroupedUserId = Meteor.users.findOne(username: "blahblah")._id - - Partitioner.clearUserGroup userId - Partitioner.setUserGroup userId, testGroupId - - Tinytest.add "partitioner - hooks - find with no args", (test) -> - ctx = - args: [] - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should replace undefined with { _groupId: ... } - test.isTrue ctx.args[0]? - test.equal ctx.args[0]._groupId, testGroupId - - test.isTrue ctx.args[1]? - test.equal ctx.args[1].fields._groupId, 0 - - Tinytest.add "partitioner - hooks - find with no group", (test) -> - ctx = - args: [] - - # Should throw if user is not logged in - test.throws -> - TestFuncs.findHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - , (e) -> e.error is 403 and e.reason is ErrMsg.userIdErr - - Tinytest.add "partitioner - hooks - find with string id", (test) -> - ctx = - args: [ "yabbadabbadoo" ] - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch a string - test.equal ctx.args[0], "yabbadabbadoo" - - test.isFalse ctx.args[1]? - - Tinytest.add "partitioner - hooks - find with single _id", (test) -> - ctx = - args: [ {_id: "yabbadabbadoo"} ] - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch an object with _id - test.equal ctx.args[0]._id, "yabbadabbadoo" - test.isFalse ctx.args[0]._groupId - - test.isFalse ctx.args[1]? - - Tinytest.add "partitioner - hooks - find with complex _id", (test) -> - ctx = - args: [ {_id: {$ne: "yabbadabbadoo"} } ] - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should modify for complex _id - test.equal ctx.args[0]._id.$ne, "yabbadabbadoo" - test.equal ctx.args[0]._groupId, testGroupId - - test.isTrue ctx.args[1]? - test.equal ctx.args[1].fields._groupId, 0 - - Tinytest.add "partitioner - hooks - find with selector", (test) -> - ctx = - args: [ { foo: "bar" } ] - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - test.equal ctx.args[0].foo, "bar" - test.equal ctx.args[0]._groupId, testGroupId - - test.isTrue ctx.args[1]? - test.equal ctx.args[1].fields._groupId, 0 - - Tinytest.add "partitioner - hooks - find with inclusion fields", (test) -> - ctx = - args: [ - { foo: "bar" }, - { fields: { foo: 1 } } - ] - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch a string - test.equal ctx.args[0].foo, "bar" - test.equal ctx.args[0]._groupId, testGroupId - - test.isTrue ctx.args[1]? - test.equal ctx.args[1].fields.foo, 1 - test.isFalse ctx.args[1].fields._groupId? - - Tinytest.add "partitioner - hooks - find with exclusion fields", (test) -> - ctx = - args: [ - { foo: "bar" }, - { fields: { foo: 0 } } - ] - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch a string - test.equal ctx.args[0].foo, "bar" - test.equal ctx.args[0]._groupId, testGroupId - - test.isTrue ctx.args[1]? - test.equal ctx.args[1].fields.foo, 0 - test.equal ctx.args[1].fields._groupId, 0 - - Tinytest.add "partitioner - hooks - insert doc", (test) -> - ctx = - args: [ { foo: "bar" } ] - - TestFuncs.insertHook.call(ctx, userId, ctx.args[0]) - # Should add the group id - test.equal ctx.args[0].foo, "bar" - test.equal ctx.args[0]._groupId, testGroupId - - Tinytest.add "partitioner - hooks - user find with no args", (test) -> - ctx = - args: [] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.length ctx.args, 0 - - # Ungrouped user should throw an error - test.throws -> - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]) - (e) -> e.error is 403 and e.reason is ErrMsg.groupErr - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should replace undefined with { _groupId: ... } - test.equal ctx.args[0].group, testGroupId - test.equal ctx.args[0].admin.$exists, false - - Tinytest.add "partitioner - hooks - user find with environment group but no userId", (test) -> - ctx = - args: [] - - Partitioner.bindGroup testGroupId, -> - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have set the extra arguments - test.equal ctx.args[0].group, testGroupId - test.equal ctx.args[0].admin.$exists, false - - Tinytest.add "partitioner - hooks - user find with string id", (test) -> - ctx = - args: [ "yabbadabbadoo" ] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.equal ctx.args[0], "yabbadabbadoo" - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch a string - test.equal ctx.args[0], "yabbadabbadoo" - - Tinytest.add "partitioner - hooks - user find with single _id", (test) -> - ctx = - args: [ {_id: "yabbadabbadoo"} ] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.equal ctx.args[0]._id, "yabbadabbadoo" - test.isFalse ctx.args[0].group - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch a single object - test.equal ctx.args[0]._id, "yabbadabbadoo" - test.isFalse ctx.args[0].group - - Tinytest.add "partitioner - hooks - user find with _id: $in", (test) -> - ctx = - args: [ {_id: $in: [ "yabbadabbadoo"] } ] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.equal ctx.args[0]._id.$in[0], "yabbadabbadoo" - test.isFalse ctx.args[0].group - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch a single object - test.equal ctx.args[0]._id.$in[0], "yabbadabbadoo" - test.isFalse ctx.args[0].group - - Tinytest.add "partitioner - hooks - user find with complex _id", (test) -> - ctx = - args: [ {_id: {$ne: "yabbadabbadoo"} } ] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.equal ctx.args[0]._id.$ne, "yabbadabbadoo" - test.isFalse ctx.args[0].group - - # Ungrouped user should throw an error - test.throws -> - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]) - (e) -> e.error is 403 and e.reason is ErrMsg.groupErr - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should be modified - test.equal ctx.args[0]._id.$ne, "yabbadabbadoo" - test.equal ctx.args[0].group, testGroupId - test.equal ctx.args[0].admin.$exists, false - - Tinytest.add "partitioner - hooks - user find with username", (test) -> - ctx = - args: [ {username: "yabbadabbadoo"} ] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.equal ctx.args[0].username, "yabbadabbadoo" - test.isFalse ctx.args[0].group - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should not touch a single object - test.equal ctx.args[0].username, "yabbadabbadoo" - test.isFalse ctx.args[0].group - - Tinytest.add "partitioner - hooks - user find with complex username", (test) -> - ctx = - args: [ {username: {$ne: "yabbadabbadoo"} } ] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.equal ctx.args[0].username.$ne, "yabbadabbadoo" - test.isFalse ctx.args[0].group - - # Ungrouped user should throw an error - test.throws -> - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]) - (e) -> e.error is 403 and e.reason is ErrMsg.groupErr - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should be modified - test.equal ctx.args[0].username.$ne, "yabbadabbadoo" - test.equal ctx.args[0].group, testGroupId - test.equal ctx.args[0].admin.$exists, false - - Tinytest.add "partitioner - hooks - user find with selector", (test) -> - ctx = - args: [ { foo: "bar" } ] - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]) - # Should have nothing changed - test.equal ctx.args[0].foo, "bar" - test.isFalse ctx.args[0].group - - # Ungrouped user should throw an error - test.throws -> - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]) - (e) -> e.error is 403 and e.reason is ErrMsg.groupErr - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]) - # Should modify the selector - test.equal ctx.args[0].foo, "bar" - test.equal ctx.args[0].group, testGroupId - test.equal ctx.args[0].admin.$exists, false diff --git a/tests/hook_tests.js b/tests/hook_tests.js new file mode 100644 index 0000000..481825e --- /dev/null +++ b/tests/hook_tests.js @@ -0,0 +1,402 @@ +const testUsername = "hooks_foo"; +const testGroupId = "hooks_bar"; + +if (Meteor.isClient) { + // XXX All async here to ensure ordering + + Tinytest.addAsync("partitioner - hooks - ensure logged in", (test, next) => { + InsecureLogin.ready(next); + }); + + Tinytest.addAsync("partitioner - hooks - add client group", (test, next) => { + Meteor.call("joinGroup", testGroupId, (err, res) => { + test.isFalse(err); + next(); + }); + }); + + Tinytest.addAsync("partitioner - hooks - vanilla client find", (test, next) => { + const ctx = { + args: [] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 0); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Also nothing changed + test.length(ctx.args, 0); + + next(); + }); + + Tinytest.addAsync("partitioner - hooks - set admin", (test, next) => { + Meteor.call("setAdmin", true, (err, res) => { + test.isFalse(err); + test.isTrue(Meteor.user().admin); + next(); + }); + }); + + Tinytest.addAsync("partitioner - hooks - admin hidden in client find", (test, next) => { + const ctx = { + args: [] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 0); + + TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); + // Admin removed from find + test.equal(ctx.args[0].admin.$exists, false); + next(); + }); + + Tinytest.addAsync("partitioner - hooks - admin hidden in selector find", (test, next) => { + const ctx = { + args: [{foo: "bar"}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 1); + test.equal(ctx.args[0].foo, "bar"); + + TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); + // Admin removed from find + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0].admin.$exists, false); + next(); + }); + + // Need to remove admin to avoid fubars in other tests + Tinytest.addAsync("partitioner - hooks - unset admin", (test, next) => { + Meteor.call("setAdmin", false, (err, res) => { + test.isFalse(err); + test.isFalse(Meteor.user().admin); + next(); + }); + }); +} + +if (Meteor.isServer) { + Meteor.methods({ + setAdmin: function(value) { + const userId = Meteor.userId(); + if (!userId) throw new Meteor.Error(403, "not logged in"); + if (value) { + Meteor.users.update(userId, {$set: {admin: true}}); + } else { + Meteor.users.update(userId, {$unset: {admin: null}}); + } + } + }); + + let userId = null; + let ungroupedUserId = null; + try { + userId = Accounts.createUser({ + username: testUsername + }); + } catch (e) { + userId = Meteor.users.findOne({username: testUsername})._id; + } + + try { + ungroupedUserId = Accounts.createUser({ + username: "blahblah" + }); + } catch (e) { + ungroupedUserId = Meteor.users.findOne({username: "blahblah"})._id; + } + + Partitioner.clearUserGroup(userId); + Partitioner.setUserGroup(userId, testGroupId); + + Tinytest.add("partitioner - hooks - find with no args", (test) => { + const ctx = { + args: [] + }; + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should replace undefined with { _groupId: ... } + test.isTrue(ctx.args[0] != null); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields._groupId, 0); + }); + + Tinytest.add("partitioner - hooks - find with no group", (test) => { + const ctx = { + args: [] + }; + + // Should throw if user is not logged in + test.throws(() => { + TestFuncs.findHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); + }); + + Tinytest.add("partitioner - hooks - find with string id", (test) => { + const ctx = { + args: ["yabbadabbadoo"] + }; + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a string + test.equal(ctx.args[0], "yabbadabbadoo"); + + test.isFalse(ctx.args[1] != null); + }); + + Tinytest.add("partitioner - hooks - find with single _id", (test) => { + const ctx = { + args: [{_id: "yabbadabbadoo"}] + }; + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch an object with _id + test.equal(ctx.args[0]._id, "yabbadabbadoo"); + test.isFalse(ctx.args[0]._groupId); + + test.isFalse(ctx.args[1] != null); + }); + + Tinytest.add("partitioner - hooks - find with complex _id", (test) => { + const ctx = { + args: [{_id: {$ne: "yabbadabbadoo"}}] + }; + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should modify for complex _id + test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields._groupId, 0); + }); + + Tinytest.add("partitioner - hooks - find with selector", (test) => { + const ctx = { + args: [{foo: "bar"}] + }; + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields._groupId, 0); + }); + + Tinytest.add("partitioner - hooks - find with inclusion fields", (test) => { + const ctx = { + args: [ + {foo: "bar"}, + {fields: {foo: 1}} + ] + }; + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a string + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields.foo, 1); + test.isFalse(ctx.args[1].fields._groupId != null); + }); + + Tinytest.add("partitioner - hooks - find with exclusion fields", (test) => { + const ctx = { + args: [ + {foo: "bar"}, + {fields: {foo: 0}} + ] + }; + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a string + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields.foo, 0); + test.equal(ctx.args[1].fields._groupId, 0); + }); + + Tinytest.add("partitioner - hooks - insert doc", (test) => { + const ctx = { + args: [{foo: "bar"}] + }; + + TestFuncs.insertHook.call(ctx, userId, ctx.args[0]); + // Should add the group id + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + }); + + Tinytest.add("partitioner - hooks - user find with no args", (test) => { + const ctx = { + args: [] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 0); + + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should replace undefined with { _groupId: ... } + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); + + Tinytest.add("partitioner - hooks - user find with environment group but no userId", (test) => { + const ctx = { + args: [] + }; + + Partitioner.bindGroup(testGroupId, () => { + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have set the extra arguments + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); + }); + + Tinytest.add("partitioner - hooks - user find with string id", (test) => { + const ctx = { + args: ["yabbadabbadoo"] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0], "yabbadabbadoo"); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a string + test.equal(ctx.args[0], "yabbadabbadoo"); + }); + + Tinytest.add("partitioner - hooks - user find with single _id", (test) => { + const ctx = { + args: [{_id: "yabbadabbadoo"}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0]._id, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a single object + test.equal(ctx.args[0]._id, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + }); + + Tinytest.add("partitioner - hooks - user find with _id: $in", (test) => { + const ctx = { + args: [{_id: {$in: ["yabbadabbadoo"]}}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0]._id.$in[0], "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a single object + test.equal(ctx.args[0]._id.$in[0], "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + }); + + Tinytest.add("partitioner - hooks - user find with complex _id", (test) => { + const ctx = { + args: [{_id: {$ne: "yabbadabbadoo"}}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should be modified + test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); + + Tinytest.add("partitioner - hooks - user find with username", (test) => { + const ctx = { + args: [{username: "yabbadabbadoo"}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0].username, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a single object + test.equal(ctx.args[0].username, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + }); + + Tinytest.add("partitioner - hooks - user find with complex username", (test) => { + const ctx = { + args: [{username: {$ne: "yabbadabbadoo"}}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should be modified + test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); + + Tinytest.add("partitioner - hooks - user find with selector", (test) => { + const ctx = { + args: [{foo: "bar"}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0].foo, "bar"); + test.isFalse(ctx.args[0].group); + + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should modify the selector + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); +} \ No newline at end of file From 6224cf00c50b1b3ec67459b7176776e5b8e360ee Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 25 Jul 2025 15:14:35 +0300 Subject: [PATCH 02/45] Convert package files to JS --- common.coffee | 15 --- common.js | 19 +++ grouping.coffee | 203 -------------------------------- grouping.js | 254 +++++++++++++++++++++++++++++++++++++++++ grouping_client.coffee | 45 -------- grouping_client.js | 54 +++++++++ package.js | 12 +- 7 files changed, 333 insertions(+), 269 deletions(-) delete mode 100644 common.coffee create mode 100644 common.js delete mode 100644 grouping.coffee create mode 100644 grouping.js delete mode 100644 grouping_client.coffee create mode 100644 grouping_client.js diff --git a/common.coffee b/common.coffee deleted file mode 100644 index e2ad9cb..0000000 --- a/common.coffee +++ /dev/null @@ -1,15 +0,0 @@ -ErrMsg = - userIdErr: "Must be logged in to operate on partitioned collection" - groupErr: "Must have group assigned to operate on partitioned collection" - -Helpers = - isDirectSelector: (selector) -> - _.isString(selector) or _.isString(selector?._id) - - # Because of https://github.com/HarvardEconCS/turkserver-meteor/issues/44 - # _id: { $in: [ ... ] } queries should be short-circuited as well for users - isDirectUserSelector: (selector) -> - _.isString(selector) or - _.isString(selector?._id) or - _.isString(selector?.username) or - ( _.isObject(selector?._id) and selector._id.$in? ) diff --git a/common.js b/common.js new file mode 100644 index 0000000..bfc8e9d --- /dev/null +++ b/common.js @@ -0,0 +1,19 @@ +const ErrMsg = { + userIdErr: "Must be logged in to operate on partitioned collection", + groupErr: "Must have group assigned to operate on partitioned collection" +}; + +const Helpers = { + isDirectSelector: function(selector) { + return _.isString(selector) || _.isString(selector != null ? selector._id : undefined); + }, + + // Because of https://github.com/HarvardEconCS/turkserver-meteor/issues/44 + // _id: { $in: [ ... ] } queries should be short-circuited as well for users + isDirectUserSelector: function(selector) { + return _.isString(selector) || + _.isString(selector != null ? selector._id : undefined) || + _.isString(selector != null ? selector.username : undefined) || + (_.isObject(selector != null ? selector._id : undefined) && (selector._id.$in != null)); + } +}; \ No newline at end of file diff --git a/grouping.coffee b/grouping.coffee deleted file mode 100644 index a2bd0a0..0000000 --- a/grouping.coffee +++ /dev/null @@ -1,203 +0,0 @@ -### - SERVER METHODS - Hook in group id to all operations, including find - - Grouping contains _id: userId and groupId: groupId -### - -Partitioner = {} -Grouping = new Mongo.Collection("ts.grouping") - -# Meteor environment variables for scoping group operations -Partitioner._currentGroup = new Meteor.EnvironmentVariable() -Partitioner._directOps = new Meteor.EnvironmentVariable() - -### - Public API -### - -Partitioner.setUserGroup = (userId, groupId) -> - check(userId, String) - check(groupId, String) - if Grouping.findOne(userId) - throw new Meteor.Error(403, "User is already in a group") - - Grouping.upsert userId, - $set: {groupId: groupId} - -Partitioner.getUserGroup = (userId) -> - check(userId, String) - Grouping.findOne(userId)?.groupId - -Partitioner.clearUserGroup = (userId) -> - check(userId, String) - Grouping.remove(userId) - -Partitioner.group = -> - # If group is overridden, return that instead - if (groupId = Partitioner._currentGroup.get())? - return groupId - try # We may be outside of a method - userId = Meteor.userId() - return unless userId - return Partitioner.getUserGroup(userId) - -Partitioner.bindGroup = (groupId, func) -> - Partitioner._currentGroup.withValue(groupId, func); - -Partitioner.bindUserGroup = (userId, func) -> - groupId = Partitioner.getUserGroup(userId) - unless groupId - Meteor._debug "Dropping operation because #{userId} is not in a group" - return - Partitioner.bindGroup(groupId, func) - -Partitioner.directOperation = (func) -> - Partitioner._directOps.withValue(true, func); - -# This can be replaced - currently not documented -Partitioner._isAdmin = (userId) -> Meteor.users.findOne(userId, {fields: groupId: 1, admin: 1}).admin is true - -getPartitionedIndex = (index) -> - defaultIndex = { _groupId : 1 } - return defaultIndex unless index - return _.extend( defaultIndex, index ) - -Partitioner.partitionCollection = (collection, options) -> - # Because of the deny below, need to create an allow validator - # on an insecure collection if there isn't one already - if collection._isInsecure() - collection.allow - insert: -> true - update: -> true - remove: -> true - - # Idiot-proof the collection against admin users - collection.deny - insert: Partitioner._isAdmin - update: Partitioner._isAdmin - remove: Partitioner._isAdmin - - collection.before.find findHook - collection.before.findOne findHook - - # These will hook the _validated methods as well - collection.before.insert insertHook - - ### - No update/remove hook necessary, see - https://github.com/matb33/meteor-collection-hooks/issues/23 - ### - - # Index the collections by groupId on the server for faster lookups across groups - collection._ensureIndex getPartitionedIndex(options?.index), options?.indexOptions - -# Publish admin and group for users that have it -Meteor.publish null, -> - return unless @userId - return Meteor.users.find @userId, - fields: { - admin: 1 - group: 1 - } - -# Special hook for Meteor.users to scope for each group -userFindHook = (userId, selector, options) -> - return true if Partitioner._directOps.get() is true - return true if Helpers.isDirectUserSelector(selector) - - groupId = Partitioner._currentGroup.get() - # This hook doesn't run if we're not in a method invocation or publish - # function, and Partitioner._currentGroup is not set - return true if (!userId and !groupId) - - unless groupId - user = Meteor.users.findOne(userId, {fields: groupId: 1, admin: 1}) - groupId = Grouping.findOne(userId)?.groupId - # If user is admin and not in a group, proceed as normal (select all users) - return true if user.admin and !groupId - # Normal users need to be in a group - throw new Meteor.Error(403, ErrMsg.groupErr) unless groupId - - # Since user is in a group, scope the find to the group - filter = - "group" : groupId - "admin": {$exists: false} - - unless @args[0] - @args[0] = filter - else - _.extend(selector, filter) - - return true - -# Attach the find hooks to Meteor.users -Meteor.users.before.find userFindHook -Meteor.users.before.findOne userFindHook - -# No allow/deny for find so we make our own checks -findHook = (userId, selector, options) -> - # Don't scope for direct operations - return true if Partitioner._directOps.get() is true - - # for find(id) we should not touch this - # TODO this may allow arbitrary finds across groups with the right _id - # We could amend this in the future to {_id: someId, _groupId: groupId} - # https://github.com/mizzao/meteor-partitioner/issues/9 - # https://github.com/mizzao/meteor-partitioner/issues/10 - return true if Helpers.isDirectSelector(selector) - if userId - # Check for global hook - groupId = Partitioner._currentGroup.get() - unless groupId - throw new Meteor.Error(403, ErrMsg.userIdErr) unless userId - groupId = Grouping.findOne(userId)?.groupId - throw new Meteor.Error(403, ErrMsg.groupErr) unless groupId - - # if object (or empty) selector, just filter by group - unless selector? - @args[0] = { _groupId : groupId } - else - selector._groupId = groupId - - # Adjust options to not return _groupId - unless options? - @args[1] = { fields: {_groupId: 0} } - else - # If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere - options.fields ?= {} - options.fields._groupId = 0 unless _.any(options.fields, (v) -> v is 1) - - return true - -insertHook = (userId, doc) -> - # Don't add group for direct inserts - return true if Partitioner._directOps.get() is true - - groupId = Partitioner._currentGroup.get() - unless groupId - throw new Meteor.Error(403, ErrMsg.userIdErr) unless userId - groupId = Grouping.findOne(userId)?.groupId - throw new Meteor.Error(403, ErrMsg.groupErr) unless groupId - - doc._groupId = groupId - return true - -# Sync grouping needed for hooking Meteor.users -Grouping.find().observeChanges - added: (id, fields) -> - unless Meteor.users.update(id, $set: {"group": fields.groupId} ) - Meteor._debug "Tried to set group for nonexistent user #{id}" - return - changed: (id, fields) -> - unless Meteor.users.update(id, $set: {"group": fields.groupId} ) - Meteor._debug "Tried to change group for nonexistent user #{id}" - removed: (id) -> - unless Meteor.users.update(id, $unset: {"group": null} ) - Meteor._debug "Tried to unset group for nonexistent user #{id}" - -TestFuncs = - getPartitionedIndex: getPartitionedIndex - userFindHook: userFindHook - findHook: findHook - insertHook: insertHook diff --git a/grouping.js b/grouping.js new file mode 100644 index 0000000..d6f53df --- /dev/null +++ b/grouping.js @@ -0,0 +1,254 @@ +/* + SERVER METHODS + Hook in group id to all operations, including find + + Grouping contains _id: userId and groupId: groupId +*/ + +Partitioner = {}; +const Grouping = new Mongo.Collection("ts.grouping"); + +// Meteor environment variables for scoping group operations +Partitioner._currentGroup = new Meteor.EnvironmentVariable(); +Partitioner._directOps = new Meteor.EnvironmentVariable(); + +/* + Public API +*/ + +Partitioner.setUserGroup = function(userId, groupId) { + check(userId, String); + check(groupId, String); + if (Grouping.findOne(userId)) { + throw new Meteor.Error(403, "User is already in a group"); + } + + Grouping.upsert(userId, { + $set: {groupId: groupId} + }); +}; + +Partitioner.getUserGroup = function(userId) { + check(userId, String); + const grouping = Grouping.findOne(userId); + return grouping != null ? grouping.groupId : undefined; +}; + +Partitioner.clearUserGroup = function(userId) { + check(userId, String); + Grouping.remove(userId); +}; + +Partitioner.group = function() { + // If group is overridden, return that instead + const groupId = Partitioner._currentGroup.get(); + if (groupId != null) { + return groupId; + } + let userId; + try { // We may be outside of a method + userId = Meteor.userId(); + } catch (e) { + // Handle the case where we're outside of a method + } + if (!userId) return; + return Partitioner.getUserGroup(userId); +}; + +Partitioner.bindGroup = function(groupId, func) { + Partitioner._currentGroup.withValue(groupId, func); +}; + +Partitioner.bindUserGroup = function(userId, func) { + const groupId = Partitioner.getUserGroup(userId); + if (!groupId) { + Meteor._debug(`Dropping operation because ${userId} is not in a group`); + return; + } + Partitioner.bindGroup(groupId, func); +}; + +Partitioner.directOperation = function(func) { + Partitioner._directOps.withValue(true, func); +}; + +// This can be replaced - currently not documented +Partitioner._isAdmin = function(userId) { + const user = Meteor.users.findOne(userId, {fields: {groupId: 1, admin: 1}}); + return user.admin === true; +}; + +const getPartitionedIndex = function(index) { + const defaultIndex = {_groupId: 1}; + if (!index) return defaultIndex; + return _.extend(defaultIndex, index); +}; + +Partitioner.partitionCollection = function(collection, options) { + // Because of the deny below, need to create an allow validator + // on an insecure collection if there isn't one already + if (collection._isInsecure()) { + collection.allow({ + insert: () => true, + update: () => true, + remove: () => true + }); + } + + // Idiot-proof the collection against admin users + collection.deny({ + insert: Partitioner._isAdmin, + update: Partitioner._isAdmin, + remove: Partitioner._isAdmin + }); + + collection.before.find(findHook); + collection.before.findOne(findHook); + + // These will hook the _validated methods as well + collection.before.insert(insertHook); + + /* + No update/remove hook necessary, see + https://github.com/matb33/meteor-collection-hooks/issues/23 + */ + + // Index the collections by groupId on the server for faster lookups across groups + collection._ensureIndex(getPartitionedIndex(options != null ? options.index : undefined), options != null ? options.indexOptions : undefined); +}; + +// Publish admin and group for users that have it +Meteor.publish(null, function() { + if (!this.userId) return; + return Meteor.users.find(this.userId, { + fields: { + admin: 1, + group: 1 + } + }); +}); + +// Special hook for Meteor.users to scope for each group +const userFindHook = function(userId, selector, options) { + if (Partitioner._directOps.get() === true) return true; + if (Helpers.isDirectUserSelector(selector)) return true; + + let groupId = Partitioner._currentGroup.get(); + // This hook doesn't run if we're not in a method invocation or publish + // function, and Partitioner._currentGroup is not set + if (!userId && !groupId) return true; + + if (!groupId) { + const user = Meteor.users.findOne(userId, {fields: {groupId: 1, admin: 1}}); + const grouping = Grouping.findOne(userId); + groupId = grouping != null ? grouping.groupId : undefined; + // If user is admin and not in a group, proceed as normal (select all users) + if (user.admin && !groupId) return true; + // Normal users need to be in a group + if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + } + + // Since user is in a group, scope the find to the group + const filter = { + "group": groupId, + "admin": {$exists: false} + }; + + if (!this.args[0]) { + this.args[0] = filter; + } else { + _.extend(selector, filter); + } + + return true; +}; + +// Attach the find hooks to Meteor.users +Meteor.users.before.find(userFindHook); +Meteor.users.before.findOne(userFindHook); + +// No allow/deny for find so we make our own checks +const findHook = function(userId, selector, options) { + // Don't scope for direct operations + if (Partitioner._directOps.get() === true) return true; + + // for find(id) we should not touch this + // TODO this may allow arbitrary finds across groups with the right _id + // We could amend this in the future to {_id: someId, _groupId: groupId} + // https://github.com/mizzao/meteor-partitioner/issues/9 + // https://github.com/mizzao/meteor-partitioner/issues/10 + if (Helpers.isDirectSelector(selector)) return true; + + if (userId) { + // Check for global hook + let groupId = Partitioner._currentGroup.get(); + if (!groupId) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); + const grouping = Grouping.findOne(userId); + groupId = grouping != null ? grouping.groupId : undefined; + if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + } + + // if object (or empty) selector, just filter by group + if (selector == null) { + this.args[0] = {_groupId: groupId}; + } else { + selector._groupId = groupId; + } + + // Adjust options to not return _groupId + if (options == null) { + this.args[1] = {fields: {_groupId: 0}}; + } else { + // If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere + if (options.fields == null) options.fields = {}; + if (!_.any(options.fields, (v) => v === 1)) { + options.fields._groupId = 0; + } + } + } + + return true; +}; + +const insertHook = function(userId, doc) { + // Don't add group for direct inserts + if (Partitioner._directOps.get() === true) return true; + + let groupId = Partitioner._currentGroup.get(); + if (!groupId) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); + const grouping = Grouping.findOne(userId); + groupId = grouping != null ? grouping.groupId : undefined; + if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + } + + doc._groupId = groupId; + return true; +}; + +// Sync grouping needed for hooking Meteor.users +Grouping.find().observeChanges({ + added: function(id, fields) { + if (!Meteor.users.update(id, {$set: {"group": fields.groupId}})) { + Meteor._debug(`Tried to set group for nonexistent user ${id}`); + } + }, + changed: function(id, fields) { + if (!Meteor.users.update(id, {$set: {"group": fields.groupId}})) { + Meteor._debug(`Tried to change group for nonexistent user ${id}`); + } + }, + removed: function(id) { + if (!Meteor.users.update(id, {$unset: {"group": null}})) { + Meteor._debug(`Tried to unset group for nonexistent user ${id}`); + } + } +}); + +const TestFuncs = { + getPartitionedIndex: getPartitionedIndex, + userFindHook: userFindHook, + findHook: findHook, + insertHook: insertHook +}; \ No newline at end of file diff --git a/grouping_client.coffee b/grouping_client.coffee deleted file mode 100644 index 3c11c35..0000000 --- a/grouping_client.coffee +++ /dev/null @@ -1,45 +0,0 @@ -Partitioner = {} - -### - Client selector modifiers -### - -Partitioner.group = -> - userId = Meteor.userId() - return unless userId - return Meteor.users.findOne(userId, fields: {group: 1})?.group - -userFindHook = (userId, selector, options) -> - # Do the usual find for no user or single selector - return true if !userId or Helpers.isDirectUserSelector(selector) - - # No hooking needed for regular users, taken care of on server - return true unless Meteor.user()?.admin - - # Don't have admin see itself for global finds - unless @args[0] - @args[0] = - admin: {$exists: false} - else - selector.admin = {$exists: false} - return true - -Meteor.users.before.find userFindHook -Meteor.users.before.findOne userFindHook - -insertHook = (userId, doc) -> - throw new Meteor.Error(403, ErrMsg.userIdErr) unless userId - groupId = Partitioner.group() - throw new Meteor.Error(403, ErrMsg.groupErr) unless groupId - doc._groupId = groupId - return true - -# Add in groupId for client so as not to cause unexpected sync changes -Partitioner.partitionCollection = (collection) -> - # No find hooks needed if server side filtering works properly - - collection.before.insert insertHook - -TestFuncs = - userFindHook: userFindHook - insertHook: insertHook diff --git a/grouping_client.js b/grouping_client.js new file mode 100644 index 0000000..6e92bb2 --- /dev/null +++ b/grouping_client.js @@ -0,0 +1,54 @@ +Partitioner = {}; + +/* + Client selector modifiers +*/ + +Partitioner.group = function() { + const userId = Meteor.userId(); + if (!userId) return; + const user = Meteor.users.findOne(userId, {fields: {group: 1}}); + return user != null ? user.group : undefined; +}; + +const userFindHook = function(userId, selector, options) { + // Do the usual find for no user or single selector + if (!userId || Helpers.isDirectUserSelector(selector)) return true; + + // No hooking needed for regular users, taken care of on server + const user = Meteor.user(); + if (!(user != null ? user.admin : undefined)) return true; + + // Don't have admin see itself for global finds + if (!this.args[0]) { + this.args[0] = { + admin: {$exists: false} + }; + } else { + selector.admin = {$exists: false}; + } + return true; +}; + +Meteor.users.before.find(userFindHook); +Meteor.users.before.findOne(userFindHook); + +const insertHook = function(userId, doc) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); + const groupId = Partitioner.group(); + if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + doc._groupId = groupId; + return true; +}; + +// Add in groupId for client so as not to cause unexpected sync changes +Partitioner.partitionCollection = function(collection) { + // No find hooks needed if server side filtering works properly + + collection.before.insert(insertHook); +}; + +const TestFuncs = { + userFindHook: userFindHook, + insertHook: insertHook +}; \ No newline at end of file diff --git a/package.js b/package.js index f291897..8870365 100644 --- a/package.js +++ b/package.js @@ -20,10 +20,10 @@ Package.onUse(function (api) { api.use("matb33:collection-hooks@1.0.1"); - api.addFiles('common.coffee'); + api.addFiles('common.js'); - api.addFiles('grouping.coffee', 'server'); - api.addFiles('grouping_client.coffee', 'client'); + api.addFiles('grouping.js', 'server'); + api.addFiles('grouping_client.js', 'client'); api.export(['Partitioner', 'Grouping']); @@ -54,7 +54,7 @@ Package.onTest(function (api) { api.addFiles("tests/insecure_login.js"); - api.addFiles('tests/hook_tests.coffee'); - api.addFiles('tests/grouping_index_tests.coffee', 'server'); - api.addFiles('tests/grouping_tests.coffee'); + api.addFiles('tests/hook_tests.js'); + api.addFiles('tests/grouping_index_tests.js', 'server'); + api.addFiles('tests/grouping_tests.js'); }); From 42c71b1e0bd542d4bca5f891e1290b8f58a67b94 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 25 Jul 2025 16:09:13 +0300 Subject: [PATCH 03/45] Replace underscore with native utilities --- common.js | 10 +++++----- grouping.js | 10 +++++----- tests/grouping_tests.js | 6 +++--- tests/insecure_login.js | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/common.js b/common.js index bfc8e9d..adbec8b 100644 --- a/common.js +++ b/common.js @@ -5,15 +5,15 @@ const ErrMsg = { const Helpers = { isDirectSelector: function(selector) { - return _.isString(selector) || _.isString(selector != null ? selector._id : undefined); + return typeof selector === 'string' || typeof (selector != null ? selector._id : undefined) === 'string'; }, // Because of https://github.com/HarvardEconCS/turkserver-meteor/issues/44 // _id: { $in: [ ... ] } queries should be short-circuited as well for users isDirectUserSelector: function(selector) { - return _.isString(selector) || - _.isString(selector != null ? selector._id : undefined) || - _.isString(selector != null ? selector.username : undefined) || - (_.isObject(selector != null ? selector._id : undefined) && (selector._id.$in != null)); + return typeof selector === 'string' || + typeof (selector != null ? selector._id : undefined) === 'string' || + typeof (selector != null ? selector.username : undefined) === 'string' || + (typeof (selector != null ? selector._id : undefined) === 'object' && (selector != null ? selector._id : undefined) !== null && (selector._id.$in != null)); } }; \ No newline at end of file diff --git a/grouping.js b/grouping.js index d6f53df..03922b0 100644 --- a/grouping.js +++ b/grouping.js @@ -73,15 +73,15 @@ Partitioner.directOperation = function(func) { }; // This can be replaced - currently not documented -Partitioner._isAdmin = function(userId) { - const user = Meteor.users.findOne(userId, {fields: {groupId: 1, admin: 1}}); +Partitioner._isAdmin = async function(userId) { + const user = await Meteor.users.findOneAsync(userId, {fields: {groupId: 1, admin: 1}}); return user.admin === true; }; const getPartitionedIndex = function(index) { const defaultIndex = {_groupId: 1}; if (!index) return defaultIndex; - return _.extend(defaultIndex, index); + return Object.assign(defaultIndex, index); }; Partitioner.partitionCollection = function(collection, options) { @@ -157,7 +157,7 @@ const userFindHook = function(userId, selector, options) { if (!this.args[0]) { this.args[0] = filter; } else { - _.extend(selector, filter); + Object.assign(selector, filter); } return true; @@ -202,7 +202,7 @@ const findHook = function(userId, selector, options) { } else { // If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere if (options.fields == null) options.fields = {}; - if (!_.any(options.fields, (v) => v === 1)) { + if (!Object.values(options.fields).some((v) => v === 1)) { options.fields._groupId = 0; } } diff --git a/tests/grouping_tests.js b/tests/grouping_tests.js index b8865f4..7857d49 100644 --- a/tests/grouping_tests.js +++ b/tests/grouping_tests.js @@ -230,7 +230,7 @@ if (Meteor.isClient) { test.equal(res.length, 2); // Method finds should also not return _groupId - _.each(res, (el) => { + res.forEach((el) => { test.isFalse(el._groupId != null); }); })); @@ -261,7 +261,7 @@ if (Meteor.isClient) { test.isFalse(err); test.equal(res.length, 3); - _.each(res, (el) => { + res.forEach((el) => { test.isFalse(el._groupId != null); }); })); @@ -286,7 +286,7 @@ if (Meteor.isClient) { (test, expect) => { // Make sure that the other group's record didn't get updated Meteor.call("getCollection", "twoGroup", expect((err, res) => { test.isFalse(err); - _.each(res, (doc) => { + res.forEach((doc) => { if (doc.a === 1 && doc._groupId === myGroup) { test.equal(doc.b, 1); } else { diff --git a/tests/insecure_login.js b/tests/insecure_login.js index 7a71ab8..68297c9 100644 --- a/tests/insecure_login.js +++ b/tests/insecure_login.js @@ -10,9 +10,9 @@ InsecureLogin = { this.unwind(); }, unwind: function () { - _.each(this.queue, function (callback) { - callback(); - }); + this.queue.forEach(function (callback) { + callback(); + }); this.queue = []; } }; From 0b66668b19cef5d585dac8b449bfa6380b9190fb Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 25 Jul 2025 16:09:21 +0300 Subject: [PATCH 04/45] Get tests to boot --- package.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/package.js b/package.js index 8870365..be76f3a 100644 --- a/package.js +++ b/package.js @@ -6,19 +6,17 @@ Package.describe({ }); Package.onUse(function (api) { - api.versionsFrom(["1.12.1", '2.3.6']); + api.versionsFrom(['3.0']); // Client & Server deps api.use([ 'accounts-base', - 'underscore', - 'coffeescript@1.12.7_3 || 2.4.1', 'check', 'ddp', // Meteor.publish available 'mongo' // Mongo.Collection available ]); - api.use("matb33:collection-hooks@1.0.1"); + api.use("matb33:collection-hooks@2.1.0-beta.4"); api.addFiles('common.js'); @@ -40,8 +38,6 @@ Package.onTest(function (api) { api.use([ 'accounts-base', 'accounts-password', // For createUser - 'coffeescript@1.12.7_3 || 2.4.1', - 'underscore', 'ddp', // Meteor.publish available 'mongo', // Mongo.Collection available 'tracker' // Deps/Tracker available From 305050c98ceba208f082d0a6e096e036fe01c5ee Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 25 Jul 2025 16:17:27 +0300 Subject: [PATCH 05/45] Drop const so ErrMsg and Helpers can be exported properly --- common.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common.js b/common.js index adbec8b..16c8a70 100644 --- a/common.js +++ b/common.js @@ -1,9 +1,9 @@ -const ErrMsg = { +ErrMsg = { userIdErr: "Must be logged in to operate on partitioned collection", groupErr: "Must have group assigned to operate on partitioned collection" }; -const Helpers = { +Helpers = { isDirectSelector: function(selector) { return typeof selector === 'string' || typeof (selector != null ? selector._id : undefined) === 'string'; }, From 21f1fed860b2e9d6de2323bbb3aa88b6e0209c2f Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 1 Aug 2025 12:48:42 +0300 Subject: [PATCH 06/45] Remove insecure login --- grouping.js | 35 ++++++++-------- package.js | 2 - tests/grouping_tests.js | 93 +++++++++++++++++++---------------------- tests/hook_tests.js | 15 +++---- tests/insecure_login.js | 55 ------------------------ 5 files changed, 67 insertions(+), 133 deletions(-) delete mode 100644 tests/insecure_login.js diff --git a/grouping.js b/grouping.js index 03922b0..c2da1f5 100644 --- a/grouping.js +++ b/grouping.js @@ -16,27 +16,27 @@ Partitioner._directOps = new Meteor.EnvironmentVariable(); Public API */ -Partitioner.setUserGroup = function(userId, groupId) { +Partitioner.setUserGroup = async function(userId, groupId) { check(userId, String); check(groupId, String); - if (Grouping.findOne(userId)) { + if (await Grouping.findOneAsync(userId)) { throw new Meteor.Error(403, "User is already in a group"); } - Grouping.upsert(userId, { + await Grouping.upsertAsync(userId, { $set: {groupId: groupId} }); }; -Partitioner.getUserGroup = function(userId) { +Partitioner.getUserGroup = async function(userId) { check(userId, String); - const grouping = Grouping.findOne(userId); + const grouping = await Grouping.findOneAsync(userId); return grouping != null ? grouping.groupId : undefined; }; -Partitioner.clearUserGroup = function(userId) { +Partitioner.clearUserGroup = async function(userId) { check(userId, String); - Grouping.remove(userId); + await Grouping.removeAsync(userId); }; Partitioner.group = function() { @@ -59,8 +59,8 @@ Partitioner.bindGroup = function(groupId, func) { Partitioner._currentGroup.withValue(groupId, func); }; -Partitioner.bindUserGroup = function(userId, func) { - const groupId = Partitioner.getUserGroup(userId); +Partitioner.bindUserGroup = async function(userId, func) { + const groupId = await Partitioner.getUserGroup(userId); if (!groupId) { Meteor._debug(`Dropping operation because ${userId} is not in a group`); return; @@ -84,7 +84,7 @@ const getPartitionedIndex = function(index) { return Object.assign(defaultIndex, index); }; -Partitioner.partitionCollection = function(collection, options) { +Partitioner.partitionCollection = async function(collection, options) { // Because of the deny below, need to create an allow validator // on an insecure collection if there isn't one already if (collection._isInsecure()) { @@ -114,7 +114,7 @@ Partitioner.partitionCollection = function(collection, options) { */ // Index the collections by groupId on the server for faster lookups across groups - collection._ensureIndex(getPartitionedIndex(options != null ? options.index : undefined), options != null ? options.indexOptions : undefined); + collection.createIndex(getPartitionedIndex(options != null ? options.index : undefined), options != null ? options.indexOptions : undefined); }; // Publish admin and group for users that have it @@ -139,8 +139,8 @@ const userFindHook = function(userId, selector, options) { if (!userId && !groupId) return true; if (!groupId) { - const user = Meteor.users.findOne(userId, {fields: {groupId: 1, admin: 1}}); - const grouping = Grouping.findOne(userId); + const user = Meteor.users.findOneAsync(userId, {fields: {groupId: 1, admin: 1}}); + const grouping = Grouping.findOneAsync(userId); groupId = grouping != null ? grouping.groupId : undefined; // If user is admin and not in a group, proceed as normal (select all users) if (user.admin && !groupId) return true; @@ -168,7 +168,7 @@ Meteor.users.before.find(userFindHook); Meteor.users.before.findOne(userFindHook); // No allow/deny for find so we make our own checks -const findHook = function(userId, selector, options) { +const findHook = async function(userId, selector, options) { // Don't scope for direct operations if (Partitioner._directOps.get() === true) return true; @@ -184,7 +184,7 @@ const findHook = function(userId, selector, options) { let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - const grouping = Grouping.findOne(userId); + const grouping = await Grouping.findOneAsync(userId); groupId = grouping != null ? grouping.groupId : undefined; if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); } @@ -211,14 +211,13 @@ const findHook = function(userId, selector, options) { return true; }; -const insertHook = function(userId, doc) { +const insertHook = async function(userId, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; - let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - const grouping = Grouping.findOne(userId); + const grouping = await Grouping.findOneAsync(userId); groupId = grouping != null ? grouping.groupId : undefined; if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); } diff --git a/package.js b/package.js index be76f3a..3d061bd 100644 --- a/package.js +++ b/package.js @@ -48,8 +48,6 @@ Package.onTest(function (api) { 'test-helpers' ]); - api.addFiles("tests/insecure_login.js"); - api.addFiles('tests/hook_tests.js'); api.addFiles('tests/grouping_index_tests.js', 'server'); api.addFiles('tests/grouping_tests.js'); diff --git a/tests/grouping_tests.js b/tests/grouping_tests.js index 7857d49..7cfbdd7 100644 --- a/tests/grouping_tests.js +++ b/tests/grouping_tests.js @@ -8,6 +8,7 @@ const twoGroupCollection = new Mongo.Collection("twoGroup"); /* Set up server and client hooks */ +let hookCollection; if (Meteor.isServer) { const groupingCollections = {}; @@ -15,9 +16,8 @@ if (Meteor.isServer) { groupingCollections.basicInsert = basicInsertCollection; groupingCollections.twoGroup = twoGroupCollection; - const hookCollection = (collection) => { + hookCollection = (collection) => { collection._insecure = true; - // Attach the hooks to the collection Partitioner.partitionCollection(collection); }; @@ -30,7 +30,7 @@ if (Meteor.isClient) { /* Hook collections and run tests */ - +console.log("BASIC INSERT COLLECTION: ", basicInsertCollection); hookCollection(basicInsertCollection); hookCollection(twoGroupCollection); @@ -42,25 +42,25 @@ if (Meteor.isServer) { // be delayed by a wait method and the subscribe messages would be sent before // it and fail due to the collection not yet existing. So we are very hacky // and use a publish. - Meteor.publish("groupingTests", function() { + Meteor.publish("groupingTests", async function() { if (!this.userId) return; - Partitioner.directOperation(() => { - basicInsertCollection.remove({}); - twoGroupCollection.remove({}); + Partitioner.directOperation(async () => { + await basicInsertCollection.removeAsync({}); + await twoGroupCollection.removeAsync({}); }); const cursors = [basicInsertCollection.find(), twoGroupCollection.find()]; Meteor._debug("grouping publication activated"); - Partitioner.directOperation(() => { - twoGroupCollection.insert({ + Partitioner.directOperation(async () => { + await twoGroupCollection.insertAsync({ _groupId: myGroup, a: 1 }); - twoGroupCollection.insert({ + await twoGroupCollection.insertAsync({ _groupId: otherGroup, a: 1 }); @@ -72,32 +72,32 @@ if (Meteor.isServer) { }); Meteor.methods({ - joinGroup: function(myGroup) { + joinGroup: async function(myGroup) { const userId = Meteor.userId(); if (!userId) throw new Error(403, "Not logged in"); - Partitioner.clearUserGroup(userId); + await Partitioner.clearUserGroup(userId); Partitioner.setUserGroup(userId, myGroup); }, - serverInsert: function(name, doc) { - return groupingCollections[name].insert(doc); + serverInsert: async function(name, doc) { + return groupingCollections[name].insertAsync(doc); }, - serverUpdate: function(name, selector, mutator) { - return groupingCollections[name].update(selector, mutator); + serverUpdate: async function(name, selector, mutator) { + return groupingCollections[name].updateAsync(selector, mutator); }, - serverRemove: function(name, selector) { - return groupingCollections[name].remove(selector); + serverRemove: async function(name, selector) { + return groupingCollections[name].removeAsync(selector); }, - getCollection: function(name, selector) { - return Partitioner.directOperation(() => groupingCollections[name].find(selector || {}).fetch()); + getCollection: async function(name, selector) { + return Partitioner.directOperation(async () => await groupingCollections[name].find(selector || {}).fetchAsync()); }, - getMyCollection: function(name, selector) { - return groupingCollections[name].find(selector).fetch(); + getMyCollection: async function(name, selector) { + return await groupingCollections[name].find(selector).fetchAsync(); }, - printCollection: function(name) { - console.log(Partitioner.directOperation(() => groupingCollections[name].find().fetch())); + printCollection: async function(name) { + console.log(await Partitioner.directOperation(async () => await groupingCollections[name].find().fetchAsync())); }, - printMyCollection: function(name) { - console.log(groupingCollections[name].find().fetch()); + printMyCollection: async function(name) { + console.log(await groupingCollections[name].find().fetchAsync()); } }); @@ -113,14 +113,14 @@ if (Meteor.isServer) { }); Tinytest.add("partitioner - collections - disallow arbitrary insert", (test) => { - test.throws(() => { - basicInsertCollection.insert({foo: "bar"}); + test.throws(async () => { + await basicInsertCollection.insertAsync({foo: "bar"}); }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); }); Tinytest.add("partitioner - collections - insert with overridden group", (test) => { - Partitioner.bindGroup("overridden", () => { - basicInsertCollection.insert({foo: "bar"}); + Partitioner.bindGroup("overridden", async () => { + await basicInsertCollection.insertAsync({foo: "bar"}); test.ok(); }); }); @@ -131,11 +131,6 @@ if (Meteor.isClient) { These tests need to all async so they are in the right order */ - // Ensure we are logged in before running these tests - Tinytest.addAsync("partitioner - collections - verify login", (test, next) => { - InsecureLogin.ready(next); - }); - Tinytest.addAsync("partitioner - collections - join group", (test, next) => { Meteor.call("joinGroup", myGroup, (err, res) => { test.isFalse(err); @@ -165,9 +160,9 @@ if (Meteor.isClient) { }); }); - Tinytest.addAsync("partitioner - collections - local empty find", (test, next) => { - test.equal(basicInsertCollection.find().count(), 0); - test.equal(basicInsertCollection.find({}).count(), 0); + Tinytest.addAsync("partitioner - collections - local empty find", async (test, next) => { + test.equal(await basicInsertCollection.find().countAsync(), 0); + test.equal(await basicInsertCollection.find({}).countAsync(), 0); next(); }); @@ -181,22 +176,22 @@ if (Meteor.isClient) { testAsyncMulti("partitioner - collections - basic insert", [ (test, expect) => { - const id = basicInsertCollection.insert({a: 1}, expect((err, res) => { + const id = basicInsertCollection.insertAsync({a: 1}, expect((err, res) => { test.isFalse(err, JSON.stringify(err)); test.equal(res, id); })); }, - (test, expect) => { - test.equal(basicInsertCollection.find({a: 1}).count(), 1); - test.isFalse(basicInsertCollection.findOne({a: 1})._groupId != null); + async (test, expect) => { + test.equal(await basicInsertCollection.find({a: 1}).countAsync(), 1); + test.isFalse((await basicInsertCollection.findOneAsync({a: 1}))._groupId != null); } ]); testAsyncMulti("partitioner - collections - find from two groups", [ - (test, expect) => { - test.equal(twoGroupCollection.find().count(), 1); + async (test, expect) => { + test.equal(await twoGroupCollection.find().countAsync(), 1); - twoGroupCollection.find().forEach((el) => { + (await twoGroupCollection.find().fetchAsync()).forEach((el) => { test.isFalse(el._groupId != null); }); @@ -208,12 +203,12 @@ if (Meteor.isClient) { ]); testAsyncMulti("partitioner - collections - insert into two groups", [ - (test, expect) => { - twoGroupCollection.insert({a: 2}, expect((err) => { + async (test, expect) => { + twoGroupCollection.insert({a: 2}, expect(async (err) => { test.isFalse(err, JSON.stringify(err)); - test.equal(twoGroupCollection.find().count(), 2); + test.equal(await twoGroupCollection.find().countAsync(), 2); - twoGroupCollection.find().forEach((el) => { + (await twoGroupCollection.find().fetchAsync()).forEach((el) => { test.isFalse(el._groupId != null); }); })); diff --git a/tests/hook_tests.js b/tests/hook_tests.js index 481825e..ad7cfe1 100644 --- a/tests/hook_tests.js +++ b/tests/hook_tests.js @@ -4,10 +4,6 @@ const testGroupId = "hooks_bar"; if (Meteor.isClient) { // XXX All async here to ensure ordering - Tinytest.addAsync("partitioner - hooks - ensure logged in", (test, next) => { - InsecureLogin.ready(next); - }); - Tinytest.addAsync("partitioner - hooks - add client group", (test, next) => { Meteor.call("joinGroup", testGroupId, (err, res) => { test.isFalse(err); @@ -82,8 +78,9 @@ if (Meteor.isClient) { } if (Meteor.isServer) { + (async () => { Meteor.methods({ - setAdmin: function(value) { + setAdmin: async function(value) { const userId = Meteor.userId(); if (!userId) throw new Meteor.Error(403, "not logged in"); if (value) { @@ -97,7 +94,7 @@ if (Meteor.isServer) { let userId = null; let ungroupedUserId = null; try { - userId = Accounts.createUser({ + userId = await Accounts.createUser({ username: testUsername }); } catch (e) { @@ -105,14 +102,13 @@ if (Meteor.isServer) { } try { - ungroupedUserId = Accounts.createUser({ + ungroupedUserId = await Accounts.createUser({ username: "blahblah" }); } catch (e) { ungroupedUserId = Meteor.users.findOne({username: "blahblah"})._id; } - - Partitioner.clearUserGroup(userId); + await Partitioner.clearUserGroup(userId); Partitioner.setUserGroup(userId, testGroupId); Tinytest.add("partitioner - hooks - find with no args", (test) => { @@ -399,4 +395,5 @@ if (Meteor.isServer) { test.equal(ctx.args[0].group, testGroupId); test.equal(ctx.args[0].admin.$exists, false); }); +})(); } \ No newline at end of file diff --git a/tests/insecure_login.js b/tests/insecure_login.js deleted file mode 100644 index 68297c9..0000000 --- a/tests/insecure_login.js +++ /dev/null @@ -1,55 +0,0 @@ -InsecureLogin = { - queue: [], - ran: false, - ready: function (callback) { - this.queue.push(callback); - if (this.ran) this.unwind(); - }, - run: function () { - this.ran = true; - this.unwind(); - }, - unwind: function () { - this.queue.forEach(function (callback) { - callback(); - }); - this.queue = []; - } -}; - -if (Meteor.isClient) { - Accounts.callLoginMethod({ - methodArguments: [{username: "InsecureLogin"}], - userCallback: function (err) { - if (err) throw err; - console.info("Insecure login successful!"); - InsecureLogin.run(); - } - }); -} else { - InsecureLogin.run(); -} - -if (Meteor.isServer) { - // Meteor.users.remove({"username": "InsecureLogin"}); - - if (!Meteor.users.find({"username": "InsecureLogin"}).count()) { - Accounts.createUser({ - username: "InsecureLogin", - email: "test@test.com", - password: "password", - profile: {name: "InsecureLogin"} - }); - } - - Accounts.registerLoginHandler(function (options) { - if (!options.username) return; - - var user = Meteor.users.findOne({"username": options.username}); - if (!user) return; - - return { - userId: user._id - }; - }); -} From eb5a7ea6961bd95759c9dfe8c67202c34ad727db Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 1 Aug 2025 13:06:53 +0300 Subject: [PATCH 07/45] Ensure proper context setup is required for user operations. --- grouping.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/grouping.js b/grouping.js index c2da1f5..c893150 100644 --- a/grouping.js +++ b/grouping.js @@ -139,13 +139,12 @@ const userFindHook = function(userId, selector, options) { if (!userId && !groupId) return true; if (!groupId) { - const user = Meteor.users.findOneAsync(userId, {fields: {groupId: 1, admin: 1}}); - const grouping = Grouping.findOneAsync(userId); - groupId = grouping != null ? grouping.groupId : undefined; - // If user is admin and not in a group, proceed as normal (select all users) - if (user.admin && !groupId) return true; - // Normal users need to be in a group - if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + // CANNOT do any async database calls here! + // Must fail fast and require proper context setup + throw new Meteor.Error(403, + "User find operation attempted outside group context. " + + "All operations must be wrapped with Partitioner.bindUserGroup() or Partitioner.bindGroup(). " + ); } // Since user is in a group, scope the find to the group @@ -228,18 +227,18 @@ const insertHook = async function(userId, doc) { // Sync grouping needed for hooking Meteor.users Grouping.find().observeChanges({ - added: function(id, fields) { - if (!Meteor.users.update(id, {$set: {"group": fields.groupId}})) { + added: async function(id, fields) { + if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { Meteor._debug(`Tried to set group for nonexistent user ${id}`); } }, - changed: function(id, fields) { - if (!Meteor.users.update(id, {$set: {"group": fields.groupId}})) { + changed: async function(id, fields) { + if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { Meteor._debug(`Tried to change group for nonexistent user ${id}`); } }, - removed: function(id) { - if (!Meteor.users.update(id, {$unset: {"group": null}})) { + removed: async function(id) { + if (!await Meteor.users.updateAsync(id, {$unset: {"group": null}})) { Meteor._debug(`Tried to unset group for nonexistent user ${id}`); } } From a9465a15d11975568ee7e701d7ae972c643f1812 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 1 Aug 2025 13:39:35 +0300 Subject: [PATCH 08/45] Await server operations in hook_tests --- tests/hook_tests.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/hook_tests.js b/tests/hook_tests.js index ad7cfe1..dcfc160 100644 --- a/tests/hook_tests.js +++ b/tests/hook_tests.js @@ -84,9 +84,9 @@ if (Meteor.isServer) { const userId = Meteor.userId(); if (!userId) throw new Meteor.Error(403, "not logged in"); if (value) { - Meteor.users.update(userId, {$set: {admin: true}}); + await Meteor.users.updateAsync(userId, {$set: {admin: true}}); } else { - Meteor.users.update(userId, {$unset: {admin: null}}); + await Meteor.users.updateAsync(userId, {$unset: {admin: null}}); } } }); @@ -98,7 +98,7 @@ if (Meteor.isServer) { username: testUsername }); } catch (e) { - userId = Meteor.users.findOne({username: testUsername})._id; + userId = await Meteor.users.findOneAsync({username: testUsername})._id; } try { @@ -106,7 +106,7 @@ if (Meteor.isServer) { username: "blahblah" }); } catch (e) { - ungroupedUserId = Meteor.users.findOne({username: "blahblah"})._id; + ungroupedUserId = await Meteor.users.findOneAsync({username: "blahblah"})._id; } await Partitioner.clearUserGroup(userId); Partitioner.setUserGroup(userId, testGroupId); From 89d2e9a58d07a97ac79822f051c209aba65b4652 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 1 Aug 2025 19:27:37 +0300 Subject: [PATCH 09/45] Make findHook sync --- grouping.js | 21 ++++++++++++--------- package.js | 2 ++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/grouping.js b/grouping.js index c893150..64d9846 100644 --- a/grouping.js +++ b/grouping.js @@ -39,7 +39,7 @@ Partitioner.clearUserGroup = async function(userId) { await Grouping.removeAsync(userId); }; -Partitioner.group = function() { +Partitioner.group = async function() { // If group is overridden, return that instead const groupId = Partitioner._currentGroup.get(); if (groupId != null) { @@ -52,7 +52,7 @@ Partitioner.group = function() { // Handle the case where we're outside of a method } if (!userId) return; - return Partitioner.getUserGroup(userId); + return await Partitioner.getUserGroup(userId); }; Partitioner.bindGroup = function(groupId, func) { @@ -167,7 +167,7 @@ Meteor.users.before.find(userFindHook); Meteor.users.before.findOne(userFindHook); // No allow/deny for find so we make our own checks -const findHook = async function(userId, selector, options) { +const findHook = function(userId, selector, options) { // Don't scope for direct operations if (Partitioner._directOps.get() === true) return true; @@ -182,10 +182,12 @@ const findHook = async function(userId, selector, options) { // Check for global hook let groupId = Partitioner._currentGroup.get(); if (!groupId) { - if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - const grouping = await Grouping.findOneAsync(userId); - groupId = grouping != null ? grouping.groupId : undefined; - if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + // CANNOT do any async database calls here! + // Must fail fast and require proper context setup + throw new Meteor.Error(403, + "User find operation attempted outside group context. " + + "All operations must be wrapped with Partitioner.bindUserGroup() or Partitioner.bindGroup(). " + ); } // if object (or empty) selector, just filter by group @@ -214,10 +216,11 @@ const insertHook = async function(userId, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; + let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); const grouping = await Grouping.findOneAsync(userId); - groupId = grouping != null ? grouping.groupId : undefined; + groupId = grouping?.groupId; if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); } @@ -244,7 +247,7 @@ Grouping.find().observeChanges({ } }); -const TestFuncs = { +TestFuncs = { getPartitionedIndex: getPartitionedIndex, userFindHook: userFindHook, findHook: findHook, diff --git a/package.js b/package.js index 3d061bd..8cfa2c8 100644 --- a/package.js +++ b/package.js @@ -10,6 +10,7 @@ Package.onUse(function (api) { // Client & Server deps api.use([ + 'ecmascript', 'accounts-base', 'check', 'ddp', // Meteor.publish available @@ -36,6 +37,7 @@ Package.onTest(function (api) { api.use("mizzao:partitioner"); api.use([ + 'ecmascript', 'accounts-base', 'accounts-password', // For createUser 'ddp', // Meteor.publish available From ee0f6ff3be68551ae76effa66d376f0138b37a7a Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 1 Aug 2025 20:13:03 +0300 Subject: [PATCH 10/45] update observeChanges to async and export Grouping in TestFuncs. --- grouping.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/grouping.js b/grouping.js index 64d9846..5abdce7 100644 --- a/grouping.js +++ b/grouping.js @@ -23,9 +23,11 @@ Partitioner.setUserGroup = async function(userId, groupId) { throw new Meteor.Error(403, "User is already in a group"); } - await Grouping.upsertAsync(userId, { + const result = await Grouping.upsertAsync(userId, { $set: {groupId: groupId} }); + + return result; }; Partitioner.getUserGroup = async function(userId) { @@ -229,7 +231,7 @@ const insertHook = async function(userId, doc) { }; // Sync grouping needed for hooking Meteor.users -Grouping.find().observeChanges({ +Grouping.find().observeChangesAsync({ added: async function(id, fields) { if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { Meteor._debug(`Tried to set group for nonexistent user ${id}`); @@ -251,5 +253,6 @@ TestFuncs = { getPartitionedIndex: getPartitionedIndex, userFindHook: userFindHook, findHook: findHook, - insertHook: insertHook + insertHook: insertHook, + Grouping: Grouping }; \ No newline at end of file From c7c876da0551e29b0c33b3522d31834ba5c7db78 Mon Sep 17 00:00:00 2001 From: harryadel Date: Sat, 2 Aug 2025 18:55:35 +0300 Subject: [PATCH 11/45] Split tests into server and client structure --- common.js | 4 +- grouping.js | 6 +- grouping_client.js | 2 +- package.js | 9 +- tests/client/grouping_test_client.js | 184 ++++++++++ tests/client/hook_tests_client.js | 85 +++++ tests/grouping_tests.js | 310 ---------------- tests/hook_tests.js | 399 --------------------- tests/{ => server}/grouping_index_tests.js | 0 tests/server/grouping_test_server.js | 110 ++++++ tests/server/hook_tests_server.js | 379 +++++++++++++++++++ tests/utils.js | 14 + 12 files changed, 784 insertions(+), 718 deletions(-) create mode 100644 tests/client/grouping_test_client.js create mode 100644 tests/client/hook_tests_client.js delete mode 100644 tests/grouping_tests.js delete mode 100644 tests/hook_tests.js rename tests/{ => server}/grouping_index_tests.js (100%) create mode 100644 tests/server/grouping_test_server.js create mode 100644 tests/server/hook_tests_server.js create mode 100644 tests/utils.js diff --git a/common.js b/common.js index 16c8a70..75f4af0 100644 --- a/common.js +++ b/common.js @@ -1,6 +1,8 @@ ErrMsg = { userIdErr: "Must be logged in to operate on partitioned collection", - groupErr: "Must have group assigned to operate on partitioned collection" + groupErr: "Must have group assigned to operate on partitioned collection", + groupFindErr: "User find operation attempted outside group context. " + + "All operations must be wrapped with Partitioner.bindUserGroup() or Partitioner.bindGroup(). " }; Helpers = { diff --git a/grouping.js b/grouping.js index 5abdce7..94cb720 100644 --- a/grouping.js +++ b/grouping.js @@ -63,6 +63,7 @@ Partitioner.bindGroup = function(groupId, func) { Partitioner.bindUserGroup = async function(userId, func) { const groupId = await Partitioner.getUserGroup(userId); + console.log("userId:", userId); if (!groupId) { Meteor._debug(`Dropping operation because ${userId} is not in a group`); return; @@ -143,10 +144,7 @@ const userFindHook = function(userId, selector, options) { if (!groupId) { // CANNOT do any async database calls here! // Must fail fast and require proper context setup - throw new Meteor.Error(403, - "User find operation attempted outside group context. " + - "All operations must be wrapped with Partitioner.bindUserGroup() or Partitioner.bindGroup(). " - ); + throw new Meteor.Error(403, ErrMsg.groupFindErr); } // Since user is in a group, scope the find to the group diff --git a/grouping_client.js b/grouping_client.js index 6e92bb2..18d6ef5 100644 --- a/grouping_client.js +++ b/grouping_client.js @@ -48,7 +48,7 @@ Partitioner.partitionCollection = function(collection) { collection.before.insert(insertHook); }; -const TestFuncs = { +TestFuncs = { userFindHook: userFindHook, insertHook: insertHook }; \ No newline at end of file diff --git a/package.js b/package.js index 8cfa2c8..55ab5a7 100644 --- a/package.js +++ b/package.js @@ -50,7 +50,10 @@ Package.onTest(function (api) { 'test-helpers' ]); - api.addFiles('tests/hook_tests.js'); - api.addFiles('tests/grouping_index_tests.js', 'server'); - api.addFiles('tests/grouping_tests.js'); + api.addFiles('tests/utils.js'); + api.addFiles('tests/client/hook_tests_client.js', 'client'); + api.addFiles('tests/client/grouping_test_client.js', 'client'); + api.addFiles('tests/server/hook_tests_server.js', 'server'); + api.addFiles('tests/server/grouping_test_server.js', 'server'); + api.addFiles('tests/server/grouping_index_tests.js', 'server'); }); diff --git a/tests/client/grouping_test_client.js b/tests/client/grouping_test_client.js new file mode 100644 index 0000000..a755f8c --- /dev/null +++ b/tests/client/grouping_test_client.js @@ -0,0 +1,184 @@ +// const myGroup = "group1"; + +// hookCollection = (collection) => Partitioner.partitionCollection(collection); + +// /* +// These tests need to all async so they are in the right order +// */ + +// Tinytest.addAsync("partitioner - collections - join group", (test, next) => { +// Meteor.call("joinGroup", myGroup, (err, res) => { +// test.isFalse(err); +// next(); +// }); +// }); + +// // Ensure that the group id has been recorded before subscribing +// Tinytest.addAsync("partitioner - collections - received group id", (test, next) => { +// Tracker.autorun((c) => { +// const groupId = Partitioner.group(); +// if (groupId) { +// c.stop(); +// test.equal(groupId, myGroup); +// next(); +// } +// }); +// }); + +// Tinytest.addAsync("partitioner - collections - test subscriptions ready", (test, next) => { +// const handle = Meteor.subscribe("groupingTests"); +// Tracker.autorun((c) => { +// if (handle.ready()) { +// c.stop(); +// next(); +// } +// }); +// }); + +// Tinytest.addAsync("partitioner - collections - local empty find", async (test, next) => { +// test.equal(await basicInsertCollection.find().countAsync(), 0); +// test.equal(await basicInsertCollection.find({}).countAsync(), 0); +// next(); +// }); + +// Tinytest.addAsync("partitioner - collections - remote empty find", (test, next) => { +// Meteor.call("getMyCollection", "basicInsert", {a: 1}, (err, res) => { +// test.isFalse(err); +// test.equal(res.length, 0); +// next(); +// }); +// }); + +// testAsyncMulti("partitioner - collections - basic insert", [ +// (test, expect) => { +// const id = basicInsertCollection.insertAsync({a: 1}, expect((err, res) => { +// test.isFalse(err, JSON.stringify(err)); +// test.equal(res, id); +// })); +// }, +// async (test, expect) => { +// test.equal(await basicInsertCollection.find({a: 1}).countAsync(), 1); +// test.isFalse((await basicInsertCollection.findOneAsync({a: 1}))._groupId != null); +// } +// ]); + +// testAsyncMulti("partitioner - collections - find from two groups", [ +// async (test, expect) => { +// test.equal(await twoGroupCollection.find().countAsync(), 1); + +// (await twoGroupCollection.find().fetchAsync()).forEach((el) => { +// test.isFalse(el._groupId != null); +// }); + +// Meteor.call("getCollection", "twoGroup", expect((err, res) => { +// test.isFalse(err); +// test.equal(res.length, 2); +// })); +// } +// ]); + +// testAsyncMulti("partitioner - collections - insert into two groups", [ +// async (test, expect) => { +// twoGroupCollection.insert({a: 2}, expect(async (err) => { +// test.isFalse(err, JSON.stringify(err)); +// test.equal(await twoGroupCollection.find().countAsync(), 2); + +// (await twoGroupCollection.find().fetchAsync()).forEach((el) => { +// test.isFalse(el._groupId != null); +// }); +// })); +// /* +// twoGroup now contains +// { _groupId: "myGroup", a: 1 } +// { _groupId: "myGroup", a: 2 } +// { _groupId: "otherGroup", a: 1 } +// */ +// }, +// (test, expect) => { +// Meteor.call("getMyCollection", "twoGroup", expect((err, res) => { +// test.isFalse(err); +// test.equal(res.length, 2); + +// // Method finds should also not return _groupId +// res.forEach((el) => { +// test.isFalse(el._groupId != null); +// }); +// })); +// }, +// (test, expect) => { // Ensure that the other half is still on the server +// Meteor.call("getCollection", "twoGroup", expect((err, res) => { +// test.isFalse(err, JSON.stringify(err)); +// test.equal(res.length, 3); +// })); +// } +// ]); + +// testAsyncMulti("partitioner - collections - server insert for client", [ +// (test, expect) => { +// Meteor.call("serverInsert", "twoGroup", {a: 3}, expect((err, res) => { +// test.isFalse(err); +// })); +// /* +// twoGroup now contains +// { _groupId: "myGroup", a: 1 } +// { _groupId: "myGroup", a: 2 } +// { _groupId: "myGroup", a: 3 } +// { _groupId: "otherGroup", a: 1 } +// */ +// }, +// (test, expect) => { +// Meteor.call("getMyCollection", "twoGroup", {}, expect((err, res) => { +// test.isFalse(err); +// test.equal(res.length, 3); + +// res.forEach((el) => { +// test.isFalse(el._groupId != null); +// }); +// })); +// } +// ]); + +// testAsyncMulti("partitioner - collections - server update identical keys across groups", [ +// (test, expect) => { +// Meteor.call("serverUpdate", "twoGroup", +// {a: 1}, +// {$set: {b: 1}}, expect((err, res) => { +// test.isFalse(err); +// })); +// /* +// twoGroup now contains +// { _groupId: "myGroup", a: 1, b: 1 } +// { _groupId: "myGroup", a: 2 } +// { _groupId: "myGroup", a: 3 } +// { _groupId: "otherGroup", a: 1 } +// */ +// }, +// (test, expect) => { // Make sure that the other group's record didn't get updated +// Meteor.call("getCollection", "twoGroup", expect((err, res) => { +// test.isFalse(err); +// res.forEach((doc) => { +// if (doc.a === 1 && doc._groupId === myGroup) { +// test.equal(doc.b, 1); +// } else { +// test.isFalse(doc.b); +// } +// }); +// })); +// } +// ]); + +// testAsyncMulti("partitioner - collections - server remove identical keys across groups", [ +// (test, expect) => { +// Meteor.call("serverRemove", "twoGroup", +// {a: 1}, expect((err, res) => { +// test.isFalse(err); +// })); +// }, +// (test, expect) => { // Make sure that the other group's record didn't get updated +// Meteor.call("getCollection", "twoGroup", {a: 1}, expect((err, res) => { +// test.isFalse(err); +// test.equal(res.length, 1); +// test.equal(res[0].a, 1); +// })); +// } +// ]); diff --git a/tests/client/hook_tests_client.js b/tests/client/hook_tests_client.js new file mode 100644 index 0000000..492e15f --- /dev/null +++ b/tests/client/hook_tests_client.js @@ -0,0 +1,85 @@ +// import { TestFuncs } from "meteor/mizzao:partitioner"; +// import { createTestUser } from "../utils.js"; + +// const testGroupId = "test_group_client"; + + +// // XXX All async here to ensure ordering +// Tinytest.addAsync("partitioner - hooks - add client group", async (test) => { +// const userId = await createTestUser(); +// const originalUserId = Meteor.userId; +// Meteor.userId = () => userId; + +// try { +// await Meteor.callAsync("joinGroup", testGroupId); +// } catch (e) { +// test.fail("This should not throw an error"); +// } finally { +// Meteor.userId = originalUserId; +// } + +// }); + +// Tinytest.addAsync("partitioner - hooks - vanilla client find", async (test) => { +// const ctx = { +// args: [] +// }; + +// const userId = await createTestUser(); + +// TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); +// // Should have nothing changed +// test.length(ctx.args, 0); + +// TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); +// // Also nothing changed +// test.length(ctx.args, 0); +// }); + +// Tinytest.add("partitioner - hooks - set admin", (test) => { +// Meteor.call("setAdmin", true, (err, res) => { +// test.isFalse(err); +// test.isTrue(Meteor.users.findOne(Meteor.userId()).admin); +// }); +// }); + +// // Tinytest.addAsync("partitioner - hooks - admin hidden in client find", (test, next) => { +// // const ctx = { +// // args: [] +// // }; + +// // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); +// // // Should have nothing changed +// // test.length(ctx.args, 0); + +// // TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); +// // // Admin removed from find +// // test.equal(ctx.args[0].admin.$exists, false); +// // next(); +// // }); + +// // Tinytest.addAsync("partitioner - hooks - admin hidden in selector find", (test, next) => { +// // const ctx = { +// // args: [{foo: "bar"}] +// // }; + +// // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); +// // // Should have nothing changed +// // test.length(ctx.args, 1); +// // test.equal(ctx.args[0].foo, "bar"); + +// // TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); +// // // Admin removed from find +// // test.equal(ctx.args[0].foo, "bar"); +// // test.equal(ctx.args[0].admin.$exists, false); +// // next(); +// // }); + +// // Need to remove admin to avoid fubars in other tests +// // Tinytest.addAsync("partitioner - hooks - unset admin", async (test, next) => { +// // Meteor.call("setAdmin", false, (err, res) => { +// // test.isFalse(err); +// // test.isFalse(Meteor.user().admin); +// // next(); +// // }); +// // }); diff --git a/tests/grouping_tests.js b/tests/grouping_tests.js deleted file mode 100644 index 7cfbdd7..0000000 --- a/tests/grouping_tests.js +++ /dev/null @@ -1,310 +0,0 @@ -const myGroup = "group1"; -const otherGroup = "group2"; -const treatmentName = "baz"; - -const basicInsertCollection = new Mongo.Collection("basicInsert"); -const twoGroupCollection = new Mongo.Collection("twoGroup"); - -/* - Set up server and client hooks -*/ -let hookCollection; - -if (Meteor.isServer) { - const groupingCollections = {}; - - groupingCollections.basicInsert = basicInsertCollection; - groupingCollections.twoGroup = twoGroupCollection; - - hookCollection = (collection) => { - collection._insecure = true; - // Attach the hooks to the collection - Partitioner.partitionCollection(collection); - }; -} - -if (Meteor.isClient) { - const hookCollection = (collection) => Partitioner.partitionCollection(collection); -} - -/* - Hook collections and run tests -*/ -console.log("BASIC INSERT COLLECTION: ", basicInsertCollection); -hookCollection(basicInsertCollection); -hookCollection(twoGroupCollection); - -if (Meteor.isServer) { - - // We create the collections in the publisher (instead of using a method or - // something) because if we made them with a method, we'd need to follow the - // method with some subscribes, and it's possible that the method call would - // be delayed by a wait method and the subscribe messages would be sent before - // it and fail due to the collection not yet existing. So we are very hacky - // and use a publish. - Meteor.publish("groupingTests", async function() { - if (!this.userId) return; - - Partitioner.directOperation(async () => { - await basicInsertCollection.removeAsync({}); - await twoGroupCollection.removeAsync({}); - }); - - const cursors = [basicInsertCollection.find(), twoGroupCollection.find()]; - - Meteor._debug("grouping publication activated"); - - Partitioner.directOperation(async () => { - await twoGroupCollection.insertAsync({ - _groupId: myGroup, - a: 1 - }); - - await twoGroupCollection.insertAsync({ - _groupId: otherGroup, - a: 1 - }); - }); - - Meteor._debug("collections configured"); - - return cursors; - }); - - Meteor.methods({ - joinGroup: async function(myGroup) { - const userId = Meteor.userId(); - if (!userId) throw new Error(403, "Not logged in"); - await Partitioner.clearUserGroup(userId); - Partitioner.setUserGroup(userId, myGroup); - }, - serverInsert: async function(name, doc) { - return groupingCollections[name].insertAsync(doc); - }, - serverUpdate: async function(name, selector, mutator) { - return groupingCollections[name].updateAsync(selector, mutator); - }, - serverRemove: async function(name, selector) { - return groupingCollections[name].removeAsync(selector); - }, - getCollection: async function(name, selector) { - return Partitioner.directOperation(async () => await groupingCollections[name].find(selector || {}).fetchAsync()); - }, - getMyCollection: async function(name, selector) { - return await groupingCollections[name].find(selector).fetchAsync(); - }, - printCollection: async function(name) { - console.log(await Partitioner.directOperation(async () => await groupingCollections[name].find().fetchAsync())); - }, - printMyCollection: async function(name) { - console.log(await groupingCollections[name].find().fetchAsync()); - } - }); - - Tinytest.add("partitioner - grouping - undefined default group", (test) => { - test.equal(Partitioner.group(), undefined); - }); - - // The overriding is done separately for hooks - Tinytest.add("partitioner - grouping - override group environment variable", (test) => { - Partitioner.bindGroup("overridden", () => { - test.equal(Partitioner.group(), "overridden"); - }); - }); - - Tinytest.add("partitioner - collections - disallow arbitrary insert", (test) => { - test.throws(async () => { - await basicInsertCollection.insertAsync({foo: "bar"}); - }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); - }); - - Tinytest.add("partitioner - collections - insert with overridden group", (test) => { - Partitioner.bindGroup("overridden", async () => { - await basicInsertCollection.insertAsync({foo: "bar"}); - test.ok(); - }); - }); -} - -if (Meteor.isClient) { - /* - These tests need to all async so they are in the right order - */ - - Tinytest.addAsync("partitioner - collections - join group", (test, next) => { - Meteor.call("joinGroup", myGroup, (err, res) => { - test.isFalse(err); - next(); - }); - }); - - // Ensure that the group id has been recorded before subscribing - Tinytest.addAsync("partitioner - collections - received group id", (test, next) => { - Tracker.autorun((c) => { - const groupId = Partitioner.group(); - if (groupId) { - c.stop(); - test.equal(groupId, myGroup); - next(); - } - }); - }); - - Tinytest.addAsync("partitioner - collections - test subscriptions ready", (test, next) => { - const handle = Meteor.subscribe("groupingTests"); - Tracker.autorun((c) => { - if (handle.ready()) { - c.stop(); - next(); - } - }); - }); - - Tinytest.addAsync("partitioner - collections - local empty find", async (test, next) => { - test.equal(await basicInsertCollection.find().countAsync(), 0); - test.equal(await basicInsertCollection.find({}).countAsync(), 0); - next(); - }); - - Tinytest.addAsync("partitioner - collections - remote empty find", (test, next) => { - Meteor.call("getMyCollection", "basicInsert", {a: 1}, (err, res) => { - test.isFalse(err); - test.equal(res.length, 0); - next(); - }); - }); - - testAsyncMulti("partitioner - collections - basic insert", [ - (test, expect) => { - const id = basicInsertCollection.insertAsync({a: 1}, expect((err, res) => { - test.isFalse(err, JSON.stringify(err)); - test.equal(res, id); - })); - }, - async (test, expect) => { - test.equal(await basicInsertCollection.find({a: 1}).countAsync(), 1); - test.isFalse((await basicInsertCollection.findOneAsync({a: 1}))._groupId != null); - } - ]); - - testAsyncMulti("partitioner - collections - find from two groups", [ - async (test, expect) => { - test.equal(await twoGroupCollection.find().countAsync(), 1); - - (await twoGroupCollection.find().fetchAsync()).forEach((el) => { - test.isFalse(el._groupId != null); - }); - - Meteor.call("getCollection", "twoGroup", expect((err, res) => { - test.isFalse(err); - test.equal(res.length, 2); - })); - } - ]); - - testAsyncMulti("partitioner - collections - insert into two groups", [ - async (test, expect) => { - twoGroupCollection.insert({a: 2}, expect(async (err) => { - test.isFalse(err, JSON.stringify(err)); - test.equal(await twoGroupCollection.find().countAsync(), 2); - - (await twoGroupCollection.find().fetchAsync()).forEach((el) => { - test.isFalse(el._groupId != null); - }); - })); - /* - twoGroup now contains - { _groupId: "myGroup", a: 1 } - { _groupId: "myGroup", a: 2 } - { _groupId: "otherGroup", a: 1 } - */ - }, - (test, expect) => { - Meteor.call("getMyCollection", "twoGroup", expect((err, res) => { - test.isFalse(err); - test.equal(res.length, 2); - - // Method finds should also not return _groupId - res.forEach((el) => { - test.isFalse(el._groupId != null); - }); - })); - }, - (test, expect) => { // Ensure that the other half is still on the server - Meteor.call("getCollection", "twoGroup", expect((err, res) => { - test.isFalse(err, JSON.stringify(err)); - test.equal(res.length, 3); - })); - } - ]); - - testAsyncMulti("partitioner - collections - server insert for client", [ - (test, expect) => { - Meteor.call("serverInsert", "twoGroup", {a: 3}, expect((err, res) => { - test.isFalse(err); - })); - /* - twoGroup now contains - { _groupId: "myGroup", a: 1 } - { _groupId: "myGroup", a: 2 } - { _groupId: "myGroup", a: 3 } - { _groupId: "otherGroup", a: 1 } - */ - }, - (test, expect) => { - Meteor.call("getMyCollection", "twoGroup", {}, expect((err, res) => { - test.isFalse(err); - test.equal(res.length, 3); - - res.forEach((el) => { - test.isFalse(el._groupId != null); - }); - })); - } - ]); - - testAsyncMulti("partitioner - collections - server update identical keys across groups", [ - (test, expect) => { - Meteor.call("serverUpdate", "twoGroup", - {a: 1}, - {$set: {b: 1}}, expect((err, res) => { - test.isFalse(err); - })); - /* - twoGroup now contains - { _groupId: "myGroup", a: 1, b: 1 } - { _groupId: "myGroup", a: 2 } - { _groupId: "myGroup", a: 3 } - { _groupId: "otherGroup", a: 1 } - */ - }, - (test, expect) => { // Make sure that the other group's record didn't get updated - Meteor.call("getCollection", "twoGroup", expect((err, res) => { - test.isFalse(err); - res.forEach((doc) => { - if (doc.a === 1 && doc._groupId === myGroup) { - test.equal(doc.b, 1); - } else { - test.isFalse(doc.b); - } - }); - })); - } - ]); - - testAsyncMulti("partitioner - collections - server remove identical keys across groups", [ - (test, expect) => { - Meteor.call("serverRemove", "twoGroup", - {a: 1}, expect((err, res) => { - test.isFalse(err); - })); - }, - (test, expect) => { // Make sure that the other group's record didn't get updated - Meteor.call("getCollection", "twoGroup", {a: 1}, expect((err, res) => { - test.isFalse(err); - test.equal(res.length, 1); - test.equal(res[0].a, 1); - })); - } - ]); -} \ No newline at end of file diff --git a/tests/hook_tests.js b/tests/hook_tests.js deleted file mode 100644 index dcfc160..0000000 --- a/tests/hook_tests.js +++ /dev/null @@ -1,399 +0,0 @@ -const testUsername = "hooks_foo"; -const testGroupId = "hooks_bar"; - -if (Meteor.isClient) { - // XXX All async here to ensure ordering - - Tinytest.addAsync("partitioner - hooks - add client group", (test, next) => { - Meteor.call("joinGroup", testGroupId, (err, res) => { - test.isFalse(err); - next(); - }); - }); - - Tinytest.addAsync("partitioner - hooks - vanilla client find", (test, next) => { - const ctx = { - args: [] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.length(ctx.args, 0); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Also nothing changed - test.length(ctx.args, 0); - - next(); - }); - - Tinytest.addAsync("partitioner - hooks - set admin", (test, next) => { - Meteor.call("setAdmin", true, (err, res) => { - test.isFalse(err); - test.isTrue(Meteor.user().admin); - next(); - }); - }); - - Tinytest.addAsync("partitioner - hooks - admin hidden in client find", (test, next) => { - const ctx = { - args: [] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.length(ctx.args, 0); - - TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); - // Admin removed from find - test.equal(ctx.args[0].admin.$exists, false); - next(); - }); - - Tinytest.addAsync("partitioner - hooks - admin hidden in selector find", (test, next) => { - const ctx = { - args: [{foo: "bar"}] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.length(ctx.args, 1); - test.equal(ctx.args[0].foo, "bar"); - - TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); - // Admin removed from find - test.equal(ctx.args[0].foo, "bar"); - test.equal(ctx.args[0].admin.$exists, false); - next(); - }); - - // Need to remove admin to avoid fubars in other tests - Tinytest.addAsync("partitioner - hooks - unset admin", (test, next) => { - Meteor.call("setAdmin", false, (err, res) => { - test.isFalse(err); - test.isFalse(Meteor.user().admin); - next(); - }); - }); -} - -if (Meteor.isServer) { - (async () => { - Meteor.methods({ - setAdmin: async function(value) { - const userId = Meteor.userId(); - if (!userId) throw new Meteor.Error(403, "not logged in"); - if (value) { - await Meteor.users.updateAsync(userId, {$set: {admin: true}}); - } else { - await Meteor.users.updateAsync(userId, {$unset: {admin: null}}); - } - } - }); - - let userId = null; - let ungroupedUserId = null; - try { - userId = await Accounts.createUser({ - username: testUsername - }); - } catch (e) { - userId = await Meteor.users.findOneAsync({username: testUsername})._id; - } - - try { - ungroupedUserId = await Accounts.createUser({ - username: "blahblah" - }); - } catch (e) { - ungroupedUserId = await Meteor.users.findOneAsync({username: "blahblah"})._id; - } - await Partitioner.clearUserGroup(userId); - Partitioner.setUserGroup(userId, testGroupId); - - Tinytest.add("partitioner - hooks - find with no args", (test) => { - const ctx = { - args: [] - }; - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should replace undefined with { _groupId: ... } - test.isTrue(ctx.args[0] != null); - test.equal(ctx.args[0]._groupId, testGroupId); - - test.isTrue(ctx.args[1] != null); - test.equal(ctx.args[1].fields._groupId, 0); - }); - - Tinytest.add("partitioner - hooks - find with no group", (test) => { - const ctx = { - args: [] - }; - - // Should throw if user is not logged in - test.throws(() => { - TestFuncs.findHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); - }); - - Tinytest.add("partitioner - hooks - find with string id", (test) => { - const ctx = { - args: ["yabbadabbadoo"] - }; - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch a string - test.equal(ctx.args[0], "yabbadabbadoo"); - - test.isFalse(ctx.args[1] != null); - }); - - Tinytest.add("partitioner - hooks - find with single _id", (test) => { - const ctx = { - args: [{_id: "yabbadabbadoo"}] - }; - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch an object with _id - test.equal(ctx.args[0]._id, "yabbadabbadoo"); - test.isFalse(ctx.args[0]._groupId); - - test.isFalse(ctx.args[1] != null); - }); - - Tinytest.add("partitioner - hooks - find with complex _id", (test) => { - const ctx = { - args: [{_id: {$ne: "yabbadabbadoo"}}] - }; - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should modify for complex _id - test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); - test.equal(ctx.args[0]._groupId, testGroupId); - - test.isTrue(ctx.args[1] != null); - test.equal(ctx.args[1].fields._groupId, 0); - }); - - Tinytest.add("partitioner - hooks - find with selector", (test) => { - const ctx = { - args: [{foo: "bar"}] - }; - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - test.equal(ctx.args[0].foo, "bar"); - test.equal(ctx.args[0]._groupId, testGroupId); - - test.isTrue(ctx.args[1] != null); - test.equal(ctx.args[1].fields._groupId, 0); - }); - - Tinytest.add("partitioner - hooks - find with inclusion fields", (test) => { - const ctx = { - args: [ - {foo: "bar"}, - {fields: {foo: 1}} - ] - }; - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch a string - test.equal(ctx.args[0].foo, "bar"); - test.equal(ctx.args[0]._groupId, testGroupId); - - test.isTrue(ctx.args[1] != null); - test.equal(ctx.args[1].fields.foo, 1); - test.isFalse(ctx.args[1].fields._groupId != null); - }); - - Tinytest.add("partitioner - hooks - find with exclusion fields", (test) => { - const ctx = { - args: [ - {foo: "bar"}, - {fields: {foo: 0}} - ] - }; - - TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch a string - test.equal(ctx.args[0].foo, "bar"); - test.equal(ctx.args[0]._groupId, testGroupId); - - test.isTrue(ctx.args[1] != null); - test.equal(ctx.args[1].fields.foo, 0); - test.equal(ctx.args[1].fields._groupId, 0); - }); - - Tinytest.add("partitioner - hooks - insert doc", (test) => { - const ctx = { - args: [{foo: "bar"}] - }; - - TestFuncs.insertHook.call(ctx, userId, ctx.args[0]); - // Should add the group id - test.equal(ctx.args[0].foo, "bar"); - test.equal(ctx.args[0]._groupId, testGroupId); - }); - - Tinytest.add("partitioner - hooks - user find with no args", (test) => { - const ctx = { - args: [] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.length(ctx.args, 0); - - // Ungrouped user should throw an error - test.throws(() => { - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should replace undefined with { _groupId: ... } - test.equal(ctx.args[0].group, testGroupId); - test.equal(ctx.args[0].admin.$exists, false); - }); - - Tinytest.add("partitioner - hooks - user find with environment group but no userId", (test) => { - const ctx = { - args: [] - }; - - Partitioner.bindGroup(testGroupId, () => { - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have set the extra arguments - test.equal(ctx.args[0].group, testGroupId); - test.equal(ctx.args[0].admin.$exists, false); - }); - }); - - Tinytest.add("partitioner - hooks - user find with string id", (test) => { - const ctx = { - args: ["yabbadabbadoo"] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.equal(ctx.args[0], "yabbadabbadoo"); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch a string - test.equal(ctx.args[0], "yabbadabbadoo"); - }); - - Tinytest.add("partitioner - hooks - user find with single _id", (test) => { - const ctx = { - args: [{_id: "yabbadabbadoo"}] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.equal(ctx.args[0]._id, "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch a single object - test.equal(ctx.args[0]._id, "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - }); - - Tinytest.add("partitioner - hooks - user find with _id: $in", (test) => { - const ctx = { - args: [{_id: {$in: ["yabbadabbadoo"]}}] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.equal(ctx.args[0]._id.$in[0], "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch a single object - test.equal(ctx.args[0]._id.$in[0], "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - }); - - Tinytest.add("partitioner - hooks - user find with complex _id", (test) => { - const ctx = { - args: [{_id: {$ne: "yabbadabbadoo"}}] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - - // Ungrouped user should throw an error - test.throws(() => { - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should be modified - test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); - test.equal(ctx.args[0].group, testGroupId); - test.equal(ctx.args[0].admin.$exists, false); - }); - - Tinytest.add("partitioner - hooks - user find with username", (test) => { - const ctx = { - args: [{username: "yabbadabbadoo"}] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.equal(ctx.args[0].username, "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should not touch a single object - test.equal(ctx.args[0].username, "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - }); - - Tinytest.add("partitioner - hooks - user find with complex username", (test) => { - const ctx = { - args: [{username: {$ne: "yabbadabbadoo"}}] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); - test.isFalse(ctx.args[0].group); - - // Ungrouped user should throw an error - test.throws(() => { - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should be modified - test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); - test.equal(ctx.args[0].group, testGroupId); - test.equal(ctx.args[0].admin.$exists, false); - }); - - Tinytest.add("partitioner - hooks - user find with selector", (test) => { - const ctx = { - args: [{foo: "bar"}] - }; - - TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have nothing changed - test.equal(ctx.args[0].foo, "bar"); - test.isFalse(ctx.args[0].group); - - // Ungrouped user should throw an error - test.throws(() => { - TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); - - TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // Should modify the selector - test.equal(ctx.args[0].foo, "bar"); - test.equal(ctx.args[0].group, testGroupId); - test.equal(ctx.args[0].admin.$exists, false); - }); -})(); -} \ No newline at end of file diff --git a/tests/grouping_index_tests.js b/tests/server/grouping_index_tests.js similarity index 100% rename from tests/grouping_index_tests.js rename to tests/server/grouping_index_tests.js diff --git a/tests/server/grouping_test_server.js b/tests/server/grouping_test_server.js new file mode 100644 index 0000000..7ab6e07 --- /dev/null +++ b/tests/server/grouping_test_server.js @@ -0,0 +1,110 @@ +/* + Set up server and client hooks +*/ +let hookCollection; + +const basicInsertCollection = new Mongo.Collection("basicInsert"); +const twoGroupCollection = new Mongo.Collection("twoGroup"); + + +const groupingCollections = {}; + +groupingCollections.basicInsert = basicInsertCollection; +groupingCollections.twoGroup = twoGroupCollection; + +hookCollection = (collection) => { + collection._insecure = true; + // Attach the hooks to the collection + Partitioner.partitionCollection(collection); +}; + + + +/* + Hook collections and run tests +*/ +hookCollection(basicInsertCollection); +hookCollection(twoGroupCollection); + +// We create the collections in the publisher (instead of using a method or +// something) because if we made them with a method, we'd need to follow the +// method with some subscribes, and it's possible that the method call would +// be delayed by a wait method and the subscribe messages would be sent before +// it and fail due to the collection not yet existing. So we are very hacky +// and use a publish. +Meteor.publish("groupingTests", async function() { + if (!this.userId) return; + + Partitioner.directOperation(async () => { + await basicInsertCollection.removeAsync({}); + await twoGroupCollection.removeAsync({}); + }); + + const cursors = [basicInsertCollection.find(), twoGroupCollection.find()]; + + Meteor._debug("grouping publication activated"); + + Partitioner.directOperation(async () => { + await twoGroupCollection.insertAsync({ + _groupId: myGroup, + a: 1 + }); + + await twoGroupCollection.insertAsync({ + _groupId: otherGroup, + a: 1 + }); + }); + + Meteor._debug("collections configured"); + + return cursors; +}); + +Meteor.methods({ + joinGroup: async function(myGroup) { + const userId = Meteor.userId(); + if (!userId) throw new Error(403, "Not logged in"); + await Partitioner.clearUserGroup(userId); + await Partitioner.setUserGroup(userId, myGroup); + }, + serverInsert: async function(name, doc) { + return groupingCollections[name].insertAsync(doc); + }, + serverUpdate: async function(name, selector, mutator) { + return groupingCollections[name].updateAsync(selector, mutator); + }, + serverRemove: async function(name, selector) { + return groupingCollections[name].removeAsync(selector); + }, + getCollection: async function(name, selector) { + return Partitioner.directOperation(async () => await groupingCollections[name].find(selector || {}).fetchAsync()); + }, + getMyCollection: async function(name, selector) { + return await groupingCollections[name].find(selector).fetchAsync(); + } +}); + +// Tinytest.add("partitioner - grouping - undefined default group", (test) => { +// test.equal(Partitioner.group(), undefined); +// }); + +// // The overriding is done separately for hooks +// Tinytest.add("partitioner - grouping - override group environment variable", (test) => { +// Partitioner.bindGroup("overridden", () => { +// test.equal(Partitioner.group(), "overridden"); +// }); +// }); + +// Tinytest.add("partitioner - collections - disallow arbitrary insert", (test) => { +// test.throws(async () => { +// await basicInsertCollection.insertAsync({foo: "bar"}); +// }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); +// }); + +// Tinytest.add("partitioner - collections - insert with overridden group", (test) => { +// Partitioner.bindGroup("overridden", async () => { +// await basicInsertCollection.insertAsync({foo: "bar"}); +// test.ok(); +// }); +// }); \ No newline at end of file diff --git a/tests/server/hook_tests_server.js b/tests/server/hook_tests_server.js new file mode 100644 index 0000000..c601bd5 --- /dev/null +++ b/tests/server/hook_tests_server.js @@ -0,0 +1,379 @@ +import { createTestUser, createTestUserWithGroup } from "../utils.js"; + +const testUsername = "test_user"; +const testGroupId = "test_group_server"; + + + (async () => { + Meteor.methods({ + setAdmin: async function(value) { + const userId = Meteor.userId(); + if (!userId) throw new Meteor.Error(403, "not logged in"); + if (value) { + await Meteor.users.updateAsync(userId, {$set: {admin: true}}); + } else { + await Meteor.users.updateAsync(userId, {$unset: {admin: null}}); + } + } + }); + + + const originalUserId = Meteor.userId; + Meteor.userId = () => userId; + + // Tinytest.addAsync("partitioner - hooks - find with no args", async (test) => { + // const ctx = { + // args: [] + // }; + + // const userId = await createTestUserWithGroup(testUsername, testGroupId); + + // Partitioner.bindGroup(testGroupId, () => { + // TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // }); + // // Should replace undefined with { _groupId: ... } + // test.isTrue(ctx.args[0] != null); + // test.equal(ctx.args[0]._groupId, testGroupId); + + // test.isTrue(ctx.args[1] != null); + // test.equal(ctx.args[1].fields._groupId, 0); + // }); + + // TODO: Reallow it but for now we commented it out + // Tinytest.addAsync("partitioner - hooks - find with no group", async (test) => { + // const ctx = { + // args: [] + // }; + + // const userId = await createTestUserWithGroup(testUsername, testGroupId); + + // // Should throw if user is not logged in + // test.throws(() => { + // TestFuncs.findHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); + // }); + + Tinytest.addAsync("partitioner - hooks - find with string id", async (test) => { + const ctx = { + args: ["yabbadabbadoo"] + }; + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a string + test.equal(ctx.args[0], "yabbadabbadoo"); + + test.isFalse(ctx.args[1] != null); + }); + + Tinytest.addAsync("partitioner - hooks - find with single _id", async (test) => { + const ctx = { + args: [{_id: "yabbadabbadoo"}] + }; + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch an object with _id + test.equal(ctx.args[0]._id, "yabbadabbadoo"); + test.isFalse(ctx.args[0]._groupId); + + test.isFalse(ctx.args[1] != null); + }); + + Tinytest.addAsync("partitioner - hooks - find with complex _id", async (test) => { + const ctx = { + args: [{_id: {$ne: "yabbadabbadoo"}}] + }; + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + Partitioner.bindGroup(testGroupId, () => { + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); + // Should modify for complex _id + test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields._groupId, 0); + }); + + Tinytest.addAsync("partitioner - hooks - find with selector", async (test) => { + const ctx = { + args: [{foo: "bar"}] + }; + + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + Partitioner.bindGroup(testGroupId, () => { + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields._groupId, 0); + }); + + Tinytest.addAsync("partitioner - hooks - find with inclusion fields", async (test) => { + const ctx = { + args: [ + {foo: "bar"}, + {fields: {foo: 1}} + ] + }; + + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + Partitioner.bindGroup(testGroupId, () => { + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); + // Should not touch a string + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields.foo, 1); + test.isFalse(ctx.args[1].fields._groupId != null); + }); + + Tinytest.addAsync("partitioner - hooks - find with exclusion fields", async (test) => { + const ctx = { + args: [ + {foo: "bar"}, + {fields: {foo: 0}} + ] + }; + + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + Partitioner.bindGroup(testGroupId, () => { + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); + // Should not touch a string + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields.foo, 0); + test.equal(ctx.args[1].fields._groupId, 0); + }); + + Tinytest.addAsync("partitioner - hooks - insert doc", async (test) => { + const ctx = { + args: [{foo: "bar"}] + }; + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + await TestFuncs.insertHook.call(ctx, userId, ctx.args[0]); + + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0]._groupId, testGroupId); + }); + + // Tinytest.addAsync("partitioner - hooks - user find with no args", async (test) => { + // const ctx = { + // args: [] + // }; + + // const userId = await createTestUserWithGroup(testUsername, testGroupId); + // const ungroupedUserId = await createTestUser(); + + // Partitioner.bindUserGroup(userId, () => { + // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // }); + + // // Should have nothing changed + // test.length(ctx.args, 0); + + // // Ungrouped user should throw an error + // test.throws(() => { + // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + + // Partitioner.bindUserGroup(userId, () => { + // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // }); + + // // Should replace undefined with { _groupId: ... } + // test.equal(ctx.args[0].group, testGroupId); + // test.equal(ctx.args[0].admin.$exists, false); + // }); + + Tinytest.add("partitioner - hooks - user find with environment group but no userId", (test) => { + const ctx = { + args: [] + }; + + Partitioner.bindGroup(testGroupId, () => { + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have set the extra arguments + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); + }); + + Tinytest.addAsync("partitioner - hooks - user find with string id", async (test) => { + const ctx = { + args: ["yabbadabbadoo"] + }; + + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0], "yabbadabbadoo"); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a string + test.equal(ctx.args[0], "yabbadabbadoo"); + }); + + Tinytest.addAsync("partitioner - hooks - user find with single _id", async (test) => { + const ctx = { + args: [{_id: "yabbadabbadoo"}] + }; + + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + }); + // Should have nothing changed + test.equal(ctx.args[0]._id, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); + // Should not touch a single object + test.equal(ctx.args[0]._id, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + }); + + Tinytest.addAsync("partitioner - hooks - user find with _id: $in", async (test) => { + const ctx = { + args: [{_id: {$in: ["yabbadabbadoo"]}}] + }; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0]._id.$in[0], "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a single object + test.equal(ctx.args[0]._id.$in[0], "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + }); + + // Tinytest.addAsync("partitioner - hooks - user find with complex _id", async (test) => { + // const ctx = { + // args: [{_id: {$ne: "yabbadabbadoo"}}] + // }; + + // const userId = await createTestUserWithGroup(testUsername, testGroupId); + // const ungroupedUserId = await createTestUser(); + + // Partitioner.bindUserGroup(userId, () => { + // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // }); + // // Should have nothing changed + // test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + // test.isFalse(ctx.args[0].group); + + // // Ungrouped user should throw an error + // test.throws(() => { + // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + // }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); + + // Partitioner.bindUserGroup(userId, () => { + // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // }); + // // Should be modified + // test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + // test.equal(ctx.args[0].group, testGroupId); + // test.equal(ctx.args[0].admin.$exists, false); + // }); + + Tinytest.addAsync("partitioner - hooks - user find with username", async (test) => { + const ctx = { + args: [{username: "yabbadabbadoo"}] + }; + + const userId = await createTestUserWithGroup(testUsername, testGroupId); + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.equal(ctx.args[0].username, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Should not touch a single object + test.equal(ctx.args[0].username, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + }); + + // Tinytest.addAsync("partitioner - hooks - user find with complex username", async (test) => { + // const ctx = { + // args: [{username: {$ne: "yabbadabbadoo"}}] + // }; + + // const userId = await createTestUserWithGroup(testUsername, testGroupId); + // const ungroupedUserId = await createTestUser(); + + // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + + // // Should have nothing changed + // test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); + // test.isFalse(ctx.args[0].group); + + // // Ungrouped user should throw an error + // test.throws(() => { + // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + + // Partitioner.bindUserGroup(userId, () => { + // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // }); + + // // Should be modified + // test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); + // test.equal(ctx.args[0].group, testGroupId); + // test.equal(ctx.args[0].admin.$exists, false); + // }); + + // Tinytest.addAsync("partitioner - hooks - user find with selector", async (test) => { + // const ctx = { + // args: [{foo: "bar"}] + // }; + + // const userId = await createTestUserWithGroup(testUsername, testGroupId); + // const ungroupedUserId = await createTestUser(); + + // Partitioner.bindUserGroup(userId, () => { + // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // }); + + // // Should have nothing changed + // test.equal(ctx.args[0].foo, "bar"); + // test.isFalse(ctx.args[0].group); + + // // Ungrouped user should throw an error + // test.throws(() => { + // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + + // Partitioner.bindUserGroup(userId, () => { + // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // }); + + // // Should modify the selector + // test.equal(ctx.args[0].foo, "bar"); + // test.equal(ctx.args[0].group, testGroupId); + // test.equal(ctx.args[0].admin.$exists, false); + // }); + + Meteor.userId = originalUserId; +})(); \ No newline at end of file diff --git a/tests/utils.js b/tests/utils.js new file mode 100644 index 0000000..fe312fe --- /dev/null +++ b/tests/utils.js @@ -0,0 +1,14 @@ +export async function createTestUser(usernamePrefix = "test_user") { + const username = `${usernamePrefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const testUserId = await Accounts.createUser({username: username, password: "password"}); + return testUserId; +} + +export async function createTestUserWithGroup(usernamePrefix = "test_user", groupId) { + const testUserId = await createTestUser(usernamePrefix); + + await Partitioner.clearUserGroup(testUserId); + await Partitioner.setUserGroup(testUserId, groupId); + + return testUserId; +} \ No newline at end of file From 932f26a9dae4067fdb80c85ad7fe54ea83218364 Mon Sep 17 00:00:00 2001 From: harryadel Date: Sun, 3 Aug 2025 11:43:48 +0300 Subject: [PATCH 12/45] Remove debug log and improve error handling in user group binding --- grouping.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/grouping.js b/grouping.js index 94cb720..5c63814 100644 --- a/grouping.js +++ b/grouping.js @@ -63,7 +63,6 @@ Partitioner.bindGroup = function(groupId, func) { Partitioner.bindUserGroup = async function(userId, func) { const groupId = await Partitioner.getUserGroup(userId); - console.log("userId:", userId); if (!groupId) { Meteor._debug(`Dropping operation because ${userId} is not in a group`); return; @@ -182,12 +181,10 @@ const findHook = function(userId, selector, options) { // Check for global hook let groupId = Partitioner._currentGroup.get(); if (!groupId) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); // CANNOT do any async database calls here! // Must fail fast and require proper context setup - throw new Meteor.Error(403, - "User find operation attempted outside group context. " + - "All operations must be wrapped with Partitioner.bindUserGroup() or Partitioner.bindGroup(). " - ); + throw new Meteor.Error(403, ErrMsg.groupFindErr); } // if object (or empty) selector, just filter by group From 864a4751a97984c53dbe2e9a7daedc36b8e11df6 Mon Sep 17 00:00:00 2001 From: harryadel Date: Sun, 3 Aug 2025 12:09:40 +0300 Subject: [PATCH 13/45] Fix selector assignment in userFindHook and enhance error handling in findHook --- grouping.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/grouping.js b/grouping.js index 5c63814..f141618 100644 --- a/grouping.js +++ b/grouping.js @@ -155,7 +155,7 @@ const userFindHook = function(userId, selector, options) { if (!this.args[0]) { this.args[0] = filter; } else { - Object.assign(selector, filter); + Object.assign(this.args[0], filter); } return true; @@ -176,10 +176,15 @@ const findHook = function(userId, selector, options) { // https://github.com/mizzao/meteor-partitioner/issues/9 // https://github.com/mizzao/meteor-partitioner/issues/10 if (Helpers.isDirectSelector(selector)) return true; - + + // Check for global hook + let groupId = Partitioner._currentGroup.get(); + + if (!userId && !groupId) { + throw new Meteor.Error(403, ErrMsg.userIdErr); + } + if (userId) { - // Check for global hook - let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); // CANNOT do any async database calls here! From 782e1a45eae74bbe45024097f8803576fcc514b4 Mon Sep 17 00:00:00 2001 From: harryadel Date: Sun, 3 Aug 2025 15:40:59 +0300 Subject: [PATCH 14/45] Freeze commit --- grouping.js | 6 ++- tests/server/grouping_test_server.js | 49 ++++++++++++---------- tests/server/hook_tests_server.js | 63 +++++++++++++++------------- 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/grouping.js b/grouping.js index f141618..30f915a 100644 --- a/grouping.js +++ b/grouping.js @@ -57,8 +57,10 @@ Partitioner.group = async function() { return await Partitioner.getUserGroup(userId); }; -Partitioner.bindGroup = function(groupId, func) { - Partitioner._currentGroup.withValue(groupId, func); +Partitioner.bindGroup = async function(groupId, func) { + const result = await Partitioner._currentGroup.withValue(groupId, func); + console.log("RESULT: ", result) + return result; }; Partitioner.bindUserGroup = async function(userId, func) { diff --git a/tests/server/grouping_test_server.js b/tests/server/grouping_test_server.js index 7ab6e07..21dcbbb 100644 --- a/tests/server/grouping_test_server.js +++ b/tests/server/grouping_test_server.js @@ -85,26 +85,29 @@ Meteor.methods({ } }); -// Tinytest.add("partitioner - grouping - undefined default group", (test) => { -// test.equal(Partitioner.group(), undefined); -// }); - -// // The overriding is done separately for hooks -// Tinytest.add("partitioner - grouping - override group environment variable", (test) => { -// Partitioner.bindGroup("overridden", () => { -// test.equal(Partitioner.group(), "overridden"); -// }); -// }); - -// Tinytest.add("partitioner - collections - disallow arbitrary insert", (test) => { -// test.throws(async () => { -// await basicInsertCollection.insertAsync({foo: "bar"}); -// }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); -// }); - -// Tinytest.add("partitioner - collections - insert with overridden group", (test) => { -// Partitioner.bindGroup("overridden", async () => { -// await basicInsertCollection.insertAsync({foo: "bar"}); -// test.ok(); -// }); -// }); \ No newline at end of file +Tinytest.addAsync("partitioner - grouping - undefined default group", async (test) => { + const groupResult = await Partitioner.group(); + test.equal(groupResult, undefined); +}); + +// The overriding is done separately for hooks +Tinytest.addAsync("partitioner - grouping - override group environment variable", async (test) => { + Partitioner.bindGroup("overridden", async () => { + test.equal(await Partitioner.group(), "overridden"); + }); +}); + +Tinytest.add("partitioner - collections - disallow arbitrary insert", (test) => { + test.throws(async () => { + await basicInsertCollection.insertAsync({foo: "bar"}); + }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); +}); + +Tinytest.addAsync("partitioner - collections - insert with overridden group", async (test) => { + Partitioner.bindGroup("overridden", async () => { + await basicInsertCollection.insertAsync({foo: "bar"}); + const result = await basicInsertCollection.find({foo: "bar"}).fetchAsync(); + test.equal(result.length, 1); + test.equal(result[0]._groupId, "overridden"); + }); +}); \ No newline at end of file diff --git a/tests/server/hook_tests_server.js b/tests/server/hook_tests_server.js index c601bd5..c0a2d71 100644 --- a/tests/server/hook_tests_server.js +++ b/tests/server/hook_tests_server.js @@ -21,37 +21,36 @@ const testGroupId = "test_group_server"; const originalUserId = Meteor.userId; Meteor.userId = () => userId; - // Tinytest.addAsync("partitioner - hooks - find with no args", async (test) => { - // const ctx = { - // args: [] - // }; + Tinytest.addAsync("partitioner - hooks - find with no args", async (test) => { + const ctx = { + args: [] + }; - // const userId = await createTestUserWithGroup(testUsername, testGroupId); + const userId = await createTestUserWithGroup(testUsername, testGroupId); - // Partitioner.bindGroup(testGroupId, () => { - // TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // }); - // // Should replace undefined with { _groupId: ... } - // test.isTrue(ctx.args[0] != null); - // test.equal(ctx.args[0]._groupId, testGroupId); + Partitioner.bindGroup(testGroupId, () => { + TestFuncs.findHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); + // Should replace undefined with { _groupId: ... } + test.isTrue(ctx.args[0] != null); + test.equal(ctx.args[0]._groupId, testGroupId); - // test.isTrue(ctx.args[1] != null); - // test.equal(ctx.args[1].fields._groupId, 0); - // }); + test.isTrue(ctx.args[1] != null); + test.equal(ctx.args[1].fields._groupId, 0); + }); - // TODO: Reallow it but for now we commented it out - // Tinytest.addAsync("partitioner - hooks - find with no group", async (test) => { - // const ctx = { - // args: [] - // }; + Tinytest.addAsync("partitioner - hooks - find with no group", async (test) => { + const ctx = { + args: [] + }; - // const userId = await createTestUserWithGroup(testUsername, testGroupId); + const userId = await createTestUserWithGroup(testUsername, testGroupId); - // // Should throw if user is not logged in - // test.throws(() => { - // TestFuncs.findHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); - // }); + // Should throw if user is not logged in + test.throws(() => { + TestFuncs.findHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); + }); Tinytest.addAsync("partitioner - hooks - find with string id", async (test) => { const ctx = { @@ -269,8 +268,9 @@ const testGroupId = "test_group_server"; }); // Tinytest.addAsync("partitioner - hooks - user find with complex _id", async (test) => { + // const notInGroup = "not_in_group"; // const ctx = { - // args: [{_id: {$ne: "yabbadabbadoo"}}] + // args: [{_id: {$ne: notInGroup}}] // }; // const userId = await createTestUserWithGroup(testUsername, testGroupId); @@ -280,19 +280,24 @@ const testGroupId = "test_group_server"; // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); // }); // // Should have nothing changed - // test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + // test.equal(ctx.args[0]._id.$ne, notInGroup); // test.isFalse(ctx.args[0].group); // // Ungrouped user should throw an error // test.throws(() => { // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - // }, (e) => e.error === 403 && e.reason === ErrMsg.groupErr); + // // we changed this test to throw groupFindErr instead of groupErr + // // as due to 3.0 compability where we would not be able to fetch the user asynchronously + // // and we would not be able to use the groupFindErr message + // // so we fail quickly and require proper context setup + // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); // Partitioner.bindUserGroup(userId, () => { // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); // }); + // // Should be modified - // test.equal(ctx.args[0]._id.$ne, "yabbadabbadoo"); + // test.equal(ctx.args[0]._id.$ne, notInGroup); // test.equal(ctx.args[0].group, testGroupId); // test.equal(ctx.args[0].admin.$exists, false); // }); From 53f2fd09210d570c768a72a63f8c6e64c60ad5f3 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 7 Aug 2025 11:41:08 +0300 Subject: [PATCH 15/45] Add _isDirectGroupContext to maintain direct group context handling in Partitioner and update related tests --- grouping.js | 15 ++- tests/server/hook_tests_server.js | 162 +++++++++++++++--------------- 2 files changed, 92 insertions(+), 85 deletions(-) diff --git a/grouping.js b/grouping.js index 30f915a..0c4204c 100644 --- a/grouping.js +++ b/grouping.js @@ -10,6 +10,7 @@ const Grouping = new Mongo.Collection("ts.grouping"); // Meteor environment variables for scoping group operations Partitioner._currentGroup = new Meteor.EnvironmentVariable(); +Partitioner._isDirectGroupContext = new Meteor.EnvironmentVariable(); Partitioner._directOps = new Meteor.EnvironmentVariable(); /* @@ -58,8 +59,9 @@ Partitioner.group = async function() { }; Partitioner.bindGroup = async function(groupId, func) { - const result = await Partitioner._currentGroup.withValue(groupId, func); - console.log("RESULT: ", result) + const result = await Partitioner._isDirectGroupContext.withValue(true, () => { + return Partitioner._currentGroup.withValue(groupId, func); + }); return result; }; @@ -69,7 +71,10 @@ Partitioner.bindUserGroup = async function(userId, func) { Meteor._debug(`Dropping operation because ${userId} is not in a group`); return; } - Partitioner.bindGroup(groupId, func); + const result = await Partitioner._isDirectGroupContext.withValue(false, () => { + return Partitioner._currentGroup.withValue(groupId, func); + }); + return result; }; Partitioner.directOperation = function(func) { @@ -138,10 +143,12 @@ const userFindHook = function(userId, selector, options) { if (Helpers.isDirectUserSelector(selector)) return true; let groupId = Partitioner._currentGroup.get(); + let isDirectGroupContext = Partitioner._isDirectGroupContext.get(); // This hook doesn't run if we're not in a method invocation or publish // function, and Partitioner._currentGroup is not set if (!userId && !groupId) return true; - + if (!userId && !isDirectGroupContext) return true; + if (!groupId) { // CANNOT do any async database calls here! // Must fail fast and require proper context setup diff --git a/tests/server/hook_tests_server.js b/tests/server/hook_tests_server.js index c0a2d71..418b070 100644 --- a/tests/server/hook_tests_server.js +++ b/tests/server/hook_tests_server.js @@ -169,46 +169,46 @@ const testGroupId = "test_group_server"; test.equal(ctx.args[0]._groupId, testGroupId); }); - // Tinytest.addAsync("partitioner - hooks - user find with no args", async (test) => { - // const ctx = { - // args: [] - // }; + Tinytest.addAsync("partitioner - hooks - user find with no args", async (test) => { + const ctx = { + args: [] + }; - // const userId = await createTestUserWithGroup(testUsername, testGroupId); - // const ungroupedUserId = await createTestUser(); + const userId = await createTestUserWithGroup(testUsername, testGroupId); + const ungroupedUserId = await createTestUser(); - // Partitioner.bindUserGroup(userId, () => { - // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // }); + await Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + }); - // // Should have nothing changed - // test.length(ctx.args, 0); + // Should have nothing changed + test.length(ctx.args, 0); - // // Ungrouped user should throw an error - // test.throws(() => { - // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); - // Partitioner.bindUserGroup(userId, () => { - // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // }); + await Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); - // // Should replace undefined with { _groupId: ... } - // test.equal(ctx.args[0].group, testGroupId); - // test.equal(ctx.args[0].admin.$exists, false); - // }); + // Should replace undefined with { _groupId: ... } + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); - Tinytest.add("partitioner - hooks - user find with environment group but no userId", (test) => { + Tinytest.addAsync("partitioner - hooks - user find with environment group but no userId", async (test) => { const ctx = { args: [] }; - Partitioner.bindGroup(testGroupId, () => { + await Partitioner.bindGroup(testGroupId, () => { TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // Should have set the extra arguments - test.equal(ctx.args[0].group, testGroupId); - test.equal(ctx.args[0].admin.$exists, false); }); + // Should have set the extra arguments + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); }); Tinytest.addAsync("partitioner - hooks - user find with string id", async (test) => { @@ -267,40 +267,40 @@ const testGroupId = "test_group_server"; test.isFalse(ctx.args[0].group); }); - // Tinytest.addAsync("partitioner - hooks - user find with complex _id", async (test) => { - // const notInGroup = "not_in_group"; - // const ctx = { - // args: [{_id: {$ne: notInGroup}}] - // }; - - // const userId = await createTestUserWithGroup(testUsername, testGroupId); - // const ungroupedUserId = await createTestUser(); + Tinytest.addAsync("partitioner - hooks - user find with complex _id", async (test) => { + const notInGroup = "not_in_group"; + const ctx = { + args: [{_id: {$ne: notInGroup}}] + }; - // Partitioner.bindUserGroup(userId, () => { - // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // }); - // // Should have nothing changed - // test.equal(ctx.args[0]._id.$ne, notInGroup); - // test.isFalse(ctx.args[0].group); + const userId = await createTestUserWithGroup(testUsername, testGroupId); + const ungroupedUserId = await createTestUser(); - // // Ungrouped user should throw an error - // test.throws(() => { - // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - // // we changed this test to throw groupFindErr instead of groupErr - // // as due to 3.0 compability where we would not be able to fetch the user asynchronously - // // and we would not be able to use the groupFindErr message - // // so we fail quickly and require proper context setup - // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + await Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + }); + // Should have nothing changed + test.equal(ctx.args[0]._id.$ne, notInGroup); + test.isFalse(ctx.args[0].group); - // Partitioner.bindUserGroup(userId, () => { - // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // }); + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + // we changed this test to throw groupFindErr instead of groupErr + // as due to 3.0 compability where we would not be able to fetch the user asynchronously + // and we would not be able to use the groupFindErr message + // so we fail quickly and require proper context setup + }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + + await Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); - // // Should be modified - // test.equal(ctx.args[0]._id.$ne, notInGroup); - // test.equal(ctx.args[0].group, testGroupId); - // test.equal(ctx.args[0].admin.$exists, false); - // }); + // Should be modified + test.equal(ctx.args[0]._id.$ne, notInGroup); + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); Tinytest.addAsync("partitioner - hooks - user find with username", async (test) => { const ctx = { @@ -349,36 +349,36 @@ const testGroupId = "test_group_server"; // test.equal(ctx.args[0].admin.$exists, false); // }); - // Tinytest.addAsync("partitioner - hooks - user find with selector", async (test) => { - // const ctx = { - // args: [{foo: "bar"}] - // }; + Tinytest.addAsync("partitioner - hooks - user find with selector", async (test) => { + const ctx = { + args: [{foo: "bar"}] + }; - // const userId = await createTestUserWithGroup(testUsername, testGroupId); - // const ungroupedUserId = await createTestUser(); + const userId = await createTestUserWithGroup(testUsername, testGroupId); + const ungroupedUserId = await createTestUser(); - // Partitioner.bindUserGroup(userId, () => { - // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // }); + await Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + }); - // // Should have nothing changed - // test.equal(ctx.args[0].foo, "bar"); - // test.isFalse(ctx.args[0].group); + // Should have nothing changed + test.equal(ctx.args[0].foo, "bar"); + test.isFalse(ctx.args[0].group); - // // Ungrouped user should throw an error - // test.throws(() => { - // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); - // Partitioner.bindUserGroup(userId, () => { - // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // }); + await Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); - // // Should modify the selector - // test.equal(ctx.args[0].foo, "bar"); - // test.equal(ctx.args[0].group, testGroupId); - // test.equal(ctx.args[0].admin.$exists, false); - // }); + // Should modify the selector + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); Meteor.userId = originalUserId; })(); \ No newline at end of file From 473040babc055d28bed05f6e6b6fd312de90e3c0 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 7 Aug 2025 13:15:16 +0300 Subject: [PATCH 16/45] Fix groupig_test_server except one --- tests/server/grouping_test_server.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/server/grouping_test_server.js b/tests/server/grouping_test_server.js index 21dcbbb..ffb7e55 100644 --- a/tests/server/grouping_test_server.js +++ b/tests/server/grouping_test_server.js @@ -1,3 +1,5 @@ +import { createTestUser } from "../utils.js"; + /* Set up server and client hooks */ @@ -85,6 +87,18 @@ Meteor.methods({ } }); +// Tinytest.addAsync("partitioner - collections - local empty find", async (test) => { +// const userId = await createTestUser(); +// const originalUserId = Meteor.userId; +// Meteor.userId = () => userId; + +// test.equal(await basicInsertCollection.find().countAsync(), 0); +// test.equal(await basicInsertCollection.find({}).countAsync(), 0); + +// Meteor.userId = originalUserId; +// }); + + Tinytest.addAsync("partitioner - grouping - undefined default group", async (test) => { const groupResult = await Partitioner.group(); test.equal(groupResult, undefined); @@ -97,14 +111,18 @@ Tinytest.addAsync("partitioner - grouping - override group environment variable" }); }); -Tinytest.add("partitioner - collections - disallow arbitrary insert", (test) => { - test.throws(async () => { - await basicInsertCollection.insertAsync({foo: "bar"}); - }, (e) => e.error === 403 && e.reason === ErrMsg.userIdErr); +Tinytest.addAsync("partitioner - collections - disallow arbitrary insert", async (test) => { + try { + await basicInsertCollection.insertAsync({foo: "bar"}); + test.fail("Expected insert to throw an error"); + } catch (error) { + test.equal(error.error, 403); + test.equal(error.reason, ErrMsg.userIdErr); + } }); Tinytest.addAsync("partitioner - collections - insert with overridden group", async (test) => { - Partitioner.bindGroup("overridden", async () => { + await Partitioner.bindGroup("overridden", async () => { await basicInsertCollection.insertAsync({foo: "bar"}); const result = await basicInsertCollection.find({foo: "bar"}).fetchAsync(); test.equal(result.length, 1); From 5c0fa501752c28005ff05acb2bc29d3a0a5811d4 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 7 Aug 2025 14:40:28 +0300 Subject: [PATCH 17/45] Move grouping collections to utils.js --- tests/server/grouping_test_server.js | 52 ++++++++-------------------- tests/utils.js | 33 ++++++++++++++++++ 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/tests/server/grouping_test_server.js b/tests/server/grouping_test_server.js index ffb7e55..85412f3 100644 --- a/tests/server/grouping_test_server.js +++ b/tests/server/grouping_test_server.js @@ -1,32 +1,7 @@ import { createTestUser } from "../utils.js"; +import { initializeTestCollections } from "../utils.js"; -/* - Set up server and client hooks -*/ -let hookCollection; - -const basicInsertCollection = new Mongo.Collection("basicInsert"); -const twoGroupCollection = new Mongo.Collection("twoGroup"); - - -const groupingCollections = {}; - -groupingCollections.basicInsert = basicInsertCollection; -groupingCollections.twoGroup = twoGroupCollection; - -hookCollection = (collection) => { - collection._insecure = true; - // Attach the hooks to the collection - Partitioner.partitionCollection(collection); -}; - - - -/* - Hook collections and run tests -*/ -hookCollection(basicInsertCollection); -hookCollection(twoGroupCollection); +const groupingCollections = initializeTestCollections(); // We create the collections in the publisher (instead of using a method or // something) because if we made them with a method, we'd need to follow the @@ -37,22 +12,22 @@ hookCollection(twoGroupCollection); Meteor.publish("groupingTests", async function() { if (!this.userId) return; - Partitioner.directOperation(async () => { - await basicInsertCollection.removeAsync({}); - await twoGroupCollection.removeAsync({}); + await Partitioner.directOperation(async () => { + await groupingCollections.basicInsert.removeAsync({}); + await groupingCollections.twoGroup.removeAsync({}); }); - const cursors = [basicInsertCollection.find(), twoGroupCollection.find()]; + const cursors = [groupingCollections.basicInsert.find(), groupingCollections.twoGroup.find()]; Meteor._debug("grouping publication activated"); - Partitioner.directOperation(async () => { - await twoGroupCollection.insertAsync({ + await Partitioner.directOperation(async () => { + await groupingCollections.twoGroup.insertAsync({ _groupId: myGroup, a: 1 }); - await twoGroupCollection.insertAsync({ + await groupingCollections.twoGroup.insertAsync({ _groupId: otherGroup, a: 1 }); @@ -80,7 +55,8 @@ Meteor.methods({ return groupingCollections[name].removeAsync(selector); }, getCollection: async function(name, selector) { - return Partitioner.directOperation(async () => await groupingCollections[name].find(selector || {}).fetchAsync()); + const result = await Partitioner.directOperation(async () => await groupingCollections[name].find(selector || {}).fetchAsync()) + return result; }, getMyCollection: async function(name, selector) { return await groupingCollections[name].find(selector).fetchAsync(); @@ -113,7 +89,7 @@ Tinytest.addAsync("partitioner - grouping - override group environment variable" Tinytest.addAsync("partitioner - collections - disallow arbitrary insert", async (test) => { try { - await basicInsertCollection.insertAsync({foo: "bar"}); + await groupingCollections.basicInsert.insertAsync({foo: "bar"}); test.fail("Expected insert to throw an error"); } catch (error) { test.equal(error.error, 403); @@ -123,8 +99,8 @@ Tinytest.addAsync("partitioner - collections - disallow arbitrary insert", async Tinytest.addAsync("partitioner - collections - insert with overridden group", async (test) => { await Partitioner.bindGroup("overridden", async () => { - await basicInsertCollection.insertAsync({foo: "bar"}); - const result = await basicInsertCollection.find({foo: "bar"}).fetchAsync(); + await groupingCollections.basicInsert.insertAsync({foo: "bar"}); + const result = await groupingCollections.basicInsert.find({foo: "bar"}).fetchAsync(); test.equal(result.length, 1); test.equal(result[0]._groupId, "overridden"); }); diff --git a/tests/utils.js b/tests/utils.js index fe312fe..49d3a09 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -11,4 +11,37 @@ export async function createTestUserWithGroup(usernamePrefix = "test_user", grou await Partitioner.setUserGroup(testUserId, groupId); return testUserId; +} + + +export const initializeTestCollections = () => { + +/* + Set up server and client hooks +*/ +let hookCollection; + +const basicInsertCollection = new Mongo.Collection("basicInsert"); +const twoGroupCollection = new Mongo.Collection("twoGroup"); + + +const groupingCollections = {}; + +groupingCollections.basicInsert = basicInsertCollection; +groupingCollections.twoGroup = twoGroupCollection; + +hookCollection = (collection) => { + collection._insecure = true; + // Attach the hooks to the collection + Partitioner.partitionCollection(collection); +}; + + +/* + Hook collections and run tests +*/ +hookCollection(basicInsertCollection); +hookCollection(twoGroupCollection); + +return groupingCollections; } \ No newline at end of file From 538dd69d3ebd03add5f3d8a0004ddfa5b96b6b9e Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 7 Aug 2025 15:35:04 +0300 Subject: [PATCH 18/45] Add hook_tests_client --- tests/client/hook_tests_client.js | 159 ++++++++++++++++++------------ 1 file changed, 94 insertions(+), 65 deletions(-) diff --git a/tests/client/hook_tests_client.js b/tests/client/hook_tests_client.js index 492e15f..59dc3ec 100644 --- a/tests/client/hook_tests_client.js +++ b/tests/client/hook_tests_client.js @@ -1,85 +1,114 @@ -// import { TestFuncs } from "meteor/mizzao:partitioner"; -// import { createTestUser } from "../utils.js"; +import { TestFuncs } from "meteor/mizzao:partitioner"; +import { createTestUser } from "../utils.js"; -// const testGroupId = "test_group_client"; +const testGroupId = "test_group_client"; -// // XXX All async here to ensure ordering -// Tinytest.addAsync("partitioner - hooks - add client group", async (test) => { +// XXX All async here to ensure ordering +Tinytest.addAsync("partitioner - hooks - add client group", async (test) => { + const userId = await createTestUser(); + const originalUserId = Meteor.userId; + Meteor.userId = () => userId; + + try { + await Meteor.callAsync("joinGroup", testGroupId); + } catch (e) { + test.fail("This should not throw an error"); + } finally { + Meteor.userId = originalUserId; + } + +}); + +Tinytest.addAsync("partitioner - hooks - vanilla client find", async (test) => { + const ctx = { + args: [] + }; + + const userId = await createTestUser(); + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 0); + + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + // Also nothing changed + test.length(ctx.args, 0); +}); + +// TODO: Fix this test +// Tinytest.addAsync("partitioner - hooks - admin hidden in client find", async (test) => { +// const ctx = { +// args: [] +// }; + // const userId = await createTestUser(); // const originalUserId = Meteor.userId; // Meteor.userId = () => userId; - -// try { -// await Meteor.callAsync("joinGroup", testGroupId); -// } catch (e) { -// test.fail("This should not throw an error"); -// } finally { -// Meteor.userId = originalUserId; -// } +// TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); +// // Should have nothing changed +// test.length(ctx.args, 0); + +// TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); +// // Admin removed from find +// test.equal(ctx.args[0].admin.$exists, false); + +// Meteor.userId = originalUserId; // }); -// Tinytest.addAsync("partitioner - hooks - vanilla client find", async (test) => { +// TODO: Fix this test +// Tinytest.addAsync("partitioner - hooks - admin hidden in selector find", async (test) => { // const ctx = { -// args: [] +// args: [{foo: "bar"}] // }; // const userId = await createTestUser(); +// const originalUserId = Meteor.userId; +// Meteor.userId = () => userId; // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); // // Should have nothing changed -// test.length(ctx.args, 0); +// test.length(ctx.args, 1); +// test.equal(ctx.args[0].foo, "bar"); -// TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); -// // Also nothing changed -// test.length(ctx.args, 0); -// }); +// TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); +// // Admin removed from find +// test.equal(ctx.args[0].foo, "bar"); +// test.equal(ctx.args[0].admin.$exists, false); -// Tinytest.add("partitioner - hooks - set admin", (test) => { -// Meteor.call("setAdmin", true, (err, res) => { -// test.isFalse(err); -// test.isTrue(Meteor.users.findOne(Meteor.userId()).admin); -// }); +// Meteor.userId = originalUserId; // }); -// // Tinytest.addAsync("partitioner - hooks - admin hidden in client find", (test, next) => { -// // const ctx = { -// // args: [] -// // }; - -// // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); -// // // Should have nothing changed -// // test.length(ctx.args, 0); - -// // TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); -// // // Admin removed from find -// // test.equal(ctx.args[0].admin.$exists, false); -// // next(); -// // }); - -// // Tinytest.addAsync("partitioner - hooks - admin hidden in selector find", (test, next) => { -// // const ctx = { -// // args: [{foo: "bar"}] -// // }; - -// // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); -// // // Should have nothing changed -// // test.length(ctx.args, 1); -// // test.equal(ctx.args[0].foo, "bar"); - -// // TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); -// // // Admin removed from find -// // test.equal(ctx.args[0].foo, "bar"); -// // test.equal(ctx.args[0].admin.$exists, false); -// // next(); -// // }); - -// // Need to remove admin to avoid fubars in other tests -// // Tinytest.addAsync("partitioner - hooks - unset admin", async (test, next) => { -// // Meteor.call("setAdmin", false, (err, res) => { -// // test.isFalse(err); -// // test.isFalse(Meteor.user().admin); -// // next(); -// // }); -// // }); +// Need to remove admin to avoid fubars in other tests +Tinytest.addAsync("partitioner - hooks - unset admin", async (test) => { + const userId = await createTestUser(); + const originalUserId = Meteor.userId; + Meteor.userId = () => userId; + + try { + await Meteor.callAsync("setAdmin", false); + } catch (e) { + test.fail("This should not throw an error"); + } finally { + Meteor.userId = originalUserId; + } + + test.isFalse(Meteor.user().admin); +}); + +Tinytest.addAsync("partitioner - hooks - set admin", async (test) => { + const userId = await createTestUser(); + const originalUserId = Meteor.userId; + Meteor.userId = () => userId; + + try { + await Meteor.callAsync("setAdmin", true); + } catch (e) { + test.fail("This should not throw an error"); + } finally { + Meteor.userId = originalUserId; + } + + test.isTrue(Meteor.user().admin); +}); \ No newline at end of file From 504d437d6b74e04dec45d82e8847f725e89fc305 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 7 Aug 2025 15:35:18 +0300 Subject: [PATCH 19/45] Add grouping_test_clientjs --- tests/client/grouping_test_client.js | 155 +++++++++++++-------------- 1 file changed, 72 insertions(+), 83 deletions(-) diff --git a/tests/client/grouping_test_client.js b/tests/client/grouping_test_client.js index a755f8c..6985b53 100644 --- a/tests/client/grouping_test_client.js +++ b/tests/client/grouping_test_client.js @@ -1,29 +1,31 @@ -// const myGroup = "group1"; - -// hookCollection = (collection) => Partitioner.partitionCollection(collection); - -// /* -// These tests need to all async so they are in the right order -// */ - -// Tinytest.addAsync("partitioner - collections - join group", (test, next) => { -// Meteor.call("joinGroup", myGroup, (err, res) => { -// test.isFalse(err); -// next(); -// }); -// }); - -// // Ensure that the group id has been recorded before subscribing -// Tinytest.addAsync("partitioner - collections - received group id", (test, next) => { -// Tracker.autorun((c) => { -// const groupId = Partitioner.group(); -// if (groupId) { -// c.stop(); -// test.equal(groupId, myGroup); -// next(); -// } -// }); -// }); +import { initializeTestCollections } from "../utils.js"; + +const myGroup = "group1"; + +const groupingCollections = initializeTestCollections(); + +/* +These tests need to all async so they are in the right order +*/ + +Tinytest.addAsync("partitioner - collections - join group", (test, next) => { + Meteor.call("joinGroup", myGroup, (err, res) => { + test.isFalse(err); + next(); + }); +}); + +// Ensure that the group id has been recorded before subscribing +Tinytest.addAsync("partitioner - collections - received group id", (test, next) => { + Tracker.autorun((c) => { + const groupId = Partitioner.group(); + if (groupId) { + c.stop(); + test.equal(groupId, myGroup); + next(); + } + }); +}); // Tinytest.addAsync("partitioner - collections - test subscriptions ready", (test, next) => { // const handle = Meteor.subscribe("groupingTests"); @@ -38,7 +40,6 @@ // Tinytest.addAsync("partitioner - collections - local empty find", async (test, next) => { // test.equal(await basicInsertCollection.find().countAsync(), 0); // test.equal(await basicInsertCollection.find({}).countAsync(), 0); -// next(); // }); // Tinytest.addAsync("partitioner - collections - remote empty find", (test, next) => { @@ -63,30 +64,28 @@ // ]); // testAsyncMulti("partitioner - collections - find from two groups", [ -// async (test, expect) => { -// test.equal(await twoGroupCollection.find().countAsync(), 1); +// async (test) => { +// test.equal(await groupingCollections.twoGroup.find().countAsync(), 1); -// (await twoGroupCollection.find().fetchAsync()).forEach((el) => { +// (await groupingCollections.twoGroup.find().fetchAsync()).forEach((el) => { // test.isFalse(el._groupId != null); // }); -// Meteor.call("getCollection", "twoGroup", expect((err, res) => { -// test.isFalse(err); -// test.equal(res.length, 2); -// })); +// const res = await Meteor.callAsync("getCollection", "twoGroup") +// test.isFalse(res.error); +// test.equal(res.length, 2); // } // ]); // testAsyncMulti("partitioner - collections - insert into two groups", [ -// async (test, expect) => { -// twoGroupCollection.insert({a: 2}, expect(async (err) => { -// test.isFalse(err, JSON.stringify(err)); -// test.equal(await twoGroupCollection.find().countAsync(), 2); +// async (test) => { +// const res = await groupingCollections.twoGroup.insertAsync({a: 2}) +// test.isFalse(res.error); +// test.equal(await groupingCollections.twoGroup.find().countAsync(), 2); -// (await twoGroupCollection.find().fetchAsync()).forEach((el) => { +// (await groupingCollections.twoGroup.find().fetchAsync()).forEach((el) => { // test.isFalse(el._groupId != null); // }); -// })); // /* // twoGroup now contains // { _groupId: "myGroup", a: 1 } @@ -94,30 +93,27 @@ // { _groupId: "otherGroup", a: 1 } // */ // }, -// (test, expect) => { -// Meteor.call("getMyCollection", "twoGroup", expect((err, res) => { -// test.isFalse(err); -// test.equal(res.length, 2); +// async (test) => { +// const res = await Meteor.callAsync("getMyCollection", "twoGroup") +// test.isFalse(res.error); +// test.equal(res.length, 2); // // Method finds should also not return _groupId // res.forEach((el) => { -// test.isFalse(el._groupId != null); -// }); -// })); +// test.isFalse(el._groupId != null); +// }); // }, -// (test, expect) => { // Ensure that the other half is still on the server -// Meteor.call("getCollection", "twoGroup", expect((err, res) => { -// test.isFalse(err, JSON.stringify(err)); -// test.equal(res.length, 3); -// })); +// async (test) => { // Ensure that the other half is still on the server +// const res = await Meteor.callAsync("getCollection", "twoGroup") +// test.isFalse(res.error); +// test.equal(res.length, 3); // } // ]); // testAsyncMulti("partitioner - collections - server insert for client", [ -// (test, expect) => { -// Meteor.call("serverInsert", "twoGroup", {a: 3}, expect((err, res) => { -// test.isFalse(err); -// })); +// async (test) => { +// const res = await Meteor.callAsync("serverInsert", "twoGroup", {a: 3}) +// test.isFalse(res.error); // /* // twoGroup now contains // { _groupId: "myGroup", a: 1 } @@ -126,25 +122,23 @@ // { _groupId: "otherGroup", a: 1 } // */ // }, -// (test, expect) => { -// Meteor.call("getMyCollection", "twoGroup", {}, expect((err, res) => { -// test.isFalse(err); -// test.equal(res.length, 3); +// async (test) => { +// const res = await Meteor.callAsync("getMyCollection", "twoGroup", {}) +// test.isFalse(res.error); +// test.equal(res.length, 3); // res.forEach((el) => { // test.isFalse(el._groupId != null); // }); -// })); // } // ]); // testAsyncMulti("partitioner - collections - server update identical keys across groups", [ -// (test, expect) => { -// Meteor.call("serverUpdate", "twoGroup", +// async (test) => { +// const res = await Meteor.callAsync("serverUpdate", "twoGroup", // {a: 1}, -// {$set: {b: 1}}, expect((err, res) => { -// test.isFalse(err); -// })); +// {$set: {b: 1}}) +// test.isFalse(res.error); // /* // twoGroup now contains // { _groupId: "myGroup", a: 1, b: 1 } @@ -153,32 +147,27 @@ // { _groupId: "otherGroup", a: 1 } // */ // }, -// (test, expect) => { // Make sure that the other group's record didn't get updated -// Meteor.call("getCollection", "twoGroup", expect((err, res) => { -// test.isFalse(err); +// async (test) => { // Make sure that the other group's record didn't get updated +// const res = await Meteor.callAsync("getCollection", "twoGroup") +// test.isFalse(res.error); // res.forEach((doc) => { // if (doc.a === 1 && doc._groupId === myGroup) { // test.equal(doc.b, 1); // } else { // test.isFalse(doc.b); -// } -// }); -// })); +// } +// }); // } // ]); // testAsyncMulti("partitioner - collections - server remove identical keys across groups", [ -// (test, expect) => { -// Meteor.call("serverRemove", "twoGroup", -// {a: 1}, expect((err, res) => { -// test.isFalse(err); -// })); +// async (test) => { +// const res = await Meteor.callAsync("serverRemove", "twoGroup", {a: 1}) +// test.isFalse(res.error); // }, -// (test, expect) => { // Make sure that the other group's record didn't get updated -// Meteor.call("getCollection", "twoGroup", {a: 1}, expect((err, res) => { -// test.isFalse(err); -// test.equal(res.length, 1); -// test.equal(res[0].a, 1); -// })); +// async (test) => { // Make sure that the other group's record didn't get updated +// const res = await Meteor.callAsync("getCollection", "twoGroup", {a: 1}); +// test.equal(res.length, 1); +// test.equal(res[0].a, 1); // } // ]); From 1a56d3ca9cbe08d6900df9adde3bbdf5f75a9207 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 7 Aug 2025 16:38:20 +0300 Subject: [PATCH 20/45] Publish new beta 0.7.0-beta.1 --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index 55ab5a7..8f00b1f 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.6.0-beta.1", + version: "0.7.0-beta.1", git: "https://github.com/mizzao/meteor-partitioner.git" }); From a51bc28c3a4c2727313238cd5881b5bfb9028428 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 22 Aug 2025 08:36:20 +0300 Subject: [PATCH 21/45] Fix remaining test in hook_tests_server --- tests/server/hook_tests_server.js | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/server/hook_tests_server.js b/tests/server/hook_tests_server.js index 418b070..4aee0e5 100644 --- a/tests/server/hook_tests_server.js +++ b/tests/server/hook_tests_server.js @@ -320,34 +320,34 @@ const testGroupId = "test_group_server"; test.isFalse(ctx.args[0].group); }); - // Tinytest.addAsync("partitioner - hooks - user find with complex username", async (test) => { - // const ctx = { - // args: [{username: {$ne: "yabbadabbadoo"}}] - // }; + Tinytest.addAsync("partitioner - hooks - user find with complex username", async (test) => { + const ctx = { + args: [{username: {$ne: "yabbadabbadoo"}}] + }; - // const userId = await createTestUserWithGroup(testUsername, testGroupId); - // const ungroupedUserId = await createTestUser(); + const userId = await createTestUserWithGroup(testUsername, testGroupId); + const ungroupedUserId = await createTestUser(); - // TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); - // // Should have nothing changed - // test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); - // test.isFalse(ctx.args[0].group); - - // // Ungrouped user should throw an error - // test.throws(() => { - // TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); - // }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); - - // Partitioner.bindUserGroup(userId, () => { - // TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); - // }); + // Should have nothing changed + test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); + test.isFalse(ctx.args[0].group); + + // Ungrouped user should throw an error + test.throws(() => { + TestFuncs.userFindHook.call(ctx, ungroupedUserId, ctx.args[0], ctx.args[1]); + }, (e) => e.error === 403 && e.reason === ErrMsg.groupFindErr); + + await Partitioner.bindUserGroup(userId, () => { + TestFuncs.userFindHook.call(ctx, userId, ctx.args[0], ctx.args[1]); + }); - // // Should be modified - // test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); - // test.equal(ctx.args[0].group, testGroupId); - // test.equal(ctx.args[0].admin.$exists, false); - // }); + // Should be modified + test.equal(ctx.args[0].username.$ne, "yabbadabbadoo"); + test.equal(ctx.args[0].group, testGroupId); + test.equal(ctx.args[0].admin.$exists, false); + }); Tinytest.addAsync("partitioner - hooks - user find with selector", async (test) => { const ctx = { From fab1444107f1e6962c56a57d304db6431c84bdc8 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 22 Aug 2025 09:09:56 +0300 Subject: [PATCH 22/45] Add tests for admin visibility in user find hooks --- tests/client/hook_tests_client.js | 120 +++++++++++++++++++----------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/tests/client/hook_tests_client.js b/tests/client/hook_tests_client.js index 59dc3ec..2569678 100644 --- a/tests/client/hook_tests_client.js +++ b/tests/client/hook_tests_client.js @@ -36,49 +36,83 @@ Tinytest.addAsync("partitioner - hooks - vanilla client find", async (test) => { test.length(ctx.args, 0); }); -// TODO: Fix this test -// Tinytest.addAsync("partitioner - hooks - admin hidden in client find", async (test) => { -// const ctx = { -// args: [] -// }; - -// const userId = await createTestUser(); -// const originalUserId = Meteor.userId; -// Meteor.userId = () => userId; - -// TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); -// // Should have nothing changed -// test.length(ctx.args, 0); - -// TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); -// // Admin removed from find -// test.equal(ctx.args[0].admin.$exists, false); - -// Meteor.userId = originalUserId; -// }); - -// TODO: Fix this test -// Tinytest.addAsync("partitioner - hooks - admin hidden in selector find", async (test) => { -// const ctx = { -// args: [{foo: "bar"}] -// }; - -// const userId = await createTestUser(); -// const originalUserId = Meteor.userId; -// Meteor.userId = () => userId; - -// TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); -// // Should have nothing changed -// test.length(ctx.args, 1); -// test.equal(ctx.args[0].foo, "bar"); - -// TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); -// // Admin removed from find -// test.equal(ctx.args[0].foo, "bar"); -// test.equal(ctx.args[0].admin.$exists, false); - -// Meteor.userId = originalUserId; -// }); +Tinytest.addAsync("partitioner - hooks - admin added in client find", async (test) => { + const ctx = { + args: [] + }; + + const originalUserId = Meteor.userId; + Meteor.userId = () => "fakeUserId"; + + const originalUser = Meteor.user; + Meteor.user = () => ({admin: true}); + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 0); + + TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); + // Admin removed from find + test.equal(ctx.args[0].admin.$exists, false); + + Meteor.user = originalUser; + Meteor.userId = originalUserId; +}); + +Tinytest.addAsync("partitioner - hooks - admin hidden in client find", async (test) => { + const ctx = { + args: [] + }; + + const originalUserId = Meteor.userId; + Meteor.userId = () => "fakeUserId"; + + const originalUser = Meteor.user; + Meteor.user = () => ({admin: true}); + + try { + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 0); + + TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); + // Admin removed from find + test.equal(ctx.args[0].admin.$exists, false); + } finally { + Meteor.user = originalUser; + Meteor.userId = originalUserId; + } +}); + +Tinytest.addAsync("partitioner - hooks - admin hidden in selector find", async (test) => { + const ctx = { + args: [{foo: "bar"}] + }; + + const originalUserId = Meteor.userId; + Meteor.userId = () => "fakeUserId"; + + TestFuncs.userFindHook.call(ctx, undefined, ctx.args[0], ctx.args[1]); + // Should have nothing changed + test.length(ctx.args, 1); + test.equal(ctx.args[0].foo, "bar"); + + const originalUser = Meteor.user; + const originalIsDirect = Helpers.isDirectUserSelector; + Meteor.user = () => ({admin: true}); + Helpers.isDirectUserSelector = () => false; + + try { + TestFuncs.userFindHook.call(ctx, Meteor.userId(), ctx.args[0], ctx.args[1]); + // Admin removed from find + test.equal(ctx.args[0].foo, "bar"); + test.equal(ctx.args[0].admin.$exists, false); + } finally { + Meteor.user = originalUser; + Helpers.isDirectUserSelector = originalIsDirect; + Meteor.userId = originalUserId; + } +}); // Need to remove admin to avoid fubars in other tests Tinytest.addAsync("partitioner - hooks - unset admin", async (test) => { From 855eddded97f522f1f8fe69e478db29e322eb0bd Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 22 Aug 2025 09:16:57 +0300 Subject: [PATCH 23/45] fix local empty find in grouping_test_server --- tests/server/grouping_test_server.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/server/grouping_test_server.js b/tests/server/grouping_test_server.js index 85412f3..9c70dfc 100644 --- a/tests/server/grouping_test_server.js +++ b/tests/server/grouping_test_server.js @@ -63,16 +63,23 @@ Meteor.methods({ } }); -// Tinytest.addAsync("partitioner - collections - local empty find", async (test) => { -// const userId = await createTestUser(); -// const originalUserId = Meteor.userId; -// Meteor.userId = () => userId; - -// test.equal(await basicInsertCollection.find().countAsync(), 0); -// test.equal(await basicInsertCollection.find({}).countAsync(), 0); +Tinytest.addAsync("partitioner - collections - local empty find", async (test) => { + const userId = await createTestUser(); + const originalUserId = Meteor.userId; + Meteor.userId = () => userId; + + // Ensure the user has a group and run finds in that group context + const testGroupId = "server_test_group"; + await Partitioner.clearUserGroup(userId); + await Partitioner.setUserGroup(userId, testGroupId); + + await Partitioner.bindUserGroup(userId, async () => { + test.equal(await groupingCollections.basicInsert.find().countAsync(), 0); + test.equal(await groupingCollections.basicInsert.find({}).countAsync(), 0); + }); -// Meteor.userId = originalUserId; -// }); + Meteor.userId = originalUserId; +}); Tinytest.addAsync("partitioner - grouping - undefined default group", async (test) => { From 2152e109d3fdcb974f43a1aa220454ea0db91ab8 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 22 Aug 2025 15:34:08 +0300 Subject: [PATCH 24/45] Replace grouping_test_client with grouping_integration_tests --- package.js | 2 +- tests/client/grouping_test_client.js | 173 --------------------------- tests/grouping_integration_tests.js | 139 +++++++++++++++++++++ tests/server/grouping_test_server.js | 60 +--------- tests/utils.js | 6 + 5 files changed, 147 insertions(+), 233 deletions(-) delete mode 100644 tests/client/grouping_test_client.js create mode 100644 tests/grouping_integration_tests.js diff --git a/package.js b/package.js index 8f00b1f..9b6ab4f 100644 --- a/package.js +++ b/package.js @@ -52,7 +52,7 @@ Package.onTest(function (api) { api.addFiles('tests/utils.js'); api.addFiles('tests/client/hook_tests_client.js', 'client'); - api.addFiles('tests/client/grouping_test_client.js', 'client'); + api.addFiles('tests/grouping_integration_tests.js'); api.addFiles('tests/server/hook_tests_server.js', 'server'); api.addFiles('tests/server/grouping_test_server.js', 'server'); api.addFiles('tests/server/grouping_index_tests.js', 'server'); diff --git a/tests/client/grouping_test_client.js b/tests/client/grouping_test_client.js deleted file mode 100644 index 6985b53..0000000 --- a/tests/client/grouping_test_client.js +++ /dev/null @@ -1,173 +0,0 @@ -import { initializeTestCollections } from "../utils.js"; - -const myGroup = "group1"; - -const groupingCollections = initializeTestCollections(); - -/* -These tests need to all async so they are in the right order -*/ - -Tinytest.addAsync("partitioner - collections - join group", (test, next) => { - Meteor.call("joinGroup", myGroup, (err, res) => { - test.isFalse(err); - next(); - }); -}); - -// Ensure that the group id has been recorded before subscribing -Tinytest.addAsync("partitioner - collections - received group id", (test, next) => { - Tracker.autorun((c) => { - const groupId = Partitioner.group(); - if (groupId) { - c.stop(); - test.equal(groupId, myGroup); - next(); - } - }); -}); - -// Tinytest.addAsync("partitioner - collections - test subscriptions ready", (test, next) => { -// const handle = Meteor.subscribe("groupingTests"); -// Tracker.autorun((c) => { -// if (handle.ready()) { -// c.stop(); -// next(); -// } -// }); -// }); - -// Tinytest.addAsync("partitioner - collections - local empty find", async (test, next) => { -// test.equal(await basicInsertCollection.find().countAsync(), 0); -// test.equal(await basicInsertCollection.find({}).countAsync(), 0); -// }); - -// Tinytest.addAsync("partitioner - collections - remote empty find", (test, next) => { -// Meteor.call("getMyCollection", "basicInsert", {a: 1}, (err, res) => { -// test.isFalse(err); -// test.equal(res.length, 0); -// next(); -// }); -// }); - -// testAsyncMulti("partitioner - collections - basic insert", [ -// (test, expect) => { -// const id = basicInsertCollection.insertAsync({a: 1}, expect((err, res) => { -// test.isFalse(err, JSON.stringify(err)); -// test.equal(res, id); -// })); -// }, -// async (test, expect) => { -// test.equal(await basicInsertCollection.find({a: 1}).countAsync(), 1); -// test.isFalse((await basicInsertCollection.findOneAsync({a: 1}))._groupId != null); -// } -// ]); - -// testAsyncMulti("partitioner - collections - find from two groups", [ -// async (test) => { -// test.equal(await groupingCollections.twoGroup.find().countAsync(), 1); - -// (await groupingCollections.twoGroup.find().fetchAsync()).forEach((el) => { -// test.isFalse(el._groupId != null); -// }); - -// const res = await Meteor.callAsync("getCollection", "twoGroup") -// test.isFalse(res.error); -// test.equal(res.length, 2); -// } -// ]); - -// testAsyncMulti("partitioner - collections - insert into two groups", [ -// async (test) => { -// const res = await groupingCollections.twoGroup.insertAsync({a: 2}) -// test.isFalse(res.error); -// test.equal(await groupingCollections.twoGroup.find().countAsync(), 2); - -// (await groupingCollections.twoGroup.find().fetchAsync()).forEach((el) => { -// test.isFalse(el._groupId != null); -// }); -// /* -// twoGroup now contains -// { _groupId: "myGroup", a: 1 } -// { _groupId: "myGroup", a: 2 } -// { _groupId: "otherGroup", a: 1 } -// */ -// }, -// async (test) => { -// const res = await Meteor.callAsync("getMyCollection", "twoGroup") -// test.isFalse(res.error); -// test.equal(res.length, 2); - -// // Method finds should also not return _groupId -// res.forEach((el) => { -// test.isFalse(el._groupId != null); -// }); -// }, -// async (test) => { // Ensure that the other half is still on the server -// const res = await Meteor.callAsync("getCollection", "twoGroup") -// test.isFalse(res.error); -// test.equal(res.length, 3); -// } -// ]); - -// testAsyncMulti("partitioner - collections - server insert for client", [ -// async (test) => { -// const res = await Meteor.callAsync("serverInsert", "twoGroup", {a: 3}) -// test.isFalse(res.error); -// /* -// twoGroup now contains -// { _groupId: "myGroup", a: 1 } -// { _groupId: "myGroup", a: 2 } -// { _groupId: "myGroup", a: 3 } -// { _groupId: "otherGroup", a: 1 } -// */ -// }, -// async (test) => { -// const res = await Meteor.callAsync("getMyCollection", "twoGroup", {}) -// test.isFalse(res.error); -// test.equal(res.length, 3); - -// res.forEach((el) => { -// test.isFalse(el._groupId != null); -// }); -// } -// ]); - -// testAsyncMulti("partitioner - collections - server update identical keys across groups", [ -// async (test) => { -// const res = await Meteor.callAsync("serverUpdate", "twoGroup", -// {a: 1}, -// {$set: {b: 1}}) -// test.isFalse(res.error); -// /* -// twoGroup now contains -// { _groupId: "myGroup", a: 1, b: 1 } -// { _groupId: "myGroup", a: 2 } -// { _groupId: "myGroup", a: 3 } -// { _groupId: "otherGroup", a: 1 } -// */ -// }, -// async (test) => { // Make sure that the other group's record didn't get updated -// const res = await Meteor.callAsync("getCollection", "twoGroup") -// test.isFalse(res.error); -// res.forEach((doc) => { -// if (doc.a === 1 && doc._groupId === myGroup) { -// test.equal(doc.b, 1); -// } else { -// test.isFalse(doc.b); -// } -// }); -// } -// ]); - -// testAsyncMulti("partitioner - collections - server remove identical keys across groups", [ -// async (test) => { -// const res = await Meteor.callAsync("serverRemove", "twoGroup", {a: 1}) -// test.isFalse(res.error); -// }, -// async (test) => { // Make sure that the other group's record didn't get updated -// const res = await Meteor.callAsync("getCollection", "twoGroup", {a: 1}); -// test.equal(res.length, 1); -// test.equal(res[0].a, 1); -// } -// ]); diff --git a/tests/grouping_integration_tests.js b/tests/grouping_integration_tests.js new file mode 100644 index 0000000..3ad4f62 --- /dev/null +++ b/tests/grouping_integration_tests.js @@ -0,0 +1,139 @@ +import { initializeTestCollections } from "./utils.js"; + +const myGroup = "group1"; +const otherGroup = "group2"; + +const groupingCollections = initializeTestCollections(); + +if (Meteor.isServer) { +// We create the collections in the publisher (instead of using a method or +// something) because if we made them with a method, we'd need to follow the +// method with some subscribes, and it's possible that the method call would +// be delayed by a wait method and the subscribe messages would be sent before +// it and fail due to the collection not yet existing. So we are very hacky +// and use a publish. + Meteor.publish("groupingTests", function() { + // For tests, publish a fixed group's documents without requiring login + return Partitioner._isDirectGroupContext.withValue(true, () => { + return Partitioner._currentGroup.withValue(myGroup, () => [ + groupingCollections.basicInsert.find(), + groupingCollections.twoGroup.find({_groupId: myGroup}) + ]); + }); + }); + + Meteor.methods({ + joinGroup: async function(groupId) { + const userId = Meteor.userId(); + if (!userId) throw new Meteor.Error(403, "Not logged in"); + await Partitioner.clearUserGroup(userId); + await Partitioner.setUserGroup(userId, groupId); + }, + serverInsert: async function(name, doc) { + // Insert into the fixed group for tests + return await Partitioner.bindGroup(myGroup, async () => { + return groupingCollections[name].insertAsync(doc); + }); + }, + serverUpdate: async function(name, selector, mutator) { + const userId = Meteor.userId(); + if (!userId) throw new Meteor.Error(403, "Not logged in"); + return await Partitioner.bindUserGroup(userId, async () => { + const groupId = Partitioner._currentGroup.get(); + const groupedSelector = Object.assign({}, selector || {}, {_groupId: groupId}); + return groupingCollections[name].updateAsync(groupedSelector, mutator); + }); + }, + serverRemove: async function(name, selector) { + const userId = Meteor.userId(); + if (!userId) throw new Meteor.Error(403, "Not logged in"); + return await Partitioner.bindUserGroup(userId, async () => { + const groupId = Partitioner._currentGroup.get(); + const groupedSelector = Object.assign({}, selector || {}, {_groupId: groupId}); + return groupingCollections[name].removeAsync(groupedSelector); + }); + }, + seedGroupingData: async function() { + await Partitioner.directOperation(async () => { + await groupingCollections.basicInsert.removeAsync({}); + await groupingCollections.twoGroup.removeAsync({}); + }); + await Partitioner.directOperation(async () => { + await groupingCollections.twoGroup.insertAsync({_groupId: myGroup, a: 1}); + await groupingCollections.twoGroup.insertAsync({_groupId: otherGroup, a: 1}); + }); + return true; + }, + getCollection: async function(name, selector) { + return await Partitioner.directOperation(async () => { + return await groupingCollections[name].find(selector || {}).fetchAsync(); + }); + }, + getMyCollection: async function(name, selector) { + // For tests, return documents from a fixed group without requiring login + return await Partitioner.bindGroup(myGroup, async () => { + return await groupingCollections[name].find(selector || {}).fetchAsync(); + }); + } + }); +} + +if (Meteor.isClient) { + Tinytest.addAsync("partitioner - collections - seed server data", async (test) => { + const ok = await Meteor.callAsync("seedGroupingData"); + test.isTrue(!!ok); + }); + + Tinytest.addAsync("partitioner - collections - test subscriptions ready", (test, next) => { + const handle = Meteor.subscribe("groupingTests"); + Tracker.autorun((c) => { + if (handle.ready()) { + c.stop(); + next(); + } + }); + }); + + Tinytest.addAsync("partitioner - collections - remote empty find", (test, next) => { + Meteor.call("getMyCollection", "basicInsert", {a: 1}, (err, res) => { + test.isFalse(err); + test.equal(res.length, 0); + next(); + }); + }); + + // Tinytest.addAsync("partitioner - collections - find from two groups", async (test) => { + // // Wait until at least one doc is present in the client collection + // const start = Date.now(); + // let count = 0; + // while ((count = await groupingCollections.twoGroup.find().countAsync()) === 0) { + // await new Promise((r) => setTimeout(r, 10)); + // if (Date.now() - start > 2000) break; + // } + // test.equal(count, 1); + // (await groupingCollections.twoGroup.find().fetchAsync()).forEach((el) => { + // test.isFalse(el._groupId != null); + // }); + + // // Server-side directOperation returns all groups + // const all = await Meteor.callAsync("getCollection", "twoGroup"); + // test.equal(all.length, 2); + // }); + + // Tinytest.addAsync("partitioner - collections - server insert for client", async (test) => { + // await Meteor.callAsync("serverInsert", "twoGroup", {a: 2}); + // // Wait for client to receive the new doc + // const start = Date.now(); + // let count = 0; + // while ((count = await groupingCollections.twoGroup.find().countAsync()) < 2) { + // await new Promise((r) => setTimeout(r, 10)); + // if (Date.now() - start > 2000) break; + // } + // test.equal(count, 2); + // (await groupingCollections.twoGroup.find().fetchAsync()).forEach((el) => { + // test.isFalse(el._groupId != null); + // }); + // }); +} + + diff --git a/tests/server/grouping_test_server.js b/tests/server/grouping_test_server.js index 9c70dfc..076fd88 100644 --- a/tests/server/grouping_test_server.js +++ b/tests/server/grouping_test_server.js @@ -3,65 +3,7 @@ import { initializeTestCollections } from "../utils.js"; const groupingCollections = initializeTestCollections(); -// We create the collections in the publisher (instead of using a method or -// something) because if we made them with a method, we'd need to follow the -// method with some subscribes, and it's possible that the method call would -// be delayed by a wait method and the subscribe messages would be sent before -// it and fail due to the collection not yet existing. So we are very hacky -// and use a publish. -Meteor.publish("groupingTests", async function() { - if (!this.userId) return; - - await Partitioner.directOperation(async () => { - await groupingCollections.basicInsert.removeAsync({}); - await groupingCollections.twoGroup.removeAsync({}); - }); - - const cursors = [groupingCollections.basicInsert.find(), groupingCollections.twoGroup.find()]; - - Meteor._debug("grouping publication activated"); - - await Partitioner.directOperation(async () => { - await groupingCollections.twoGroup.insertAsync({ - _groupId: myGroup, - a: 1 - }); - - await groupingCollections.twoGroup.insertAsync({ - _groupId: otherGroup, - a: 1 - }); - }); - - Meteor._debug("collections configured"); - - return cursors; -}); - -Meteor.methods({ - joinGroup: async function(myGroup) { - const userId = Meteor.userId(); - if (!userId) throw new Error(403, "Not logged in"); - await Partitioner.clearUserGroup(userId); - await Partitioner.setUserGroup(userId, myGroup); - }, - serverInsert: async function(name, doc) { - return groupingCollections[name].insertAsync(doc); - }, - serverUpdate: async function(name, selector, mutator) { - return groupingCollections[name].updateAsync(selector, mutator); - }, - serverRemove: async function(name, selector) { - return groupingCollections[name].removeAsync(selector); - }, - getCollection: async function(name, selector) { - const result = await Partitioner.directOperation(async () => await groupingCollections[name].find(selector || {}).fetchAsync()) - return result; - }, - getMyCollection: async function(name, selector) { - return await groupingCollections[name].find(selector).fetchAsync(); - } -}); +// Publisher and methods are defined in tests/grouping_integration_tests.js to avoid duplication Tinytest.addAsync("partitioner - collections - local empty find", async (test) => { const userId = await createTestUser(); diff --git a/tests/utils.js b/tests/utils.js index 49d3a09..096e69f 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -16,6 +16,11 @@ export async function createTestUserWithGroup(usernamePrefix = "test_user", grou export const initializeTestCollections = () => { +// Reuse collections across test files to avoid duplicate collection errors +if (globalThis.__partitionerTestCollections) { + return globalThis.__partitionerTestCollections; +} + /* Set up server and client hooks */ @@ -43,5 +48,6 @@ hookCollection = (collection) => { hookCollection(basicInsertCollection); hookCollection(twoGroupCollection); +globalThis.__partitionerTestCollections = groupingCollections; return groupingCollections; } \ No newline at end of file From 52b2fded2b780b525d2b193fa118e63a2aefcd6e Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 22 Aug 2025 17:00:02 +0300 Subject: [PATCH 25/45] Update README --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7b5b49d..3b266c6 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,26 @@ Install with Meteor: meteor add mizzao:partitioner ``` +## Compatibility + +- Meteor: 3.0+ +- Hooks: matb33:collection-hooks@2.x (used internally) + ## Usage Partitioner uses the [collection-hooks](https://github.com/matb33/meteor-collection-hooks) package to transparently intercept collection operations on the client and server side so that writing code for each group of users is almost the same as writing for the whole app. Only minor modifications from a standalone app designed for a single group of users is necessary. -Partitioner operates at the collection level. On the server and client, call `Partition.partitionCollection` immediately after declaring a collection: +Partitioner operates at the collection level. On the server and client, call `Partitioner.partitionCollection` immediately after declaring a collection: ``` Foo = new Mongo.Collection("foo"); +// Client: synchronous hook registration Partitioner.partitionCollection(Foo, options); + +// Server: recommended to await during startup +Meteor.startup(async () => { + await Partitioner.partitionCollection(Foo, options); +}); ``` `options` determines how the partitioned collection will behave. The fields that are supported are @@ -46,44 +57,75 @@ Collections that have been partitioned will behave as if there is a separate ins This is accomplished using selector rewriting based on the current `userId` both on the client and in server methods, and Meteor's environment variables. For more details see the source. +### Async database APIs in Meteor 3 + +Meteor 3 collection methods are async. Helpers used in this repo/tests include: + +- `findOneAsync`, `fetchAsync`, `countAsync`, `insertAsync`, `updateAsync`, `removeAsync`, and `createIndex(...)`. + +Use `await` for server code and tests where appropriate. + ## Common (Client/Server) API #### `Partitioner.partitionCollection(Mongo.Collection, options)` Adds hooks to a particular collection so that it supports partition operations. This should be declared immediately after `new Mongo.Collection` on both the server and the client. +- Client: synchronous +- Server: async (await recommended during startup) + **NOTE**: Any documents in the collection that were not created from a group will not be visible to any groups in the partition. You should think of creating a partitioned collection as an atomic operation consisting of declaring the collection and calling `partitionCollection`; we will consider rolling this into a single API call in the future. #### `Partitioner.group()` -On the server and client, gets the group of the current user. Returns `undefined` if the user is not logged in or not part of a group. A reactive variable. +Gets the group of the current user. Returns `undefined` if the user is not logged in or not part of a group. + +- Client: synchronous and reactive (depends on the logged-in user document) +- Server: async (returns a Promise) ## Server API -#### `Partitioner.setUserGroup(userId, groupId)` +#### `Partitioner.setUserGroup(userId, groupId)` (async) Adds a particular user to the group identified by `groupId`. The user will now be able to operate on partitioned collections and will only be able to affect documents scoped to the group. An error will be thrown if the user is already in a group. -#### `Partitioner.getUserGroup(userId)` +#### `Partitioner.getUserGroup(userId)` (async) Gets the group of the current user. -#### `Partitioner.clearUserGroup(userId)` +#### `Partitioner.clearUserGroup(userId)` (async) Removes the current group assignment of the user. The user will no longer be able to operate on any partitioned collections. -#### `Partitioner.bindGroup(groupId, func)` +#### `Partitioner.bindGroup(groupId, func)` (async) Run a function (presumably doing collection operations) masquerading as a particular group. This is necessary for server-originated code that isn't caused by one particular user. -#### `Partitioner.bindUserGroup(userId, func)` +#### `Partitioner.bindUserGroup(userId, func)` (async) A convenience function for running `Partitioner.bindGroup` as the group of a particular user. -#### `Partitioner.directOperation(func)` +#### `Partitioner.directOperation(func)` (sync wrapper) Sometimes we need to do operations over the entire underlying collection, including all groups. This provides a way to do that, and will not throw an error if the current user method invocation context is not part of a group. +Notes: +- This is a synchronous wrapper around an environment flag. It does not return the value of `func`. +- If `func` is async, capture/return its Promise yourself from the calling site and `await` that, rather than awaiting `Partitioner.directOperation`. + +Example: + +```js +// GOOD +const result = await (async () => { + let value; + Partitioner.directOperation(async () => { + value = await SomeCollection.find(selector).fetchAsync(); + }); + return value; +})(); +``` + ## Configuring Subscriptions Suppose you have a publication on the server such as the following: @@ -116,13 +158,20 @@ Deps.autorun(function() { ## Partitioning of `Meteor.users` -`Meteor.users` is partitioned by default. Users will only see other users in their group in default publications. However, unscoped operations do not throw an error, because server operations (login, etc) need to proceed as normal when groups are not specified. This generally causes everything to work as expected, but please report any unexpected behavior that you see. +`Meteor.users` is partitioned by default. + +- Server: user finds must run inside a group context (`Partitioner.bindUserGroup` or `Partitioner.bindGroup`). Outside of a group context, user find operations throw `403` with reason `User find operation attempted outside group context`. +- Client: regular users are filtered server-side; for admin users, the client hook additionally merges `{ admin: { $exists: false } }` into global user finds so admins don’t see themselves in global lists. +- The package publishes `admin` and `group` fields of the current user so `Partitioner.group()` can be reactive on the client. ## Admin users -Partitioner treats users with `admin: true` as special. These users are able to see the entire contents of partitioned collections as well as all users when they are not assigned to a group, and operations will not result in errors. +Admin users are identified via `Meteor.user().admin === true`. -However, when admin users join a group, they will only see the data and users in that group (if you set up the subscriptions as noted above.) They will also, currently, be unable to do **any** operations on partitioned collections. The idea is to allow admin users to be able to join games, chatrooms, etc for observational purposes, but to prevent them from making unintended edits from the user interface. +- Admins can see all partitioned collections when not assigned to a group. +- When an admin joins a group, they only see that group's data (consistent with non-admin behavior). +- Admins are prevented from writes to partitioned collections via a deny rule. +- Client-only: global finds on `Meteor.users` for admins exclude admin users themselves. If you would like to see other ways to define admin permissions, please open an issue. @@ -155,13 +204,18 @@ ChatMessages.insert({text: "hello world", room: currentRoom, timestamp: Date.now This looks simple enough, until you realize that you need to keep track of the `room` for each message that is entered in to the collection. Why not have some code do it for you automagically? -### After +### After (Meteor 3, async APIs) With this package, you can create a partition of the `ChatMessages` collection: ```js ChatMessages = new Mongo.Collection("messages"); +// Client Partitioner.partitionCollection(ChatMessages, {index: {timestamp: 1}}); +// Server +Meteor.startup(async () => { + await Partitioner.partitionCollection(ChatMessages, {index: {timestamp: 1}}); +}); ``` The second argument tells the partitioner that you want an index of `timestamp` within each group. Partitioned lookups using `timestamp` will be done efficiently. Then, you can just write your publication as follows: From 5954cf9659dbf7c6439d4fa943da62c63efb5f39 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 28 Aug 2025 16:56:25 +0300 Subject: [PATCH 26/45] Make Partitioner.directOperation return value --- grouping.js | 4 +-- tests/server/grouping_test_server.js | 39 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/grouping.js b/grouping.js index 0c4204c..1bbc185 100644 --- a/grouping.js +++ b/grouping.js @@ -77,8 +77,8 @@ Partitioner.bindUserGroup = async function(userId, func) { return result; }; -Partitioner.directOperation = function(func) { - Partitioner._directOps.withValue(true, func); +Partitioner.directOperation = async function(func) { + return await Partitioner._directOps.withValue(true, func); }; // This can be replaced - currently not documented diff --git a/tests/server/grouping_test_server.js b/tests/server/grouping_test_server.js index 076fd88..18bb175 100644 --- a/tests/server/grouping_test_server.js +++ b/tests/server/grouping_test_server.js @@ -53,4 +53,43 @@ Tinytest.addAsync("partitioner - collections - insert with overridden group", as test.equal(result.length, 1); test.equal(result[0]._groupId, "overridden"); }); +}); + +Tinytest.addAsync("partitioner - directOperation - returns value from async function", async (test) => { + // Create a test collection for this specific test + let TestAccessCodes; + if (!Mongo.getCollection("test_access_codes")) { + TestAccessCodes = new Mongo.Collection("test_access_codes"); + TestAccessCodes._insecure = true; + await Partitioner.partitionCollection(TestAccessCodes); + } else { + TestAccessCodes = Mongo.getCollection("test_access_codes"); + } + + const testCode = "TEST123"; + const testData = { accessCode: testCode, userId: "test_user_123", createdAt: new Date() }; + + // Insert test data using directOperation to bypass group restrictions + await Partitioner.directOperation(async () => { + await TestAccessCodes.insertAsync(testData); + }); + + // Test that directOperation returns the value from the async function + const group = await (async () => { + let value; + Partitioner.directOperation(() => { + value = TestAccessCodes.findOneAsync({ accessCode: testCode }); + }); + return await value; + })(); + + // Verify the returned value is what we expect + test.isNotNull(group); + test.equal(group.accessCode, testCode); + test.equal(group.userId, "test_user_123"); + + // Clean up test data + await Partitioner.directOperation(async () => { + await TestAccessCodes.removeAsync({}); + }); }); \ No newline at end of file From 0c3b10becb949232e4651bd6b8f747a9ce20d340 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 28 Aug 2025 16:56:39 +0300 Subject: [PATCH 27/45] Publish a new beta --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index 9b6ab4f..13fb75f 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.1", + version: "0.7.0-beta.2", git: "https://github.com/mizzao/meteor-partitioner.git" }); From a7573d0276c21cd04564b8729d137d119c73e91b Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 11:42:29 +0300 Subject: [PATCH 28/45] Refactor error handling in hooks to use throwVerboseError helper --- common.js | 9 +++++++++ grouping.js | 8 +++++--- package.js | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/common.js b/common.js index 75f4af0..4dab4c5 100644 --- a/common.js +++ b/common.js @@ -17,5 +17,14 @@ Helpers = { typeof (selector != null ? selector._id : undefined) === 'string' || typeof (selector != null ? selector.username : undefined) === 'string' || (typeof (selector != null ? selector._id : undefined) === 'object' && (selector != null ? selector._id : undefined) !== null && (selector._id.$in != null)); + }, + + // Helper function to log verbose error details and throw appropriate error + throwVerboseError: function(hookContext, errorMessage, defaultOperation = 'unknown') { + const operation = hookContext.name || defaultOperation; + const collection = hookContext.collection?.name || 'unknown collection'; + const params = hookContext.args ? JSON.stringify(hookContext.args, null, 2) : 'no parameters'; + Meteor._debug(`Collection: ${collection}, Operation: ${operation}, Parameters: ${params}`); + throw new Meteor.Error(403, errorMessage); } }; \ No newline at end of file diff --git a/grouping.js b/grouping.js index 1bbc185..c811db1 100644 --- a/grouping.js +++ b/grouping.js @@ -152,7 +152,7 @@ const userFindHook = function(userId, selector, options) { if (!groupId) { // CANNOT do any async database calls here! // Must fail fast and require proper context setup - throw new Meteor.Error(403, ErrMsg.groupFindErr); + Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } // Since user is in a group, scope the find to the group @@ -198,7 +198,7 @@ const findHook = function(userId, selector, options) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); // CANNOT do any async database calls here! // Must fail fast and require proper context setup - throw new Meteor.Error(403, ErrMsg.groupFindErr); + Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } // if object (or empty) selector, just filter by group @@ -232,7 +232,9 @@ const insertHook = async function(userId, doc) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); const grouping = await Grouping.findOneAsync(userId); groupId = grouping?.groupId; - if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + if (!groupId) { + Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); + } } doc._groupId = groupId; diff --git a/package.js b/package.js index 13fb75f..5fc82e4 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.2", + version: "0.7.0-beta.3", git: "https://github.com/mizzao/meteor-partitioner.git" }); From 7e9de3c386643436ac08c540385f7da71615f474 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 13:12:15 +0300 Subject: [PATCH 29/45] Allow to configure whether to use Meteor.users or Grouping collection --- grouping.js | 133 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 23 deletions(-) diff --git a/grouping.js b/grouping.js index c811db1..80ec24a 100644 --- a/grouping.js +++ b/grouping.js @@ -6,13 +6,87 @@ */ Partitioner = {}; -const Grouping = new Mongo.Collection("ts.grouping"); + +// Configuration options +Partitioner.config = { + useMeteorUsers: false, // Set to true to use Meteor.users instead of separate Grouping collection + groupingCollectionName: "ts.grouping", // Name of the grouping collection when not using Meteor.users + disableUserManagementHooks: false // Set to true to disable hooks on user management operations when using Meteor.users +}; + +// Initialize collections based on configuration +const Grouping = new Mongo.Collection(Partitioner.config.groupingCollectionName); // Meteor environment variables for scoping group operations Partitioner._currentGroup = new Meteor.EnvironmentVariable(); Partitioner._isDirectGroupContext = new Meteor.EnvironmentVariable(); Partitioner._directOps = new Meteor.EnvironmentVariable(); +// Helper functions to abstract collection operations +const GroupingHelpers = { + async findOne(userId) { + if (Partitioner.config.useMeteorUsers) { + return await Meteor.users.findOneAsync(userId, { fields: { groupId: 1 } }); + } else { + return await Grouping.findOneAsync(userId); + } + }, + + async upsert(userId, updateDoc) { + if (Partitioner.config.useMeteorUsers) { + return await Meteor.users.upsertAsync(userId, updateDoc); + } else { + return await Grouping.upsertAsync(userId, updateDoc); + } + }, + + async remove(userId) { + if (Partitioner.config.useMeteorUsers) { + return await Meteor.users.updateAsync(userId, { $unset: { groupId: 1 } }); + } else { + return await Grouping.removeAsync(userId); + } + }, + + observeChanges(callbacks) { + if (Partitioner.config.useMeteorUsers) { + return Meteor.users.find({ groupId: { $exists: true } }).observeChangesAsync(callbacks); + } else { + return Grouping.find().observeChangesAsync(callbacks); + } + } +}; + +// Configuration method +Partitioner.configure = function(options) { + check(options, { + useMeteorUsers: Match.Optional(Boolean), + groupingCollectionName: Match.Optional(String), + disableUserManagementHooks: Match.Optional(Boolean) + }); + + if (options.useMeteorUsers !== undefined) { + Partitioner.config.useMeteorUsers = options.useMeteorUsers; + } + + if (options.groupingCollectionName !== undefined) { + Partitioner.config.groupingCollectionName = options.groupingCollectionName; + } + + if (options.disableUserManagementHooks !== undefined) { + Partitioner.config.disableUserManagementHooks = options.disableUserManagementHooks; + } + + // Validate configuration + if (Partitioner.config.useMeteorUsers && Partitioner.config.groupingCollectionName === "ts.grouping") { + Meteor._debug("Warning: Using Meteor.users for grouping but groupingCollectionName is still set to 'ts.grouping'"); + } + + if (Partitioner.config.disableUserManagementHooks && !Partitioner.config.useMeteorUsers) { + Meteor._debug("Warning: disableUserManagementHooks is true but useMeteorUsers is false. This setting only applies when using Meteor.users collection."); + } +}; + /* Public API */ @@ -20,11 +94,11 @@ Partitioner._directOps = new Meteor.EnvironmentVariable(); Partitioner.setUserGroup = async function(userId, groupId) { check(userId, String); check(groupId, String); - if (await Grouping.findOneAsync(userId)) { + if (await GroupingHelpers.findOne(userId)) { throw new Meteor.Error(403, "User is already in a group"); } - const result = await Grouping.upsertAsync(userId, { + const result = await GroupingHelpers.upsert(userId, { $set: {groupId: groupId} }); @@ -33,13 +107,13 @@ Partitioner.setUserGroup = async function(userId, groupId) { Partitioner.getUserGroup = async function(userId) { check(userId, String); - const grouping = await Grouping.findOneAsync(userId); + const grouping = await GroupingHelpers.findOne(userId); return grouping != null ? grouping.groupId : undefined; }; Partitioner.clearUserGroup = async function(userId) { check(userId, String); - await Grouping.removeAsync(userId); + await GroupingHelpers.remove(userId); }; Partitioner.group = async function() { @@ -142,6 +216,14 @@ const userFindHook = function(userId, selector, options) { if (Partitioner._directOps.get() === true) return true; if (Helpers.isDirectUserSelector(selector)) return true; + // Skip user management operations when configured to do so + if (Partitioner.config.useMeteorUsers && Partitioner.config.disableUserManagementHooks) { + const userManagementOps = ['createUser', 'findUserByEmail', 'findUserByUsername', '_attemptLogin']; + if (userManagementOps.includes(this.name)) { + return true; // Skip hook for user management operations + } + } + let groupId = Partitioner._currentGroup.get(); let isDirectGroupContext = Partitioner._isDirectGroupContext.get(); // This hook doesn't run if we're not in a method invocation or publish @@ -230,7 +312,7 @@ const insertHook = async function(userId, doc) { let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - const grouping = await Grouping.findOneAsync(userId); + const grouping = await GroupingHelpers.findOne(userId); groupId = grouping?.groupId; if (!groupId) { Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); @@ -242,28 +324,33 @@ const insertHook = async function(userId, doc) { }; // Sync grouping needed for hooking Meteor.users -Grouping.find().observeChangesAsync({ - added: async function(id, fields) { - if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { - Meteor._debug(`Tried to set group for nonexistent user ${id}`); - } - }, - changed: async function(id, fields) { - if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { - Meteor._debug(`Tried to change group for nonexistent user ${id}`); - } - }, - removed: async function(id) { - if (!await Meteor.users.updateAsync(id, {$unset: {"group": null}})) { - Meteor._debug(`Tried to unset group for nonexistent user ${id}`); +// Only sync when using separate grouping collection +if (!Partitioner.config.useMeteorUsers) { + GroupingHelpers.observeChanges({ + added: async function(id, fields) { + if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { + Meteor._debug(`Tried to set group for nonexistent user ${id}`); + } + }, + changed: async function(id, fields) { + if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { + Meteor._debug(`Tried to change group for nonexistent user ${id}`); + } + }, + removed: async function(id) { + if (!await Meteor.users.updateAsync(id, {$unset: {"group": null}})) { + Meteor._debug(`Tried to unset group for nonexistent user ${id}`); + } } - } -}); + }); +} TestFuncs = { getPartitionedIndex: getPartitionedIndex, userFindHook: userFindHook, findHook: findHook, insertHook: insertHook, - Grouping: Grouping + Grouping: Grouping, + GroupingHelpers: GroupingHelpers, + config: Partitioner.config }; \ No newline at end of file From fa892fbc32f9a6b93084a9c53c5247a87b15843c Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 13:15:17 +0300 Subject: [PATCH 30/45] Document new options --- README.md | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/README.md b/README.md index 3b266c6..8e59626 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,110 @@ Install with Meteor: meteor add mizzao:partitioner ``` +## Configuration + +The partitioner package supports flexible configuration options to adapt to different storage strategies and performance requirements. + +### Basic Configuration + +```js +Partitioner.configure({ + useMeteorUsers: false, // Use Meteor.users collection instead of separate grouping collection + groupingCollectionName: "ts.grouping", // Custom name for grouping collection + disableUserManagementHooks: false // Disable hooks on user management operations +}); +``` + +### Configuration Options + +#### `useMeteorUsers` (Boolean, default: `false`) + +When `true`, stores group data directly in the `Meteor.users` collection using a `groupId` field instead of a separate grouping collection. + +**Benefits:** +- Simpler data model (no separate collection) +- Better performance for user-group lookups +- Reduced database complexity + +**Example:** +```js +Partitioner.configure({ useMeteorUsers: true }); +``` + +#### `groupingCollectionName` (String, default: `"ts.grouping"`) + +Custom name for the grouping collection when not using `Meteor.users`. Useful for avoiding naming conflicts or organizing collections. + +**Example:** +```js +Partitioner.configure({ + useMeteorUsers: false, + groupingCollectionName: "myapp.user_groups" +}); +``` + +#### `disableUserManagementHooks` (Boolean, default: `false`) + +When `true` and `useMeteorUsers: true`, disables partitioning hooks on user management operations: +- `createUser` +- `findUserByEmail` +- `findUserByUsername` +- `_attemptLogin` + +**Benefits:** +- Improved performance for user management operations +- Prevents unnecessary group scoping on user creation/login +- Reduces overhead when using `Meteor.users` collection + +**Example:** +```js +Partitioner.configure({ + useMeteorUsers: true, + disableUserManagementHooks: true +}); +``` + +### Storage Strategies + +#### Separate Collection Strategy (Default) +```js +Partitioner.configure({ useMeteorUsers: false }); +``` +- Uses dedicated `ts.grouping` collection +- Stores `{_id: userId, groupId: groupId}` documents +- Syncs to `Meteor.users.group` field for hooks +- Best for: Complex group management, multiple group types + +#### Meteor.users Collection Strategy +```js +Partitioner.configure({ + useMeteorUsers: true, + disableUserManagementHooks: true +}); +``` +- Stores group data in `Meteor.users.groupId` field +- No separate collection needed +- Optimized user management operations +- Best for: Simple group assignments, performance-critical applications + +### Configuration Validation + +The package includes built-in validation with helpful warnings: + +```js +// Warning: disableUserManagementHooks only applies when using Meteor.users +Partitioner.configure({ + useMeteorUsers: false, + disableUserManagementHooks: true // Will show warning +}); + +// Warning: Using Meteor.users but groupingCollectionName still set +Partitioner.configure({ + useMeteorUsers: true, + groupingCollectionName: "ts.grouping" // Will show warning +}); +``` + ## Compatibility - Meteor: 3.0+ @@ -85,6 +189,25 @@ Gets the group of the current user. Returns `undefined` if the user is not logge ## Server API +#### `Partitioner.configure(options)` (sync) + +Configures the partitioner package with the specified options. Should be called before any other partitioner operations. + +**Parameters:** +- `options.useMeteorUsers` (Boolean, optional): Use Meteor.users collection instead of separate grouping collection +- `options.groupingCollectionName` (String, optional): Custom name for grouping collection +- `options.disableUserManagementHooks` (Boolean, optional): Disable hooks on user management operations + +**Example:** +```js +Meteor.startup(() => { + Partitioner.configure({ + useMeteorUsers: true, + disableUserManagementHooks: true + }); +}); +``` + #### `Partitioner.setUserGroup(userId, groupId)` (async) Adds a particular user to the group identified by `groupId`. The user will now be able to operate on partitioned collections and will only be able to affect documents scoped to the group. An error will be thrown if the user is already in a group. @@ -209,6 +332,14 @@ This looks simple enough, until you realize that you need to keep track of the ` With this package, you can create a partition of the `ChatMessages` collection: ```js +// Configure the partitioner (optional - defaults work fine) +Meteor.startup(() => { + Partitioner.configure({ + useMeteorUsers: false, // Use separate grouping collection (default) + groupingCollectionName: "ts.grouping" + }); +}); + ChatMessages = new Mongo.Collection("messages"); // Client Partitioner.partitionCollection(ChatMessages, {index: {timestamp: 1}}); @@ -218,6 +349,18 @@ Meteor.startup(async () => { }); ``` +**Alternative configuration using Meteor.users collection:** + +```js +// Configure to use Meteor.users collection for better performance +Meteor.startup(() => { + Partitioner.configure({ + useMeteorUsers: true, + disableUserManagementHooks: true // Optimize user management operations + }); +}); +``` + The second argument tells the partitioner that you want an index of `timestamp` within each group. Partitioned lookups using `timestamp` will be done efficiently. Then, you can just write your publication as follows: ```js From 8423cace6ed42c3d66e371f646688626475cb6ad Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 13:15:43 +0300 Subject: [PATCH 31/45] Publish new version --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index 5fc82e4..d8b38f0 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.3", + version: "0.7.0-beta.4", git: "https://github.com/mizzao/meteor-partitioner.git" }); From 54ef17f33958d1feef0cad96a3d8c2343a7b82b1 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 13:29:47 +0300 Subject: [PATCH 32/45] Remove "Benefits" sections --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index 8e59626..1804da6 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,6 @@ Partitioner.configure({ When `true`, stores group data directly in the `Meteor.users` collection using a `groupId` field instead of a separate grouping collection. -**Benefits:** -- Simpler data model (no separate collection) -- Better performance for user-group lookups -- Reduced database complexity - **Example:** ```js Partitioner.configure({ useMeteorUsers: true }); @@ -71,11 +66,6 @@ When `true` and `useMeteorUsers: true`, disables partitioning hooks on user mana - `findUserByUsername` - `_attemptLogin` -**Benefits:** -- Improved performance for user management operations -- Prevents unnecessary group scoping on user creation/login -- Reduces overhead when using `Meteor.users` collection - **Example:** ```js Partitioner.configure({ From 3d1b8561dc988bdcec9b1cf4080f185e56bdcee1 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 13:51:39 +0300 Subject: [PATCH 33/45] Enhance configuration management in Partitioner.configure to auto-disable conflicting settings based on useMeteorUsers option --- README.md | 19 ++++++++----------- grouping.js | 43 ++++++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1804da6..25c99df 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Partitioner.configure({ }); ``` +**Note:** The package automatically manages conflicting configurations. When you set `useMeteorUsers: true`, it automatically disables separate collection features. When you set `useMeteorUsers: false`, it automatically disables Meteor.users specific features. + ### Configuration Options #### `useMeteorUsers` (Boolean, default: `false`) @@ -99,20 +101,15 @@ Partitioner.configure({ ### Configuration Validation -The package includes built-in validation with helpful warnings: +The package includes built-in validation with helpful debug messages: ```js -// Warning: disableUserManagementHooks only applies when using Meteor.users -Partitioner.configure({ - useMeteorUsers: false, - disableUserManagementHooks: true // Will show warning -}); +// Debug messages show automatic configuration changes +Partitioner.configure({ useMeteorUsers: true }); +// Output: "Configuration: Using Meteor.users collection for grouping. Separate grouping collection features disabled." -// Warning: Using Meteor.users but groupingCollectionName still set -Partitioner.configure({ - useMeteorUsers: true, - groupingCollectionName: "ts.grouping" // Will show warning -}); +Partitioner.configure({ useMeteorUsers: false }); +// Output: "Configuration: Using separate grouping collection. Meteor.users specific features disabled." ``` ## Compatibility diff --git a/grouping.js b/grouping.js index 80ec24a..f7cdce1 100644 --- a/grouping.js +++ b/grouping.js @@ -65,25 +65,42 @@ Partitioner.configure = function(options) { disableUserManagementHooks: Match.Optional(Boolean) }); - if (options.useMeteorUsers !== undefined) { - Partitioner.config.useMeteorUsers = options.useMeteorUsers; - } - - if (options.groupingCollectionName !== undefined) { - Partitioner.config.groupingCollectionName = options.groupingCollectionName; - } - - if (options.disableUserManagementHooks !== undefined) { - Partitioner.config.disableUserManagementHooks = options.disableUserManagementHooks; + // Auto-disable conflicting configurations + if (options.useMeteorUsers === true) { + // When using Meteor.users, automatically disable separate collection features + Partitioner.config.useMeteorUsers = true; + Partitioner.config.disableUserManagementHooks = options.disableUserManagementHooks !== undefined ? + options.disableUserManagementHooks : Partitioner.config.disableUserManagementHooks; + + Meteor._debug("Configuration: Using Meteor.users collection for grouping. Separate grouping collection features disabled."); + } else if (options.useMeteorUsers === false) { + // When using separate collection, automatically disable Meteor.users specific features + Partitioner.config.useMeteorUsers = false; + Partitioner.config.disableUserManagementHooks = false; // Force disable when not using Meteor.users + + if (options.groupingCollectionName !== undefined) { + Partitioner.config.groupingCollectionName = options.groupingCollectionName; + } + + Meteor._debug("Configuration: Using separate grouping collection. Meteor.users specific features disabled."); + } else { + // Only update individual settings if useMeteorUsers is not explicitly set + if (options.groupingCollectionName !== undefined) { + Partitioner.config.groupingCollectionName = options.groupingCollectionName; + } + + if (options.disableUserManagementHooks !== undefined) { + Partitioner.config.disableUserManagementHooks = options.disableUserManagementHooks; + } } - // Validate configuration + // Validate final configuration if (Partitioner.config.useMeteorUsers && Partitioner.config.groupingCollectionName === "ts.grouping") { - Meteor._debug("Warning: Using Meteor.users for grouping but groupingCollectionName is still set to 'ts.grouping'"); + Meteor._debug("Note: Using Meteor.users for grouping. groupingCollectionName setting is ignored."); } if (Partitioner.config.disableUserManagementHooks && !Partitioner.config.useMeteorUsers) { - Meteor._debug("Warning: disableUserManagementHooks is true but useMeteorUsers is false. This setting only applies when using Meteor.users collection."); + Meteor._debug("Note: disableUserManagementHooks automatically disabled when not using Meteor.users collection."); } }; From 7379b42bed3833db708fbaf7ba9fe298059028e6 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 13:51:55 +0300 Subject: [PATCH 34/45] Update version to 0.7.0-beta.5 in package.js --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index d8b38f0..028db85 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.4", + version: "0.7.0-beta.5", git: "https://github.com/mizzao/meteor-partitioner.git" }); From 5cc1f7e3b0fab1a0bace68a039c1196d500a386d Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 14:19:07 +0300 Subject: [PATCH 35/45] Remove extra logs --- grouping.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/grouping.js b/grouping.js index f7cdce1..88fc3d7 100644 --- a/grouping.js +++ b/grouping.js @@ -93,15 +93,6 @@ Partitioner.configure = function(options) { Partitioner.config.disableUserManagementHooks = options.disableUserManagementHooks; } } - - // Validate final configuration - if (Partitioner.config.useMeteorUsers && Partitioner.config.groupingCollectionName === "ts.grouping") { - Meteor._debug("Note: Using Meteor.users for grouping. groupingCollectionName setting is ignored."); - } - - if (Partitioner.config.disableUserManagementHooks && !Partitioner.config.useMeteorUsers) { - Meteor._debug("Note: disableUserManagementHooks automatically disabled when not using Meteor.users collection."); - } }; /* From 5bfc6403393c1fb2b2ab554c4ef515f47c77c33e Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 11 Sep 2025 14:19:19 +0300 Subject: [PATCH 36/45] Update version to 0.7.0-beta.6 in package.js --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index 028db85..8160113 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.5", + version: "0.7.0-beta.6", git: "https://github.com/mizzao/meteor-partitioner.git" }); From a301310e319d60430d538292949fb9eda28888a6 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 18 Sep 2025 16:43:42 +0300 Subject: [PATCH 37/45] Publish new beta 0.7.0-beta.7 --- grouping.js | 19 +++++++++++-------- package.js | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/grouping.js b/grouping.js index 88fc3d7..1b4564d 100644 --- a/grouping.js +++ b/grouping.js @@ -26,9 +26,11 @@ Partitioner._directOps = new Meteor.EnvironmentVariable(); const GroupingHelpers = { async findOne(userId) { if (Partitioner.config.useMeteorUsers) { - return await Meteor.users.findOneAsync(userId, { fields: { groupId: 1 } }); - } else { - return await Grouping.findOneAsync(userId); + const group = (await Meteor.users.findOneAsync(userId, { fields: { group: 1 } })).group; + return group; + } else { + const groupId = (await Grouping.findOneAsync(userId)).groupId; + return groupId; } }, @@ -42,7 +44,7 @@ const GroupingHelpers = { async remove(userId) { if (Partitioner.config.useMeteorUsers) { - return await Meteor.users.updateAsync(userId, { $unset: { groupId: 1 } }); + return await Meteor.users.updateAsync(userId, { $unset: { group: 1 } }); } else { return await Grouping.removeAsync(userId); } @@ -115,8 +117,7 @@ Partitioner.setUserGroup = async function(userId, groupId) { Partitioner.getUserGroup = async function(userId) { check(userId, String); - const grouping = await GroupingHelpers.findOne(userId); - return grouping != null ? grouping.groupId : undefined; + return await GroupingHelpers.findOne(userId); }; Partitioner.clearUserGroup = async function(userId) { @@ -285,9 +286,11 @@ const findHook = function(userId, selector, options) { if (userId) { if (!groupId) { + // debugger; if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); // CANNOT do any async database calls here! // Must fail fast and require proper context setup + // debugger; Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } @@ -320,9 +323,9 @@ const insertHook = async function(userId, doc) { let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - const grouping = await GroupingHelpers.findOne(userId); - groupId = grouping?.groupId; + groupId = await GroupingHelpers.findOne(userId); if (!groupId) { + // debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); } } diff --git a/package.js b/package.js index 8160113..a7346bb 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.6", + version: "0.7.0-beta.7", git: "https://github.com/mizzao/meteor-partitioner.git" }); From fd9e1041f05c4b2852ac1fa3b43a964c91042c19 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 25 Sep 2025 18:13:42 +0300 Subject: [PATCH 38/45] Publish new beta 0.7.0-beta.8 --- common.js | 24 ++++- grouping.js | 293 ++++++++++++++++++++++++++++++++++++++++------------ package.js | 2 +- 3 files changed, 253 insertions(+), 66 deletions(-) diff --git a/common.js b/common.js index 4dab4c5..18d717d 100644 --- a/common.js +++ b/common.js @@ -9,6 +9,26 @@ Helpers = { isDirectSelector: function(selector) { return typeof selector === 'string' || typeof (selector != null ? selector._id : undefined) === 'string'; }, + // Helper function to detect login token verification queries + isLoginTokenQuery: function(selector) { + if (!selector || typeof selector !== 'object') return false; + + // Handle direct login token queries + if (selector['services.resume.loginTokens.hashedToken'] !== undefined || + selector['services.resume.loginTokens.token'] !== undefined) { + return true; + } + + // Handle $or queries that contain login token conditions + if (selector.$or && Array.isArray(selector.$or)) { + return selector.$or.some(condition => + condition['services.resume.loginTokens.hashedToken'] !== undefined || + condition['services.resume.loginTokens.token'] !== undefined + ); + } + + return false; + }, // Because of https://github.com/HarvardEconCS/turkserver-meteor/issues/44 // _id: { $in: [ ... ] } queries should be short-circuited as well for users @@ -16,7 +36,9 @@ Helpers = { return typeof selector === 'string' || typeof (selector != null ? selector._id : undefined) === 'string' || typeof (selector != null ? selector.username : undefined) === 'string' || - (typeof (selector != null ? selector._id : undefined) === 'object' && (selector != null ? selector._id : undefined) !== null && (selector._id.$in != null)); + (typeof (selector != null ? selector._id : undefined) === 'object' && (selector != null ? selector._id : undefined) !== null && (selector._id.$in != null)) || + // Handle login token verification during authentication + Helpers.isLoginTokenQuery(selector); }, // Helper function to log verbose error details and throw appropriate error diff --git a/grouping.js b/grouping.js index 1b4564d..0482eb1 100644 --- a/grouping.js +++ b/grouping.js @@ -6,12 +6,15 @@ */ Partitioner = {}; +// allowDirectIdSelectors is now managed through Partitioner.config +const multipleGroupCollections = {} // Configuration options Partitioner.config = { useMeteorUsers: false, // Set to true to use Meteor.users instead of separate Grouping collection groupingCollectionName: "ts.grouping", // Name of the grouping collection when not using Meteor.users - disableUserManagementHooks: false // Set to true to disable hooks on user management operations when using Meteor.users + disableUserManagementHooks: false, // Set to true to disable hooks on user management operations when using Meteor.users + allowDirectIdSelectors: false, // Set to true to allow direct id selectors }; // Initialize collections based on configuration @@ -21,40 +24,41 @@ const Grouping = new Mongo.Collection(Partitioner.config.groupingCollectionName) Partitioner._currentGroup = new Meteor.EnvironmentVariable(); Partitioner._isDirectGroupContext = new Meteor.EnvironmentVariable(); Partitioner._directOps = new Meteor.EnvironmentVariable(); +Partitioner._searchAllUsers = new Meteor.EnvironmentVariable(); // Helper functions to abstract collection operations const GroupingHelpers = { async findOne(userId) { if (Partitioner.config.useMeteorUsers) { - const group = (await Meteor.users.findOneAsync(userId, { fields: { group: 1 } })).group; - return group; - } else { - const groupId = (await Grouping.findOneAsync(userId)).groupId; - return groupId; + const user = await Meteor.users.direct.findOneAsync(userId, { fields: { group: 1 } }); + return user?.group; // Safe null access + } else { + const grouping = await Grouping.direct.findOneAsync(userId); + return grouping?.groupId; // Safe null access } }, async upsert(userId, updateDoc) { if (Partitioner.config.useMeteorUsers) { - return await Meteor.users.upsertAsync(userId, updateDoc); + return await Meteor.users.direct.upsertAsync(userId, updateDoc); } else { - return await Grouping.upsertAsync(userId, updateDoc); + return await Grouping.direct.upsertAsync(userId, updateDoc); } }, async remove(userId) { if (Partitioner.config.useMeteorUsers) { - return await Meteor.users.updateAsync(userId, { $unset: { group: 1 } }); + return await Meteor.users.direct.updateAsync(userId, { $unset: { group: 1 } }); } else { - return await Grouping.removeAsync(userId); + return await Grouping.direct.removeAsync(userId); } }, observeChanges(callbacks) { if (Partitioner.config.useMeteorUsers) { - return Meteor.users.find({ groupId: { $exists: true } }).observeChangesAsync(callbacks); + return Meteor.users.direct.find({ groupId: { $exists: true } }).observeChangesAsync(callbacks); } else { - return Grouping.find().observeChangesAsync(callbacks); + return Grouping.direct.find().observeChangesAsync(callbacks); } } }; @@ -64,7 +68,8 @@ Partitioner.configure = function(options) { check(options, { useMeteorUsers: Match.Optional(Boolean), groupingCollectionName: Match.Optional(String), - disableUserManagementHooks: Match.Optional(Boolean) + disableUserManagementHooks: Match.Optional(Boolean), + allowDirectIdSelectors: Match.Optional(Boolean) }); // Auto-disable conflicting configurations @@ -94,6 +99,10 @@ Partitioner.configure = function(options) { if (options.disableUserManagementHooks !== undefined) { Partitioner.config.disableUserManagementHooks = options.disableUserManagementHooks; } + + if (options.allowDirectIdSelectors !== undefined) { + Partitioner.config.allowDirectIdSelectors = options.allowDirectIdSelectors; + } } }; @@ -166,7 +175,7 @@ Partitioner.directOperation = async function(func) { // This can be replaced - currently not documented Partitioner._isAdmin = async function(userId) { - const user = await Meteor.users.findOneAsync(userId, {fields: {groupId: 1, admin: 1}}); + const user = await Meteor.users.direct.findOneAsync(userId, {fields: {groupId: 1, admin: 1}}); return user.admin === true; }; @@ -176,7 +185,7 @@ const getPartitionedIndex = function(index) { return Object.assign(defaultIndex, index); }; -Partitioner.partitionCollection = async function(collection, options) { +Partitioner.partitionCollection = async function(collection, options = {}) { // Because of the deny below, need to create an allow validator // on an insecure collection if there isn't one already if (collection._isInsecure()) { @@ -198,21 +207,81 @@ Partitioner.partitionCollection = async function(collection, options) { collection.before.findOne(findHook); // These will hook the _validated methods as well - collection.before.insert(insertHook); + collection.before.insert((userId, doc) => insertHook(options.multipleGroups, userId, doc)); + collection.before.upsert((userId, selector, modifier) => upsertHook(options.multipleGroups, userId, selector, modifier)); /* No update/remove hook necessary, see https://github.com/matb33/meteor-collection-hooks/issues/23 */ + // store a hash of which collections allow multiple groups + if (options.multipleGroups) { + multipleGroupCollections[collection._name] = true; +} + +// Index the collections by groupId on the server for faster lookups across groups +return collection.createIndex ? collection.createIndex(getPartitionedIndex(options.index), options.indexOptions) + : collection._ensureIndex(getPartitionedIndex(options.index), options.indexOptions); +}; + +Partitioner.getAllowDirectIdSelectors = function() { + return Partitioner.config.allowDirectIdSelectors; +}; + +Partitioner.setAllowDirectIdSelectors = function(val) { + if (typeof val != 'boolean') { + throw new Error('Partitioner.allowDirectIdSelectors can only be boolean'); + } + Partitioner.config.allowDirectIdSelectors = val; + if (val) { + console.warn('WARNING: setting Partitioner.allowDirectIdSelectors = true may allow unsafe operations!'); + } +}; - // Index the collections by groupId on the server for faster lookups across groups - collection.createIndex(getPartitionedIndex(options != null ? options.index : undefined), options != null ? options.indexOptions : undefined); +Partitioner.addToGroup = async function(collection, entityId, groupId) { + if (!multipleGroupCollections[collection._name]) { + throw new Meteor.Error(403, ErrMsg.multiGroupErr); + } + + let currentGroupIds = collection.direct.findOne(entityId, {fields: {_groupId: 1}})?._groupId; + if (!currentGroupIds) { + currentGroupIds = [groupId]; + } else if (typeof currentGroupIds == 'string') { + currentGroupIds = [currentGroupIds]; + } + + if (currentGroupIds.indexOf(groupId) == -1) { + currentGroupIds.push(groupId); + collection.direct.update(entityId, {$set: {_groupId: currentGroupIds}}); + } + return currentGroupIds; +}; + +Partitioner.removeFromGroup = async function(collection, entityId, groupId) { + if (!multipleGroupCollections[collection._name]) { + throw new Meteor.Error(403, ErrMsg.multiGroupErr); + } + + let currentGroupIds = collection.direct.findOne(entityId, {fields: {_groupId: 1}})?._groupId; + if (!currentGroupIds) { + return []; + } + + if (typeof currentGroupIds == 'string') { + currentGroupIds = [currentGroupIds]; + } + const index = currentGroupIds.indexOf(groupId); + if (index != -1) { + currentGroupIds.splice(index, 1); + collection.direct.update(entityId, {$set: {_groupId: currentGroupIds}}); + } + + return currentGroupIds; }; // Publish admin and group for users that have it Meteor.publish(null, function() { - if (!this.userId) return; - return Meteor.users.find(this.userId, { + return Meteor.users.direct.find(this.userId, { fields: { admin: 1, group: 1 @@ -222,16 +291,11 @@ Meteor.publish(null, function() { // Special hook for Meteor.users to scope for each group const userFindHook = function(userId, selector, options) { - if (Partitioner._directOps.get() === true) return true; - if (Helpers.isDirectUserSelector(selector)) return true; - - // Skip user management operations when configured to do so - if (Partitioner.config.useMeteorUsers && Partitioner.config.disableUserManagementHooks) { - const userManagementOps = ['createUser', 'findUserByEmail', 'findUserByUsername', '_attemptLogin']; - if (userManagementOps.includes(this.name)) { - return true; // Skip hook for user management operations - } - } + const isDirectSelector = Helpers.isDirectUserSelector(selector); +if ( + ((Partitioner.config.allowDirectIdSelectors || Partitioner._searchAllUsers.get()) && isDirectSelector) + || Partitioner._directOps.get() === true +) return true; let groupId = Partitioner._currentGroup.get(); let isDirectGroupContext = Partitioner._isDirectGroupContext.get(); @@ -240,43 +304,49 @@ const userFindHook = function(userId, selector, options) { if (!userId && !groupId) return true; if (!userId && !isDirectGroupContext) return true; + // Handle queries specifically looking for users without groups + if (!groupId && selector && selector.group === null) { + // Allow the query to proceed unchanged - it's specifically looking for ungrouped users + return true; + } + if (!groupId) { + // debugger; // CANNOT do any async database calls here! // Must fail fast and require proper context setup Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } - + // debugger; // Since user is in a group, scope the find to the group - const filter = { - "group": groupId, - "admin": {$exists: false} - }; - - if (!this.args[0]) { - this.args[0] = filter; - } else { - Object.assign(this.args[0], filter); - } + filter = { + "group": groupId, + }; + if (!isDirectSelector) { + filter.admin = {$exists: false} + } + if (selector == null) { + this.args[0] = filter; + } else if (typeof selector == 'string') { + filter._id = selector; + this.args[0] = filter; + } else { + Object.assign(selector, filter); + } return true; }; -// Attach the find hooks to Meteor.users -Meteor.users.before.find(userFindHook); -Meteor.users.before.findOne(userFindHook); - // No allow/deny for find so we make our own checks const findHook = function(userId, selector, options) { // Don't scope for direct operations - if (Partitioner._directOps.get() === true) return true; - // for find(id) we should not touch this // TODO this may allow arbitrary finds across groups with the right _id // We could amend this in the future to {_id: someId, _groupId: groupId} // https://github.com/mizzao/meteor-partitioner/issues/9 // https://github.com/mizzao/meteor-partitioner/issues/10 - if (Helpers.isDirectSelector(selector)) return true; + if (Partitioner._directOps.get() === true || (Partitioner.config.allowDirectIdSelectors && Helpers.isDirectSelector(selector))) return true; + // Check for global hook let groupId = Partitioner._currentGroup.get(); @@ -293,30 +363,76 @@ const findHook = function(userId, selector, options) { // debugger; Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } + // debugger; + + // force the selector to scope for the _groupId + if (selector == null) { + this.args[0] = { + _groupId: groupId, + }; + } else if (typeof selector == 'string') { + this.args[0] = { + _id: selector, + _groupId: groupId, + }; + } else { + selector._groupId = groupId; + } - // if object (or empty) selector, just filter by group - if (selector == null) { - this.args[0] = {_groupId: groupId}; - } else { - selector._groupId = groupId; + // Adjust options to not return _groupId + if (options == null) { + this.args[1] = {fields: {_groupId: 0}}; + } else { + // If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere + if (options.fields == null) options.fields = {}; + if (!Object.values(options.fields).some(v => v)) options.fields._groupId = 0; + } + } + + return true; +}; + +const insertHook = async function(multipleGroups, userId, doc) { + // Don't add group for direct inserts + if (Partitioner._directOps.get() === true) return true; + + let groupId = Partitioner._currentGroup.get(); + if (!groupId) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); + groupId = await GroupingHelpers.findOne(userId); + if (!groupId) { + // debugger; + Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); } + } - // Adjust options to not return _groupId - if (options == null) { - this.args[1] = {fields: {_groupId: 0}}; - } else { - // If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere - if (options.fields == null) options.fields = {}; - if (!Object.values(options.fields).some((v) => v === 1)) { - options.fields._groupId = 0; - } + // Handle multipleGroups: array vs string + doc._groupId = multipleGroups ? [groupId] : groupId; + return true; +}; + +const upsertHook = async function(multipleGroups, userId, selector, modifier) { + // Don't add group for direct upserts + if (Partitioner._directOps.get() === true) return true; + + let groupId = Partitioner._currentGroup.get(); + if (!groupId) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); + groupId = await GroupingHelpers.findOne(userId); + if (!groupId) { + // debugger; + Helpers.throwVerboseError(this, ErrMsg.groupErr, 'upsert'); } } + // Handle multipleGroups: array vs string + // For upserts, we need to add to $set + if (!modifier.$set) modifier.$set = {}; + modifier.$set._groupId = multipleGroups ? [groupId] : groupId; return true; }; -const insertHook = async function(userId, doc) { +const userInsertHook = async function(userId, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; @@ -330,10 +446,42 @@ const insertHook = async function(userId, doc) { } } - doc._groupId = groupId; + // For users, we use 'group' field instead of '_groupId' + doc.group = groupId; return true; }; +const userUpsertHook = async function(userId, selector, modifier) { + // Don't add group for direct upserts + if (Partitioner._directOps.get() === true) return true; + + let groupId = Partitioner._currentGroup.get(); + if (!groupId) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); + groupId = await GroupingHelpers.findOne(userId); + if (!groupId) { + // debugger; + Helpers.throwVerboseError(this, ErrMsg.groupErr, 'upsert'); + } + } + + // For users, we use 'group' field instead of '_groupId' + if (!modifier.$set) modifier.$set = {}; + modifier.$set.group = groupId; + return true; +}; + +// Attach the find hooks to Meteor.users +Meteor.users.before.find(userFindHook); +Meteor.users.before.findOne(userFindHook); + +// Insert/upsert hooks only needed when using Meteor.users to store group info +if (Partitioner.config.useMeteorUsers) { + Meteor.users.before.insert(userInsertHook); + Meteor.users.before.upsert(userUpsertHook); +} + + // Sync grouping needed for hooking Meteor.users // Only sync when using separate grouping collection if (!Partitioner.config.useMeteorUsers) { @@ -356,6 +504,23 @@ if (!Partitioner.config.useMeteorUsers) { }); } +// Accounts.createUser, etc, checks for case-insensitive matches of the email address +// however, it uses Meteor.users.find which only operates on the partitioned collection +// so will not find a matching user in a different group. +// Hence make them use Meteor.users._partitionerDirect.find instead. +// Don't wrap createUser with Partitioner.directOperation because want inserted user doc to be +// automatically assigned to the group +if (Partitioner.config.useMeteorUsers) { + ['createUser', 'findUserByEmail', 'findUserByUsername', '_attemptLogin'].forEach(fn => { + const orig = Accounts[fn]; + if (orig) { + Accounts[fn] = function() { + return Partitioner._searchAllUsers.withValue(true, () => orig.apply(this, arguments)); + }; + } + }); +} + TestFuncs = { getPartitionedIndex: getPartitionedIndex, userFindHook: userFindHook, diff --git a/package.js b/package.js index a7346bb..b260a5d 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.7", + version: "0.7.0-beta.8", git: "https://github.com/mizzao/meteor-partitioner.git" }); From 848b245dedb5103486de684689ffc54d53203628 Mon Sep 17 00:00:00 2001 From: harryadel Date: Thu, 25 Sep 2025 20:41:41 +0300 Subject: [PATCH 39/45] save changes --- common.js | 3 ++- grouping.js | 40 +++++++++++++++++++--------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/common.js b/common.js index 18d717d..ccc9375 100644 --- a/common.js +++ b/common.js @@ -2,7 +2,8 @@ ErrMsg = { userIdErr: "Must be logged in to operate on partitioned collection", groupErr: "Must have group assigned to operate on partitioned collection", groupFindErr: "User find operation attempted outside group context. " + - "All operations must be wrapped with Partitioner.bindUserGroup() or Partitioner.bindGroup(). " + "All operations must be wrapped with Partitioner.bindUserGroup() or Partitioner.bindGroup(). ", + multiGroupErr: "Operation attempted on collection that does not support multiple groups" }; Helpers = { diff --git a/grouping.js b/grouping.js index 0482eb1..220a57b 100644 --- a/grouping.js +++ b/grouping.js @@ -40,7 +40,13 @@ const GroupingHelpers = { async upsert(userId, updateDoc) { if (Partitioner.config.useMeteorUsers) { - return await Meteor.users.direct.upsertAsync(userId, updateDoc); + // When using Meteor.users, we need to adapt the field name from 'groupId' to 'group' + const adaptedUpdateDoc = { ...updateDoc }; + if (adaptedUpdateDoc.$set && adaptedUpdateDoc.$set.groupId !== undefined) { + adaptedUpdateDoc.$set.group = adaptedUpdateDoc.$set.groupId; + delete adaptedUpdateDoc.$set.groupId; + } + return await Meteor.users.direct.upsertAsync(userId, adaptedUpdateDoc); } else { return await Grouping.direct.upsertAsync(userId, updateDoc); } @@ -52,14 +58,6 @@ const GroupingHelpers = { } else { return await Grouping.direct.removeAsync(userId); } - }, - - observeChanges(callbacks) { - if (Partitioner.config.useMeteorUsers) { - return Meteor.users.direct.find({ groupId: { $exists: true } }).observeChangesAsync(callbacks); - } else { - return Grouping.direct.find().observeChangesAsync(callbacks); - } } }; @@ -281,7 +279,7 @@ Partitioner.removeFromGroup = async function(collection, entityId, groupId) { // Publish admin and group for users that have it Meteor.publish(null, function() { - return Meteor.users.direct.find(this.userId, { + return Meteor.users.direct.find({ _id:this.userId }, { fields: { admin: 1, group: 1 @@ -311,12 +309,12 @@ if ( } if (!groupId) { - // debugger; + debugger; // CANNOT do any async database calls here! // Must fail fast and require proper context setup Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } - // debugger; + debugger; // Since user is in a group, scope the find to the group filter = { "group": groupId, @@ -356,14 +354,14 @@ const findHook = function(userId, selector, options) { if (userId) { if (!groupId) { - // debugger; + debugger; if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); // CANNOT do any async database calls here! // Must fail fast and require proper context setup - // debugger; + debugger; Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } - // debugger; + debugger; // force the selector to scope for the _groupId if (selector == null) { @@ -401,7 +399,7 @@ const insertHook = async function(multipleGroups, userId, doc) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); groupId = await GroupingHelpers.findOne(userId); if (!groupId) { - // debugger; + debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); } } @@ -420,7 +418,7 @@ const upsertHook = async function(multipleGroups, userId, selector, modifier) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); groupId = await GroupingHelpers.findOne(userId); if (!groupId) { - // debugger; + debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'upsert'); } } @@ -441,7 +439,7 @@ const userInsertHook = async function(userId, doc) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); groupId = await GroupingHelpers.findOne(userId); if (!groupId) { - // debugger; + debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); } } @@ -460,7 +458,7 @@ const userUpsertHook = async function(userId, selector, modifier) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); groupId = await GroupingHelpers.findOne(userId); if (!groupId) { - // debugger; + debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'upsert'); } } @@ -485,7 +483,7 @@ if (Partitioner.config.useMeteorUsers) { // Sync grouping needed for hooking Meteor.users // Only sync when using separate grouping collection if (!Partitioner.config.useMeteorUsers) { - GroupingHelpers.observeChanges({ + Grouping.direct.find().observeChangesAsync({ added: async function(id, fields) { if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { Meteor._debug(`Tried to set group for nonexistent user ${id}`); @@ -497,7 +495,7 @@ if (!Partitioner.config.useMeteorUsers) { } }, removed: async function(id) { - if (!await Meteor.users.updateAsync(id, {$unset: {"group": null}})) { + if (!await Meteor.users.updateAsync(id, {$unset: {"group": 1}})) { Meteor._debug(`Tried to unset group for nonexistent user ${id}`); } } From 997ade402ef4d0b73e91487aec5773df931379c4 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Thu, 2 Oct 2025 19:44:01 +0300 Subject: [PATCH 40/45] Publish new beta --- grouping.js | 65 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/grouping.js b/grouping.js index 220a57b..d0e999b 100644 --- a/grouping.js +++ b/grouping.js @@ -26,19 +26,36 @@ Partitioner._isDirectGroupContext = new Meteor.EnvironmentVariable(); Partitioner._directOps = new Meteor.EnvironmentVariable(); Partitioner._searchAllUsers = new Meteor.EnvironmentVariable(); -// Helper functions to abstract collection operations +// Internal helper functions to abstract partitioner storage operations +// Note: These are NOT MongoDB operations - they abstract whether we're using +// Meteor.users.group field or a separate ts.grouping collection const GroupingHelpers = { - async findOne(userId) { + // Gets the group ID for a user from the appropriate storage location + async getGroupIdForUser(userId) { if (Partitioner.config.useMeteorUsers) { - const user = await Meteor.users.direct.findOneAsync(userId, { fields: { group: 1 } }); - return user?.group; // Safe null access + const user = await Meteor.users.direct.findOneAsync(userId, { fields: { group: 1, _id: 1 } }); + + if (!user) { + Meteor._debug(`[Partitioner] getGroupIdForUser: User ${userId} not found`); + return null; + } + if (!user.group) { + Meteor._debug(`[Partitioner] getGroupIdForUser: User ${userId} has no group field`); + return null; + } + return user.group; } else { - const grouping = await Grouping.direct.findOneAsync(userId); - return grouping?.groupId; // Safe null access + const groupingDoc = await Grouping.direct.findOneAsync(userId); + if (!groupingDoc) { + Meteor._debug(`[Partitioner] getGroupIdForUser: No grouping document for user ${userId}`); + return null; + } + return groupingDoc.groupId; } }, - async upsert(userId, updateDoc) { + // Sets the group for a user in the appropriate storage location + async setUserGrouping(userId, updateDoc) { if (Partitioner.config.useMeteorUsers) { // When using Meteor.users, we need to adapt the field name from 'groupId' to 'group' const adaptedUpdateDoc = { ...updateDoc }; @@ -52,7 +69,8 @@ const GroupingHelpers = { } }, - async remove(userId) { + // Removes the group assignment for a user + async removeUserGrouping(userId) { if (Partitioner.config.useMeteorUsers) { return await Meteor.users.direct.updateAsync(userId, { $unset: { group: 1 } }); } else { @@ -111,12 +129,14 @@ Partitioner.configure = function(options) { Partitioner.setUserGroup = async function(userId, groupId) { check(userId, String); check(groupId, String); - if (await GroupingHelpers.findOne(userId)) { + if (await GroupingHelpers.getGroupIdForUser(userId)) { throw new Meteor.Error(403, "User is already in a group"); } - const result = await GroupingHelpers.upsert(userId, { - $set: {groupId: groupId} + // When using Meteor.users, set 'group' field; otherwise set 'groupId' in separate collection + const fieldName = Partitioner.config.useMeteorUsers ? 'group' : 'groupId'; + const result = await GroupingHelpers.setUserGrouping(userId, { + $set: {[fieldName]: groupId} }); return result; @@ -124,12 +144,12 @@ Partitioner.setUserGroup = async function(userId, groupId) { Partitioner.getUserGroup = async function(userId) { check(userId, String); - return await GroupingHelpers.findOne(userId); + return await GroupingHelpers.getGroupIdForUser(userId); }; Partitioner.clearUserGroup = async function(userId) { check(userId, String); - await GroupingHelpers.remove(userId); + await GroupingHelpers.removeUserGrouping(userId); }; Partitioner.group = async function() { @@ -157,10 +177,12 @@ Partitioner.bindGroup = async function(groupId, func) { Partitioner.bindUserGroup = async function(userId, func) { const groupId = await Partitioner.getUserGroup(userId); + if (!groupId) { - Meteor._debug(`Dropping operation because ${userId} is not in a group`); + Meteor._debug(`[Partitioner] bindUserGroup: Dropping operation because ${userId} is not in a group`); return; } + const result = await Partitioner._isDirectGroupContext.withValue(false, () => { return Partitioner._currentGroup.withValue(groupId, func); }); @@ -290,10 +312,10 @@ Meteor.publish(null, function() { // Special hook for Meteor.users to scope for each group const userFindHook = function(userId, selector, options) { const isDirectSelector = Helpers.isDirectUserSelector(selector); -if ( - ((Partitioner.config.allowDirectIdSelectors || Partitioner._searchAllUsers.get()) && isDirectSelector) - || Partitioner._directOps.get() === true -) return true; + if ( + ((Partitioner.config.allowDirectIdSelectors || Partitioner._searchAllUsers.get()) && isDirectSelector) + || Partitioner._directOps.get() === true + ) return true; let groupId = Partitioner._currentGroup.get(); let isDirectGroupContext = Partitioner._isDirectGroupContext.get(); @@ -314,7 +336,6 @@ if ( // Must fail fast and require proper context setup Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } - debugger; // Since user is in a group, scope the find to the group filter = { "group": groupId, @@ -354,14 +375,11 @@ const findHook = function(userId, selector, options) { if (userId) { if (!groupId) { - debugger; if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); // CANNOT do any async database calls here! // Must fail fast and require proper context setup - debugger; Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } - debugger; // force the selector to scope for the _groupId if (selector == null) { @@ -397,9 +415,8 @@ const insertHook = async function(multipleGroups, userId, doc) { let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - groupId = await GroupingHelpers.findOne(userId); + groupId = await GroupingHelpers.getGroupIdForUser(userId); if (!groupId) { - debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); } } From 0855bc980595d14b713763e2cedd4964c844a48c Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Thu, 2 Oct 2025 19:56:37 +0300 Subject: [PATCH 41/45] Fix merge conflicts --- grouping.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/grouping.js b/grouping.js index d0e999b..86469d3 100644 --- a/grouping.js +++ b/grouping.js @@ -433,9 +433,8 @@ const upsertHook = async function(multipleGroups, userId, selector, modifier) { let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - groupId = await GroupingHelpers.findOne(userId); + groupId = await GroupingHelpers.getGroupIdForUser(userId); if (!groupId) { - debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'upsert'); } } @@ -454,9 +453,8 @@ const userInsertHook = async function(userId, doc) { let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - groupId = await GroupingHelpers.findOne(userId); + groupId = await GroupingHelpers.getGroupIdForUser(userId); if (!groupId) { - debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'insert'); } } @@ -473,9 +471,8 @@ const userUpsertHook = async function(userId, selector, modifier) { let groupId = Partitioner._currentGroup.get(); if (!groupId) { if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - groupId = await GroupingHelpers.findOne(userId); + groupId = await GroupingHelpers.getGroupIdForUser(userId); if (!groupId) { - debugger; Helpers.throwVerboseError(this, ErrMsg.groupErr, 'upsert'); } } From 7a725f9eaaf5c10a5dc680627dc00c48b7c3cc36 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Fri, 3 Oct 2025 14:25:03 +0300 Subject: [PATCH 42/45] Publish new beta --- common.js | 1 + grouping.js | 123 +++++++++++++++++++++++++++++++++++++--------------- package.js | 2 +- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/common.js b/common.js index ccc9375..44395bb 100644 --- a/common.js +++ b/common.js @@ -10,6 +10,7 @@ Helpers = { isDirectSelector: function(selector) { return typeof selector === 'string' || typeof (selector != null ? selector._id : undefined) === 'string'; }, + // Helper function to detect login token verification queries isLoginTokenQuery: function(selector) { if (!selector || typeof selector !== 'object') return false; diff --git a/grouping.js b/grouping.js index 86469d3..7cb5ee3 100644 --- a/grouping.js +++ b/grouping.js @@ -143,6 +143,10 @@ Partitioner.setUserGroup = async function(userId, groupId) { }; Partitioner.getUserGroup = async function(userId) { + // Handle null/undefined userId gracefully - return null instead of throwing + if (!userId) { + return null; + } check(userId, String); return await GroupingHelpers.getGroupIdForUser(userId); }; @@ -179,7 +183,7 @@ Partitioner.bindUserGroup = async function(userId, func) { const groupId = await Partitioner.getUserGroup(userId); if (!groupId) { - Meteor._debug(`[Partitioner] bindUserGroup: Dropping operation because ${userId} is not in a group`); + Meteor._debug(`[Partitioner] bindUserGroup: Dropping operation because user ${userId || '(null)'} is not in a group`); return; } @@ -299,12 +303,13 @@ Partitioner.removeFromGroup = async function(collection, entityId, groupId) { return currentGroupIds; }; -// Publish admin and group for users that have it +// Publish admin, group, and username for users that have it Meteor.publish(null, function() { return Meteor.users.direct.find({ _id:this.userId }, { fields: { admin: 1, - group: 1 + group: 1, + username: 1 } }); }); @@ -312,13 +317,31 @@ Meteor.publish(null, function() { // Special hook for Meteor.users to scope for each group const userFindHook = function(userId, selector, options) { const isDirectSelector = Helpers.isDirectUserSelector(selector); + const searchAllUsers = Partitioner._searchAllUsers.get(); + const directOps = Partitioner._directOps.get(); + + // Allow direct selectors in these cases: + // 1. allowDirectIdSelectors config is true + // 2. _searchAllUsers context is set + // 3. _directOps context is set + // 4. No userId context (pre-authentication) + // 5. Has userId but no groupId yet (during authentication) if ( - ((Partitioner.config.allowDirectIdSelectors || Partitioner._searchAllUsers.get()) && isDirectSelector) - || Partitioner._directOps.get() === true - ) return true; + ((Partitioner.config.allowDirectIdSelectors || searchAllUsers) && isDirectSelector) + || directOps === true + || (!userId && isDirectSelector) // Pre-auth + ) { + return true; + } let groupId = Partitioner._currentGroup.get(); let isDirectGroupContext = Partitioner._isDirectGroupContext.get(); + + // NEW: Allow direct selectors during authentication (userId exists but no groupId) + if (userId && isDirectSelector && !groupId) { + return true; + } + // This hook doesn't run if we're not in a method invocation or publish // function, and Partitioner._currentGroup is not set if (!userId && !groupId) return true; @@ -331,7 +354,6 @@ const userFindHook = function(userId, selector, options) { } if (!groupId) { - debugger; // CANNOT do any async database calls here! // Must fail fast and require proper context setup Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); @@ -363,46 +385,49 @@ const findHook = function(userId, selector, options) { // We could amend this in the future to {_id: someId, _groupId: groupId} // https://github.com/mizzao/meteor-partitioner/issues/9 // https://github.com/mizzao/meteor-partitioner/issues/10 - if (Partitioner._directOps.get() === true || (Partitioner.config.allowDirectIdSelectors && Helpers.isDirectSelector(selector))) return true; - + if (Partitioner._directOps.get() === true || + (Partitioner.config.allowDirectIdSelectors && Helpers.isDirectSelector(selector))) + return true; // Check for global hook let groupId = Partitioner._currentGroup.get(); - + if (!userId && !groupId) { throw new Meteor.Error(403, ErrMsg.userIdErr); } + + // If direct selector and no groupId, allow it to pass through unchanged + if (Helpers.isDirectSelector(selector) && !groupId) { + return true; + } if (userId) { if (!groupId) { - if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - // CANNOT do any async database calls here! - // Must fail fast and require proper context setup + // Non-direct selectors require context Helpers.throwVerboseError(this, ErrMsg.groupFindErr, 'find'); } - // force the selector to scope for the _groupId - if (selector == null) { - this.args[0] = { - _groupId: groupId, - }; - } else if (typeof selector == 'string') { - this.args[0] = { - _id: selector, - _groupId: groupId, - }; - } else { - selector._groupId = groupId; - } + // force the selector to scope for the _groupId + if (selector == null) { + this.args[0] = { + _groupId: groupId, + }; + } else if (typeof selector == 'string') { + this.args[0] = { + _id: selector, + _groupId: groupId, + }; + } else { + selector._groupId = groupId; + } - // Adjust options to not return _groupId - if (options == null) { - this.args[1] = {fields: {_groupId: 0}}; - } else { - // If options already exist, add {_groupId: 0} unless fields has {foo: 1} somewhere - if (options.fields == null) options.fields = {}; - if (!Object.values(options.fields).some(v => v)) options.fields._groupId = 0; - } + // Adjust options to not return _groupId + if (options == null) { + this.args[1] = {fields: {_groupId: 0}}; + } else { + if (options.fields == null) options.fields = {}; + if (!Object.values(options.fields).some(v => v)) options.fields._groupId = 0; + } } return true; @@ -523,7 +548,26 @@ if (!Partitioner.config.useMeteorUsers) { // Don't wrap createUser with Partitioner.directOperation because want inserted user doc to be // automatically assigned to the group if (Partitioner.config.useMeteorUsers) { - ['createUser', 'findUserByEmail', 'findUserByUsername', '_attemptLogin'].forEach(fn => { + // Wrap all authentication-related methods that query/modify users across partitions + [ + 'createUser', + 'findUserByEmail', + 'findUserByUsername', + '_attemptLogin', + '_findUserByQuery', // Used internally for login token verification + '_loginUser', // Used during login process + 'updateOrCreateUserFromExternalService', // OAuth logins + '_expireTokens', // Token expiration during logout + 'removeOtherTokens', // Remove other tokens during logout + '_clearAllLoginTokens', // Clear all tokens + '_setLoginToken', // Set login token + '_insertLoginToken', // Insert login token + '_getTokenLifetimeMs', // Get token lifetime + '_tokenExpiration', // Token expiration calculation + 'setPassword', // Password reset + '_checkPassword', // Password verification + '_hashPassword', // Password hashing + ].forEach(fn => { const orig = Accounts[fn]; if (orig) { Accounts[fn] = function() { @@ -537,7 +581,14 @@ TestFuncs = { getPartitionedIndex: getPartitionedIndex, userFindHook: userFindHook, findHook: findHook, - insertHook: insertHook, + // Export insertHook wrapper that matches how tests call it (userId, doc) + // and defaults multipleGroups to false for single-group collections + insertHook: async function(userId, doc) { + // Call with multipleGroups=false to match basic test expectations + return await insertHook.call(this, false, userId, doc); + }, + // Export the raw insertHook for advanced testing if needed + insertHookRaw: insertHook, Grouping: Grouping, GroupingHelpers: GroupingHelpers, config: Partitioner.config diff --git a/package.js b/package.js index b260a5d..907f28c 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.8", + version: "0.7.0-beta.10", git: "https://github.com/mizzao/meteor-partitioner.git" }); From 805714561964dd202ee9080a2a36b052c4ef8e36 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Fri, 3 Oct 2025 16:57:31 +0300 Subject: [PATCH 43/45] Add logging for direct access --- common.js | 37 +++++++++++++++++++++++++++++++++++++ grouping.js | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/common.js b/common.js index 44395bb..4825064 100644 --- a/common.js +++ b/common.js @@ -43,6 +43,43 @@ Helpers = { Helpers.isLoginTokenQuery(selector); }, + // Helper function to warn about direct selector bypassing partition filter + warnDirectSelectorBypass: function(hookContext, selector, reason) { + const collectionName = hookContext.rawCollection?.().collectionName || hookContext._name || 'unknown'; + const operation = 'find'; + + // Get stack trace to show where this is being called from + const stack = new Error().stack; + const stackLines = stack.split('\n'); + + // Find the first meaningful line (skip internal frames) + let callerLine = 'unknown'; + for (let i = 2; i < stackLines.length; i++) { + const line = stackLines[i]; + // Skip internal/anonymous frames + if (line.includes('packages/') || + line.includes('node_modules/') || + line.includes('()') || + line.includes('Array.forEach')) { + continue; + } + callerLine = line.trim(); + break; + } + + console.warn( + `[Partitioner Security Warning]\n` + + ` Collection: ${collectionName}\n` + + ` Operation: ${operation}\n` + + ` Selector: ${JSON.stringify(selector)}\n` + + ` Reason: ${reason}\n` + + ` Called from: ${callerLine}\n` + + ` Issue: Direct selector query bypassing partition filter.\n` + + ` Risk: May allow cross-partition access.\n` + + ` Fix: Wrap with Partitioner.bindUserGroup() or Partitioner.bindGroup().` + ); + }, + // Helper function to log verbose error details and throw appropriate error throwVerboseError: function(hookContext, errorMessage, defaultOperation = 'unknown') { const operation = hookContext.name || defaultOperation; diff --git a/grouping.js b/grouping.js index 7cb5ee3..8e8974c 100644 --- a/grouping.js +++ b/grouping.js @@ -326,11 +326,25 @@ const userFindHook = function(userId, selector, options) { // 3. _directOps context is set // 4. No userId context (pre-authentication) // 5. Has userId but no groupId yet (during authentication) - if ( - ((Partitioner.config.allowDirectIdSelectors || searchAllUsers) && isDirectSelector) - || directOps === true - || (!userId && isDirectSelector) // Pre-auth - ) { + if ((Partitioner.config.allowDirectIdSelectors || searchAllUsers) && isDirectSelector) { + let reason = Partitioner.config.allowDirectIdSelectors + ? 'allowDirectIdSelectors config is enabled' + : 'searchAllUsers context (authentication flow)'; + Helpers.warnDirectSelectorBypass(this, selector, reason); + return true; + } + + if (directOps === true) { + // directOps bypasses ALL filtering, not just direct selectors + return true; + } + + if (!userId && isDirectSelector) { + Helpers.warnDirectSelectorBypass( + this, + selector, + 'Pre-authentication - no userId context yet' + ); return true; } @@ -339,6 +353,11 @@ const userFindHook = function(userId, selector, options) { // NEW: Allow direct selectors during authentication (userId exists but no groupId) if (userId && isDirectSelector && !groupId) { + Helpers.warnDirectSelectorBypass( + this, + selector, + 'User authentication flow - userId exists but no groupId context' + ); return true; } @@ -385,11 +404,21 @@ const findHook = function(userId, selector, options) { // We could amend this in the future to {_id: someId, _groupId: groupId} // https://github.com/mizzao/meteor-partitioner/issues/9 // https://github.com/mizzao/meteor-partitioner/issues/10 - if (Partitioner._directOps.get() === true || - (Partitioner.config.allowDirectIdSelectors && Helpers.isDirectSelector(selector))) + // directOps or allowDirectIdSelectors with direct selector - bypass with warning + if (Partitioner._directOps.get() === true) { + // directOps bypasses ALL filtering return true; + } + + if (Partitioner.config.allowDirectIdSelectors && Helpers.isDirectSelector(selector)) { + Helpers.warnDirectSelectorBypass( + this, + selector, + 'allowDirectIdSelectors config is enabled' + ); + return true; + } - // Check for global hook let groupId = Partitioner._currentGroup.get(); if (!userId && !groupId) { @@ -398,6 +427,11 @@ const findHook = function(userId, selector, options) { // If direct selector and no groupId, allow it to pass through unchanged if (Helpers.isDirectSelector(selector) && !groupId) { + Helpers.warnDirectSelectorBypass( + this, + selector, + 'Direct ID selector without explicit partition context' + ); return true; } From 782fdc9df42c58c424eabe57fc09c11639704f0f Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Fri, 3 Oct 2025 16:57:47 +0300 Subject: [PATCH 44/45] 0.7.0-beta.11 --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index 907f28c..51932ea 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.10", + version: "0.7.0-beta.11", git: "https://github.com/mizzao/meteor-partitioner.git" }); From cf8cf53c564b8d7907d9b0cb18f7a4d67723fc26 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Fri, 3 Oct 2025 18:19:38 +0300 Subject: [PATCH 45/45] Publish new beta --- common.js | 30 ++++++++++++++++++++++++++++++ grouping.js | 20 ++++++++++++++++---- package.js | 2 +- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/common.js b/common.js index 4825064..224de0e 100644 --- a/common.js +++ b/common.js @@ -87,5 +87,35 @@ Helpers = { const params = hookContext.args ? JSON.stringify(hookContext.args, null, 2) : 'no parameters'; Meteor._debug(`Collection: ${collection}, Operation: ${operation}, Parameters: ${params}`); throw new Meteor.Error(403, errorMessage); + }, + + // Helper function to log informational messages with stack trace + logWithStackTrace: function(message, data) { + // Get stack trace to show where this is being called from + const stack = new Error().stack; + const stackLines = stack.split('\n'); + + // Find the first meaningful line (skip internal frames) + let callerLine = 'unknown'; + for (let i = 2; i < stackLines.length; i++) { + const line = stackLines[i]; + // Skip internal/anonymous frames + if (line.includes('packages/') || + line.includes('node_modules/') || + line.includes('()') || + line.includes('Array.forEach')) { + continue; + } + callerLine = line.trim(); + break; + } + + let output = `[Partitioner Info] ${message}\n`; + if (data) { + output += ` Data: ${JSON.stringify(data)}\n`; + } + output += ` Called from: ${callerLine}`; + + Meteor._debug(output); } }; \ No newline at end of file diff --git a/grouping.js b/grouping.js index 8e8974c..7e60dae 100644 --- a/grouping.js +++ b/grouping.js @@ -183,7 +183,10 @@ Partitioner.bindUserGroup = async function(userId, func) { const groupId = await Partitioner.getUserGroup(userId); if (!groupId) { - Meteor._debug(`[Partitioner] bindUserGroup: Dropping operation because user ${userId || '(null)'} is not in a group`); + Helpers.logWithStackTrace( + 'bindUserGroup: Dropping operation because user is not in a group', + { userId: userId || '(null)' } + ); return; } @@ -559,17 +562,26 @@ if (!Partitioner.config.useMeteorUsers) { Grouping.direct.find().observeChangesAsync({ added: async function(id, fields) { if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { - Meteor._debug(`Tried to set group for nonexistent user ${id}`); + Helpers.logWithStackTrace( + 'Tried to set group for nonexistent user', + { userId: id, groupId: fields.groupId } + ); } }, changed: async function(id, fields) { if (!await Meteor.users.updateAsync(id, {$set: {"group": fields.groupId}})) { - Meteor._debug(`Tried to change group for nonexistent user ${id}`); + Helpers.logWithStackTrace( + 'Tried to change group for nonexistent user', + { userId: id, groupId: fields.groupId } + ); } }, removed: async function(id) { if (!await Meteor.users.updateAsync(id, {$unset: {"group": 1}})) { - Meteor._debug(`Tried to unset group for nonexistent user ${id}`); + Helpers.logWithStackTrace( + 'Tried to unset group for nonexistent user', + { userId: id } + ); } } }); diff --git a/package.js b/package.js index 51932ea..1b4c5b3 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "0.7.0-beta.11", + version: "0.7.0-beta.12", git: "https://github.com/mizzao/meteor-partitioner.git" });