aboutsummaryrefslogtreecommitdiffhomepage
path: root/test/test-search-cgi.sh
blob: 1667a43783c786fd0e494fec0c148b9a6987c523 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#!/usr/bin/env sh
# test-search-cgi.sh
#
# End to end check that the search cgi works against a database spine has
# just built.
#
# The schema check (test-search-db-schema.sh) compares names. This one runs
# the real binary against the real database, which is the only thing that
# tests the query spine's schema actually has to satisfy: the joins, the
# ordering, the types, and the fields the form fills in from a request.
#
# No web server is involved. The cgi binary is invoked as CGI, with the
# request in the environment, which is what a web server would do.
#
# Usage:
#   ./test/test-search-cgi.sh [/path/to/spine] [/path/to/spine_search]
#
# Environment:
#   SpinePOD           pod directory to build the db from, relative to the
#                      spine directory (default: data/pod, the bundled set)
#   SpineSearchCGIbin  the built search cgi
#                      (default: ../sisudoc-spine-search-cgi/result/cgi-bin/spine_search,
#                      then .../cgi-bin/spine-search-ldc.cgi)
#   SpineSearchDB      an existing search database to test against; when set,
#                      nothing is built and this database is used as is
#
# The cgi is pointed at the database with SPINE_SEARCH_DB_PATH, so the binary
# under test needs no rebuild and its compiled-in path is left alone.
#
# Exit codes:
#   0  every request behaved
#   1  a request did not (printed)
#   2  set-up problem (no binary, no cgi, no samples)

set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# sqlite3 is not required here: the parts that use it fall back to fixed probe
# values and to skipping the schema version gate. Both are checks, though, and
# a run that quietly leaves them out still says PASS, so where sqlite3 is
# absent and nix is to hand, re-run inside a nix shell that has it. With
# neither, the test runs as before and says what it skipped.
if ! command -v sqlite3 > /dev/null 2>&1; then
  if command -v nix > /dev/null 2>&1; then
    echo "sqlite3 not on PATH, re-running inside nix shell \"nixpkgs#sqlite\""
    exec nix shell "nixpkgs#sqlite" -c "$0" "$@"
  fi
fi

SPINE_BIN="${1:-}"
if [ -z "$SPINE_BIN" ]; then
  if   [ -x "$SPINE_DIR/result/bin/spine" ]; then SPINE_BIN="$SPINE_DIR/result/bin/spine"
  elif [ -x "$SPINE_DIR/bin/spine-ldc" ];    then SPINE_BIN="$SPINE_DIR/bin/spine-ldc"
  elif [ -x "$SPINE_DIR/bin/spine" ];        then SPINE_BIN="$SPINE_DIR/bin/spine"
  else
    echo "ERROR: spine binary not found. Specify path as first argument." >&2
    exit 2
  fi
fi

CGI_BIN="${2:-${SpineSearchCGIbin:-}}"
if [ -z "$CGI_BIN" ]; then
  CGI_REPO="$SPINE_DIR/../sisudoc-spine-search-cgi"
  for c in \
    "$CGI_REPO/result/cgi-bin/spine_search" \
    "$CGI_REPO/cgi-bin/spine-search-ldc.cgi" \
    "$CGI_REPO/cgi-bin/spine_search"
  do
    [ -x "$c" ] && CGI_BIN="$c" && break
  done
fi
if [ -z "$CGI_BIN" ] || [ ! -x "$CGI_BIN" ]; then
  echo "ERROR: search cgi binary not found." >&2
  echo "  build it: cd ../sisudoc-spine-search-cgi && nix build \".#spine-search-overlay-ldc\"" >&2
  echo "  or set \$SpineSearchCGIbin / pass it as the second argument" >&2
  exit 2
fi

OUT_DIR="$SCRIPT_DIR/current-search-cgi"
DB_NAME="spine.search.db"

if [ -n "$SpineSearchDB" ]; then
  DB_DIR="$(cd "$(dirname "$SpineSearchDB")" && pwd)"
  DB_NAME="$(basename "$SpineSearchDB")"
  echo "spine binary: (not used, \$SpineSearchDB set)"
else
  SAMPLES_RAW="$SPINE_DIR/${SpinePOD:-data/pod}"
  if [ -d "$SAMPLES_RAW" ]; then
    SAMPLES_DIR="$(cd "$SAMPLES_RAW" && pwd)"
  else
    echo "ERROR: sample documents not found at $SAMPLES_RAW" >&2
    exit 2
  fi
  DB_DIR="$OUT_DIR/sqlite-database"
  echo "spine binary: $SPINE_BIN"
  echo "samples:      $SAMPLES_DIR"
  rm -rf "$OUT_DIR"
  mkdir -p "$DB_DIR"
  echo "Building search database ..."
  "$SPINE_BIN" --sqlite-db-recreate \
    --sqlite-db-path="$DB_DIR" --sqlite-db-filename="$DB_NAME" > /dev/null
  if ! "$SPINE_BIN" --sqlite-update \
    --sqlite-db-path="$DB_DIR" --sqlite-db-filename="$DB_NAME" \
    --output="$OUT_DIR" "$SAMPLES_DIR"/* > "$OUT_DIR/build.log" 2>&1; then
    echo "FAIL: spine --sqlite-update reported failure"
    grep "ERROR SQLite\|run FAILED" "$OUT_DIR/build.log" | head -20
    exit 1
  fi
fi
echo "search cgi:   $CGI_BIN"
echo "database:     $DB_DIR/$DB_NAME"

mkdir -p "$OUT_DIR"
RESP="$OUT_DIR/response.html"
FAILURES=0

# one GET request, as a web server would present it
request() {
  SPINE_SEARCH_DB_PATH="$DB_DIR" \
  REQUEST_METHOD=GET \
  SCRIPT_NAME=/spine_search \
  SERVER_NAME=localhost \
  SERVER_PORT=80 \
  REQUEST_SCHEME=http \
  GATEWAY_INTERFACE=CGI/1.1 \
  SERVER_PROTOCOL=HTTP/1.1 \
  QUERY_STRING="$1" \
  "$CGI_BIN" > "$RESP" 2>"$OUT_DIR/response.err" || true
}

# a request that has to answer 200, name no sql error, and find documents
expect_hits() {
  _query="$1"
  _what="$2"
  request "$_query"
  if ! head -1 "$RESP" | grep -q "200 OK"; then
    echo "  FAIL $_what: no 200 status ($(head -1 "$RESP"))"
    FAILURES=$((FAILURES + 1))
    return
  fi
  if grep -qi "SqliteException\|no such column\|no such table" "$RESP" "$OUT_DIR/response.err"; then
    echo "  FAIL $_what: sql error"
    grep -i "SqliteException\|no such column\|no such table" "$RESP" "$OUT_DIR/response.err" | head -3
    FAILURES=$((FAILURES + 1))
    return
  fi
  _hits=$(grep -o 'class="publication"' "$RESP" | wc -l)
  _hits=$((_hits / 2))
  if [ "$_hits" -lt 1 ]; then
    echo "  FAIL $_what: no results"
    FAILURES=$((FAILURES + 1))
    return
  fi
  echo "  ok   $_what ($_hits document(s))"
}

# a request that has to answer 200 and say the named thing
expect_says() {
  _query="$1"
  _text="$2"
  _what="$3"
  request "$_query"
  if ! head -1 "$RESP" | grep -q "200 OK"; then
    echo "  FAIL $_what: no 200 status ($(head -1 "$RESP"))"
    FAILURES=$((FAILURES + 1))
    return
  fi
  if ! grep -q "$_text" "$RESP"; then
    echo "  FAIL $_what: response does not say \"$_text\""
    FAILURES=$((FAILURES + 1))
    return
  fi
  echo "  ok   $_what"
}

# the queries below name a different column of the schema each, so between
# them they exercise the whole of what the search form selects on. The values
# searched for are read out of the database itself where sqlite3 is present,
# so that the test says something whichever corpus it is pointed at; the
# fallbacks are words the bundled data/pod documents contain. Every field is
# matched with LIKE '%value%', so a single safe word out of a longer value
# still matches the row it came from.
PROBE_TEXT="document"
PROBE_AUTHOR="Amissah"
PROBE_TITLE="markup"
PROBE_FN="sisu-spine-markup"
PROBE_UID="markup"
PROBE_LNG="en"
if command -v sqlite3 > /dev/null 2>&1; then
  # first word of at least four letters or digits, nothing else: the values in
  # the database carry punctuation, markup and newlines, none of which belong
  # in a query string
  _word() {
    printf '%s' "$1" | tr -c 'A-Za-z0-9' ' ' \
      | awk '{for (i = 1; i <= NF; i++) if (length($i) >= 4) { print $i; exit } }'
  }
  _ask() { sqlite3 "$DB_DIR/$DB_NAME" "$1" 2>/dev/null | head -1 || true; }
  _v=$(_word "$(_ask "select clean from doc_objects where length(clean) > 60 limit 1;")")
  [ -n "$_v" ] && PROBE_TEXT="$_v"
  _v=$(_word "$(_ask "select creator_author from metadata_and_text where creator_author != '' limit 1;")")
  [ -n "$_v" ] && PROBE_AUTHOR="$_v"
  _v=$(_word "$(_ask "select title from metadata_and_text where title != '' limit 1;")")
  [ -n "$_v" ] && PROBE_TITLE="$_v"
  _v=$(_word "$(_ask "select src_filename_base from metadata_and_text limit 1;")")
  [ -n "$_v" ] && PROBE_FN="$_v"
  _v=$(_word "$(_ask "select uid from metadata_and_text limit 1;")")
  [ -n "$_v" ] && PROBE_UID="$_v"
  _v=$(_ask "select language_document_char from metadata_and_text limit 1;")
  case "$_v" in [a-z][a-z]) PROBE_LNG="$_v" ;; esac
fi
echo "Requests (text \"$PROBE_TEXT\", author \"$PROBE_AUTHOR\", title \"$PROBE_TITLE\"):"
expect_hits "sf=$PROBE_TEXT&rt=idx&sml=1000"          "text, index results"
expect_hits "sf=$PROBE_TEXT&rt=txt&sml=1000"          "text, text results"
expect_hits "sf=$PROBE_TEXT%20AND%20$PROBE_TEXT&rt=idx&sml=1000" "text, boolean AND"
expect_hits "sf=$PROBE_TEXT%20OR%20zzzznotfound&rt=idx&sml=1000" "text, boolean OR"
expect_hits "au=$PROBE_AUTHOR&sf=$PROBE_TEXT&rt=idx&sml=1000" "creator_author"
expect_hits "ti=$PROBE_TITLE&sf=$PROBE_TEXT&rt=idx&sml=1000"  "title"
expect_hits "lng=$PROBE_LNG&sf=$PROBE_TEXT&rt=idx&sml=1000"   "language_document_char"
expect_hits "fn=$PROBE_FN&sf=$PROBE_TEXT&rt=idx&sml=1000"     "src_filename_base"
expect_hits "uid=$PROBE_UID&sf=$PROBE_TEXT&rt=idx&sml=1000"   "uid"

# the failure modes a database change would show up as, each of which used to
# be a blank page, a 500, or a silently created empty file
echo "Refusals:"
expect_says 'sf=document&rt=idx&selected_db=no_such_file.db' \
  "search database not found" "a database that is not there is named, not created"
if [ -e "$DB_DIR/no_such_file.db" ]; then
  echo "  FAIL a missing database was created by the request"
  FAILURES=$((FAILURES + 1))
fi
expect_hits "sf=$PROBE_TEXT&rt=idx&sml=1000&selected_db=../../../../etc/passwd" \
  "a path in selected_db falls back to the configured database"


# the schema version gate, end to end: a database stamped with a version the
# search form does not expect has to be named as such, not searched. Needs
# sqlite3 to do the stamping, so it is skipped when sqlite3 is absent.
if command -v sqlite3 > /dev/null 2>&1; then
  echo "Schema version gate:"
  cp "$DB_DIR/$DB_NAME" "$DB_DIR/wrong_version.db"
  sqlite3 "$DB_DIR/wrong_version.db" "pragma user_version = 99;"
  expect_says 'sf=document&rt=idx&selected_db=wrong_version.db' \
    "schema version" "a database of another schema version is refused, with the version named"
  rm -f "$DB_DIR/wrong_version.db"
else
  echo "Schema version gate: skipped, sqlite3 not on PATH"
fi
echo ""
if [ "$FAILURES" -eq 0 ]; then
  echo "PASS: the search cgi works against this database"
  exit 0
else
  echo "FAIL: $FAILURES request(s) misbehaved"
  exit 1
fi