11const auth = { username : 'admin' , password : '' } ;
22
3- // These tests rely on the `doc` app (eXist documentation) being installed and
4- // full-text indexed — it ships with the existdb/existdb image. "xquery" is a
5- // term with many hits across the docs.
3+ // /api/search queries the shared `site-content` Lucene field that content apps
4+ // contribute to. A bare existdb-openapi install (CI image) has no producer app
5+ // populating `site-content`, so these tests seed their own self-contained
6+ // fixture — a small collection under /db/apps with a `site-content` index — and
7+ // search that. The term "xquery" appears across the fixture; the app id is
8+ // `cypress-search-test`. Setup/teardown run admin XQuery via /api/query.
9+ const APP = 'cypress-search-test' ;
10+ const TERM = 'xquery' ;
11+ const DATA = `/db/apps/${ APP } /data` ;
12+ const CONF = `/db/system/config/db/apps/${ APP } /data` ;
13+
14+ const SETUP = `
15+ let $xconf :=
16+ <collection xmlns="http://exist-db.org/collection-config/1.0">
17+ <index><lucene>
18+ <analyzer class="org.apache.lucene.analysis.standard.StandardAnalyzer"/>
19+ <text qname="page">
20+ <field name="site-content" expression="string-join((title, body)//text(), ' ')"/>
21+ <field name="site-title" expression="string(title)"/>
22+ <field name="site-url" expression="string(url)"/>
23+ <facet dimension="site-app" expression="'${ APP } '"/>
24+ <facet dimension="site-section" expression="string(@section)"/>
25+ </text>
26+ </lucene></index>
27+ </collection>
28+ let $mk := function($n as xs:string, $sec as xs:string, $title as xs:string, $body as xs:string) {
29+ <page section="{$sec}"><title>{$title}</title><url>/exist/apps/${ APP } /{$sec}/{$n}</url><body>{$body}</body></page>
30+ }
31+ let $docs := (
32+ $mk("p1","guide","XQuery basics","An introduction to XQuery, the query language for XML."),
33+ $mk("p2","guide","XQuery functions","XQuery has many built-in functions for sequences."),
34+ $mk("p3","guide","FLWOR in XQuery","The XQuery FLWOR expression iterates and filters."),
35+ $mk("p4","reference","XQuery modules","Library modules organize reusable XQuery code."),
36+ $mk("p5","reference","XQuery serialization","Control how XQuery serializes XML and JSON."),
37+ $mk("p6","reference","XQuery updates","The XQuery update facility modifies stored XML.")
38+ )
39+ return (
40+ xmldb:create-collection("/db/system/config/db", "apps"),
41+ xmldb:create-collection("/db/system/config/db/apps", "${ APP } "),
42+ xmldb:create-collection("/db/system/config/db/apps/${ APP } ", "data"),
43+ xmldb:store("${ CONF } ", "collection.xconf", $xconf),
44+ xmldb:create-collection("/db/apps", "${ APP } "),
45+ xmldb:create-collection("/db/apps/${ APP } ", "data"),
46+ (for $d at $i in $docs return xmldb:store("${ DATA } ", "p" || $i || ".xml", $d)),
47+ xmldb:reindex("${ DATA } "),
48+ "indexed=" || count(collection("${ DATA } ")/page)
49+ )[last()]
50+ ` ;
51+
52+ const TEARDOWN = `
53+ (if (xmldb:collection-available("/db/apps/${ APP } ")) then xmldb:remove("/db/apps/${ APP } ") else (),
54+ if (xmldb:collection-available("/db/system/config/db/apps/${ APP } ")) then xmldb:remove("/db/system/config/db/apps/${ APP } ") else (),
55+ "cleaned")[last()]
56+ ` ;
57+
58+ function runAdmin ( query ) {
59+ return cy . request ( { url : '/api/query' , method : 'POST' , auth, body : { query } } ) . then ( r => {
60+ // The query runs (and its side effects materialize) on POST; release the cursor.
61+ if ( r . body && r . body . cursor ) {
62+ cy . request ( { url : `/api/query/${ r . body . cursor } ` , method : 'DELETE' , auth, failOnStatusCode : false } ) ;
63+ }
64+ } ) ;
65+ }
66+
667describe ( '/api/search' , ( ) => {
68+ before ( ( ) => runAdmin ( SETUP ) ) ;
69+ after ( ( ) => runAdmin ( TEARDOWN ) ) ;
70+
771 describe ( 'GET /api/search' , ( ) => {
872 it ( 'requires the q parameter' , ( ) => {
973 cy . request ( { url : '/api/search' , auth, failOnStatusCode : false } ) . then ( response => {
@@ -13,9 +77,9 @@ describe('/api/search', () => {
1377 } ) ;
1478
1579 it ( 'returns the documented shape: query, total, offset, limit, results' , ( ) => {
16- cy . request ( { url : ' /api/search?q=xquery &limit=5' , auth } ) . then ( response => {
80+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=5` , auth } ) . then ( response => {
1781 expect ( response . status ) . to . eq ( 200 ) ;
18- expect ( response . body ) . to . have . property ( 'query' , 'xquery' ) ;
82+ expect ( response . body ) . to . have . property ( 'query' , TERM ) ;
1983 expect ( response . body ) . to . have . property ( 'total' ) . that . is . a ( 'number' ) ;
2084 expect ( response . body ) . to . have . property ( 'offset' , 0 ) ;
2185 expect ( response . body ) . to . have . property ( 'limit' , 5 ) ;
@@ -25,7 +89,7 @@ describe('/api/search', () => {
2589 } ) ;
2690
2791 it ( 'orders results by descending relevance score' , ( ) => {
28- cy . request ( { url : ' /api/search?q=xquery &limit=10' , auth } ) . then ( response => {
92+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=10` , auth } ) . then ( response => {
2993 const scores = response . body . results . map ( r => r . score ) ;
3094 scores . forEach ( s => expect ( s ) . to . be . a ( 'number' ) . and . to . be . greaterThan ( 0 ) ) ;
3195 for ( let i = 1 ; i < scores . length ; i ++ ) {
@@ -35,7 +99,7 @@ describe('/api/search', () => {
3599 } ) ;
36100
37101 it ( 'wraps matched terms in <mark> in snippet and highlights' , ( ) => {
38- cy . request ( { url : ' /api/search?q=xquery &limit=5' , auth } ) . then ( response => {
102+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=5` , auth } ) . then ( response => {
39103 const hit = response . body . results . find (
40104 r => r . snippet && / < m a r k > / i. test ( r . snippet )
41105 ) ;
@@ -47,7 +111,7 @@ describe('/api/search', () => {
47111 } ) ;
48112
49113 it ( 'exposes a canonical document identifier (uri / path)' , ( ) => {
50- cy . request ( { url : ' /api/search?q=xquery &limit=1' , auth } ) . then ( response => {
114+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=1` , auth } ) . then ( response => {
51115 const hit = response . body . results [ 0 ] ;
52116 expect ( hit ) . to . have . property ( 'uri' ) . that . matches ( / ^ \/ d b \/ / ) ;
53117 expect ( hit ) . to . have . property ( 'path' , hit . uri ) ;
@@ -57,7 +121,7 @@ describe('/api/search', () => {
57121 } ) ;
58122
59123 it ( 'returns well-formed, single-rooted <span> XML snippets (parse-xml friendly)' , ( ) => {
60- cy . request ( { url : ' /api/search?q=xquery &limit=5' , auth } ) . then ( response => {
124+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=5` , auth } ) . then ( response => {
61125 const isWellFormed = ( s ) => {
62126 expect ( s , 'fragment is wrapped in a single <span> root' ) . to . match (
63127 / ^ < s p a n > [ \s \S ] * < \/ s p a n > $ /
@@ -74,17 +138,17 @@ describe('/api/search', () => {
74138 } ) ;
75139
76140 it ( 'returns one result per document (deduplicated)' , ( ) => {
77- cy . request ( { url : ' /api/search?q=xquery &limit=50' , auth } ) . then ( response => {
141+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=50` , auth } ) . then ( response => {
78142 const uris = response . body . results . map ( r => r . uri ) ;
79143 expect ( uris . length ) . to . eq ( new Set ( uris ) . size ) ;
80144 } ) ;
81145 } ) ;
82146
83147 it ( 'pages through results with offset/limit' , ( ) => {
84- cy . request ( { url : ' /api/search?q=xquery &limit=3&offset=0' , auth } ) . then ( page1 => {
148+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=3&offset=0` , auth } ) . then ( page1 => {
85149 // Only meaningful when there is more than one page of results.
86150 if ( page1 . body . total <= 3 ) return ;
87- cy . request ( { url : ' /api/search?q=xquery &limit=3&offset=3' , auth } ) . then ( page2 => {
151+ cy . request ( { url : ` /api/search?q=${ TERM } &limit=3&offset=3` , auth } ) . then ( page2 => {
88152 expect ( page2 . body . offset ) . to . eq ( 3 ) ;
89153 const u1 = new Set ( page1 . body . results . map ( r => r . uri ) ) ;
90154 page2 . body . results . forEach ( r => expect ( u1 . has ( r . uri ) ) . to . eq ( false ) ) ;
@@ -93,9 +157,9 @@ describe('/api/search', () => {
93157 } ) ;
94158
95159 it ( 'restricts to a single app with the app parameter' , ( ) => {
96- cy . request ( { url : ' /api/search?q=xquery &app=doc &limit=10' , auth } ) . then ( response => {
160+ cy . request ( { url : ` /api/search?q=${ TERM } &app=${ APP } &limit=10` , auth } ) . then ( response => {
97161 expect ( response . status ) . to . eq ( 200 ) ;
98- response . body . results . forEach ( r => expect ( r . app ) . to . eq ( 'doc' ) ) ;
162+ response . body . results . forEach ( r => expect ( r . app ) . to . eq ( APP ) ) ;
99163 } ) ;
100164 } ) ;
101165 } ) ;
0 commit comments