Refactored and added unit tests for configuration

This commit is contained in:
Jeromy Streets 2018-07-15 00:09:51 -07:00
parent 571765d935
commit 3700c31599
7 changed files with 565 additions and 22 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
vendor/

View File

@ -1,7 +1,19 @@
FROM golang:latest as builder
COPY serve.go /
WORKDIR /
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o serve .
FROM golang:1.10.3 as builder
ENV BUILD_DIR /go/src/github.com/halverneus/static-file-server
ENV MAIN github.com/halverneus/static-file-server/bin/serve
RUN curl -fsSL -o /usr/local/bin/dep \
https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64 && \
chmod +x /usr/local/bin/dep
RUN mkdir -p ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY . .
RUN dep ensure -vendor-only
RUN go test ./...
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o /serve ${MAIN}
FROM scratch
COPY --from=builder /serve /

15
Gopkg.lock generated Normal file
View File

@ -0,0 +1,15 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "ce4cfa8aa3eb29f45e7ba341fdeac9820969e663181e81bddfc4a3aa2d5169bb"
solver-name = "gps-cdcl"
solver-version = 1

34
Gopkg.toml Normal file
View File

@ -0,0 +1,34 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "2.2.1"
[prune]
go-tests = true
unused-packages = true

View File

@ -6,10 +6,12 @@ import (
"net/http"
"os"
"strings"
"github.com/halverneus/static-file-server/config"
)
var (
version = "Version 1.1"
version = "Version 1.3"
help = `
NAME
@ -126,17 +128,13 @@ func main() {
}
// Collect environment variables.
folder := env("FOLDER", "/web") + "/"
host := env("HOST", "")
port := env("PORT", "8080")
showListing := envAsBool("SHOW_LISTING", true)
tlsCert := env("TLS_CERT", "")
tlsKey := env("TLS_KEY", "")
urlPrefix := env("URL_PREFIX", "")
if err := config.Load(""); nil != err {
log.Fatalf("While loading configuration got %v", err)
}
// If HTTPS is to be used, verify both TLS_* environment variables are set.
if 0 < len(tlsCert) || 0 < len(tlsKey) {
if 0 == len(tlsCert) || 0 == len(tlsKey) {
if 0 < len(config.Get.TLSCert) || 0 < len(config.Get.TLSKey) {
if 0 == len(config.Get.TLSCert) || 0 == len(config.Get.TLSKey) {
log.Fatalln(
"If value for environment variable 'TLS_CERT' or 'TLS_KEY' is set " +
"then value for environment variable 'TLS_KEY' or 'TLS_CERT' must " +
@ -146,8 +144,8 @@ func main() {
}
// If the URL path prefix is to be used, verify it is properly formatted.
if 0 < len(urlPrefix) &&
(!strings.HasPrefix(urlPrefix, "/") || strings.HasSuffix(urlPrefix, "/")) {
if 0 < len(config.Get.URLPrefix) &&
(!strings.HasPrefix(config.Get.URLPrefix, "/") || strings.HasSuffix(config.Get.URLPrefix, "/")) {
log.Fatalln(
"Value for environment variable 'URL_PREFIX' must start " +
"with '/' and not end with '/'. Example: '/my/prefix'",
@ -156,18 +154,24 @@ func main() {
// Choose and set the appropriate, optimized static file serving function.
var handler http.HandlerFunc
if 0 == len(urlPrefix) {
handler = handleListing(showListing, basicHandler(folder))
if 0 == len(config.Get.URLPrefix) {
handler = handleListing(config.Get.ShowListing, basicHandler(config.Get.Folder))
} else {
handler = handleListing(showListing, prefixHandler(folder, urlPrefix))
handler = handleListing(config.Get.ShowListing, prefixHandler(config.Get.Folder, config.Get.URLPrefix))
}
http.HandleFunc("/", handler)
// Serve files over HTTP or HTTPS based on paths to TLS files being provided.
if 0 == len(tlsCert) {
log.Fatalln(http.ListenAndServe(host+":"+port, nil))
binding := fmt.Sprintf("%s:%d", config.Get.Host, config.Get.Port)
if 0 == len(config.Get.TLSCert) {
log.Fatalln(http.ListenAndServe(binding, nil))
} else {
log.Fatalln(http.ListenAndServeTLS(host+":"+port, tlsCert, tlsKey, nil))
log.Fatalln(http.ListenAndServeTLS(
binding,
config.Get.TLSCert,
config.Get.TLSKey,
nil,
))
}
}

164
config/config.go Normal file
View File

@ -0,0 +1,164 @@
package config
import (
"fmt"
"io/ioutil"
"log"
"os"
"strconv"
"strings"
yaml "gopkg.in/yaml.v2"
)
var (
// Get the desired configuration value.
Get struct {
Folder string `yaml:"folder"`
Host string `yaml:"host"`
Port uint16 `yaml:"port"`
ShowListing bool `yaml:"show-listing"`
TLSCert string `yaml:"tls-cert"`
TLSKey string `yaml:"tls-key"`
URLPrefix string `yaml:"url-prefix"`
}
)
const (
folderKey = "FOLDER"
hostKey = "HOST"
portKey = "PORT"
showListingKey = "SHOW_LISTING"
tlsCertKey = "TLS_CERT"
tlsKeyKey = "TLS_KEY"
urlPrefixKey = "URL_PREFIX"
)
const (
defaultFolder = "/web"
defaultHost = ""
defaultPort = uint16(8080)
defaultShowListing = true
defaultTLSCert = ""
defaultTLSKey = ""
defaultURLPrefix = ""
)
func init() {
// init calls setDefaults to better support testing.
setDefaults()
}
func setDefaults() {
Get.Folder = defaultFolder
Get.Host = defaultHost
Get.Port = defaultPort
Get.ShowListing = defaultShowListing
Get.TLSCert = defaultTLSCert
Get.TLSKey = defaultTLSKey
Get.URLPrefix = defaultURLPrefix
}
// Load the configuration file.
func Load(filename string) (err error) {
// If no filename provided, assign envvars.
if "" == filename {
overrideWithEnvVars()
return
}
// Read contents from configuration file.
var contents []byte
if contents, err = ioutil.ReadFile(filename); nil != err {
return
}
// Parse contents into 'Get' configuration.
if err = yaml.Unmarshal(contents, &Get); nil != err {
return
}
overrideWithEnvVars()
return
}
// overrideWithEnvVars the default values and the configuration file values.
func overrideWithEnvVars() {
// Assign envvars, if set.
Get.Folder = envAsStr(folderKey, Get.Folder)
Get.Host = envAsStr(hostKey, Get.Host)
Get.Port = envAsUint16(portKey, Get.Port)
Get.ShowListing = envAsBool(showListingKey, Get.ShowListing)
Get.TLSCert = envAsStr(tlsCertKey, Get.TLSCert)
Get.TLSKey = envAsStr(tlsKeyKey, Get.TLSKey)
Get.URLPrefix = envAsStr(urlPrefixKey, Get.URLPrefix)
}
// envAsStr returns the value of the environment variable as a string if set.
func envAsStr(key, fallback string) string {
if value := os.Getenv(key); "" != value {
return value
}
return fallback
}
// envAsUint16 returns the value of the environment variable as a uint16 if set.
func envAsUint16(key string, fallback uint16) uint16 {
// Retrieve the string value of the environment variable. If not set,
// fallback is used.
valueStr := os.Getenv(key)
if "" == valueStr {
return fallback
}
// Parse the string into a uint16.
base := 10
bitSize := 16
valueAsUint64, err := strconv.ParseUint(valueStr, base, bitSize)
if nil != err {
log.Printf(
"Invalid value for '%s': %v\nUsing fallback: %d",
key, err, fallback,
)
return fallback
}
return uint16(valueAsUint64)
}
// envAsBool returns the value for an environment variable or, if not set, a
// fallback value as a boolean.
func envAsBool(key string, fallback bool) bool {
// Retrieve the string value of the environment variable. If not set,
// fallback is used.
valueStr := os.Getenv(key)
if "" == valueStr {
return fallback
}
// Parse the string into a boolean.
value, err := strAsBool(valueStr)
if nil != err {
log.Printf(
"Invalid value for '%s': %v\nUsing fallback: %t",
key, err, fallback,
)
return fallback
}
return value
}
// strAsBool converts the intent of the passed value into a boolean
// representation.
func strAsBool(value string) (result bool, err error) {
lvalue := strings.ToLower(value)
switch lvalue {
case "0", "false", "f", "no", "n":
result = false
case "1", "true", "t", "yes", "y":
result = true
default:
result = false
msg := "Unknown conversion from string to bool for value '%s'"
err = fmt.Errorf(msg, value)
}
return
}

312
config/config_test.go Normal file
View File

@ -0,0 +1,312 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"testing"
)
func TestLoad(t *testing.T) {
// Verify envvars are set.
testFolder := "/my/directory"
os.Setenv(folderKey, testFolder)
if err := Load(""); nil != err {
t.Errorf(
"While loading an empty file name expected no error but got %v",
err,
)
}
if Get.Folder != testFolder {
t.Errorf(
"While loading an empty file name expected folder %s but got %s",
testFolder, Get.Folder,
)
}
// Verify error if file doesn't exist.
if err := Load("/this/file/should/never/exist"); nil == err {
t.Error("While loading non-existing file expected error but got nil")
}
// Verify bad YAML returns an error.
func(t *testing.T) {
filename := "testing.tmp"
contents := []byte("{")
defer os.Remove(filename)
if err := ioutil.WriteFile(filename, contents, 0666); nil != err {
t.Errorf("Failed to save bad YAML file with: %v\n", err)
}
if err := Load(filename); nil == err {
t.Error("While loading bad YAML expected error but got nil")
}
}(t)
// Verify good YAML returns no error and sets value.
func(t *testing.T) {
filename := "testing.tmp"
testFolder := "/test/folder"
contents := []byte(fmt.Sprintf(
`{"folder": "%s"}`, testFolder,
))
defer os.Remove(filename)
if err := ioutil.WriteFile(filename, contents, 0666); nil != err {
t.Errorf("Failed to save good YAML file with: %v\n", err)
}
if err := Load(filename); nil != err {
t.Errorf(
"While loading good YAML expected nil but got %v",
err,
)
}
}(t)
}
func TestOverrideWithEnvvars(t *testing.T) {
// Choose values that are different than defaults.
testFolder := "/my/directory"
testHost := "apets.life"
testPort := uint16(666)
testShowListing := false
testTLSCert := "my.pem"
testTLSKey := "my.key"
testURLPrefix := "/url/prefix"
// Set all environment variables with test values.
os.Setenv(folderKey, testFolder)
os.Setenv(hostKey, testHost)
os.Setenv(portKey, strconv.Itoa(int(testPort)))
os.Setenv(showListingKey, fmt.Sprintf("%t", testShowListing))
os.Setenv(tlsCertKey, testTLSCert)
os.Setenv(tlsKeyKey, testTLSKey)
os.Setenv(urlPrefixKey, testURLPrefix)
// Verification functions.
equalStrings := func(t *testing.T, name, key, expected, result string) {
if expected != result {
t.Errorf(
"While checking %s for '%s' expected '%s' but got '%s'",
name, key, expected, result,
)
}
}
equalUint16 := func(t *testing.T, name, key string, expected, result uint16) {
if expected != result {
t.Errorf(
"While checking %s for '%s' expected %d but got %d",
name, key, expected, result,
)
}
}
equalBool := func(t *testing.T, name, key string, expected, result bool) {
if expected != result {
t.Errorf(
"While checking %s for '%s' expected %t but got %t",
name, key, expected, result,
)
}
}
// Verify defaults.
setDefaults()
phase := "defaults"
equalStrings(t, phase, folderKey, defaultFolder, Get.Folder)
equalStrings(t, phase, hostKey, defaultHost, Get.Host)
equalUint16(t, phase, portKey, defaultPort, Get.Port)
equalBool(t, phase, showListingKey, defaultShowListing, Get.ShowListing)
equalStrings(t, phase, tlsCertKey, defaultTLSCert, Get.TLSCert)
equalStrings(t, phase, tlsKeyKey, defaultTLSKey, Get.TLSKey)
equalStrings(t, phase, urlPrefixKey, defaultURLPrefix, Get.URLPrefix)
// Apply overrides.
overrideWithEnvVars()
// Verify overrides.
phase = "overrides"
equalStrings(t, phase, folderKey, testFolder, Get.Folder)
equalStrings(t, phase, hostKey, testHost, Get.Host)
equalUint16(t, phase, portKey, testPort, Get.Port)
equalBool(t, phase, showListingKey, testShowListing, Get.ShowListing)
equalStrings(t, phase, tlsCertKey, testTLSCert, Get.TLSCert)
equalStrings(t, phase, tlsKeyKey, testTLSKey, Get.TLSKey)
equalStrings(t, phase, urlPrefixKey, testURLPrefix, Get.URLPrefix)
}
func TestEnvAsStr(t *testing.T) {
sv := "STRING_VALUE"
fv := "FLOAT_VALUE"
iv := "INT_VALUE"
bv := "BOOL_VALUE"
ev := "EMPTY_VALUE"
uv := "UNSET_VALUE"
sr := "String Cheese" // String result
fr := "123.456" // Float result
ir := "-123" // Int result
br := "true" // Bool result
er := "" // Empty result
fbr := "fallback result" // Fallback result
efbr := "" // Empty fallback result
os.Setenv(sv, sr)
os.Setenv(fv, fr)
os.Setenv(iv, ir)
os.Setenv(bv, br)
os.Setenv(ev, er)
testCases := []struct {
name string
key string
fallback string
result string
}{
{"Good string", sv, fbr, sr},
{"Float string", fv, fbr, fr},
{"Int string", iv, fbr, ir},
{"Bool string", bv, fbr, br},
{"Empty string", ev, fbr, fbr},
{"Unset", uv, fbr, fbr},
{"Good string with empty fallback", sv, efbr, sr},
{"Unset with empty fallback", uv, efbr, efbr},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := envAsStr(tc.key, tc.fallback)
if tc.result != result {
t.Errorf(
"For %s with a '%s' fallback expected '%s' but got '%s'",
tc.key, tc.fallback, tc.result, result,
)
}
})
}
}
func TestEnvAsUint16(t *testing.T) {
ubv := "UPPER_BOUNDS_VALUE"
lbv := "LOWER_BOUNDS_VALUE"
hv := "HIGH_VALUE"
lv := "LOW_VALUE"
bv := "BOOL_VALUE"
sv := "STRING_VALUE"
uv := "UNSET_VALUE"
fbr := uint16(666) // Fallback result
ubr := uint16(65535) // Upper bounds result
lbr := uint16(0) // Lower bounds result
os.Setenv(ubv, "65535")
os.Setenv(lbv, "0")
os.Setenv(hv, "65536")
os.Setenv(lv, "-1")
os.Setenv(bv, "true")
os.Setenv(sv, "Cheese")
testCases := []struct {
name string
key string
fallback uint16
result uint16
}{
{"Upper bounds", ubv, fbr, ubr},
{"Lower bounds", lbv, fbr, lbr},
{"Out-of-bounds high", hv, fbr, fbr},
{"Out-of-bounds low", lv, fbr, fbr},
{"Boolean", bv, fbr, fbr},
{"String", sv, fbr, fbr},
{"Unset", uv, fbr, fbr},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := envAsUint16(tc.key, tc.fallback)
if tc.result != result {
t.Errorf(
"For %s with a %d fallback expected %d but got %d",
tc.key, tc.fallback, tc.result, result,
)
}
})
}
}
func TestEnvAsBool(t *testing.T) {
tv := "TRUE_VALUE"
fv := "FALSE_VALUE"
bv := "BAD_VALUE"
uv := "UNSET_VALUE"
os.Setenv(tv, "True")
os.Setenv(fv, "NO")
os.Setenv(bv, "BAD")
testCases := []struct {
name string
key string
fallback bool
result bool
}{
{"True with true fallback", tv, true, true},
{"True with false fallback", tv, false, true},
{"False with true fallback", fv, true, false},
{"False with false fallback", fv, false, false},
{"Bad with true fallback", bv, true, true},
{"Bad with false fallback", bv, false, false},
{"Unset with true fallback", uv, true, true},
{"Unset with false fallback", uv, false, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := envAsBool(tc.key, tc.fallback)
if tc.result != result {
t.Errorf(
"For %s with a %t fallback expected %t but got %t",
tc.key, tc.fallback, tc.result, result,
)
}
})
}
}
func TestStrAsBool(t *testing.T) {
testCases := []struct {
name string
value string
result bool
isError bool
}{
{"Empty value", "", false, true},
{"False value", "0", false, false},
{"True value", "1", true, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := strAsBool(tc.value)
if result != tc.result {
t.Errorf(
"Expected %t for %s but got %t",
tc.result, tc.value, result,
)
}
if tc.isError && nil == err {
t.Errorf(
"Expected error for %s but got no error",
tc.value,
)
}
if !tc.isError && nil != err {
t.Errorf(
"Expected no error for %s but got %v",
tc.value, err,
)
}
})
}
}