Skip to content

Commit 61f9034

Browse files
committed
feat(tests): add e2e tests
1 parent a5e80d7 commit 61f9034

File tree

8 files changed

+669
-2
lines changed

8 files changed

+669
-2
lines changed

.github/workflows/go.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ jobs:
2121
with:
2222
go-version: '1.25'
2323

24-
# - name: Build
25-
# run: go build -v ./...
24+
- name: Build
25+
run: make build
26+
27+
- name: Run CacheServer
28+
run: |
29+
./bin/tavern -c ./test/config.yaml 2>&1 &
30+
sleep 2
31+
echo "CacheServer is running"
2632
2733
- name: Test
2834
run: go test -v ./...

internal/constants/global.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ const (
1414
InternalSwapfile = "i-x-swapfile"
1515
InternalFillRangePercent = "i-x-fp"
1616
InternalCacheErrCode = "i-x-ct-code"
17+
InternalUpstreamAddr = "i-x-ups-addr"
1718
)

pkg/e2e/e2e.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"net/http/httptest"
9+
"net/http/httputil"
10+
"net/url"
11+
"strings"
12+
"sync"
13+
"sync/atomic"
14+
"testing"
15+
"time"
16+
17+
"github.com/omalloc/tavern/internal/constants"
18+
"github.com/stretchr/testify/assert"
19+
)
20+
21+
var (
22+
mu sync.RWMutex
23+
localAddr = "127.0.0.1:8888"
24+
dump = atomic.Bool{}
25+
dumpReq = atomic.Bool{}
26+
27+
manual = atomic.Bool{}
28+
)
29+
30+
type E2E struct {
31+
caseUrl string
32+
srcHandler http.Handler
33+
ts *httptest.Server
34+
cs *http.Client
35+
req *http.Request
36+
resp *http.Response
37+
err error
38+
}
39+
40+
func New(caseUrl string, srcHandler http.HandlerFunc) *E2E {
41+
e := &E2E{
42+
caseUrl: caseUrl,
43+
srcHandler: srcHandler,
44+
}
45+
46+
u, err := url.Parse(caseUrl)
47+
if err != nil {
48+
return e
49+
}
50+
51+
if e.srcHandler == nil {
52+
e.srcHandler = http.NotFoundHandler()
53+
}
54+
55+
e.ts = httptest.NewServer(e.srcHandler)
56+
57+
dialer := &net.Dialer{}
58+
59+
// replace the default transport with a custom one that uses the local address
60+
e.ts.Client().Transport = &http.Transport{
61+
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
62+
mu.RLock()
63+
addr := localAddr
64+
mu.RUnlock()
65+
66+
if strings.HasSuffix(addr, ".sock") {
67+
return dialer.DialContext(ctx, "unix", addr)
68+
}
69+
return dialer.DialContext(ctx, network, addr)
70+
},
71+
}
72+
e.cs = e.ts.Client()
73+
74+
req, err := http.NewRequest(http.MethodGet, e.caseUrl, nil)
75+
if err != nil {
76+
panic(err)
77+
}
78+
79+
req.Header.Set("X-Test-Case", u.Path)
80+
req.Header.Set("X-Store-Url", u.String())
81+
82+
e.req = req
83+
return e
84+
}
85+
86+
func (e *E2E) Do(rewrite func(r *http.Request)) (*http.Response, error) {
87+
rewrite(e.req)
88+
89+
method := e.req.Method
90+
91+
e.req.Header.Set(constants.InternalUpstreamAddr, e.ts.Listener.Addr().String())
92+
93+
if dumpReq.Load() && method != "PURGE" {
94+
DumpReq(e.req)
95+
}
96+
97+
if manual.Load() {
98+
fmt.Printf("manual mode wait 20s, src addr %q\n", e.ts.Listener.Addr().String())
99+
time.Sleep(time.Second * 20)
100+
}
101+
102+
resp, err := e.cs.Do(e.req)
103+
e.resp = resp
104+
e.err = err
105+
106+
// PURGE 不打印响应
107+
if dump.Load() && method != "PURGE" {
108+
DumpResp(resp)
109+
}
110+
111+
// wait for a while to let the connection close properly
112+
// time.Sleep(time.Millisecond * 200)
113+
114+
return resp, err
115+
}
116+
117+
func (e *E2E) Close() {
118+
e.ts.Close()
119+
if e.resp != nil && e.resp.Body != nil {
120+
_ = e.resp.Body.Close()
121+
}
122+
}
123+
124+
func SetLocalAddr(addr string) {
125+
mu.Lock()
126+
defer mu.Unlock()
127+
128+
localAddr = addr
129+
}
130+
131+
func SetDump(b bool) {
132+
dump.Store(b)
133+
}
134+
135+
func SetDumpReq(b bool) {
136+
dumpReq.Store(b)
137+
}
138+
139+
func SetManual(b bool) {
140+
manual.Store(b)
141+
}
142+
143+
func DumpReq(req *http.Request) {
144+
if req == nil {
145+
fmt.Println("request is nil")
146+
return
147+
}
148+
149+
buf, err := httputil.DumpRequest(req, false)
150+
if err != nil {
151+
return
152+
}
153+
fmt.Println()
154+
fmt.Println(string(buf))
155+
}
156+
157+
func DumpResp(resp *http.Response) {
158+
if resp == nil {
159+
fmt.Println("response is nil")
160+
return
161+
}
162+
163+
buf, err := httputil.DumpResponse(resp, false)
164+
if err != nil {
165+
return
166+
}
167+
fmt.Println()
168+
fmt.Println(string(buf))
169+
}
170+
171+
func Purge(t *testing.T, url string) {
172+
resp, err := New(url, func(w http.ResponseWriter, r *http.Request) {
173+
w.WriteHeader(http.StatusBadGateway)
174+
}).Do(func(r *http.Request) {
175+
r.Method = "PURGE"
176+
})
177+
178+
assert.NoError(t, err, "purge should not error")
179+
180+
assert.Contains(t, []int{http.StatusOK, http.StatusNotFound}, resp.StatusCode, "PURGE should be 404 or 200")
181+
182+
if dump.Load() {
183+
t.Logf("Purge %s success", url)
184+
}
185+
}

pkg/e2e/e2e_file.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package e2e
2+
3+
import (
4+
"crypto/md5"
5+
"crypto/rand"
6+
"encoding/hex"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/omalloc/tavern/pkg/iobuf"
15+
)
16+
17+
type MockFile struct {
18+
Path string
19+
MD5 string
20+
Size int
21+
}
22+
23+
func GenFile(t *testing.T, size int) *MockFile {
24+
buf := make([]byte, size)
25+
_, _ = rand.Read(buf)
26+
27+
fpath := filepath.Join(t.TempDir(), fmt.Sprintf("file-%d.bin", size))
28+
_ = os.WriteFile(fpath, buf, 0o644)
29+
30+
return &MockFile{
31+
Path: fpath,
32+
MD5: SumMD5(buf),
33+
Size: size,
34+
}
35+
}
36+
37+
func SumMD5(buf []byte) string {
38+
h := md5.New()
39+
_, _ = h.Write(buf)
40+
return hex.EncodeToString(h.Sum(nil))
41+
}
42+
43+
func DiscardBody(resp *http.Response) int64 {
44+
if resp == nil || resp.Body == nil {
45+
return 0
46+
}
47+
48+
n, _ := io.Copy(io.Discard, resp.Body)
49+
resp.Body.Close()
50+
return n
51+
}
52+
53+
func HashBody(resp *http.Response) string {
54+
if resp == nil || resp.Body == nil {
55+
return ""
56+
}
57+
58+
h := md5.New()
59+
_, _ = io.Copy(h, resp.Body)
60+
return hex.EncodeToString(h.Sum(nil))
61+
}
62+
63+
func HashFile(path string, offset, length int) string {
64+
f, err := os.OpenFile(path, os.O_RDONLY, 0o644)
65+
if err != nil {
66+
return ""
67+
}
68+
defer f.Close()
69+
70+
_, _ = f.Seek(int64(offset), io.SeekStart)
71+
72+
h := md5.New()
73+
_, _ = io.CopyN(h, f, int64(length))
74+
return hex.EncodeToString(h.Sum(nil))
75+
}
76+
77+
func SplitFile(path string, offset, length int) io.ReadCloser {
78+
f, err := os.OpenFile(path, os.O_RDONLY, 0o644)
79+
if err != nil {
80+
return nil
81+
}
82+
_, _ = f.Seek(int64(offset), io.SeekStart)
83+
return iobuf.RangeReader(f, offset, offset+length-1, offset, offset+length-1)
84+
}

pkg/e2e/e2e_hander.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package e2e
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func WrongHit(t *testing.T) http.HandlerFunc {
12+
return func(w http.ResponseWriter, r *http.Request) {
13+
w.WriteHeader(http.StatusBadGateway)
14+
15+
assert.Error(t, errors.New("equal request HIT but NOT HIT"))
16+
}
17+
}
18+
19+
func RespSimpleFile(f *MockFile) http.HandlerFunc {
20+
return func(w http.ResponseWriter, r *http.Request) {
21+
w.Header().Set("Cache-Control", "max-age=10")
22+
w.Header().Set("Content-MD5", f.MD5)
23+
w.Header().Set("ETag", f.MD5)
24+
w.Header().Set("X-Server", "tavern-e2e/1.0.0")
25+
26+
http.ServeFile(w, r, f.Path)
27+
}
28+
}
29+
30+
func RespCallbackFile(f *MockFile, cb func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
31+
return func(w http.ResponseWriter, r *http.Request) {
32+
w.Header().Set("Cache-Control", "max-age=10")
33+
w.Header().Set("Content-MD5", f.MD5)
34+
w.Header().Set("ETag", f.MD5)
35+
w.Header().Set("X-Server", "tavern-e2e/1.0.0")
36+
37+
cb(w, r)
38+
39+
http.ServeFile(w, r, f.Path)
40+
}
41+
}
42+
43+
func RespCallback(cb func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
44+
return func(w http.ResponseWriter, r *http.Request) {
45+
cb(w, r)
46+
}
47+
}

server/middleware/caching/internal.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"syscall"
1818
"time"
1919

20+
"github.com/omalloc/proxy/selector"
2021
"github.com/omalloc/tavern/api/defined/v1/storage"
2122
"github.com/omalloc/tavern/api/defined/v1/storage/object"
2223
"github.com/omalloc/tavern/contrib/log"
@@ -292,6 +293,13 @@ func cloneRequest(req *http.Request) *http.Request {
292293
xhttp.CopyHeader(proxyReq.Header, req.Header)
293294
xhttp.RemoveHopByHopHeaders(proxyReq.Header)
294295

296+
// custom upstream addr
297+
if upsAddr := req.Header.Get(constants.InternalUpstreamAddr); upsAddr != "" {
298+
proxyReq = proxyReq.WithContext(
299+
selector.NewPeerContext(context.Background(), selector.NewPeer(selector.NewNode("http", upsAddr, map[string]string{}))),
300+
)
301+
}
302+
295303
return proxyReq
296304
}
297305

0 commit comments

Comments
 (0)