Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
This file describes changes in the curlInterface package.

Next
- Make HTTP tests hermetic with a local IO-backed server

2.4.4 (2026-07-13)
- Remove dependency on GAPDoc

Expand Down
1 change: 1 addition & 0 deletions PackageInfo.g
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Dependencies := rec(
GAP := ">= 4.12",
NeededOtherPackages := [ ],
SuggestedOtherPackages := [ ],
TestPackages := [ [ "io", ">= 4.7.0" ] ],
NeededSystemPackages := rec(
Ubuntu := [["libcurl4-gnutls-dev"]],
Homebrew := [["curl"]],
Expand Down
239 changes: 77 additions & 162 deletions tst/basic.tst
Original file line number Diff line number Diff line change
@@ -1,198 +1,113 @@
gap> LoadPackage("curlInterface", false);
#@local r, url, postString, requestType, server, baseurl
gap> LoadPackage( "curlInterface", false );
true

# Check HTTP
gap> r := DownloadURL("http://www.google.com");;
gap> SortedList(RecNames(r));
[ "result", "success" ]
gap> r.success;
gap> LoadPackage( "io", false );
true
gap> PositionSublist(r.result, "google") <> fail;
true
gap> PositionSublist("google", r.result) <> fail;
false
gap> ReadPackage( "curlInterface", "tst/http-server.g" );;
gap> server := CURLINTERFACE_StartHTTPTestServer();;
gap> baseurl := Concatenation( "http://127.0.0.1:",
> String( server.port ) );;

# Sanity check the result without actually printing it
gap> PositionSublist(r.result, "google") <> fail;
true
gap> r.result[1];
'<'

# Check HTTPS
gap> r := DownloadURL("https://www.google.com");;
gap> SortedList(RecNames(r));
# Successful GET
gap> r := DownloadURL( Concatenation( baseurl, "/success" ) );;
gap> SortedList( RecNames( r ) );
[ "result", "success" ]
gap> r.success;
true
gap> PositionSublist(r.result, "google") <> fail;
true
gap> PositionSublist("google", r.result) <> fail;
false
gap> r.result;
"download test response\n"

# Sanity check the result without actually printing it
gap> PositionSublist(r.result, "google") <> fail;
true
gap> r.result[1];
'<'

# Check FTP
# Removed since we do not officially support FTP and it may fail in Docker
#gap> r := DownloadURL("ftp://fra36-speedtest-1.tele2.net/1KB.zip");;
#gap> SortedList(RecNames(r));
#[ "result", "success" ]
#gap> r.success;
#true
#gap> r.result = ListWithIdenticalEntries(1024, '\000');
#true

# Check bad URL
gap> r := DownloadURL("https://www.google.cheesebadger");;
gap> SortedList(RecNames(r));
[ "error", "success" ]
gap> r.success;
# A URL that is a string but not in IsStringRep
gap> url := List( Concatenation( baseurl, "/success" ), x -> x );;
gap> IsStringRep( url );
false
gap> PositionSublist(r.error, "Could not resolve host") <> fail;
true
gap> DownloadURL( url ).result;
"download test response\n"

# Check another bad URL
gap> r := DownloadURL("https://www.gap-system.org/Packages/curlInterface.x");;
# Successful POST and exact body echo
gap> r := PostToURL( Concatenation( baseurl, "/post" ),
> "field1=true&field2=17" );;
gap> r.success;
true
gap> PositionSublist(r.result, "URL was not found") <> fail;
true
gap> r := DownloadURL("https://www.gap-system.org/Packages/curlInterface.x",
> rec(failOnError:= true));;
gap> r.success;
false
gap> PositionSublist(r.error, "404") <> fail;
true

# Check successful POST requests
gap> r := PostToURL("httpbun.com/post", "field1=true&field2=17");;
gap> SortedList(RecNames(r));
[ "result", "success" ]
gap> r.success;
true
gap> PositionSublist(r.result, "\"field1\": \"true\"") <> fail;
true
gap> PositionSublist(r.result, "\"field2\": \"17\"") <> fail;
true
gap> PositionSublist(r.result, "field3") <> fail;
false
gap> r.result;
"field1=true&field2=17"

# Check POST on a string with null characters
gap> r := PostToURL("httpbun.com/post", "field1=my\000first\000field");;
gap> SortedList(RecNames(r));
[ "result", "success" ]
# POST preserves embedded NUL bytes
gap> postString := "field1=my\000first\000field";;
gap> r := PostToURL( Concatenation( baseurl, "/post" ), postString );;
gap> r.success;
true
gap> PositionSublist(r.result, "my\\u0000first\\u0000field") <> fail;
gap> r.result = postString;
true
gap> PositionSublist(r.result, "field3") <> fail;
false

# Check POST method not allowed (405)
gap> r := PostToURL("www.google.com", "myfield=42");;
gap> SortedList(RecNames(r));
[ "result", "success" ]
gap> r.success;
true
gap> PositionSublist(r.result, "myfield") <> fail;
# A POST body that is a string but not in IsStringRep
gap> postString := List( "animal=tiger&material=cotton", x -> x );;
gap> IsStringRep( postString );
false
gap> PositionSublist(r.result, "405") <> fail;
gap> r := PostToURL( Concatenation( baseurl, "/post" ), postString,
> rec( verifyCert := true ) );;
gap> r.result = postString;
true

# Check bad URL with POST
gap> r := PostToURL("https://www.google.cheesebadger", "hello");;
gap> SortedList(RecNames(r));
[ "error", "success" ]
gap> r.success;
# A request type that is a string but not in IsStringRep
gap> requestType := List( "GET", x -> x );;
gap> IsStringRep( requestType );
false
gap> PositionSublist(r.error, "Could not resolve host") <> fail;
true
gap> r := CurlRequest( Concatenation( baseurl, "/success" ),
> requestType, "" );;
gap> r.result;
"download test response\n"

# Check not IsStringRep (url)
gap> url := List("https://www.google.com", letter -> letter);;
gap> IsStringRep(url);
false
gap> r := DownloadURL(url);;
gap> SortedList(RecNames(r));
[ "result", "success" ]
gap> r.success;
true
gap> PositionSublist(r.result, "google") <> fail;
true
# HEAD returns no body
gap> CurlRequest( Concatenation( baseurl, "/success" ), "HEAD", "" );
rec( result := "", success := true )

# Check not IsStringRep (post_string)
gap> post_string := List("animal=tiger&material=cotton", letter -> letter);;
gap> IsStringRep(post_string);
false
gap> r := PostToURL("httpbun.com/post", post_string, rec(verifyCert := true));;
# DELETE reaches the deterministic route
gap> r := DeleteURL( Concatenation( baseurl, "/delete" ) );;
gap> r.success;
true
gap> PositionSublist(r.result, "\"animal\": \"tiger\"") <> fail;
true
gap> PositionSublist(r.result, "\"material\": \"cotton\"") <> fail;
true
gap> PositionSublist(r.result, "lion") <> fail;
false
gap> r.result;
"delete test response\n"

# Check not IsStringRep (request type)
gap> r := CurlRequest("www.google.com", ['G', 'E', 'T'] , "");;
gap> r.success;
# Verbose output does not change the result
gap> r := DownloadURL( Concatenation( baseurl, "/success" ),
> rec( verbose := true ) );;
gap> r.success and r.result = "download test response\n";
true
gap> SortedList(RecNames(r));
[ "result", "success" ]
gap> PositionSublist(r.result, "google") <> fail;
true
gap> PositionSublist(r.result, "tiger") <> fail;
false

# HEAD requests
gap> CurlRequest("www.google.com", "HEAD" , "");
rec( result := "", success := true )
gap> r := CurlRequest("www.google.cheesebadger", "HEAD" , "");;
gap> r.success;
false
# Redirects are followed by default and when explicitly enabled
gap> url := Concatenation( baseurl, "/redirect" );;
gap> DownloadURL( url ).result;
"download test response\n"
gap> DownloadURL( url, rec( followRedirect := true ) ).result;
"download test response\n"
gap> DownloadURL( url, rec( followRedirect := false ) ).result;
"redirect response\n"

# DELETE requests
gap> r := DeleteURL("https://www.google.com");;
# 404 responses and failOnError
gap> url := Concatenation( baseurl, "/missing" );;
gap> r := DownloadURL( url );;
gap> r.success;
true
gap> PositionSublist(r.result, "405 ") <> fail;
true
gap> PositionSublist(r.result, "tiger") <> fail;
false
gap> r := DeleteURL("httpbun.com/delete");;
gap> r.result;
"not found\n"
gap> r := DownloadURL( url, rec( failOnError := true ) );;
gap> r.success;
true
gap> PositionSublist(r.result, "405 ") <> fail;
false

# Check verbose requests don't break anything (we can't catch the output here)
gap> r := DownloadURL("httpbun.com/get", rec(verbose := true));;
gap> r.success;
true
gap> PositionSublist(r.result, "httpbun") <> fail;
gap> PositionSublist( r.error, "404" ) <> fail;
true

#gap> PositionSublist(r.result, "404 ") <> fail;
#false

# Follow redirects
gap> url := "http://www.icm.tu-bs.de/ag_algebra/software/polycyclic";;
gap> r := DownloadURL(url);;
gap> PositionSublist(r.result, "GitHub Pages") <> fail;
true
gap> r := DownloadURL(url, rec(followRedirect := true));;
gap> PositionSublist(r.result, "GitHub Pages") <> fail;
true
gap> r := DownloadURL(url, rec(followRedirect := false));;
gap> PositionSublist(r.result, "GitHub Pages") <> fail;
# A local disconnect exercises the transport-error result shape
gap> r := DownloadURL( Concatenation( baseurl, "/disconnect" ) );;
gap> SortedList( RecNames( r ) );
[ "error", "success" ]
gap> r.success;
false
gap> PositionSublist(r.result, "301 ") <> fail;
true

# Check timeout works
gap> CurlRequest("www.google.com", "HEAD" , "", rec(maxTime := 1000000));
rec( result := "", success := true )
# Timeout failure and success
gap> url := Concatenation( baseurl, "/delay" );;
gap> DownloadURL( url, rec( maxTime := 1 ) ).success;
false
gap> DownloadURL( url, rec( maxTime := 5 ) ).result;
"download test response\n"
gap> CURLINTERFACE_StopHTTPTestServer( server );;
21 changes: 11 additions & 10 deletions tst/errors.tst
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,44 @@ gap> PostToURL(42, "hello");
Error, CurlRequest: <URL> must be a string

# request type not a string
gap> CurlRequest("www.google.com", 637, "hello", rec(verifyCert := true));
gap> CurlRequest("http://127.0.0.1/", 637, "hello", rec(verifyCert := true));
Error, CurlRequest: <type> must be a string

# post_string not a string
gap> PostToURL("httpbun.com/post", 17);
gap> PostToURL("http://127.0.0.1/", 17);
Error, CurlRequest: <out_string> must be a string

# invalid verifyCert
gap> DownloadURL("https://www.google.com", rec(verifyCert := "maybe"));
gap> DownloadURL("http://127.0.0.1/", rec(verifyCert := "maybe"));
Error, CurlRequest: <opts>.verifyCert must be true or false

# invalid verbose
gap> DownloadURL("https://www.google.com", rec(verbose := "yes"));
gap> DownloadURL("http://127.0.0.1/", rec(verbose := "yes"));
Error, CurlRequest: <opts>.verbose must be true or false

# invalid followRedirect
gap> DownloadURL("https://www.google.com", rec(followRedirect := "always"));
gap> DownloadURL("http://127.0.0.1/", rec(followRedirect := "always"));
Error, CurlRequest: <opts>.followRedirect must be true or false

# invalid failOnError
gap> DownloadURL("https://www.google.com", rec(failOnError := "yes"));
gap> DownloadURL("http://127.0.0.1/", rec(failOnError := "yes"));
Error, CurlRequest: <opts>.failOnError must be true or false

# invalid opts
gap> DownloadURL("https://www.google.com", "please verify the cert");
gap> DownloadURL("http://127.0.0.1/", "please verify the cert");
Error, CurlRequest: <opts> must be a record

# too many arguments
gap> CurlRequest("www.google.com", "GET", "", rec(verifyCert := true), 3, true);
gap> CurlRequest("http://127.0.0.1/", "GET", "",
> rec(verifyCert := true), 3, true);
Error, CurlRequest: usage: requires 3 or 4 arguments, but 6 were given

# invalid time
gap> CurlRequest("www.google.com", "GET", "", rec(maxTime := -1));
gap> CurlRequest("http://127.0.0.1/", "GET", "", rec(maxTime := -1));
Error, CurlRequest: <opts>.maxTime must be a non-negative integer

# invalid time
gap> CurlRequest("www.google.com", "GET", "", rec(maxTime := "abc"));
gap> CurlRequest("http://127.0.0.1/", "GET", "", rec(maxTime := "abc"));
Error, CurlRequest: <opts>.maxTime must be a non-negative integer

# number of arguments
Expand Down
Loading
Loading