aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKumar Damani <me@kumardamani.net>2022-06-07 19:53:26 +0000
committerKumar Damani <me@kumardamani.net>2022-06-07 19:53:26 +0000
commit4ec6622f7bb891fa68f8bf45f5b8d84dcc46dae7 (patch)
treed2eec632bf0712fe6730f85bc9f29dcb5294a066
parent1e59fb1623ccefc60631d964634640a776eaaec0 (diff)
added unit tests.
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--Dockerfile11
-rw-r--r--nccli.go95
-rw-r--r--nccli_test.go185
4 files changed, 266 insertions, 32 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 09211e8..7cc1dd6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,6 +3,7 @@ services:
- docker:dind
stages:
+ - test
- build
variables:
@@ -12,6 +13,12 @@ variables:
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+unit-test:
+ stage: test
+ coverage: '/coverage: \d+.\d+% of statements/'
+ script:
+ - docker build --pull --target test -t $IMAGE:temp .
+
build:
stage: build
script:
diff --git a/Dockerfile b/Dockerfile
index eb1744e..2932337 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,17 @@
-FROM golang:alpine as builder
+FROM golang:alpine as base
-RUN apk update && apk add ca-certificates
+RUN apk add --update \
+ ca-certificates \
+ gcc \
+ libc-dev
COPY . $GOPATH/src/gitlab.com/kdam0/nccli
WORKDIR $GOPATH/src/gitlab.com/kdam0/nccli
+FROM base as test
+RUN ["go", "test", "-cover"]
+
+FROM base as builder
RUN go get -d -v
ARG opts
RUN env ${opts} go build -o /bin/nccli
diff --git a/nccli.go b/nccli.go
index 87d578a..fec69c3 100644
--- a/nccli.go
+++ b/nccli.go
@@ -15,7 +15,7 @@ import (
var (
verbose = false
- url string
+ apiUrl string
)
// Based on: https://www.namecheap.com/support/api/methods/domains-dns/set-hosts/
@@ -53,28 +53,44 @@ type Host struct {
// Helper functions //
-// Check if a env var is defined. If not defined, show error.
+// Check if a env var is defined. If not defined, return error.
// Args: key - the env var to check for.
// Returns if exists, the value of the var.
-func getEnv(key string) string {
+func getEnv(key string) (string, error) {
value := os.Getenv(key)
if len(value) == 0 {
- log.Fatalf("Env var %s is required and missing.", key)
+ return value, fmt.Errorf("Env var %s is required and missing.", key)
}
if verbose {
fmt.Println(key, value)
}
- return value
+ return value, nil
}
-// Load ENV vars into the url var.
-func loadConfig() {
- url = "https://api.namecheap.com/xml.response?ApiUser=" + getEnv("API_USER") +
- "&ApiKey=" + getEnv("API_KEY") +
- "&UserName=" + getEnv("API_USER") +
- "&ClientIp=" + getEnv("ACCESS_IP") +
- "&SLD=" + getEnv("DOMAIN_MAIN") +
- "&TLD=" + getEnv("DOMAIN_END")
+// Load ENV vars into the apiUrl var.
+// Returns nil if config loaded successfully, error otherwise.
+func loadConfig() error {
+ keys := [5]string{"API_USER", "API_KEY", "ACCESS_IP", "DOMAIN_MAIN", "DOMAIN_END"}
+ for _, key := range keys {
+ _, err := getEnv(key)
+ if err != nil {
+ return err
+ }
+ }
+ user, _ := getEnv("API_USER")
+ apiKey, _ := getEnv("API_KEY")
+ accessIP, _ := getEnv("ACCESS_IP")
+ domainMain, _ := getEnv("DOMAIN_MAIN")
+ domainEnd, _ := getEnv("DOMAIN_END")
+
+ apiUrl = "https://api.namecheap.com/xml.response?ApiUser=" + user +
+ "&ApiKey=" + apiKey +
+ "&UserName=" + user +
+ "&ClientIp=" + accessIP +
+ "&SLD=" + domainMain +
+ "&TLD=" + domainEnd
+
+ return nil
}
// Print host recrods to stdout.
@@ -96,17 +112,17 @@ func logRecords(hosts []Host) {
// Make the API request to Namecheap.
// Args: endPoint - the api method to call.
-// extraParams (optional) - more params to add to the url.
+// extraParams (optional) - more params to add to the apiUrl.
// Returns the response byte stream.
func makeReq(endPoint string, extraParams ...string) []byte {
- apiUrl := url + "&Command=namecheap.domains.dns." + endPoint
+ methodUrl := apiUrl + "&Command=namecheap.domains.dns." + endPoint
for _, param := range extraParams {
- apiUrl += param
+ methodUrl += param
}
if verbose {
- fmt.Println("Using api url:", apiUrl)
+ fmt.Println("Using api url:", methodUrl)
}
var (
@@ -115,9 +131,9 @@ func makeReq(endPoint string, extraParams ...string) []byte {
)
switch endPoint {
case "gethosts":
- response, err = http.Get(apiUrl)
+ response, err = http.Get(methodUrl)
case "sethosts":
- response, err = http.Post(apiUrl, "application/xml", nil)
+ response, err = http.Post(methodUrl, "application/xml", nil)
default:
log.Fatalln("Error: Invalid request type specified.")
}
@@ -137,24 +153,27 @@ func makeReq(endPoint string, extraParams ...string) []byte {
// Args: userChoicesStrs - slice of strings corresponding to user choices.
// maxVal - the maximum int value allowed as input.
// Returns the slice of integers.
-func normalizeUserChoices(userChoicesStrs []string, maxVal int) []int {
+func normalizeUserChoices(userChoicesStrs []string, maxVal int) ([]int, error) {
var chosenInts []int
for _, choice := range userChoicesStrs {
- if len(strings.TrimSpace(choice)) == 0 {
+ text := strings.TrimSpace(choice)
+ if len(text) == 0 {
continue
}
- val, err := strconv.Atoi(choice)
+ val, err := strconv.Atoi(text)
if err != nil {
- log.Fatalln("Error: Bad user input detected. Please only enter numbers.")
+ //log.Fatalln("Error: Bad user input detected. Please only enter numbers.")
+ return nil, fmt.Errorf("Error: Bad input detected: %s. Please only enter numbers.", text)
}
// validate
if (val >= 0) && (val < maxVal) {
chosenInts = append(chosenInts, val)
} else {
- log.Fatalln("Error: Bad user input detected. One of the numbers is not correct.")
+ //log.Fatalln("Error: Bad user input detected. One of the numbers is not correct.")
+ return nil, fmt.Errorf("Error: Bad user input detected. The number %d is out of range.", val)
}
}
- return chosenInts
+ return chosenInts, nil
}
// Updates the hostRecords set with new data.
@@ -296,7 +315,11 @@ func removeRecord() {
// get user input for which ones to delete
userChoicesStrs := userInputHandler()
- userChoicesInts := normalizeUserChoices(userChoicesStrs, len(hostRecords))
+ userChoicesInts, err := normalizeUserChoices(userChoicesStrs, len(hostRecords))
+ if err != nil {
+ log.Fatalln(err)
+ }
+
// build the desired set of host records.
var newHostRecords []Host
@@ -375,25 +398,37 @@ func main() {
switch os.Args[1] {
case "list":
listCmd.Parse(os.Args[2:])
- loadConfig()
+ err := loadConfig()
+ if err != nil {
+ log.Fatalln(err)
+ }
logRecords(getAllRecords())
case "add":
addCmd.Parse(os.Args[2:])
if *addName == "" || *addType == "" || *addAddress == "" {
log.Fatalln(addUpdateErrorMsg)
}
- loadConfig()
+ err := loadConfig()
+ if err != nil {
+ log.Fatalln(err)
+ }
addRecord(*addName, *addType, *addAddress, *addMXPref, *addTTL)
case "remove":
removeCmd.Parse(os.Args[2:])
- loadConfig()
+ err := loadConfig()
+ if err != nil {
+ log.Fatalln(err)
+ }
removeRecord()
case "update":
updateCmd.Parse(os.Args[2:])
if *updateName == "" || *updateType == "" || *updateAddress == "" {
log.Fatalln(addUpdateErrorMsg)
}
- loadConfig()
+ err := loadConfig()
+ if err != nil {
+ log.Fatalln(err)
+ }
updateRecord(*updateName, *updateType, *updateAddress, *updateMXPref, *updateTTL)
default:
log.Fatalln(subCmdErrorMsg)
diff --git a/nccli_test.go b/nccli_test.go
new file mode 100644
index 0000000..43f0308
--- /dev/null
+++ b/nccli_test.go
@@ -0,0 +1,185 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "testing"
+ "reflect"
+)
+
+// checking for a valid return value.
+func TestGetEnvKey(t *testing.T) {
+ key := "TEST_VAR"
+ val := "42"
+ os.Setenv(key, val)
+ have, err := getEnv(key)
+ if have != val || err != nil {
+ t.Fatalf(`getEnv("%s") = %s, %s, want %s, nil`, key, have, err, val)
+ }
+}
+
+// checking for an error.
+func TestGetEnvKeyEmptyKey(t *testing.T) {
+ have, err := getEnv("")
+ if have != "" || err == nil {
+ t.Fatalf(`getEnv("") = %s, %s, want "", error`, have, err)
+ }
+}
+
+// checking for an error.
+func TestLoadConfigMissingKey(t *testing.T) {
+
+ fmt.Println("")
+ os.Setenv("API_USER", "user")
+ os.Setenv("API_KEY", "mykey")
+ os.Setenv("ACCESS_IP", "1.1.1.1")
+ os.Setenv("DOMAIN_MAIN", "mysite")
+ // NOTE: DOMAIN_END is missing.
+
+ err := loadConfig()
+ _, errWant := getEnv("DOMAIN_END")
+
+ if err.Error() != errWant.Error() || err == nil {
+ t.Fatalf(`have %v, want %v`, err, errWant)
+ }
+}
+
+// NOTE: this test must happen AFTER TestLoadConfigMissingKey
+// checking for a valid return value.
+func TestLoadConfigAllKeys(t *testing.T) {
+ os.Setenv("API_USER", "user")
+ os.Setenv("API_KEY", "mykey")
+ os.Setenv("ACCESS_IP", "1.1.1.1")
+ os.Setenv("DOMAIN_MAIN", "mysite")
+ os.Setenv("DOMAIN_END", "xyz")
+
+ loadConfig()
+ want := "https://api.namecheap.com/xml.response?ApiUser=user&ApiKey=mykey&UserName=user&ClientIp=1.1.1.1&SLD=mysite&TLD=xyz"
+ if apiUrl != want {
+ t.Fatalf(`apiUrl = %s, want %s`, apiUrl, want)
+ }
+}
+
+// checking for a valid return value.
+func TestNormalizeUserChoicesCleanInput(t *testing.T) {
+ userChoicesStrs := []string{"0", "1", "6"}
+ maxVal := 7
+ want := []int{0, 1, 6}
+
+ have, err := normalizeUserChoices(userChoicesStrs, maxVal)
+ if !reflect.DeepEqual(have, want) || err != nil {
+ t.Fatalf(`normalizeUserChoices(%v, %d) = %v, %v, want %v,nil`, userChoicesStrs, maxVal, have, err, want)
+ }
+}
+
+// checking for a valid return value.
+func TestNormalizeUserChoicesExtraSpaces(t *testing.T) {
+ userChoicesStrs := []string{" ", " ", "1", "", " 5 ", " "}
+ maxVal := 7
+ want := []int{1, 5}
+
+ have, err := normalizeUserChoices(userChoicesStrs, maxVal)
+ if !reflect.DeepEqual(have, want) || err != nil {
+ t.Fatalf(`normalizeUserChoices(%v, %d) = %v, %v, want %v,nil`, userChoicesStrs, maxVal, have, err, want)
+ }
+}
+
+// checking for an error.
+func TestNormalizeUserChoicesWrongChar(t *testing.T) {
+ userChoicesStrs := []string{"6", "5b"}
+ maxVal := 7
+ errWant := "Error: Bad input detected: 5b. Please only enter numbers."
+
+ have, err := normalizeUserChoices(userChoicesStrs, maxVal)
+ if have != nil || err.Error() != errWant {
+ t.Fatalf(`normalizeUserChoices(%v, %d) = %v, %v, want nil,%v`, userChoicesStrs, maxVal, have, err, errWant)
+ }
+}
+
+// checking for an error.
+func TestNormalizeUserChoicesOutOfRangeLower(t *testing.T) {
+ userChoicesStrs := []string{"-1", "5"}
+ maxVal := 7
+ errWant := "Error: Bad user input detected. The number -1 is out of range."
+
+ have, err := normalizeUserChoices(userChoicesStrs, maxVal)
+ if have != nil || err.Error() != errWant {
+ t.Fatalf(`normalizeUserChoices(%v, %d) = %v, %v, want nil,%v`, userChoicesStrs, maxVal, have, err, errWant)
+ }
+}
+
+// checking for an error.
+func TestNormalizeUserChoicesOutOfRangeUpper(t *testing.T) {
+ userChoicesStrs := []string{"0", "7"}
+ maxVal := 7
+ errWant := "Error: Bad user input detected. The number 7 is out of range."
+
+ have, err := normalizeUserChoices(userChoicesStrs, maxVal)
+ if have != nil || err.Error() != errWant {
+ t.Fatalf(`normalizeUserChoices(%v, %d) = %v, %v, want nil,%v`, userChoicesStrs, maxVal, have, err, errWant)
+ }
+}
+
+// checking for a valid return value.
+func TestUpdateHostRecordSet(t *testing.T) {
+ rec1 := Host{
+ Name: "@",
+ Type: "T",
+ Address: "foo",
+ MXPref: 1,
+ TTL: 1,
+ }
+ rec2 := Host{
+ Name: "www",
+ Type: "T",
+ Address: "bar",
+ MXPref: 1,
+ TTL: 1,
+ }
+ updatedRec2 := Host{
+ Name: "www",
+ Type: "T",
+ Address: "new",
+ MXPref: 10,
+ TTL: 99,
+ }
+ recs := []Host{rec1, rec2}
+ want := []Host{rec1, updatedRec2}
+ // update recs in place
+ updateHostRecordSet(recs, updatedRec2)
+ if !reflect.DeepEqual(recs, want) {
+ t.Fatalf(`have %v, want %v`, recs, want)
+ }
+}
+
+// checking for a valid return value.
+func TestUpdateHostRecordSetNoOp(t *testing.T) {
+ rec1 := Host{
+ Name: "@",
+ Type: "T",
+ Address: "foo",
+ MXPref: 1,
+ TTL: 1,
+ }
+ rec2 := Host{
+ Name: "www",
+ Type: "T",
+ Address: "bar",
+ MXPref: 1,
+ TTL: 1,
+ }
+ updatedRec2 := Host{
+ Name: "www",
+ Type: "A",
+ Address: "new",
+ MXPref: 10,
+ TTL: 99,
+ }
+ recs := []Host{rec1, rec2}
+ want := []Host{rec1, rec2}
+ // update recs in place
+ updateHostRecordSet(recs, updatedRec2)
+ if !reflect.DeepEqual(recs, want) {
+ t.Fatalf(`have %v, want %v`, recs, want)
+ }
+}