Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
Go Httpbin
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Jira
Code
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Deploy
Container registry
Monitor
Service Desk
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
OSS
Nix
Go Httpbin
Commits
00323fa6
Unverified
Commit
00323fa6
authored
Nov 13, 2022
by
Will McCutchen
Committed by
GitHub
Nov 13, 2022
Browse files
Options
Downloads
Patches
Plain Diff
Refactor package main to enable testing of CLI interface (#98)
parent
aaf674eb
No related branches found
No related tags found
No related merge requests found
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
cmd/go-httpbin/main.go
+2
-179
2 additions, 179 deletions
cmd/go-httpbin/main.go
httpbin/cmd/cmd.go
+267
-0
267 additions, 0 deletions
httpbin/cmd/cmd.go
httpbin/cmd/cmd_test.go
+498
-0
498 additions, 0 deletions
httpbin/cmd/cmd_test.go
with
767 additions
and
179 deletions
cmd/go-httpbin/main.go
+
2
−
179
View file @
00323fa6
package
main
import
(
"context"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"github.com/mccutchen/go-httpbin/v2/httpbin"
)
const
(
defaultHost
=
"0.0.0.0"
defaultPort
=
8080
)
var
(
allowedRedirectDomains
string
host
string
httpsCertFile
string
httpsKeyFile
string
maxBodySize
int64
maxDuration
time
.
Duration
port
int
useRealHostname
bool
"github.com/mccutchen/go-httpbin/v2/httpbin/cmd"
)
func
main
()
{
flag
.
BoolVar
(
&
useRealHostname
,
"use-real-hostname"
,
false
,
"Expose value of os.Hostname() in the /hostname endpoint instead of dummy value"
)
flag
.
DurationVar
(
&
maxDuration
,
"max-duration"
,
httpbin
.
DefaultMaxDuration
,
"Maximum duration a response may take"
)
flag
.
Int64Var
(
&
maxBodySize
,
"max-body-size"
,
httpbin
.
DefaultMaxBodySize
,
"Maximum size of request or response, in bytes"
)
flag
.
IntVar
(
&
port
,
"port"
,
defaultPort
,
"Port to listen on"
)
flag
.
StringVar
(
&
allowedRedirectDomains
,
"allowed-redirect-domains"
,
""
,
"Comma-separated list of domains the /redirect-to endpoint will allow"
)
flag
.
StringVar
(
&
host
,
"host"
,
defaultHost
,
"Host to listen on"
)
flag
.
StringVar
(
&
httpsCertFile
,
"https-cert-file"
,
""
,
"HTTPS Server certificate file"
)
flag
.
StringVar
(
&
httpsKeyFile
,
"https-key-file"
,
""
,
"HTTPS Server private key file"
)
flag
.
Parse
()
// Command line flags take precedence over environment vars, so we only
// check for environment vars if we have default values for our command
// line flags.
var
err
error
if
maxBodySize
==
httpbin
.
DefaultMaxBodySize
&&
os
.
Getenv
(
"MAX_BODY_SIZE"
)
!=
""
{
maxBodySize
,
err
=
strconv
.
ParseInt
(
os
.
Getenv
(
"MAX_BODY_SIZE"
),
10
,
64
)
if
err
!=
nil
{
fmt
.
Fprintf
(
os
.
Stderr
,
"Error: invalid value %#v for env var MAX_BODY_SIZE: %s
\n\n
"
,
os
.
Getenv
(
"MAX_BODY_SIZE"
),
err
)
flag
.
Usage
()
os
.
Exit
(
1
)
}
}
if
maxDuration
==
httpbin
.
DefaultMaxDuration
&&
os
.
Getenv
(
"MAX_DURATION"
)
!=
""
{
maxDuration
,
err
=
time
.
ParseDuration
(
os
.
Getenv
(
"MAX_DURATION"
))
if
err
!=
nil
{
fmt
.
Fprintf
(
os
.
Stderr
,
"Error: invalid value %#v for env var MAX_DURATION: %s
\n\n
"
,
os
.
Getenv
(
"MAX_DURATION"
),
err
)
flag
.
Usage
()
os
.
Exit
(
1
)
}
}
if
host
==
defaultHost
&&
os
.
Getenv
(
"HOST"
)
!=
""
{
host
=
os
.
Getenv
(
"HOST"
)
}
if
port
==
defaultPort
&&
os
.
Getenv
(
"PORT"
)
!=
""
{
port
,
err
=
strconv
.
Atoi
(
os
.
Getenv
(
"PORT"
))
if
err
!=
nil
{
fmt
.
Fprintf
(
os
.
Stderr
,
"Error: invalid value %#v for env var PORT: %s
\n\n
"
,
os
.
Getenv
(
"PORT"
),
err
)
flag
.
Usage
()
os
.
Exit
(
1
)
}
}
if
httpsCertFile
==
""
&&
os
.
Getenv
(
"HTTPS_CERT_FILE"
)
!=
""
{
httpsCertFile
=
os
.
Getenv
(
"HTTPS_CERT_FILE"
)
}
if
httpsKeyFile
==
""
&&
os
.
Getenv
(
"HTTPS_KEY_FILE"
)
!=
""
{
httpsKeyFile
=
os
.
Getenv
(
"HTTPS_KEY_FILE"
)
}
var
serveTLS
bool
if
httpsCertFile
!=
""
||
httpsKeyFile
!=
""
{
serveTLS
=
true
if
httpsCertFile
==
""
||
httpsKeyFile
==
""
{
fmt
.
Fprintf
(
os
.
Stderr
,
"Error: https cert and key must both be provided
\n\n
"
)
flag
.
Usage
()
os
.
Exit
(
1
)
}
}
// useRealHostname will be true if either the `-use-real-hostname`
// arg is given on the command line or if the USE_REAL_HOSTNAME env var
// is one of "1" or "true".
if
useRealHostnameEnv
:=
os
.
Getenv
(
"USE_REAL_HOSTNAME"
);
useRealHostnameEnv
==
"1"
||
useRealHostnameEnv
==
"true"
{
useRealHostname
=
true
}
var
allowedRedirectDomainsList
[]
string
if
allowedRedirectDomains
==
""
&&
os
.
Getenv
(
"ALLOWED_REDIRECT_DOMAINS"
)
!=
""
{
allowedRedirectDomains
=
os
.
Getenv
(
"ALLOWED_REDIRECT_DOMAINS"
)
}
for
_
,
domain
:=
range
strings
.
Split
(
allowedRedirectDomains
,
","
)
{
if
strings
.
TrimSpace
(
domain
)
!=
""
{
allowedRedirectDomainsList
=
append
(
allowedRedirectDomainsList
,
strings
.
TrimSpace
(
domain
))
}
}
logger
:=
log
.
New
(
os
.
Stderr
,
""
,
0
)
// A hacky log helper function to ensure that shutdown messages are
// formatted the same as other messages. See StdLogObserver in
// httpbin/middleware.go for the format we're matching here.
serverLog
:=
func
(
msg
string
,
args
...
interface
{})
{
const
(
logFmt
=
"time=%q msg=%q"
dateFmt
=
"2006-01-02T15:04:05.9999"
)
logger
.
Printf
(
logFmt
,
time
.
Now
()
.
Format
(
dateFmt
),
fmt
.
Sprintf
(
msg
,
args
...
))
}
opts
:=
[]
httpbin
.
OptionFunc
{
httpbin
.
WithMaxBodySize
(
maxBodySize
),
httpbin
.
WithMaxDuration
(
maxDuration
),
httpbin
.
WithObserver
(
httpbin
.
StdLogObserver
(
logger
)),
}
if
useRealHostname
{
hostname
,
err
:=
os
.
Hostname
()
if
err
!=
nil
{
fmt
.
Fprintf
(
os
.
Stderr
,
"Error: use-real-hostname=true but hostname lookup failed: %s
\n
"
,
err
)
os
.
Exit
(
1
)
}
opts
=
append
(
opts
,
httpbin
.
WithHostname
(
hostname
))
}
if
len
(
allowedRedirectDomainsList
)
>
0
{
opts
=
append
(
opts
,
httpbin
.
WithAllowedRedirectDomains
(
allowedRedirectDomainsList
))
}
h
:=
httpbin
.
New
(
opts
...
)
listenAddr
:=
net
.
JoinHostPort
(
host
,
strconv
.
Itoa
(
port
))
server
:=
&
http
.
Server
{
Addr
:
listenAddr
,
Handler
:
h
.
Handler
(),
}
// shutdownCh triggers graceful shutdown on SIGINT or SIGTERM
shutdownCh
:=
make
(
chan
os
.
Signal
,
1
)
signal
.
Notify
(
shutdownCh
,
syscall
.
SIGINT
,
syscall
.
SIGTERM
)
// exitCh will be closed when it is safe to exit, after graceful shutdown
exitCh
:=
make
(
chan
struct
{})
go
func
()
{
sig
:=
<-
shutdownCh
serverLog
(
"shutdown started by signal: %s"
,
sig
)
shutdownTimeout
:=
maxDuration
+
1
*
time
.
Second
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
shutdownTimeout
)
defer
cancel
()
server
.
SetKeepAlivesEnabled
(
false
)
if
err
:=
server
.
Shutdown
(
ctx
);
err
!=
nil
{
serverLog
(
"shutdown error: %s"
,
err
)
}
close
(
exitCh
)
}()
var
listenErr
error
if
serveTLS
{
serverLog
(
"go-httpbin listening on https://%s"
,
listenAddr
)
listenErr
=
server
.
ListenAndServeTLS
(
httpsCertFile
,
httpsKeyFile
)
}
else
{
serverLog
(
"go-httpbin listening on http://%s"
,
listenAddr
)
listenErr
=
server
.
ListenAndServe
()
}
if
listenErr
!=
nil
&&
listenErr
!=
http
.
ErrServerClosed
{
logger
.
Fatalf
(
"failed to listen: %s"
,
listenErr
)
}
<-
exitCh
serverLog
(
"shutdown finished"
)
os
.
Exit
(
cmd
.
Main
())
}
This diff is collapsed.
Click to expand it.
httpbin/cmd/cmd.go
0 → 100644
+
267
−
0
View file @
00323fa6
package
cmd
import
(
"bytes"
"context"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"github.com/mccutchen/go-httpbin/v2/httpbin"
)
const
(
defaultListenHost
=
"0.0.0.0"
defaultListenPort
=
8080
// Reasonable defaults for our http server
srvReadTimeout
=
5
*
time
.
Second
srvReadHeaderTimeout
=
1
*
time
.
Second
srvMaxHeaderBytes
=
16
*
1024
// 16kb
)
// Main is the main entrypoint for the go-httpbin binary. See loadConfig() for
// command line argument parsing.
func
Main
()
int
{
return
mainImpl
(
os
.
Args
[
1
:
],
os
.
Getenv
,
os
.
Hostname
,
os
.
Stderr
)
}
// mainImpl is the real implementation of Main(), extracted for better
// testability.
func
mainImpl
(
args
[]
string
,
getEnv
func
(
string
)
string
,
getHostname
func
()
(
string
,
error
),
out
io
.
Writer
)
int
{
logger
:=
log
.
New
(
out
,
""
,
0
)
cfg
,
err
:=
loadConfig
(
args
,
getEnv
,
getHostname
)
if
err
!=
nil
{
if
cfgErr
,
ok
:=
err
.
(
ConfigError
);
ok
{
// for -h/-help, just print usage and exit without error
if
cfgErr
.
Err
==
flag
.
ErrHelp
{
fmt
.
Fprint
(
out
,
cfgErr
.
Usage
)
return
0
}
// anything else indicates a problem with CLI arguments and/or
// environment vars, so print error and usage and exit with an
// error status.
//
// note: seems like there's consensus that an exit code of 2 is
// often used to indicate a problem with the way a command was
// called, e.g.:
// https://stackoverflow.com/a/40484670/151221
// https://linuxconfig.org/list-of-exit-codes-on-linux
fmt
.
Fprintf
(
out
,
"error: %s
\n\n
%s"
,
cfgErr
.
Err
,
cfgErr
.
Usage
)
return
2
}
fmt
.
Fprintf
(
out
,
"error: %s"
,
err
)
return
1
}
opts
:=
[]
httpbin
.
OptionFunc
{
httpbin
.
WithMaxBodySize
(
cfg
.
MaxBodySize
),
httpbin
.
WithMaxDuration
(
cfg
.
MaxDuration
),
httpbin
.
WithObserver
(
httpbin
.
StdLogObserver
(
logger
)),
}
if
cfg
.
RealHostname
!=
""
{
opts
=
append
(
opts
,
httpbin
.
WithHostname
(
cfg
.
RealHostname
))
}
if
len
(
cfg
.
AllowedRedirectDomains
)
>
0
{
opts
=
append
(
opts
,
httpbin
.
WithAllowedRedirectDomains
(
cfg
.
AllowedRedirectDomains
))
}
app
:=
httpbin
.
New
(
opts
...
)
srv
:=
&
http
.
Server
{
Addr
:
net
.
JoinHostPort
(
cfg
.
ListenHost
,
strconv
.
Itoa
(
cfg
.
ListenPort
)),
Handler
:
app
.
Handler
(),
MaxHeaderBytes
:
srvMaxHeaderBytes
,
ReadHeaderTimeout
:
srvReadHeaderTimeout
,
ReadTimeout
:
srvReadTimeout
,
}
if
err
:=
listenAndServeGracefully
(
srv
,
cfg
,
logger
);
err
!=
nil
{
logger
.
Printf
(
"error: %s"
,
err
)
return
1
}
return
0
}
// config holds the configuration needed to initialize and run go-httpbin as a
// standalone server.
type
config
struct
{
AllowedRedirectDomains
[]
string
ListenHost
string
ListenPort
int
MaxBodySize
int64
MaxDuration
time
.
Duration
RealHostname
string
TLSCertFile
string
TLSKeyFile
string
// temporary placeholders for arguments that need extra processing
rawAllowedRedirectDomains
string
rawUseRealHostname
bool
}
// ConfigError is used to signal an error with a command line argument or
// environmment variable.
//
// It carries the command's usage output, so that we can decouple configuration
// parsing from error reporting for better testability.
type
ConfigError
struct
{
Err
error
Usage
string
}
// Error implements the error interface.
func
(
e
ConfigError
)
Error
()
string
{
return
e
.
Err
.
Error
()
}
// loadConfig parses command line arguments and env vars into a fully resolved
// Config struct. Command line arguments take precedence over env vars.
func
loadConfig
(
args
[]
string
,
getEnv
func
(
string
)
string
,
getHostname
func
()
(
string
,
error
))
(
*
config
,
error
)
{
cfg
:=
&
config
{}
fs
:=
flag
.
NewFlagSet
(
""
,
flag
.
ContinueOnError
)
fs
.
BoolVar
(
&
cfg
.
rawUseRealHostname
,
"use-real-hostname"
,
false
,
"Expose value of os.Hostname() in the /hostname endpoint instead of dummy value"
)
fs
.
DurationVar
(
&
cfg
.
MaxDuration
,
"max-duration"
,
httpbin
.
DefaultMaxDuration
,
"Maximum duration a response may take"
)
fs
.
Int64Var
(
&
cfg
.
MaxBodySize
,
"max-body-size"
,
httpbin
.
DefaultMaxBodySize
,
"Maximum size of request or response, in bytes"
)
fs
.
IntVar
(
&
cfg
.
ListenPort
,
"port"
,
defaultListenPort
,
"Port to listen on"
)
fs
.
StringVar
(
&
cfg
.
rawAllowedRedirectDomains
,
"allowed-redirect-domains"
,
""
,
"Comma-separated list of domains the /redirect-to endpoint will allow"
)
fs
.
StringVar
(
&
cfg
.
ListenHost
,
"host"
,
defaultListenHost
,
"Host to listen on"
)
fs
.
StringVar
(
&
cfg
.
TLSCertFile
,
"https-cert-file"
,
""
,
"HTTPS Server certificate file"
)
fs
.
StringVar
(
&
cfg
.
TLSKeyFile
,
"https-key-file"
,
""
,
"HTTPS Server private key file"
)
// in order to fully control error output whether CLI arguments or env vars
// are used to configure the app, we need to take control away from the
// flagset, which by defaults prints errors automatically.
//
// so, we capture the "usage" output it would generate and then trick it
// into generating no output on errors, since they'll be handled by the
// caller.
//
// yes, this is goofy, but it makes the CLI testable!
buf
:=
&
bytes
.
Buffer
{}
fs
.
SetOutput
(
buf
)
fs
.
Usage
()
usage
:=
buf
.
String
()
fs
.
SetOutput
(
io
.
Discard
)
if
err
:=
fs
.
Parse
(
args
);
err
!=
nil
{
return
nil
,
ConfigError
{
err
,
usage
}
}
// helper to generate a new ConfigError to return
configErr
:=
func
(
format
string
,
a
...
interface
{})
error
{
return
ConfigError
{
Err
:
fmt
.
Errorf
(
format
,
a
...
),
Usage
:
usage
,
}
}
var
err
error
// Command line flags take precedence over environment vars, so we only
// check for environment vars if we have default values for our command
// line flags.
if
cfg
.
MaxBodySize
==
httpbin
.
DefaultMaxBodySize
&&
getEnv
(
"MAX_BODY_SIZE"
)
!=
""
{
cfg
.
MaxBodySize
,
err
=
strconv
.
ParseInt
(
getEnv
(
"MAX_BODY_SIZE"
),
10
,
64
)
if
err
!=
nil
{
return
nil
,
configErr
(
"invalid value %#v for env var MAX_BODY_SIZE: parse error"
,
getEnv
(
"MAX_BODY_SIZE"
))
}
}
if
cfg
.
MaxDuration
==
httpbin
.
DefaultMaxDuration
&&
getEnv
(
"MAX_DURATION"
)
!=
""
{
cfg
.
MaxDuration
,
err
=
time
.
ParseDuration
(
getEnv
(
"MAX_DURATION"
))
if
err
!=
nil
{
return
nil
,
configErr
(
"invalid value %#v for env var MAX_DURATION: parse error"
,
getEnv
(
"MAX_DURATION"
))
}
}
if
cfg
.
ListenHost
==
defaultListenHost
&&
getEnv
(
"HOST"
)
!=
""
{
cfg
.
ListenHost
=
getEnv
(
"HOST"
)
}
if
cfg
.
ListenPort
==
defaultListenPort
&&
getEnv
(
"PORT"
)
!=
""
{
cfg
.
ListenPort
,
err
=
strconv
.
Atoi
(
getEnv
(
"PORT"
))
if
err
!=
nil
{
return
nil
,
configErr
(
"invalid value %#v for env var PORT: parse error"
,
getEnv
(
"PORT"
))
}
}
if
cfg
.
TLSCertFile
==
""
&&
getEnv
(
"HTTPS_CERT_FILE"
)
!=
""
{
cfg
.
TLSCertFile
=
getEnv
(
"HTTPS_CERT_FILE"
)
}
if
cfg
.
TLSKeyFile
==
""
&&
getEnv
(
"HTTPS_KEY_FILE"
)
!=
""
{
cfg
.
TLSKeyFile
=
getEnv
(
"HTTPS_KEY_FILE"
)
}
if
cfg
.
TLSCertFile
!=
""
||
cfg
.
TLSKeyFile
!=
""
{
if
cfg
.
TLSCertFile
==
""
||
cfg
.
TLSKeyFile
==
""
{
return
nil
,
configErr
(
"https cert and key must both be provided"
)
}
}
// useRealHostname will be true if either the `-use-real-hostname`
// arg is given on the command line or if the USE_REAL_HOSTNAME env var
// is one of "1" or "true".
if
useRealHostnameEnv
:=
getEnv
(
"USE_REAL_HOSTNAME"
);
useRealHostnameEnv
==
"1"
||
useRealHostnameEnv
==
"true"
{
cfg
.
rawUseRealHostname
=
true
}
if
cfg
.
rawUseRealHostname
{
cfg
.
RealHostname
,
err
=
getHostname
()
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"could not look up real hostname: %w"
,
err
)
}
}
// split comma-separated list of domains into a slice, if given
if
cfg
.
rawAllowedRedirectDomains
==
""
&&
getEnv
(
"ALLOWED_REDIRECT_DOMAINS"
)
!=
""
{
cfg
.
rawAllowedRedirectDomains
=
getEnv
(
"ALLOWED_REDIRECT_DOMAINS"
)
}
for
_
,
domain
:=
range
strings
.
Split
(
cfg
.
rawAllowedRedirectDomains
,
","
)
{
if
strings
.
TrimSpace
(
domain
)
!=
""
{
cfg
.
AllowedRedirectDomains
=
append
(
cfg
.
AllowedRedirectDomains
,
strings
.
TrimSpace
(
domain
))
}
}
// reset temporary fields to their zero values
cfg
.
rawAllowedRedirectDomains
=
""
cfg
.
rawUseRealHostname
=
false
return
cfg
,
nil
}
func
listenAndServeGracefully
(
srv
*
http
.
Server
,
cfg
*
config
,
logger
*
log
.
Logger
)
error
{
doneCh
:=
make
(
chan
error
,
1
)
go
func
()
{
sigCh
:=
make
(
chan
os
.
Signal
,
1
)
signal
.
Notify
(
sigCh
,
syscall
.
SIGTERM
,
syscall
.
SIGINT
)
<-
sigCh
logger
.
Printf
(
"shutting down ..."
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
cfg
.
MaxDuration
+
1
*
time
.
Second
)
defer
cancel
()
doneCh
<-
srv
.
Shutdown
(
ctx
)
}()
var
err
error
if
cfg
.
TLSCertFile
!=
""
&&
cfg
.
TLSKeyFile
!=
""
{
logger
.
Printf
(
"go-httpbin listening on https://%s"
,
srv
.
Addr
)
err
=
srv
.
ListenAndServeTLS
(
cfg
.
TLSCertFile
,
cfg
.
TLSKeyFile
)
}
else
{
logger
.
Printf
(
"go-httpbin listening on http://%s"
,
srv
.
Addr
)
err
=
srv
.
ListenAndServe
()
}
if
err
!=
nil
&&
err
!=
http
.
ErrServerClosed
{
return
err
}
return
<-
doneCh
}
This diff is collapsed.
Click to expand it.
httpbin/cmd/cmd_test.go
0 → 100644
+
498
−
0
View file @
00323fa6
package
cmd
import
(
"bytes"
"errors"
"flag"
"os"
"reflect"
"testing"
"time"
"github.com/mccutchen/go-httpbin/v2/httpbin"
)
// To update, run:
// make && ./dist/go-httpbin -h 2>&1 | pbcopy
const
usage
=
`Usage:
-allowed-redirect-domains string
Comma-separated list of domains the /redirect-to endpoint will allow
-host string
Host to listen on (default "0.0.0.0")
-https-cert-file string
HTTPS Server certificate file
-https-key-file string
HTTPS Server private key file
-max-body-size int
Maximum size of request or response, in bytes (default 1048576)
-max-duration duration
Maximum duration a response may take (default 10s)
-port int
Port to listen on (default 8080)
-use-real-hostname
Expose value of os.Hostname() in the /hostname endpoint instead of dummy value
`
func
TestLoadConfig
(
t
*
testing
.
T
)
{
t
.
Parallel
()
testDefaultRealHostname
:=
"real-hostname.test"
getHostnameDefault
:=
func
()
(
string
,
error
)
{
return
testDefaultRealHostname
,
nil
}
testCases
:=
map
[
string
]
struct
{
args
[]
string
env
map
[
string
]
string
getHostname
func
()
(
string
,
error
)
wantCfg
*
config
wantErr
error
wantOut
string
}{
"defaults"
:
{
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"-h"
:
{
args
:
[]
string
{
"-h"
},
wantErr
:
flag
.
ErrHelp
,
},
"-help"
:
{
args
:
[]
string
{
"-help"
},
wantErr
:
flag
.
ErrHelp
,
},
// max body size
"invalid -max-body-size"
:
{
args
:
[]
string
{
"-max-body-size"
,
"foo"
},
wantErr
:
errors
.
New
(
"invalid value
\"
foo
\"
for flag -max-body-size: parse error"
),
},
"invalid MAX_BODY_SIZE"
:
{
env
:
map
[
string
]
string
{
"MAX_BODY_SIZE"
:
"foo"
},
wantErr
:
errors
.
New
(
"invalid value
\"
foo
\"
for env var MAX_BODY_SIZE: parse error"
),
},
"ok -max-body-size"
:
{
args
:
[]
string
{
"-max-body-size"
,
"99"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
99
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"ok MAX_BODY_SIZE"
:
{
env
:
map
[
string
]
string
{
"MAX_BODY_SIZE"
:
"9999"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
9999
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"ok max body size CLI takes precedence over env"
:
{
args
:
[]
string
{
"-max-body-size"
,
"1234"
},
env
:
map
[
string
]
string
{
"MAX_BODY_SIZE"
:
"5678"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
1234
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
// max duration
"invalid -max-duration"
:
{
args
:
[]
string
{
"-max-duration"
,
"foo"
},
wantErr
:
errors
.
New
(
"invalid value
\"
foo
\"
for flag -max-duration: parse error"
),
},
"invalid MAX_DURATION"
:
{
env
:
map
[
string
]
string
{
"MAX_DURATION"
:
"foo"
},
wantErr
:
errors
.
New
(
"invalid value
\"
foo
\"
for env var MAX_DURATION: parse error"
),
},
"ok -max-duration"
:
{
args
:
[]
string
{
"-max-duration"
,
"99s"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
99
*
time
.
Second
,
},
},
"ok MAX_DURATION"
:
{
env
:
map
[
string
]
string
{
"MAX_DURATION"
:
"9999s"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
9999
*
time
.
Second
,
},
},
"ok max duration size CLI takes precedence over env"
:
{
args
:
[]
string
{
"-max-duration"
,
"1234s"
},
env
:
map
[
string
]
string
{
"MAX_DURATION"
:
"5678s"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
1234
*
time
.
Second
,
},
},
// host
"ok -host"
:
{
args
:
[]
string
{
"-host"
,
"192.0.0.1"
},
wantCfg
:
&
config
{
ListenHost
:
"192.0.0.1"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"ok HOST"
:
{
env
:
map
[
string
]
string
{
"HOST"
:
"192.0.0.2"
},
wantCfg
:
&
config
{
ListenHost
:
"192.0.0.2"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"ok host cli takes precedence over end"
:
{
args
:
[]
string
{
"-host"
,
"99.99.99.99"
},
env
:
map
[
string
]
string
{
"HOST"
:
"11.11.11.11"
},
wantCfg
:
&
config
{
ListenHost
:
"99.99.99.99"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
// port
"invalid -port"
:
{
args
:
[]
string
{
"-port"
,
"foo"
},
wantErr
:
errors
.
New
(
"invalid value
\"
foo
\"
for flag -port: parse error"
),
},
"invalid PORT"
:
{
env
:
map
[
string
]
string
{
"PORT"
:
"foo"
},
wantErr
:
errors
.
New
(
"invalid value
\"
foo
\"
for env var PORT: parse error"
),
},
"ok -port"
:
{
args
:
[]
string
{
"-port"
,
"99"
},
wantCfg
:
&
config
{
ListenHost
:
defaultListenHost
,
ListenPort
:
99
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"ok PORT"
:
{
env
:
map
[
string
]
string
{
"PORT"
:
"9999"
},
wantCfg
:
&
config
{
ListenHost
:
defaultListenHost
,
ListenPort
:
9999
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"ok port CLI takes precedence over env"
:
{
args
:
[]
string
{
"-port"
,
"1234"
},
env
:
map
[
string
]
string
{
"PORT"
:
"5678"
},
wantCfg
:
&
config
{
ListenHost
:
defaultListenHost
,
ListenPort
:
1234
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
// https cert file
"https cert and key must both be provided, cert only"
:
{
args
:
[]
string
{
"-https-cert-file"
,
"/tmp/test.crt"
},
wantErr
:
errors
.
New
(
"https cert and key must both be provided"
),
},
"https cert and key must both be provided, key only"
:
{
args
:
[]
string
{
"-https-key-file"
,
"/tmp/test.crt"
},
wantErr
:
errors
.
New
(
"https cert and key must both be provided"
),
},
"ok https CLI"
:
{
args
:
[]
string
{
"-https-cert-file"
,
"/tmp/test.crt"
,
"-https-key-file"
,
"/tmp/test.key"
,
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
TLSCertFile
:
"/tmp/test.crt"
,
TLSKeyFile
:
"/tmp/test.key"
,
},
},
"ok https env"
:
{
env
:
map
[
string
]
string
{
"HTTPS_CERT_FILE"
:
"/tmp/test.crt"
,
"HTTPS_KEY_FILE"
:
"/tmp/test.key"
,
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
TLSCertFile
:
"/tmp/test.crt"
,
TLSKeyFile
:
"/tmp/test.key"
,
},
},
"ok https CLI takes precedence over env"
:
{
args
:
[]
string
{
"-https-cert-file"
,
"/tmp/cli.crt"
,
"-https-key-file"
,
"/tmp/cli.key"
,
},
env
:
map
[
string
]
string
{
"HTTPS_CERT_FILE"
:
"/tmp/env.crt"
,
"HTTPS_KEY_FILE"
:
"/tmp/env.key"
,
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
TLSCertFile
:
"/tmp/cli.crt"
,
TLSKeyFile
:
"/tmp/cli.key"
,
},
},
// use-real-hostname
"ok -use-real-hostname"
:
{
args
:
[]
string
{
"-use-real-hostname"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
RealHostname
:
testDefaultRealHostname
,
},
},
"ok -use-real-hostname=1"
:
{
args
:
[]
string
{
"-use-real-hostname"
,
"1"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
RealHostname
:
testDefaultRealHostname
,
},
},
"ok -use-real-hostname=true"
:
{
args
:
[]
string
{
"-use-real-hostname"
,
"true"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
RealHostname
:
testDefaultRealHostname
,
},
},
// any value for the argument is interpreted as true
"ok -use-real-hostname=0"
:
{
args
:
[]
string
{
"-use-real-hostname"
,
"0"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
RealHostname
:
testDefaultRealHostname
,
},
},
"ok USE_REAL_HOSTNAME=1"
:
{
env
:
map
[
string
]
string
{
"USE_REAL_HOSTNAME"
:
"1"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
RealHostname
:
testDefaultRealHostname
,
},
},
"ok USE_REAL_HOSTNAME=true"
:
{
env
:
map
[
string
]
string
{
"USE_REAL_HOSTNAME"
:
"true"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
RealHostname
:
testDefaultRealHostname
,
},
},
// case sensitive
"ok USE_REAL_HOSTNAME=TRUE"
:
{
env
:
map
[
string
]
string
{
"USE_REAL_HOSTNAME"
:
"TRUE"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"ok USE_REAL_HOSTNAME=false"
:
{
env
:
map
[
string
]
string
{
"USE_REAL_HOSTNAME"
:
"false"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
},
},
"err real hostname error"
:
{
env
:
map
[
string
]
string
{
"USE_REAL_HOSTNAME"
:
"true"
},
getHostname
:
func
()
(
string
,
error
)
{
return
""
,
errors
.
New
(
"hostname error"
)
},
wantErr
:
errors
.
New
(
"could not look up real hostname: hostname error"
),
},
// allowed-redirect-domains
"ok -allowed-redirect-domains"
:
{
args
:
[]
string
{
"-allowed-redirect-domains"
,
"foo,bar"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
AllowedRedirectDomains
:
[]
string
{
"foo"
,
"bar"
},
},
},
"ok ALLOWED_REDIRECT_DOMAINS"
:
{
env
:
map
[
string
]
string
{
"ALLOWED_REDIRECT_DOMAINS"
:
"foo,bar"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
AllowedRedirectDomains
:
[]
string
{
"foo"
,
"bar"
},
},
},
"ok allowed redirect domains CLI takes precedence over env"
:
{
args
:
[]
string
{
"-allowed-redirect-domains"
,
"foo.cli,bar.cli"
},
env
:
map
[
string
]
string
{
"ALLOWED_REDIRECT_DOMAINS"
:
"foo.env,bar.env"
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
AllowedRedirectDomains
:
[]
string
{
"foo.cli"
,
"bar.cli"
},
},
},
"ok allowed redirect domains are normalized"
:
{
args
:
[]
string
{
"-allowed-redirect-domains"
,
"foo, bar ,, baz "
},
wantCfg
:
&
config
{
ListenHost
:
"0.0.0.0"
,
ListenPort
:
8080
,
MaxBodySize
:
httpbin
.
DefaultMaxBodySize
,
MaxDuration
:
httpbin
.
DefaultMaxDuration
,
AllowedRedirectDomains
:
[]
string
{
"foo"
,
"bar"
,
"baz"
},
},
},
}
for
name
,
tc
:=
range
testCases
{
tc
:=
tc
t
.
Run
(
name
,
func
(
t
*
testing
.
T
)
{
t
.
Parallel
()
if
tc
.
getHostname
==
nil
{
tc
.
getHostname
=
getHostnameDefault
}
cfg
,
err
:=
loadConfig
(
tc
.
args
,
func
(
key
string
)
string
{
return
tc
.
env
[
key
]
},
tc
.
getHostname
)
switch
{
case
tc
.
wantErr
!=
nil
&&
err
!=
nil
:
if
tc
.
wantErr
.
Error
()
!=
err
.
Error
()
{
t
.
Fatalf
(
"incorrect error
\n
want: %q
\n
got: %q"
,
tc
.
wantErr
,
err
)
}
case
tc
.
wantErr
!=
nil
:
t
.
Fatalf
(
"want error %q, got nil"
,
tc
.
wantErr
)
case
err
!=
nil
:
t
.
Fatalf
(
"got unexpected error: %q"
,
err
)
}
if
!
reflect
.
DeepEqual
(
tc
.
wantCfg
,
cfg
)
{
t
.
Fatalf
(
"bad config
\n
want: %#v
\n
got: %#v"
,
tc
.
wantCfg
,
cfg
)
}
})
}
}
func
TestMainImpl
(
t
*
testing
.
T
)
{
t
.
Parallel
()
testCases
:=
map
[
string
]
struct
{
args
[]
string
env
map
[
string
]
string
getHostname
func
()
(
string
,
error
)
wantCode
int
wantOut
string
}{
"help"
:
{
args
:
[]
string
{
"-h"
},
wantCode
:
0
,
wantOut
:
usage
,
},
"cli error"
:
{
args
:
[]
string
{
"-max-body-size"
,
"foo"
},
wantCode
:
2
,
wantOut
:
"error: invalid value
\"
foo
\"
for flag -max-body-size: parse error
\n\n
"
+
usage
,
},
"unknown argument"
:
{
args
:
[]
string
{
"-zzz"
},
wantCode
:
2
,
wantOut
:
"error: flag provided but not defined: -zzz
\n\n
"
+
usage
,
},
"real hostname error"
:
{
args
:
[]
string
{
"-use-real-hostname"
},
getHostname
:
func
()
(
string
,
error
)
{
return
""
,
errors
.
New
(
"hostname failure"
)
},
wantCode
:
1
,
wantOut
:
"error: could not look up real hostname: hostname failure"
,
},
"server error"
:
{
args
:
[]
string
{
"-port"
,
"-256"
},
wantCode
:
1
,
wantOut
:
"go-httpbin listening on http://0.0.0.0:-256
\n
error: listen tcp: address -256: invalid port
\n
"
,
},
"tls cert error"
:
{
args
:
[]
string
{
"-port"
,
"0"
,
"-https-cert-file"
,
"./https-cert-does-not-exist"
,
"-https-key-file"
,
"./https-key-does-not-exist"
,
},
wantCode
:
1
,
wantOut
:
"go-httpbin listening on https://0.0.0.0:0
\n
error: open ./https-cert-does-not-exist: no such file or directory
\n
"
,
},
}
for
name
,
tc
:=
range
testCases
{
tc
:=
tc
t
.
Run
(
name
,
func
(
t
*
testing
.
T
)
{
t
.
Parallel
()
if
tc
.
getHostname
==
nil
{
tc
.
getHostname
=
os
.
Hostname
}
buf
:=
&
bytes
.
Buffer
{}
gotCode
:=
mainImpl
(
tc
.
args
,
func
(
key
string
)
string
{
return
tc
.
env
[
key
]
},
tc
.
getHostname
,
buf
)
out
:=
buf
.
String
()
if
gotCode
!=
tc
.
wantCode
{
t
.
Logf
(
"unexpected error: output:
\n
%s"
,
out
)
t
.
Fatalf
(
"expected return code %d, got %d"
,
tc
.
wantCode
,
gotCode
)
}
if
out
!=
tc
.
wantOut
{
t
.
Fatalf
(
"output mismatch error:
\n
want: %q
\n
got: %q"
,
tc
.
wantOut
,
out
)
}
})
}
}
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment