-
-
Notifications
You must be signed in to change notification settings - Fork 26
test: Expose critical AccessPolicy permission validation failures #2759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Comprehensive test suite reveals multiple permission system bugs: - Type validation failures in AppModel configuration - Broken permission hierarchy (changePermission without write) - Silent failures in policy creation and broadcasting - Integration inconsistencies across views and models Tests document expected failures and provide roadmap for fixes.
robyngit
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all these tests @mlhenderson, this is fantastic!! I like the new coverage report also, very helpful! I left comments on a few of the tests. Some of the "expected failures" are actually just because of little mix ups about how backbone behaves - it's not always intuitive. And the others are questions about the rationale behind testing serializing/parsing the dataONEObject as JSON. We usually use the serialize method to save as XML, but I could be missing something here.
Finally, I'm having trouble running a number of the new package.json scripts. For example, I can see the test site when I run view-tests, and for most of the new scripts I'm seeing errors like [PAGE error] Failed to load resource: the server responded with a status of 404 (404). I ran npm install to update my local deps, but is there something else I need to do to get these working?
Thanks again for working on this!
| it.skip("EXPECTED FAILURE: always returns an <accesspolicy> element (never null)", function() { | ||
| const el = state.policy.serialize(); | ||
| expect(el && el.nodeType).to.equal(1); | ||
| expect(el.tagName.toLowerCase()).to.equal("accesspolicy"); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning null when a node is empty is the general serialize pattern that MetacatUI uses. We use AccessPolicy.serialize in DataONEObject.serializeSysMeta:
https://github.com/NCEAS/metacatui/blob/main/src/js/models/DataONEObject.js#L1129-L1149
We don't want to add an empty accessPolicy to the sysMeta. However, is there a reason you're thinking of that makes it better to return <accesspolicy></accesspolicy>?
| rule.trigger("removeMe"); | ||
| expect(state.policy.length).to.equal(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| rule.trigger("removeMe"); | |
| expect(state.policy.length).to.equal(0); | |
| state.policy.listenToOnce(state.policy, "remove", () => { | |
| expect(state.policy.length).to.equal(0); | |
| }); | |
| rule.trigger("removeMe"); |
The original test fails because it checks the collection length immediately after triggering "removeMe", before the asynchronous removal actually completes. Waiting for the "remove" event makes sure the assertion runs when the model has actually been removed. It was backbone's solution of ensuring async events had completed, back before we had Promises, await/async!
| it.skip("EXPECTED FAILURE: set() with partial list should NOT remove rules not included", function() { | ||
| const editedBob = new AccessRule({ | ||
| subject: "uid:bob", | ||
| read: true, | ||
| write: true, | ||
| dataONEObject: obj | ||
| }); | ||
|
|
||
| policy.set([editedBob]); | ||
|
|
||
| // This should fail - we expect ALL rules to be preserved | ||
| expect(policy.length).to.equal(4, "All rules should be preserved"); | ||
| expect(policy.findWhere({ subject: "uid:alice" })).to.exist; | ||
| expect(policy.findWhere({ subject: "uid:carol" })).to.exist; | ||
| expect(policy.findWhere({ subject: "public" })).to.exist; | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this is how we expect collection.set to work, see the backbone.js docs:
... and if the collection contains any models that aren't present in the list, they'll be removed
We can pass the options { remove: false } to set to keep other models, but then we will actually have 5 rules, not 4 as your test asserts. I believe backbone will only merge the two models if the model IDs are the same (pretty sure, but verify)
| it.skip("EXPECTED FAILURE: safe usage: set() with remove:false merges edits without removing other rules", function() { | ||
| // Simulate UI sending only an edited subset | ||
| const editedBob = makeRule("uid:bob", ["read", "write"]); | ||
| policy.set([editedBob], { merge: true, remove: false }); | ||
|
|
||
| expect(policy).to.have.lengthOf(4); | ||
| expect(getSubjects(policy)).to.have.members(["uid:alice", "uid:bob", "uid:carol", "public"]); | ||
| expect(policy.findWhere({ subject: "uid:bob" }).get("permissions")).to.have.members(["read", "write"]); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above about merging. This isn't an expected failure, backbone has no way of knowing that the subject is the key that is being used to identify models.
| it.skip("EXPECTED FAILURE: re-adding a rule for the same subject merges permissions and does not remove or duplicate", function() { | ||
| expect(policy.length).to.equal(4); | ||
|
|
||
| // Try to add another rule for alice | ||
| policy.add(new AccessRule({ | ||
| subject: "uid:alice", | ||
| changePermission: true, | ||
| dataONEObject: obj | ||
| })); | ||
|
|
||
| // Should not create duplicate - still 4 rules | ||
| expect(policy.length).to.equal(4); | ||
| const aliceRules = policy.where({ subject: "uid:alice" }); | ||
| expect(aliceRules.length).to.equal(1); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above re: merging
| it.skip("EXPECTED FAILURE: rebuilding from a visible subset should NOT drop non-visible rules", function() { | ||
| const visibleRules = policy.first(2); | ||
| const visibleRulesJSON = visibleRules.map(rule => rule.toJSON()); | ||
|
|
||
| policy.reset(visibleRulesJSON); | ||
|
|
||
| // This should fail - we expect NO data loss | ||
| expect(policy.length).to.equal(4, "No rules should be lost during rebuild"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the expected behaviour for collection.reset
Use reset to replace a collection with a new list of models
So in this case, expect(policy.length).to.equal(2) <--- 2 not 4
| console.log("Has serialize method:", typeof state.collection.serialize === 'function'); | ||
|
|
||
| // Simulate what happens during save/sync - JSON serialization | ||
| var jsonData = state.dataONEObject.toJSON(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm missing something here, why are we converting the dataONEObject to JSON? We send the access policy to the server as serialized XML (sys meta), so just need help understanding what you mean when you suggest we're simulating what happens during save to server?
| var savedJson = state.dataONEObject.toJSON(); | ||
| var reloadedObject = new DataONEObject(savedJson); | ||
|
|
||
| // Check how many students retained access | ||
| var reloadedPolicy = reloadedObject.get("accessPolicy"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you planning to implement a strategy to cache to access policy in case of a reload?
| var jsonData = state.dataObject.toJSON(); | ||
| var reloadedObject = new DataONEObject(jsonData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar questions here, why to JSON and when/how is the dataone obj data being reloaded in the app?
| let triggerSpy = null; | ||
| // Add sinon spy for tests that need it | ||
| if (typeof sinon !== "undefined") { | ||
| sinon.spy(policy, "trigger"); | ||
| } | ||
|
|
||
| return { dataONEObject, policy }; | ||
| }, beforeEach); | ||
|
|
||
| // Add afterEach to clean up spies | ||
| afterEach(function() { | ||
| if (state.triggerSpy) { | ||
| state.triggerSpy.restore(); | ||
| } | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can remove triggerSpy, not used here
Comprehensive test suite reveals multiple permission system bugs:
Tests document expected failures and provide roadmap for fixes.