Skip to content

Commit 24bb26a

Browse files
committed
Baseline for acceptance tests
1 parent 84e2e3b commit 24bb26a

File tree

5 files changed

+349
-15
lines changed

5 files changed

+349
-15
lines changed

acceptance/acceptance_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package acceptance
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"os/exec"
12+
"path/filepath"
13+
"strings"
14+
"testing"
15+
"time"
16+
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/require"
19+
"golang.org/x/tools/txtar"
20+
)
21+
22+
const addr = "http://localhost:3000"
23+
24+
func Test(t *testing.T) {
25+
// For every test directory
26+
tcs := collectTestCases(t)
27+
28+
for _, tc := range tcs {
29+
tc := tc
30+
t.Run(tc.name, func(t *testing.T) {
31+
// Prepare the config directory
32+
path, clean := createTmpCfgDir(t, tc)
33+
t.Cleanup(clean)
34+
35+
// Start the application
36+
stop := runApplication(t, path)
37+
t.Cleanup(stop)
38+
39+
// For every request and response pair
40+
rrs := collectRequestResponses(t, tc.path)
41+
for _, rr := range rrs {
42+
rr := rr
43+
t.Run(rr.name, func(t *testing.T) {
44+
// Send the request
45+
res, err := http.DefaultClient.Do(rr.req)
46+
require.NoError(t, err)
47+
48+
// Assert the res
49+
rr.assertResponse(res)
50+
})
51+
}
52+
})
53+
}
54+
}
55+
56+
type tc struct {
57+
name string
58+
path string
59+
}
60+
61+
func collectTestCases(t *testing.T) (cases []tc) {
62+
cwd, err := os.Getwd()
63+
require.NoError(t, err)
64+
65+
testsDir := filepath.Join(cwd, "tests")
66+
entries, err := os.ReadDir(testsDir)
67+
require.NoError(t, err)
68+
69+
for _, entry := range entries {
70+
if entry.IsDir() {
71+
cases = append(cases, tc{
72+
name: entry.Name(),
73+
path: filepath.Join(testsDir, entry.Name()),
74+
})
75+
}
76+
}
77+
78+
return
79+
}
80+
81+
type rr struct {
82+
*testing.T
83+
name string
84+
req *http.Request
85+
res []byte
86+
}
87+
88+
func (rr rr) assertResponse(response *http.Response) {
89+
// Read the response body
90+
body, err := io.ReadAll(response.Body)
91+
require.NoError(rr, err)
92+
93+
// Format the status line
94+
statusLine := fmt.Sprintf("HTTP/%d.%d %s", response.ProtoMajor, response.ProtoMinor, response.Status)
95+
96+
// Strip dynamic headers (to prevent false negatives)
97+
response.Header.Del("Date")
98+
99+
// Format the headers
100+
var headersBuilder strings.Builder
101+
err = response.Header.Write(&headersBuilder)
102+
require.NoError(rr, err)
103+
headers := strings.ReplaceAll(headersBuilder.String(), "\r\n", "\n")
104+
105+
// Format the response
106+
res := fmt.Sprintf("%s\n%s\n\n%s", statusLine, headers, body)
107+
assert.Equal(rr, string(rr.res), res)
108+
}
109+
110+
func collectRequestResponses(t *testing.T, path string) (rrs []rr) {
111+
httpDir := filepath.Join(path, "http")
112+
entries, err := os.ReadDir(httpDir)
113+
require.NoError(t, err)
114+
115+
for _, entry := range entries {
116+
if entry.IsDir() {
117+
continue
118+
}
119+
120+
rrFilePath := filepath.Join(httpDir, entry.Name())
121+
122+
contents, err := os.ReadFile(rrFilePath)
123+
require.NoError(t, err)
124+
125+
archive := txtar.Parse(contents)
126+
127+
rrs = append(rrs, rr{
128+
T: t,
129+
name: entry.Name(),
130+
req: readRequest(t, find(archive.Files, "req.http")),
131+
res: find(archive.Files, "res.http"),
132+
})
133+
}
134+
135+
return
136+
}
137+
138+
func find(ff []txtar.File, name string) []byte {
139+
for _, f := range ff {
140+
if f.Name == name {
141+
return f.Data
142+
}
143+
}
144+
145+
return nil
146+
}
147+
148+
func readRequest(t *testing.T, raw []byte) *http.Request {
149+
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw)))
150+
require.NoError(t, err)
151+
152+
baseURL, err := url.Parse(addr)
153+
require.NoError(t, err)
154+
155+
req.RequestURI = ""
156+
req.URL = baseURL.ResolveReference(req.URL)
157+
158+
return req
159+
}
160+
161+
func createTmpCfgDir(t *testing.T, tc tc) (string, func()) {
162+
tmpCfgDir := filepath.Join(os.TempDir(), tc.name)
163+
164+
cfgFilePath := filepath.Join(tc.path, "config.txtar")
165+
166+
contents, err := os.ReadFile(cfgFilePath)
167+
require.NoError(t, err)
168+
169+
archive := txtar.Parse(contents)
170+
171+
for _, f := range archive.Files {
172+
filePath := filepath.Join(tmpCfgDir, f.Name)
173+
fileDir := filepath.Dir(filePath)
174+
175+
err := os.MkdirAll(fileDir, os.ModePerm)
176+
require.NoError(t, err)
177+
178+
err = os.WriteFile(filePath, f.Data, os.ModePerm)
179+
require.NoError(t, err)
180+
}
181+
182+
return tmpCfgDir, func() {
183+
err := os.RemoveAll(tmpCfgDir)
184+
require.NoError(t, err)
185+
}
186+
}
187+
188+
func runApplication(t *testing.T, from string) func() {
189+
cmd := exec.Command("killgrave", "--imposters", filepath.Join(from, "imposters"))
190+
cmd.Stdout = os.Stdout
191+
cmd.Stderr = os.Stderr
192+
err := cmd.Start()
193+
require.NoError(t, err)
194+
195+
// Trick to give time to the app to start
196+
time.Sleep(1 * time.Second)
197+
198+
return func() {
199+
err := cmd.Process.Signal(os.Interrupt)
200+
require.NoError(t, err)
201+
202+
err = cmd.Wait()
203+
require.NoError(t, err)
204+
}
205+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
-- imposters/create_gopher.imp.json --
2+
[
3+
{
4+
"request": {
5+
"method": "POST",
6+
"endpoint": "/gophers",
7+
"schemaFile": "schemas/create_gopher_request.json",
8+
"headers": {
9+
"Content-Type": "application/json"
10+
},
11+
"params": {
12+
"gopherColor": "{v:[a-z]+}"
13+
}
14+
},
15+
"response": {
16+
"status": 200,
17+
"headers": {
18+
"Content-Type": "application/json"
19+
},
20+
"bodyFile": "responses/create_gopher_response.json"
21+
}
22+
},
23+
{
24+
"t": "random_text"
25+
}
26+
]
27+
-- imposters/schemas/create_gopher_request.json --
28+
{
29+
"type": "object",
30+
"properties": {
31+
"data": {
32+
"type": "object",
33+
"properties": {
34+
"type": {
35+
"type": "string",
36+
"enum": [
37+
"gophers"
38+
]
39+
},
40+
"attributes": {
41+
"type": "object",
42+
"properties": {
43+
"name": {
44+
"type": "string"
45+
},
46+
"color": {
47+
"type": "string"
48+
},
49+
"age": {
50+
"type": "integer"
51+
}
52+
},
53+
"required": [
54+
"name",
55+
"color",
56+
"age"
57+
]
58+
}
59+
},
60+
"required": [
61+
"type",
62+
"attributes"
63+
]
64+
}
65+
},
66+
"required": [
67+
"data"
68+
]
69+
}
70+
-- imposters/responses/create_gopher_response.json --
71+
{
72+
"data": {
73+
"type": "gophers",
74+
"id": "01D8EMQ185CA8PRGE20DKZTGSR",
75+
"attributes": {
76+
"name": "Zebediah",
77+
"color": "Purple",
78+
"age": 54
79+
}
80+
}
81+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- req.http --
2+
POST /gophers?gopherColor=red HTTP/1.1
3+
Content-Length: 95
4+
Content-Type: application/json
5+
6+
{"data": {"type": "gophers", "attributes": {"name": "Zebediah", "color": "Purple", "age": 54}}}
7+
8+
-- res.http --
9+
HTTP/1.1 200 OK
10+
Content-Length: 214
11+
Content-Type: application/json
12+
13+
14+
{
15+
"data": {
16+
"type": "gophers",
17+
"id": "01D8EMQ185CA8PRGE20DKZTGSR",
18+
"attributes": {
19+
"name": "Zebediah",
20+
"color": "Purple",
21+
"age": 54
22+
}
23+
}
24+
}

go.mod

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
11
module github.com/friendsofgo/killgrave
22

3-
go 1.20
3+
go 1.16
44

55
require (
66
github.com/gorilla/handlers v1.5.1
77
github.com/gorilla/mux v1.8.0
88
github.com/radovskyb/watcher v1.0.7
99
github.com/spf13/afero v1.6.0
1010
github.com/spf13/cobra v1.0.0
11-
github.com/stretchr/testify v1.7.0
12-
github.com/xeipuuv/gojsonschema v1.2.0
13-
gopkg.in/yaml.v2 v2.4.0
14-
)
15-
16-
require (
17-
github.com/davecgh/go-spew v1.1.1 // indirect
18-
github.com/felixge/httpsnoop v1.0.1 // indirect
19-
github.com/inconshreveable/mousetrap v1.0.0 // indirect
20-
github.com/pmezard/go-difflib v1.0.0 // indirect
2111
github.com/spf13/pflag v1.0.5 // indirect
12+
github.com/stretchr/testify v1.7.0
2213
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
23-
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
24-
golang.org/x/text v0.3.3 // indirect
14+
github.com/xeipuuv/gojsonschema v1.2.0
15+
golang.org/x/tools v0.9.1
2516
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
26-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
17+
gopkg.in/yaml.v2 v2.4.0
2718
)

0 commit comments

Comments
 (0)