Skip to content

Commit f4bc354

Browse files
joewizclaude
andcommitted
test(query): Cypress e2e for external variable bindings
Adds a `variables` describe block to query.cy.js covering: scalar string binding; JSON number → xs:double; JSON array → array(*) (members via ?*); JSON object → map(*); a supplied name with no matching external declaration is ignored; and a missing required external returns an error. All use no-default externals, so the spec is independent of the eXist-core default-override fix (eXist-db/exist#6475). 37/37 green locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 850b847 commit f4bc354

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

src/test/cypress/e2e/query.cy.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,4 +464,85 @@ describe('/api/query', () => {
464464
});
465465
});
466466
});
467+
468+
describe('external variable bindings (variables)', () => {
469+
// POST a query with a `variables` map, fetch the first page, hand it to `then`, then close.
470+
function runWithVars(query, variables, then) {
471+
cy.request({
472+
url: '/api/query', method: 'POST', auth, body: { query, variables }
473+
}).then(postResponse => {
474+
expect(postResponse.status).to.eq(200);
475+
const cursor = postResponse.body.cursor;
476+
cy.request({
477+
url: `/api/query/${cursor}/results?start=1&count=10`, auth
478+
}).then(fetchResponse => {
479+
then(fetchResponse.body);
480+
cy.request({ url: `/api/query/${cursor}`, method: 'DELETE', auth });
481+
});
482+
});
483+
}
484+
485+
it('binds a scalar string to an external variable', () => {
486+
runWithVars(
487+
'declare variable $greeting external; <g>{$greeting}</g>',
488+
{ greeting: 'hello' },
489+
items => {
490+
expect(items).to.have.length(1);
491+
expect(items[0].value).to.eq('<g>hello</g>');
492+
});
493+
});
494+
495+
it('binds a JSON number as xs:double', () => {
496+
runWithVars(
497+
'declare variable $n external; (xs:integer($n) * 2, $n instance of xs:double)',
498+
{ n: 21 },
499+
items => {
500+
expect(items[0].value).to.eq('42');
501+
expect(items[1].value).to.eq('true()');
502+
});
503+
});
504+
505+
it('binds a JSON array as array(*); members are reached with ?*', () => {
506+
runWithVars(
507+
'declare variable $ids external; (array:size($ids), count($ids?*), string-join($ids?*, "|"))',
508+
{ ids: ['d278', 'd300', 'd12'] },
509+
items => {
510+
expect(items[0].value).to.eq('3'); // array:size — bound as an array
511+
expect(items[1].value).to.eq('3'); // count($ids?*) — members as a sequence
512+
expect(items[2].value).to.eq('"d278|d300|d12"'); // adaptive-quoted xs:string
513+
});
514+
});
515+
516+
it('binds a JSON object as map(*)', () => {
517+
runWithVars(
518+
'declare variable $opts external; $opts?indent',
519+
{ opts: { indent: 'yes', method: 'xml' } },
520+
items => {
521+
expect(items[0].value).to.eq('"yes"');
522+
});
523+
});
524+
525+
it('ignores a supplied name with no matching external declaration', () => {
526+
runWithVars(
527+
'declare variable $used external; <r>{$used}</r>',
528+
{ used: 'X', unused: 'Y' },
529+
items => {
530+
expect(items[0].value).to.eq('<r>X</r>');
531+
});
532+
});
533+
534+
it('a missing required external variable propagates as an XQuery error', () => {
535+
// No default and no supplied value → XPDY0002 from cursor:eval, which
536+
// propagates with a non-200 status and structured fields (not a 200 error map).
537+
cy.request({
538+
url: '/api/query', method: 'POST', auth,
539+
body: { query: 'declare variable $required external; $required' },
540+
failOnStatusCode: false
541+
}).then(response => {
542+
expect(response.status).to.be.oneOf([400, 500]);
543+
expect(response.body).to.have.property('code');
544+
expect(response.body.code).to.match(/XPDY0002/);
545+
});
546+
});
547+
});
467548
});

0 commit comments

Comments
 (0)