diff --git a/.github/workflows/courtlistener.yml b/.github/workflows/courtlistener.yml new file mode 100644 index 0000000..315a9d6 --- /dev/null +++ b/.github/workflows/courtlistener.yml @@ -0,0 +1,83 @@ +name: CourtListener through ICU + +on: + push: + branches: + - courtlistener-icu-foundation + pull_request: + workflow_dispatch: + +jobs: + courtlistener: + runs-on: ubuntu-latest + steps: + - name: Checkout Idriç CLI + uses: actions/checkout@v4 + + - name: Checkout Idriç compiler + uses: actions/checkout@v4 + with: + repository: isomorphisms/Idric + ref: 47557b43053c829d4f8aa1581007002b57f9c59f + path: .tools/Idric + + - name: Checkout Idric-Net + uses: actions/checkout@v4 + with: + repository: isomorphisms/Idric-Net + ref: 3e7643c8d8dd2a940d5b6dc96403ddae765402cd + path: .tools/Idric-Net + + - name: Checkout ICU API-client boundary + uses: actions/checkout@v4 + with: + repository: dilapidated-shed/icu + ref: 4ea624544d5b033bf83ab155e3783538e2ba1d58 + path: .tools/icu + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y chezscheme libssl-dev + + - name: Build Idriç compiler + run: make -C .tools/Idric bootstrap SCHEME=scheme + + - name: Install Idric-Net + env: + IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/bootstrap-build + run: | + cd .tools/Idric-Net + ../Idric/build/exec/idris2 --install idric-net.ipkg + + - name: Install contrib + env: + IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/bootstrap-build + run: | + cd .tools/Idric/libs/contrib + ../../build/exec/idris2 --install contrib.ipkg + + - name: Build ICU response adapter + env: + IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/bootstrap-build + run: | + make -C .tools/icu libicu_transport.so + sed -i \ + 's/^modules =.*/modules = Http, TransportModel, Transport, Ingestion/' \ + .tools/icu/icu.ipkg + sed -i '/^main =/d; /^executable =/d' .tools/icu/icu.ipkg + cd .tools/icu + ../Idric/build/exec/idris2 --install icu.ipkg + cd ../.. + .tools/Idric/build/exec/idris2 -p icu \ + test/courtlistener/IcuRunner.idric -o icu-courtlistener + test -x build/exec/icu-courtlistener + + - name: Run deterministic and live receipts + env: + COURTLISTENER_LIVE: 1 + ICU: ${{ github.workspace }}/build/exec/icu-courtlistener + IDRIC: ${{ github.workspace }}/.tools/Idric/build/exec/idris2 + IDRIS2_PREFIX: ${{ github.workspace }}/.tools/Idric/bootstrap-build + LD_LIBRARY_PATH: ${{ github.workspace }}/.tools/icu + run: sh test/courtlistener/check diff --git a/.gitignore b/.gitignore index ea0265f..6c9ad3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ config/amazon-secret .state/ .cache/ +build/ diff --git a/CourtListener.idric b/CourtListener.idric new file mode 120000 index 0000000..362b652 --- /dev/null +++ b/CourtListener.idric @@ -0,0 +1 @@ +src/CourtListener.idric \ No newline at end of file diff --git a/Makefile b/Makefile index 418650d..c9ad2dd 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,24 @@ PREFIX ?= /usr/local DESTDIR ?= SHELL ?= /bin/sh +IDRIC ?= idris2 -.PHONY: test install +.PHONY: test courtlistener courtlistener-check install test: bash test/az-test.sh bash test/abe-test.sh +courtlistener: + $(IDRIC) --build courtlistener.ipkg + +courtlistener-check: + sh test/courtlistener/check + install: install -d "$(DESTDIR)$(PREFIX)/bin" install -m 0755 bin/az "$(DESTDIR)$(PREFIX)/bin/az" install -m 0755 bin/abe "$(DESTDIR)$(PREFIX)/bin/abe" + if test -x build/exec/edric; then \ + install -m 0755 build/exec/edric "$(DESTDIR)$(PREFIX)/bin/edric"; \ + fi diff --git a/README.md b/README.md index abe7022..b318cdb 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,40 @@ This repository is the consolidation point for the CLI/API-access programs that - `Reddit.idric` — Reddit Data API checkpoint, with a synthetic fixture and manual receipt. - `Reuters.idric` — Reuters GraphQL checkpoint. - `Wayback.idric` — Internet Archive Wayback/CDX checkpoint. +- `CourtListener.idric` — executable CourtListener v4 case-law search through ICU. -The top-level `.idric` files are symbolic links to the canonical sources under `checkpoints/`, so the important source is visible without digging through directories. +The top-level `.idric` files are symbolic links to their canonical sources, so the important source is visible without digging through directories. Some Idriç clients intentionally contain named holes for compiler/library boundaries that are not implemented yet. Keep those boundaries visible; do not make a client appear green by silently substituting another HTTP implementation. +## CourtListener + +Build and run the first legal-search command with: + +```text +make courtlistener IDRIC=/opt/Idric/build/exec/idris2 +edric courtlistener search "Brown v. Board of Education" +``` + +The installed ICU command owns HTTP and TLS. `ICU=/path/to/icu` selects it when +it is not on `PATH`; `COURTLISTENER_API_TOKEN` supplies optional token +authentication through ICU's checked header boundary. See +[`docs/courtlistener.md`](docs/courtlistener.md) for the exact ownership and +response contracts. + ## Networking -Where these clients need networking, ICU/Idric-Net remains the intended transport boundary. ICU is deliberately **not** a Git submodule here and is not pinned by this repository. +Where these clients need networking, ICU/Idric-Net remains the intended transport boundary. ICU is deliberately **not** a Git submodule or pinned runtime dependency here. The CourtListener acceptance workflow pins a reviewed ICU revision only so its receipt is reproducible. ## Tests `make test` runs the existing Amazon and AbeBooks smoke tests. Reddit has a separate manual compiler checkpoint at `checkpoints/reddit/check`; it is not part of `make test` while named Idriç holes remain. See `PROVENANCE.md` for the source branches copied into this repository. + +The CourtListener receipt is separate because it requires the Idriç compiler, +Idric-Net, contrib, and ICU: + +```text +make courtlistener-check IDRIC=/opt/Idric/build/exec/idris2 +``` diff --git a/courtlistener.ipkg b/courtlistener.ipkg new file mode 100644 index 0000000..23e6b0b --- /dev/null +++ b/courtlistener.ipkg @@ -0,0 +1,9 @@ +package courtlistener + +version = 0.1.0 +depends = contrib, idric_net +sourcedir = "src" + +modules = CourtListener, Main +main = Main +executable = edric diff --git a/docs/courtlistener.md b/docs/courtlistener.md new file mode 100644 index 0000000..1b61bbd --- /dev/null +++ b/docs/courtlistener.md @@ -0,0 +1,53 @@ +# CourtListener CLI boundary + +The first command is: + +```text +edric courtlistener search "Brown v. Board of Education" +``` + +The Idriç layer owns the command grammar, the `CourtListener_Query`, the typed +search request, the CourtListener JSON fields, and human-readable output. It +constructs the documented v4 case-law search request with `type=o`. + +ICU owns HTTP, TLS, redirects, response capture, and transport outcomes. The +CLI invokes the installed ICU command through an escaped argument list. It +does not contain sockets, TLS, HTTP framing, curl, or a second native adapter. +The required ICU response boundary is the existing API-client work that: + +- accepts checked `-H` request headers; +- writes the response body to stdout; +- exits `10` for a non-2xx response; +- uses its other nonzero outcomes for request/network/transport failures. + +ICU remains a moving external runtime dependency rather than a submodule. The +acceptance workflow alone pins a reviewed ICU response-boundary revision so a +given receipt can be reproduced. + +Set `ICU` to the ICU executable when it is not on `PATH`. If supplied, +`COURTLISTENER_API_TOKEN` becomes `Authorization: Token ...` at the ICU header +boundary. Tokens are never accepted as command arguments or stored here. + +The initial query encoder deliberately refuses non-ASCII input until the Idriç +UTF-8 percent-encoding boundary is available. It does not send malformed text +or silently reinterpret it. + +The response decoder preserves fields that are present in the v4 opinion-search +response: `caseName`, `court`, `dateFiled`, `citation`, and `absolute_url`. +`court` and `dateFiled` remain optional; `citation` remains plural. + +## Next records + +`search` is the current resource. The next obvious endpoint is `dockets`, +followed by `docket-entries`. Likely later targets are `recap-documents`, +`parties`, `attorneys`, `clusters`, `opinions`, `opinions-cited`, +`citation-lookup`, `people`, `positions`, and `educations`. + +These records can eventually support: + +```text +school → person → position → court → case → docket entry → attorney → party +case → cites → case +``` + +This repository does not build that graph yet. diff --git a/src/CourtListener.idric b/src/CourtListener.idric new file mode 100644 index 0000000..45d7b43 --- /dev/null +++ b/src/CourtListener.idric @@ -0,0 +1,254 @@ +module CourtListener + +import Language.JSON +import Network.HTTP +import Network.URL +import System + +%default total + +public export +record CourtListener_Query where + constructor Make_CourtListener_Query + query_text : String + +public export +record CourtListener_Case_Name where + constructor Make_CourtListener_Case_Name + case_name_text : String + +public export +record CourtListener_URL where + constructor Make_CourtListener_URL + url_text : String + +public export +record CourtListener_Search_Request where + constructor Make_CourtListener_Search_Request + search_query : CourtListener_Query + search_url : CourtListener_URL + http_request : request + +public export +record CourtListener_Search_Result where + constructor Make_CourtListener_Search_Result + case_name : CourtListener_Case_Name + court : Maybe String + date_filed : Maybe String + citations : List String + absolute_url : CourtListener_URL + +public export +choice courtlistener_failure one_of + invalid_query String + invalid_request String + network_failure Int + http_failure String + invalid_json + invalid_search_response String + +api_base : String +api_base = "https://www.courtlistener.com/api/rest/v4/search/" + +site_base : String +site_base = "https://www.courtlistener.com" + +hex_digit : Int → Char +hex_digit 0 = '0' +hex_digit 1 = '1' +hex_digit 2 = '2' +hex_digit 3 = '3' +hex_digit 4 = '4' +hex_digit 5 = '5' +hex_digit 6 = '6' +hex_digit 7 = '7' +hex_digit 8 = '8' +hex_digit 9 = '9' +hex_digit 10 = 'A' +hex_digit 11 = 'B' +hex_digit 12 = 'C' +hex_digit 13 = 'D' +hex_digit 14 = 'E' +hex_digit _ = 'F' + +unreserved : Char → Bool +unreserved character = + (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || + character == '-' || character == '.' || character == '_' || character == '~' + +ascii_query : List Char → Bool +ascii_query [] = True +ascii_query (character :: rest) = ord character < 128 && ascii_query rest + +percent_encode_ascii_chars : List Char → List Char +percent_encode_ascii_chars [] = [] +percent_encode_ascii_chars (character :: rest) = + if unreserved character + then character :: percent_encode_ascii_chars rest + else + let number = ord character in + '%' :: hex_digit (number `div` 16) :: hex_digit (number `mod` 16) :: + percent_encode_ascii_chars rest + +percent_encode_ascii : String → String +percent_encode_ascii text = pack (percent_encode_ascii_chars (unpack text)) + +public export +courtlistener_query : String → Either courtlistener_failure CourtListener_Query +courtlistener_query text = + if text == "" + then Left (invalid_query "query must not be empty") + else if ascii_query (unpack text) + then Right (Make_CourtListener_Query text) + else Left (invalid_query "the first slice accepts ASCII queries only") + +public export +build_search_request : + CourtListener_Query → Either courtlistener_failure CourtListener_Search_Request +build_search_request query = + let text = api_base ++ "?q=" ++ percent_encode_ascii query.query_text ++ "&type=o" in + case parse_url text of + Left problem => Left (invalid_request problem) + Right target => Right + (Make_CourtListener_Search_Request + query + (Make_CourtListener_URL text) + (get target)) + +object_field : String → List (String, JSON) → Maybe JSON +object_field wanted [] = Nothing +object_field wanted ((name, value) :: rest) = + if wanted == name then Just value else object_field wanted rest + +required_string : String → List (String, JSON) → Either String String +required_string name object = + case object_field name object of + Just (JString value) => Right value + Just _ => Left (name ++ " is not a string") + Nothing => Left ("missing " ++ name) + +optional_string : String → List (String, JSON) → Either String (Maybe String) +optional_string name object = + case object_field name object of + Just (JString value) => Right (Just value) + Just JNull => Right Nothing + Just _ => Left (name ++ " is neither a string nor null") + Nothing => Right Nothing + +string_array_values : List JSON → Either String (List String) +string_array_values [] = Right [] +string_array_values (JString value :: rest) = + case string_array_values rest of + Left problem => Left problem + Right values => Right (value :: values) +string_array_values (_ :: _) = Left "citation contains a non-string value" + +citations_field : List (String, JSON) → Either String (List String) +citations_field object = + case object_field "citation" object of + Just (JArray values) => string_array_values values + Just JNull => Right [] + Just _ => Left "citation is not an array" + Nothing => Right [] + +decode_result : JSON → Either String CourtListener_Search_Result +decode_result (JObject object) = do + case_name_text ← required_string "caseName" object + court_name ← optional_string "court" object + filed ← optional_string "dateFiled" object + citation_values ← citations_field object + path ← required_string "absolute_url" object + pure + (Make_CourtListener_Search_Result + (Make_CourtListener_Case_Name case_name_text) + court_name + filed + citation_values + (Make_CourtListener_URL (site_base ++ path))) +decode_result _ = Left "search result is not an object" + +decode_results : List JSON → Either String (List CourtListener_Search_Result) +decode_results [] = Right [] +decode_results (value :: rest) = do + decoded ← decode_result value + remaining ← decode_results rest + pure (decoded :: remaining) + +decode_root : JSON → Either String (List CourtListener_Search_Result) +decode_root (JObject object) = + case object_field "results" object of + Just (JArray values) => decode_results values + Just _ => Left "results is not an array" + Nothing => Left "missing results" +decode_root _ = Left "response root is not an object" + +public export +decode_search_response : + String → Either courtlistener_failure (List CourtListener_Search_Result) +decode_search_response body = + case Language.JSON.parse body of + Nothing => Left invalid_json + Just value => + case decode_root value of + Left problem => Left (invalid_search_response problem) + Right results => Right results + +icu_arguments : String → Maybe String → CourtListener_Search_Request → List String +icu_arguments executable Nothing value = + [executable, "get", value.search_url.url_text] +icu_arguments executable (Just token) value = + [ executable + , "get" + , "-H" + , "Authorization: Token " ++ token + , value.search_url.url_text + ] + +public export +covering +perform_search : + CourtListener_Search_Request → IO (Either courtlistener_failure (List CourtListener_Search_Result)) +perform_search value = do + maybe_icu ← getEnv "ICU" + maybe_token ← getEnv "COURTLISTENER_API_TOKEN" + let executable = case maybe_icu of + Nothing => "icu" + Just path => path + (body, status) ← run (icu_arguments executable maybe_token value) + if status == 0 + then pure (decode_search_response body) + else if status == 10 + then pure (Left (http_failure body)) + else pure (Left (network_failure status)) + +join_with : String → List String → String +join_with separator [] = "" +join_with separator [value] = value +join_with separator (value :: rest) = value ++ separator ++ join_with separator rest + +show_optional : Maybe String → String +show_optional Nothing = "—" +show_optional (Just value) = value + +public export +render_search_result : CourtListener_Search_Result → String +render_search_result result = + result.case_name.case_name_text ++ "\n" ++ + " court: " ++ show_optional result.court ++ "\n" ++ + " filed: " ++ show_optional result.date_filed ++ "\n" ++ + " citation: " ++ + (if result.citations == [] then "—" else join_with "; " result.citations) ++ "\n" ++ + " url: " ++ result.absolute_url.url_text + +public export +render_failure : courtlistener_failure → String +render_failure (invalid_query problem) = "courtlistener: invalid query: " ++ problem +render_failure (invalid_request problem) = "courtlistener: invalid request: " ++ problem +render_failure (network_failure status) = + "courtlistener: ICU network/transport failure (exit " ++ show status ++ ")" +render_failure (http_failure _) = "courtlistener: CourtListener returned a non-2xx HTTP response" +render_failure invalid_json = "courtlistener: refused invalid JSON" +render_failure (invalid_search_response problem) = + "courtlistener: refused unexpected search response: " ++ problem diff --git a/src/Main.idric b/src/Main.idric new file mode 100644 index 0000000..227919f --- /dev/null +++ b/src/Main.idric @@ -0,0 +1,48 @@ +module Main + +import CourtListener +import System +import System.File + +usage : String +usage = "usage: edric courtlistener search QUERY" + +fail_with : String → IO () +fail_with problem = do + _ ← fPutStrLn stderr problem + exitFailure + +print_results : List CourtListener_Search_Result → IO () +print_results [] = putStrLn "No CourtListener results." +print_results [result] = putStrLn (render_search_result result) +print_results (result :: rest) = do + putStrLn (render_search_result result) + putStrLn "" + print_results rest + +covering +run_search : String → IO () +run_search text = + case courtlistener_query text of + Left problem => fail_with (render_failure problem) + Right query => + case build_search_request query of + Left problem => fail_with (render_failure problem) + Right request => do + outcome ← perform_search request + case outcome of + Left problem => fail_with (render_failure problem) + Right results => print_results results + +covering +run : List String → IO () +run ["courtlistener", "search", query] = run_search query +run _ = fail_with usage + +covering +main : IO () +main = do + arguments ← getArgs + case arguments of + _ :: rest => run rest + [] => fail_with usage diff --git a/test/courtlistener/CourtListenerTests.idric b/test/courtlistener/CourtListenerTests.idric new file mode 100644 index 0000000..61bf785 --- /dev/null +++ b/test/courtlistener/CourtListenerTests.idric @@ -0,0 +1,62 @@ +module Main + +import CourtListener +import System +import System.File + +check : String → Bool → IO Bool +check name True = do + putStrLn ("PASS\t" ++ name) + pure True +check name False = do + putStrLn ("FAIL\t" ++ name) + pure False + +request_shape : Bool +request_shape = + case courtlistener_query "Brown v. Board" of + Left _ => False + Right query => + case build_search_request query of + Left _ => False + Right value => + value.search_url.url_text == + "https://www.courtlistener.com/api/rest/v4/search/?q=Brown%20v.%20Board&type=o" + +decoded_shape : String → Bool +decoded_shape body = + case decode_search_response body of + Right [result] => + result.case_name.case_name_text == "Brown v. Board of Education" && + result.court == Just "Supreme Court of the United States" && + result.date_filed == Just "1954-05-17" && + result.citations == ["347 U.S. 483", "74 S. Ct. 686"] && + result.absolute_url.url_text == + "https://www.courtlistener.com/opinion/105221/brown-v-board-of-education/" + _ => False + +invalid_json_refused : String → Bool +invalid_json_refused body = + case decode_search_response body of + Left invalid_json => True + _ => False + +non_search_refused : String → Bool +non_search_refused body = + case decode_search_response body of + Left (invalid_search_response _) => True + _ => False + +main : IO () +main = do + Right fixture ← readFile "test/courtlistener/fixture/search.json" + | Left _ => exitFailure + Right broken ← readFile "test/courtlistener/fixture/invalid.json" + | Left _ => exitFailure + Right non_search ← readFile "test/courtlistener/fixture/non-search.json" + | Left _ => exitFailure + one ← check "CourtListener request shape" request_shape + two ← check "typed search result decode" (decoded_shape fixture) + three ← check "invalid JSON refused" (invalid_json_refused broken) + four ← check "valid non-search JSON refused" (non_search_refused non_search) + if one && two && three && four then pure () else exitFailure diff --git a/test/courtlistener/IcuRunner.idric b/test/courtlistener/IcuRunner.idric new file mode 100644 index 0000000..e014429 --- /dev/null +++ b/test/courtlistener/IcuRunner.idric @@ -0,0 +1,30 @@ +module Main + +import Http +import System +import Transport + +run_request : request → List request_header → IO () +run_request value headers = do + outcome ← send_request_with_headers value headers + case outcome of + Left _ => exitFailure + Right status => exitWith (cast status) + +run : List String → IO () +run ["get", text] = + case parse_url text of + Left _ => exitFailure + Right target => run_request (get target) [] +run ["get", "-H", header_text, text] = + case (parse_header header_text, parse_url text) of + (Right value, Right target) => run_request (get target) [value] + _ => exitFailure +run _ = exitFailure + +main : IO () +main = do + arguments ← getArgs + case arguments of + _ :: rest => run rest + [] => exitFailure diff --git a/test/courtlistener/check b/test/courtlistener/check new file mode 100755 index 0000000..70d6d53 --- /dev/null +++ b/test/courtlistener/check @@ -0,0 +1,130 @@ +#!/bin/sh + +set -u + +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT=$(CDPATH= cd -- "$HERE/../.." && pwd) +IDRIC=${IDRIC:-idris2} + +status=0 + +pass() { + printf 'PASS\t%s\n' "$1" +} + +fail() { + printf 'FAIL\t%s\n' "$1" + status=1 +} + +skip() { + printf 'SKIP\t%s\n' "$1" +} + +available() { + case "$1" in + */*) test -x "$1" ;; + *) command -v "$1" >/dev/null 2>&1 ;; + esac +} + +if ! available "$IDRIC"; then + skip "courtlistener/idric (not found: $IDRIC)" + exit 0 +fi + +cd "$ROOT" || exit 1 + +if "$IDRIC" --build courtlistener.ipkg >/tmp/courtlistener-build.out 2>/tmp/courtlistener-build.err; then + pass "courtlistener/build" +else + fail "courtlistener/build" + sed 's/^/ /' /tmp/courtlistener-build.out /tmp/courtlistener-build.err + exit "$status" +fi + +if IDRIS2_PATH="$ROOT/src" "$IDRIC" -p contrib -p idric_net \ + test/courtlistener/CourtListenerTests.idric -o courtlistener-tests \ + >/tmp/courtlistener-tests-build.out 2>/tmp/courtlistener-tests-build.err && + "$ROOT/build/exec/courtlistener-tests"; then + : +else + fail "courtlistener/unit-receipts" + sed 's/^/ /' /tmp/courtlistener-tests-build.out /tmp/courtlistener-tests-build.err +fi + +export COURTLISTENER_FIXTURE_ROOT="$HERE/fixture" + +if ICU="$HERE/fake/icu-success" "$ROOT/build/exec/edric" \ + courtlistener search "Brown v. Board of Education" \ + | grep -F "Brown v. Board of Education" >/dev/null; then + pass "courtlistener/cli-success" +else + fail "courtlistener/cli-success" +fi + +if ICU="$HERE/fake/icu-success" "$ROOT/build/exec/edric" \ + courtlistener search "" \ + >/tmp/courtlistener-malformed.out 2>/tmp/courtlistener-malformed.err; then + fail "courtlistener/malformed-request" +else + if grep -F "invalid query" /tmp/courtlistener-malformed.err >/dev/null; then + pass "courtlistener/malformed-request" + else + fail "courtlistener/malformed-request" + fi +fi + +if ICU="$HERE/fake/icu-network-failure" "$ROOT/build/exec/edric" \ + courtlistener search "Brown v. Board of Education" \ + >/tmp/courtlistener-network.out 2>/tmp/courtlistener-network.err; then + fail "courtlistener/network-failure" +else + pass "courtlistener/network-failure" +fi + +if ICU="$HERE/fake/icu-http-failure" "$ROOT/build/exec/edric" \ + courtlistener search "Brown v. Board of Education" \ + >/tmp/courtlistener-http.out 2>/tmp/courtlistener-http.err; then + fail "courtlistener/non-2xx" +else + if grep -F "non-2xx HTTP response" /tmp/courtlistener-http.err >/dev/null; then + pass "courtlistener/non-2xx" + else + fail "courtlistener/non-2xx" + fi +fi + +if ICU="$HERE/fake/icu-invalid-json" "$ROOT/build/exec/edric" \ + courtlistener search "Brown v. Board of Education" \ + >/tmp/courtlistener-json.out 2>/tmp/courtlistener-json.err; then + fail "courtlistener/invalid-json" +else + if grep -F "refused invalid JSON" /tmp/courtlistener-json.err >/dev/null; then + pass "courtlistener/invalid-json" + else + fail "courtlistener/invalid-json" + fi +fi + +if grep -En "curl|prim_send_http|prim_send_https|socket\(|openssl" src/CourtListener.idric \ + >/tmp/courtlistener-second-http.out; then + fail "courtlistener/no-second-http" + sed 's/^/ /' /tmp/courtlistener-second-http.out +else + pass "courtlistener/no-second-http" +fi + +if test "${COURTLISTENER_LIVE:-0}" = 1; then + if ICU="${ICU:-icu}" "$ROOT/build/exec/edric" \ + courtlistener search "Brown v. Board of Education" \ + | grep -Ei "Brown|Board of Education|347 U[.]S[.] 483" >/dev/null; then + pass "courtlistener/live-icu" + else + fail "courtlistener/live-icu" + fi +else + skip "courtlistener/live-icu (set COURTLISTENER_LIVE=1)" +fi + +exit "$status" diff --git a/test/courtlistener/fake/icu-http-failure b/test/courtlistener/fake/icu-http-failure new file mode 100755 index 0000000..25a4b6b --- /dev/null +++ b/test/courtlistener/fake/icu-http-failure @@ -0,0 +1,3 @@ +#!/bin/sh +cat "${COURTLISTENER_FIXTURE_ROOT}/non-search.json" +exit 10 diff --git a/test/courtlistener/fake/icu-invalid-json b/test/courtlistener/fake/icu-invalid-json new file mode 100755 index 0000000..35ec74e --- /dev/null +++ b/test/courtlistener/fake/icu-invalid-json @@ -0,0 +1,2 @@ +#!/bin/sh +cat "${COURTLISTENER_FIXTURE_ROOT}/invalid.json" diff --git a/test/courtlistener/fake/icu-network-failure b/test/courtlistener/fake/icu-network-failure new file mode 100755 index 0000000..b340006 --- /dev/null +++ b/test/courtlistener/fake/icu-network-failure @@ -0,0 +1,2 @@ +#!/bin/sh +exit 5 diff --git a/test/courtlistener/fake/icu-success b/test/courtlistener/fake/icu-success new file mode 100755 index 0000000..6a8dbf7 --- /dev/null +++ b/test/courtlistener/fake/icu-success @@ -0,0 +1,2 @@ +#!/bin/sh +cat "${COURTLISTENER_FIXTURE_ROOT}/search.json" diff --git a/test/courtlistener/fixture/invalid.json b/test/courtlistener/fixture/invalid.json new file mode 100644 index 0000000..a0dcfda --- /dev/null +++ b/test/courtlistener/fixture/invalid.json @@ -0,0 +1 @@ +{"results": [ diff --git a/test/courtlistener/fixture/non-search.json b/test/courtlistener/fixture/non-search.json new file mode 100644 index 0000000..70ccafc --- /dev/null +++ b/test/courtlistener/fixture/non-search.json @@ -0,0 +1 @@ +{"detail": "Authentication credentials were not provided."} diff --git a/test/courtlistener/fixture/search.json b/test/courtlistener/fixture/search.json new file mode 100644 index 0000000..adcd71a --- /dev/null +++ b/test/courtlistener/fixture/search.json @@ -0,0 +1,14 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "absolute_url": "/opinion/105221/brown-v-board-of-education/", + "caseName": "Brown v. Board of Education", + "citation": ["347 U.S. 483", "74 S. Ct. 686"], + "court": "Supreme Court of the United States", + "dateFiled": "1954-05-17" + } + ] +}