Skip to content

Commit fae2d6a

Browse files
committed
test: add comprehensive test suite with testability refactoring
Extract error-returning internal functions (performRequest, toFile, buildItems, sendItems, deleteItem) from their log.Fatal wrappers to enable unit testing. Add 28 tests covering models, HTTP client, file I/O, and send/receive workflows using httptest and t.TempDir.
1 parent 7bd0db0 commit fae2d6a

File tree

10 files changed

+748
-41
lines changed

10 files changed

+748
-41
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## In Development
44

55
- fix: Il comando `get --json` scriveva anche il file su disco invece di restituire solo il JSON.
6+
- new: Test suite completa (28 test) con refactoring per testabilità delle funzioni critiche.
7+
- new: Sezione Contributing nel README con istruzioni per build, test e cross-compile.
68

79
## [1.0.0-beta.1] - 2025-07-08
810

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,43 @@ See the [Installation guide][3].
4040

4141
Quickstart and context available at the [Invoicetronic website][1].
4242

43+
## Contributing
44+
45+
### Prerequisites
46+
47+
- Go 1.23+
48+
- An [Invoicetronic API][1] key (for manual/integration testing)
49+
50+
### Build
51+
52+
```bash
53+
go build
54+
```
55+
56+
### Test
57+
58+
```bash
59+
go test ./...
60+
```
61+
62+
Run with the race detector:
63+
64+
```bash
65+
go test -race ./...
66+
```
67+
68+
Tests use the standard library only (`testing` + `net/http/httptest`), with no external dependencies. API calls are mocked via `httptest.NewServer`; filesystem tests use `t.TempDir()`.
69+
70+
### Cross-compile
71+
72+
Requires `PACKAGE_VERSION` to be set:
73+
74+
```bash
75+
PACKAGE_VERSION=1.0.0 make all
76+
```
77+
78+
Target a single platform with `make windows`, `make linux`, or `make macos`.
79+
4380
[1]: https://invoicetronic.com/docs/quickstart/invoice-quickstart/
4481
[2]: https://invoicetronic.com/docs/sdk/
4582
[3]: https://invoicetronic.com/docs/cli/#installation-guide

cmd/helpers_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cmd
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/spf13/viper"
11+
)
12+
13+
func setupViper(t *testing.T) {
14+
t.Helper()
15+
viper.Reset()
16+
viper.Set("host", "https://api.invoicetronic.com")
17+
viper.Set("apikey", "test-api-key")
18+
viper.Set("verbose", false)
19+
}
20+
21+
func newTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
22+
t.Helper()
23+
server := httptest.NewServer(handler)
24+
t.Cleanup(server.Close)
25+
viper.Set("host", server.URL)
26+
return server
27+
}
28+
29+
func createTempFile(t *testing.T, dir, name, content string) string {
30+
t.Helper()
31+
p := filepath.Join(dir, name)
32+
if err := os.WriteFile(p, []byte(content), 0644); err != nil {
33+
t.Fatal(err)
34+
}
35+
return p
36+
}

cmd/receive.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,20 @@ func receiveRun(_ *cobra.Command, _ []string) {
7878
}
7979

8080
}
81-
func Delete(id int) {
81+
func deleteItem(id int, client *http.Client) error {
8282
url := BuildEndpointUrl("receive", strconv.Itoa(id))
8383
req, err := http.NewRequest("DELETE", url.String(), nil)
8484
if err != nil {
85-
log.Fatal(err)
85+
return err
8686
}
87+
_, _, err = performRequest(req, client)
88+
return err
89+
}
8790

88-
PerformRequest(req, &http.Client{})
91+
func Delete(id int) {
92+
if err := deleteItem(id, &http.Client{}); err != nil {
93+
log.Fatal(err)
94+
}
8995
}
9096

9197
func getFullFilePath(dest, filename string) (string, error) {

cmd/receive_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cmd
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
)
7+
8+
func TestDeleteItem(t *testing.T) {
9+
t.Run("success", func(t *testing.T) {
10+
setupViper(t)
11+
12+
var method, path string
13+
newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
14+
method = r.Method
15+
path = r.URL.Path
16+
w.WriteHeader(200)
17+
_, _ = w.Write([]byte(`{}`))
18+
})
19+
20+
err := deleteItem(42, &http.Client{})
21+
if err != nil {
22+
t.Fatalf("unexpected error: %v", err)
23+
}
24+
if method != "DELETE" {
25+
t.Errorf("method = %q, want DELETE", method)
26+
}
27+
if path != "/v1/receive/42" {
28+
t.Errorf("path = %q, want /v1/receive/42", path)
29+
}
30+
})
31+
32+
t.Run("not found", func(t *testing.T) {
33+
setupViper(t)
34+
newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
35+
w.WriteHeader(404)
36+
_, _ = w.Write([]byte("not found"))
37+
})
38+
39+
err := deleteItem(999, &http.Client{})
40+
if err == nil {
41+
t.Fatal("expected error for 404, got nil")
42+
}
43+
})
44+
45+
t.Run("server error", func(t *testing.T) {
46+
setupViper(t)
47+
newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
48+
w.WriteHeader(500)
49+
_, _ = w.Write([]byte("internal error"))
50+
})
51+
52+
err := deleteItem(1, &http.Client{})
53+
if err == nil {
54+
t.Fatal("expected error for 500, got nil")
55+
}
56+
})
57+
}

cmd/root.go

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"encoding/base64"
5+
"fmt"
56
"io"
67
"log"
78
"net/http"
@@ -56,32 +57,43 @@ func BasicAuth() string {
5657
return base64.StdEncoding.EncodeToString([]byte(auth))
5758
}
5859

59-
func PerformRequest(req *http.Request, client *http.Client) (*http.Response, []byte) {
60+
func performRequest(req *http.Request, client *http.Client) (*http.Response, []byte, error) {
6061
req.Header.Set("Authorization", "Basic "+BasicAuth())
6162
req.Header.Set("Content-Type", "application/json")
6263

6364
resp, err := client.Do(req)
6465
if err != nil {
65-
log.Fatal(err)
66+
return nil, nil, err
6667
}
6768
defer func(Body io.ReadCloser) {
6869
_ = Body.Close()
6970
}(resp.Body)
7071

7172
respBody, err := io.ReadAll(resp.Body)
7273
if err != nil {
73-
log.Fatal(err)
74+
return nil, nil, err
7475
}
7576

7677
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
77-
log.Printf("Send failed (%v)", resp.Status)
78-
if len(respBody) > 0 {
79-
log.Println(string(respBody))
80-
}
81-
os.Exit(1)
78+
return resp, respBody, fmt.Errorf("request failed (%v): %s", resp.Status, string(respBody))
8279
}
8380

84-
return resp, respBody
81+
return resp, respBody, nil
82+
}
83+
84+
func PerformRequest(req *http.Request, client *http.Client) (*http.Response, []byte) {
85+
resp, body, err := performRequest(req, client)
86+
if err != nil {
87+
if resp != nil {
88+
log.Printf("Send failed (%v)", resp.Status)
89+
if len(body) > 0 {
90+
log.Println(string(body))
91+
}
92+
os.Exit(1)
93+
}
94+
log.Fatal(err)
95+
}
96+
return resp, body
8597
}
8698

8799
func init() {
@@ -101,33 +113,32 @@ func init() {
101113

102114
}
103115

104-
func ToFile(filename string, payload string, encoding string) {
116+
func toFile(filename, payload, encoding, destDir string) error {
105117
Verbose("Processing payload for %v with encoding %v\n", filename, encoding)
106118
var decodedData []byte
107-
119+
108120
if encoding == "Base64" {
109121
Verbose("Decoding base64 payload for %v\n", filename)
110122
var err error
111123
decodedData, err = base64.StdEncoding.DecodeString(payload)
112124
if err != nil {
113-
log.Fatalf("Error decoding base64 payload for %v: %v", filename, err)
125+
return fmt.Errorf("error decoding base64 payload for %v: %v", filename, err)
114126
}
115127
} else {
116128
Verbose("Using payload as plain string for %v\n", filename)
117129
decodedData = []byte(payload)
118130
}
119131

120132
var filePath string
121-
var err error
122-
if outputDir != "" {
123-
filePath, err = getFullFilePath(outputDir, filename)
133+
if destDir != "" {
134+
var err error
135+
filePath, err = getFullFilePath(destDir, filename)
124136
if err != nil {
125-
log.Fatal(err)
137+
return err
126138
}
127-
128-
err = createDirectoryIfNotExists(outputDir)
139+
err = createDirectoryIfNotExists(destDir)
129140
if err != nil {
130-
log.Fatal(err)
141+
return err
131142
}
132143
} else {
133144
filePath = filename
@@ -136,7 +147,7 @@ func ToFile(filename string, payload string, encoding string) {
136147
Verbose("Creating file %v\n", filename)
137148
file, err := os.Create(filePath)
138149
if err != nil {
139-
log.Fatalf("Error creating "+filename+": %v", err)
150+
return fmt.Errorf("error creating %s: %v", filename, err)
140151
}
141152
defer func(file *os.File) {
142153
_ = file.Close()
@@ -145,10 +156,17 @@ func ToFile(filename string, payload string, encoding string) {
145156
Verbose("Writing to file %v\n", filename)
146157
_, err = file.Write(decodedData)
147158
if err != nil {
148-
log.Fatalf("Error writing to file "+filename+": %v", err)
159+
return fmt.Errorf("error writing to file %s: %v", filename, err)
149160
}
150161

151162
Verbose("Write to %v succeeded\n", filename)
163+
return nil
164+
}
165+
166+
func ToFile(filename, payload, encoding string) {
167+
if err := toFile(filename, payload, encoding, outputDir); err != nil {
168+
log.Fatal(err)
169+
}
152170
}
153171

154172
func createDirectoryIfNotExists(dest string) error {

0 commit comments

Comments
 (0)