143 lines
4.1 KiB
Go
143 lines
4.1 KiB
Go
package expose
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.veillax.com/Veillax/shark-expose/internal/api"
|
|
)
|
|
|
|
// fakeHammerhead implements just enough of the expose API to watch what the client does.
|
|
type fakeHammerhead struct {
|
|
mu sync.Mutex
|
|
calls []string
|
|
renewErr int // status to answer renewals with; 0 = ok
|
|
gone atomic.Bool
|
|
srv *httptest.Server
|
|
created api.Request
|
|
}
|
|
|
|
func newFake(t *testing.T) *fakeHammerhead {
|
|
f := &fakeHammerhead{}
|
|
mux := http.NewServeMux()
|
|
reply := func(w http.ResponseWriter, code int, v any) {
|
|
w.WriteHeader(code)
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
exp := api.Exposure{ID: "abc", Proto: "tcp", Host: "edge.test", Port: 20001, SiteToken: "tok", Live: true}
|
|
mux.HandleFunc("POST /api/orgs/-/expose", func(w http.ResponseWriter, r *http.Request) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, "create")
|
|
if r.Header.Get("Authorization") != "Bearer hhk_x_y" {
|
|
reply(w, 401, map[string]string{"error": "Sign in to continue."})
|
|
return
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&f.created)
|
|
reply(w, 200, exp)
|
|
})
|
|
mux.HandleFunc("GET /api/orgs/-/expose/abc", func(w http.ResponseWriter, r *http.Request) { reply(w, 200, exp) })
|
|
mux.HandleFunc("POST /api/orgs/-/expose/abc/renew", func(w http.ResponseWriter, r *http.Request) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, "renew")
|
|
if f.renewErr != 0 {
|
|
reply(w, f.renewErr, map[string]string{"error": "no such exposure"})
|
|
return
|
|
}
|
|
reply(w, 200, exp)
|
|
})
|
|
mux.HandleFunc("DELETE /api/orgs/-/expose/abc", func(w http.ResponseWriter, r *http.Request) {
|
|
f.mu.Lock()
|
|
f.calls = append(f.calls, "delete")
|
|
f.mu.Unlock()
|
|
f.gone.Store(true)
|
|
reply(w, 200, map[string]bool{"ok": true})
|
|
})
|
|
// the tunnel agent long-polls this; there is nothing for it to do
|
|
mux.HandleFunc("/site/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) })
|
|
f.srv = httptest.NewServer(mux)
|
|
t.Cleanup(f.srv.Close)
|
|
return f
|
|
}
|
|
|
|
func (f *fakeHammerhead) count(call string) int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
n := 0
|
|
for _, c := range f.calls {
|
|
if c == call {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestRunRenewsAndDeletesOnStop(t *testing.T) {
|
|
f := newFake(t)
|
|
c := api.New(f.srv.URL, "hhk_x_y")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
ready := make(chan api.Exposure, 1)
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- Run(ctx, c, Options{Request: api.Request{Proto: "tcp", TargetPort: 3000}, TTL: 15 * time.Second}, func(e api.Exposure) { ready <- e })
|
|
}()
|
|
e := <-ready
|
|
if e.Address() != "tcp://edge.test:20001" {
|
|
t.Errorf("address %q", e.Address())
|
|
}
|
|
if f.created.TTLSeconds != 15 || f.created.TargetPort != 3000 {
|
|
t.Errorf("request %+v", f.created)
|
|
}
|
|
cancel()
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("clean stop returned %v", err)
|
|
}
|
|
if !f.gone.Load() {
|
|
t.Fatal("exposure was not deleted on stop")
|
|
}
|
|
}
|
|
|
|
func TestRunStopsWhenHammerheadForgetsTheExposure(t *testing.T) {
|
|
f := newFake(t)
|
|
f.renewErr = 404
|
|
c := api.New(f.srv.URL, "hhk_x_y")
|
|
// renewals are never sooner than 5s; a short TTL is enough to reach one
|
|
err := Run(context.Background(), c, Options{Request: api.Request{Proto: "tcp", TargetPort: 1}, TTL: 3 * time.Second}, nil)
|
|
if err == nil {
|
|
t.Fatal("expected an error once the exposure expired")
|
|
}
|
|
if !f.gone.Load() {
|
|
t.Error("cleanup should still ask Hammerhead to delete")
|
|
}
|
|
}
|
|
|
|
func TestRunRejectedToken(t *testing.T) {
|
|
f := newFake(t)
|
|
err := Run(context.Background(), api.New(f.srv.URL, "wrong"), Options{Request: api.Request{Proto: "tcp", TargetPort: 1}}, nil)
|
|
if !api.IsStatus(err, 401) {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
if f.count("delete") != 0 {
|
|
t.Error("deleted something that was never created")
|
|
}
|
|
}
|
|
|
|
func TestRunLoopSurvivesFlakyRenewals(t *testing.T) {
|
|
f := newFake(t)
|
|
f.renewErr = 500
|
|
c := api.New(f.srv.URL, "hhk_x_y")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
|
|
defer cancel()
|
|
err := keepAlive(ctx, c, "abc", Options{TTL: time.Minute})
|
|
if err != nil {
|
|
t.Fatalf("a failing renewal inside the TTL should not end the run: %v", err)
|
|
}
|
|
}
|