Skip to content

Commit

Permalink
Validate savetos on structural fields (#952)
Browse files Browse the repository at this point in the history
* Add more dataset validation tests

* Disallowed savetos in repeat groups

* Updated tests a little bit
  • Loading branch information
ktuite authored Aug 22, 2023
1 parent 0fb4ebd commit 841f215
Show file tree
Hide file tree
Showing 3 changed files with 277 additions and 1 deletion.
5 changes: 4 additions & 1 deletion lib/data/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ const _recurseFormFields = (instance, bindings, repeats, selectMultiples, envelo
if (binding != null) {
field.type = binding.type || 'unknown'; // binding should have a type.
const prop = saveToAttr(binding);
if (prop)
if (prop) {
if (Array.from(repeats).find((repeat) => bindingPath.startsWith(repeat)))
throw Problem.user.invalidEntityForm({ reason: 'Currently, entities cannot be populated from fields in repeat groups.' });
field.propertyName = binding[prop];
}
} else if (tag.children != null) {
// if we have no binding node but we have children, assume this is a
// structural node with no repeat or direct data binding; recurse.
Expand Down
108 changes: 108 additions & 0 deletions test/integration/api/datasets.js
Original file line number Diff line number Diff line change
Expand Up @@ -2030,6 +2030,114 @@ describe('datasets and entities', () => {
}));
}));

it('should ignore a saveto incorrrectly placed on a bind on a structural field', testService(async (service) => {
const alice = await service.login('alice');
const xml = `<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml" xmlns:jr="http://openrosa.org/javarosa" xmlns:entities="http://www.opendatakit.org/xforms">
<h:head>
<model entities:entities-version='2022.1.0'>
<instance>
<data id="validate_structure">
<name/>
<age/>
<group>
<inner_field/>
</group>
<meta>
<entity dataset="things" id="" create="1">
<label/>
</entity>
</meta>
</data>
</instance>
<bind nodeset="/data/name" type="string" entities:saveto="prop1"/>
<bind nodeset="/data/group" entities:saveto="prop2"/>
<bind nodeset="/data/group/inner_field" type="string" entities:saveto="prop3"/>
</model>
</h:head>
</h:html>`;
await alice.post('/v1/projects/1/forms')
.send(xml)
.set('Content-Type', 'application/xml')
.expect(200);
await alice.get('/v1/projects/1/forms/validate_structure/draft/dataset-diff')
.expect(200)
.then(({ body }) => {
const { properties } = body[0];
properties.length.should.equal(2);
properties[0].name.should.equal('prop1');
properties[1].name.should.equal('prop3');
});
await alice.post('/v1/projects/1/forms/validate_structure/draft/publish')
.expect(200);
await alice.get('/v1/projects/1/datasets/things')
.expect(200)
.then(({ body }) => {
body.name.should.be.eql('things');
const { properties } = body;
properties.length.should.equal(2);
properties[0].name.should.equal('prop1');
properties[1].name.should.equal('prop3');
});
}));

it('should throw an error when a saveto is on a field inside a repeat group', testService(async (service) => {
// Entities made from repeat groups are not yet supported. pyxform also throws an error about this.
const form = `<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:entities="http://www.opendatakit.org/xforms/entities" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:h="http://www.w3.org/1999/xhtml" xmlns:jr="http://openrosa.org/javarosa" xmlns:odk="http://www.opendatakit.org/xforms" xmlns:orx="http://openrosa.org/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<h:head>
<h:title>Repeat Children Entities</h:title>
<model entities:entities-version="2022.1.0" odk:xforms-version="1.0.0">
<instance>
<data id="repeat_entity" version="2">
<num_children/>
<child>
<child_name/>
</child>
<meta>
<instanceID/>
<instanceName/>
<entity create="1" dataset="children" id="">
<label/>
</entity>
</meta>
</data>
</instance>
<bind entities:saveto="num_children" nodeset="/data/num_children" type="int"/>
<bind entities:saveto="child_name" nodeset="/data/child/child_name" type="string"/>
<bind jr:preload="uid" nodeset="/data/meta/instanceID" readonly="true()" type="string"/>
<bind calculate=" /data/num_children " nodeset="/data/meta/instanceName" type="string"/>
<bind calculate="1" nodeset="/data/meta/entity/@create" readonly="true()" type="string"/>
<bind nodeset="/data/meta/entity/@id" readonly="true()" type="string"/>
<setvalue event="odk-instance-first-load" readonly="true()" ref="/data/meta/entity/@id" type="string" value="uuid()"/>
<bind calculate="concat(&quot;Num children:&quot;, /data/num_children )" nodeset="/data/meta/entity/label" readonly="true()" type="string"/>
</model>
</h:head>
<h:body>
<input ref="/data/num_children">
<label>Num Children</label>
</input>
<group ref="/data/child">
<label>Child</label>
<repeat nodeset="/data/child">
<input ref="/data/child/child_name">
<label>Child Name</label>
</input>
</repeat>
</group>
</h:body>
</h:html>
`;
const alice = await service.login('alice');
await alice.post('/v1/projects/1/forms?publish=true')
.send(form)
.set('Content-Type', 'application/xml')
.expect(400)
.then(({ body }) => {
body.code.should.equal(400.25);
body.details.reason.should.equal('Currently, entities cannot be populated from fields in repeat groups.');
});
}));

it('should publish dataset when any dataset creating form is published', testService(async (service) => {
const alice = await service.login('alice');

Expand Down
165 changes: 165 additions & 0 deletions test/unit/data/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,171 @@ describe('form schema', () => {
]);
});
});

describe('datasets', () => {
it('should ignore entities:saveto in bindings on structural nodes', () => { // gh cb#670
// binds must have a 'type' attribute to be picked up by XML parsing.
const xml = `
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jr="http://openrosa.org/javarosa">
<h:head>
<model>
<instance>
<data id="form">
<name/>
<occupation>
<title/>
<dates>
<joined/>
<departed/>
</dates>
<salary/>
</occupation>
</data>
</instance>
<bind nodeset="/data/name" type="string"/>
<bind nodeset="/data/occupation" relevant="/data/name='liz'" entities:saveto="occupation"/>
<bind nodeset="/data/occupation/title" type="string"/>
<bind nodeset="/data/occupation/dates" relevant="true()"/>
<bind nodeset="/data/occupation/dates/joined" type="date"/>
<bind nodeset="/data/occupation/dates/departed" type="date"/>
<bind nodeset="/data/occupation/salary" type="decimal"/>
</model>
</h:head>
</h:html>`;
return getFormFields(xml).then((schema) => {
schema.should.eql([
{ name: 'name', path: '/name', type: 'string', order: 0 },
{ name: 'occupation', path: '/occupation', type: 'structure', order: 1 },
{ name: 'title', path: '/occupation/title', type: 'string', order: 2 },
{ name: 'dates', path: '/occupation/dates', type: 'structure', order: 3 },
{ name: 'joined', path: '/occupation/dates/joined', type: 'date', order: 4 },
{ name: 'departed', path: '/occupation/dates/departed', type: 'date', order: 5 },
{ name: 'salary', path: '/occupation/salary', type: 'decimal', order: 6 }
]);
});
});

it('should reject binds on fields in repeats', () => { // gh cb#670
const xml = `
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jr="http://openrosa.org/javarosa">
<h:head>
<model>
<instance>
<data id="form">
<name/>
<children>
<child>
<name/>
<toy>
<name/>
</toy>
</child>
</children>
</data>
</instance>
<bind nodeset="/data/name" type="string" entities:saveto="parent_name"/>
<bind nodeset="/data/children/child/name" type="string" entities:saveto="child_name"/>
<bind nodeset="/data/children/child/toy/name" type="string"/>
</model>
</h:head>
<h:body>
<input ref="/data/name">
<label>What is your name?</label>
</input>
<group ref="/data/children/child">
<label>Child</label>
<repeat nodeset="/data/children/child">
<input ref="/data/children/child/name">
<label>What is the child's name?</label>
</input>
<group ref="/data/children/child/toy">
<label>Child</label>
<repeat nodeset="/data/children/child/toy">
<input ref="/data/children/child/toy/name">
<label>What is the toy's name?</label>
</input>
</repeat>
</group>
</repeat>
</group>
</h:body>
</h:html>`;
return getFormFields(xml).should.be.rejected().then((p) => p.problemCode.should.equal(400.25));
});

it('should reject binds on fields in nested repeats inside groups', () => { // gh cb#670
const xml = `
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:entities="http://www.opendatakit.org/xforms/entities" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:h="http://www.w3.org/1999/xhtml" xmlns:jr="http://openrosa.org/javarosa" xmlns:odk="http://www.opendatakit.org/xforms" xmlns:orx="http://openrosa.org/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<h:head>
<h:title>Repeat Children Entities</h:title>
<model entities:entities-version="2022.1.0" odk:xforms-version="1.0.0">
<instance>
<data id="repeat_entity" version="1">
<num_children/>
<children>
<child jr:template="">
<child_name/>
<possessions>
<toys jr:template="">
<toy/>
</toys>
</possessions>
</child>
</children>
<meta>
<instanceID/>
<instanceName/>
<entity create="1" dataset="children" id="">
<label/>
</entity>
</meta>
</data>
</instance>
<bind nodeset="/data/num_children" type="int"/>
<bind nodeset="/data/children/child/child_name" type="string"/>
<bind entities:saveto="toy_name" nodeset="/data/children/child/possessions/toys/toy" type="string"/>
<bind jr:preload="uid" nodeset="/data/meta/instanceID" readonly="true()" type="string"/>
<bind calculate=" /data/num_children " nodeset="/data/meta/instanceName" type="string"/>
<bind calculate="1" nodeset="/data/meta/entity/@create" readonly="true()" type="string"/>
<bind nodeset="/data/meta/entity/@id" readonly="true()" type="string"/>
<setvalue event="odk-instance-first-load" readonly="true()" ref="/data/meta/entity/@id" type="string" value="uuid()"/>
<bind calculate="concat(&quot;Num children:&quot;, /data/num_children )" nodeset="/data/meta/entity/label" readonly="true()" type="string"/>
</model>
</h:head>
<h:body>
<input ref="/data/num_children">
<label>Num Children</label>
</input>
<group ref="/data/children">
<label>Children</label>
<group ref="/data/children/child">
<label>Child</label>
<repeat nodeset="/data/children/child">
<input ref="/data/children/child/child_name">
<label>Child Name</label>
</input>
<group ref="/data/children/child/possessions">
<label>Posessions</label>
<group ref="/data/children/child/possessions/toys">
<label>Toys</label>
<repeat nodeset="/data/children/child/possessions/toys">
<input ref="/data/children/child/possessions/toys/toy">
<label>Toy</label>
</input>
</repeat>
</group>
</group>
</repeat>
</group>
</group>
</h:body>
</h:html>`;
return getFormFields(xml).should.be.rejected().then((p) => p.problemCode.should.equal(400.25));
});
});
});

describe('SchemaStack', () => {
Expand Down

0 comments on commit 841f215

Please sign in to comment.