author | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-11-02 17:57:40 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-11-12 23:24:21 UTC |
parent | 025cb2d96a1032d32249ccbf179d9df20de1cf9a |
.gitignore | +1 | -0 |
test/t-09-loop/run.sh | +12 | -19 |
test/util/fexp.go | +195 | -0 |
test/util/lib.sh | +5 | -0 |
diff --git a/.gitignore b/.gitignore index fc8151c..d3529c4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ cmd/spf-check/spf-check cmd/mda-lmtp/mda-lmtp cmd/dovecot-auth-cli/dovecot-auth-cli test/util/minidns +test/util/fexp # Test binary, generated during coverage tests. chasquid.test diff --git a/test/t-09-loop/run.sh b/test/t-09-loop/run.sh index 1469502..ef051b5 100755 --- a/test/t-09-loop/run.sh +++ b/test/t-09-loop/run.sh @@ -34,29 +34,22 @@ run_msmtp aliasB@srv-B < content # Get some of the debugging pages, for troubleshooting, and to make sure they # work reasonably well. -function fetch() { - wget -q -o /dev/null -O $2 $1 +function fexp_gt10() { + fexp $1 -save $2 && \ + [ $( cat $2 | wc -l ) -gt 10 ] } -function linesgt10() { - [ $( cat $1 | wc -l ) -gt 10 ] -} - -fetch http://localhost:1099/ .data-A/dbg-root \ - && linesgt10 .data-A/dbg-root \ +fexp_gt10 http://localhost:1099/ .data-A/dbg-root \ || fail "failed to fetch /" -fetch http://localhost:1099/debug/flags .data-A/dbg-flags \ - && linesgt10 .data-A/dbg-flags \ +fexp_gt10 http://localhost:1099/debug/flags .data-A/dbg-flags \ || fail "failed to fetch /debug/flags" -fetch http://localhost:1099/debug/queue .data-A/dbg-queue \ +fexp http://localhost:1099/debug/queue -save .data-A/dbg-queue \ || fail "failed to fetch /debug/queue" -fetch http://localhost:1099/debug/config .data-A/dbg-config \ - && linesgt10 .data-A/dbg-config \ +fexp_gt10 http://localhost:1099/debug/config .data-A/dbg-config \ || fail "failed to fetch /debug/config" -fetch http://localhost:1099/404 .data-A/dbg-404 \ - && fail "fetch /404 worked, should have failed" -fetch http://localhost:1099/metrics .data-A/metrics \ - && linesgt10 .data-A/metrics \ +fexp http://localhost:1099/404 -status 404 \ + || fail "fetch /404 worked, should have failed" +fexp_gt10 http://localhost:1099/metrics .data-A/metrics \ || fail "failed to fetch /metrics" # Quick sanity-check of the /metrics page, just in case. @@ -65,8 +58,8 @@ grep -q '^chasquid_queue_itemsWritten [0-9]\+$' .data-A/metrics \ # Wait until one of them has noticed and stopped the loop. while sleep 0.1; do - wget -q -o /dev/null -O .data-A/vars http://localhost:1099/debug/vars - wget -q -o /dev/null -O .data-B/vars http://localhost:2099/debug/vars + fexp http://localhost:1099/debug/vars -save .data-A/vars + fexp http://localhost:2099/debug/vars -save .data-B/vars # Allow for up to 2 loops to be detected, because if chasquid is fast # enough the DSN will also loop before this check notices it. if grep -q '"chasquid/smtpIn/loopsDetected": [12],' .data-?/vars; then diff --git a/test/util/fexp.go b/test/util/fexp.go new file mode 100644 index 0000000..eb1f820 --- /dev/null +++ b/test/util/fexp.go @@ -0,0 +1,195 @@ +// Fetch an URL, and check if the response matches what we expect. +// +// Useful for testing HTTP(s) servers. +package main + +import ( + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + "regexp" + "sort" + "strconv" + "strings" +) + +var exitCode int + +func main() { + if len(os.Args) < 2 { + fatalf("Usage: fexp <URL> <options>\n") + } + + // The first arg is the URL, and then we shift. + url := os.Args[1] + os.Args = append([]string{os.Args[0]}, os.Args[2:]...) + + var ( + body = flag.String("body", "", + "expect body with these exact contents") + bodyRE = flag.String("bodyre", "", + "expect body matching these contents (regexp match)") + bodyNotRE = flag.String("bodynotre", "", + "expect body NOT matching these contents (regexp match)") + redir = flag.String("redir", "", + "expect a redirect to this URL") + status = flag.Int("status", 200, + "expect this status code") + verbose = flag.Bool("v", false, + "enable verbose output") + save = flag.String("save", "", + "save body to this file") + method = flag.String("method", "GET", + "request method to use") + hdrRE = flag.String("hdrre", "", + "expect a header matching these contents (regexp match)") + caCert = flag.String("cacert", "", + "file to read CA cert from") + ) + flag.Parse() + + client := &http.Client{ + CheckRedirect: noRedirect, + Transport: mkTransport(*caCert), + } + + req, err := http.NewRequest(*method, url, nil) + if err != nil { + fatalf("error building request: %q", err) + } + + resp, err := client.Do(req) + if err != nil { + fatalf("error getting %q: %v\n", url, err) + } + defer resp.Body.Close() + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + errorf("error reading body: %v\n", err) + } + + if *save != "" { + err = ioutil.WriteFile(*save, rbody, 0664) + if err != nil { + errorf("error writing body to file %q: %v\n", *save, err) + } + } + + if *verbose { + fmt.Printf("Request: %s\n", url) + fmt.Printf("Response:\n") + fmt.Printf(" %v %v\n", resp.Proto, resp.Status) + ks := []string{} + for k := range resp.Header { + ks = append(ks, k) + } + sort.Strings(ks) + for _, k := range ks { + fmt.Printf(" %v: %s\n", k, + strings.Join(resp.Header[k], ", ")) + } + fmt.Printf("\n") + } + + if resp.StatusCode != *status { + errorf("status is not %d: %q\n", *status, resp.Status) + } + + if *body != "" { + // Unescape the body to allow control characters more easily. + *body, _ = strconv.Unquote("\"" + *body + "\"") + if string(rbody) != *body { + errorf("unexpected body: %q\n", rbody) + } + } + + if *bodyRE != "" { + matched, err := regexp.Match(*bodyRE, rbody) + if err != nil { + errorf("regexp error: %q\n", err) + } + if !matched { + errorf("body did not match regexp: %q\n", rbody) + } + } + + if *bodyNotRE != "" { + matched, err := regexp.Match(*bodyNotRE, rbody) + if err != nil { + errorf("regexp error: %q\n", err) + } + if matched { + errorf("body matched regexp: %q\n", rbody) + } + } + + if *redir != "" { + if loc := resp.Header.Get("Location"); loc != *redir { + errorf("unexpected redir location: %q\n", loc) + } + } + + if *hdrRE != "" { + match := false + outer: + for k, vs := range resp.Header { + for _, v := range vs { + hdr := fmt.Sprintf("%s: %s", k, v) + matched, err := regexp.MatchString(*hdrRE, hdr) + if err != nil { + errorf("regexp error: %q\n", err) + } + if matched { + match = true + break outer + } + } + } + + if !match { + errorf("header did not match: %v\n", resp.Header) + } + } + + os.Exit(exitCode) +} + +func noRedirect(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse +} + +func mkTransport(caCert string) http.RoundTripper { + if caCert == "" { + return nil + } + + certs, err := ioutil.ReadFile(caCert) + if err != nil { + fatalf("error reading CA file %q: %v\n", caCert, err) + } + + rootCAs := x509.NewCertPool() + if ok := rootCAs.AppendCertsFromPEM(certs); !ok { + fatalf("error adding certs to root\n") + } + + return &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, + } +} + +func fatalf(s string, a ...interface{}) { + fmt.Fprintf(os.Stderr, s, a...) + os.Exit(1) +} + +func errorf(s string, a ...interface{}) { + fmt.Fprintf(os.Stderr, s, a...) + exitCode = 1 +} diff --git a/test/util/lib.sh b/test/util/lib.sh index 0deea41..10870b6 100644 --- a/test/util/lib.sh +++ b/test/util/lib.sh @@ -118,6 +118,11 @@ function minidns_bg() { MINIDNS=$! } +function fexp() { + ( cd ${UTILDIR}; go build fexp.go ) + ${UTILDIR}/fexp "$@" +} + function success() { echo success }