diff options
| author | Kumar Damani <me@kumardamani.net> | 2022-06-07 19:53:26 +0000 |
|---|---|---|
| committer | Kumar Damani <me@kumardamani.net> | 2022-06-07 19:53:26 +0000 |
| commit | 4ec6622f7bb891fa68f8bf45f5b8d84dcc46dae7 (patch) | |
| tree | d2eec632bf0712fe6730f85bc9f29dcb5294a066 | |
| parent | 1e59fb1623ccefc60631d964634640a776eaaec0 (diff) | |
added unit tests.
| -rw-r--r-- | .gitlab-ci.yml | 7 | ||||
| -rw-r--r-- | Dockerfile | 11 | ||||
| -rw-r--r-- | nccli.go | 95 | ||||
| -rw-r--r-- | nccli_test.go | 185 |
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: @@ -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 @@ -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) + } +} |
