2026-08-18 12:18:35 +02:00
|
|
|
package httpapi
|
|
|
|
|
|
|
|
|
|
import (
|
2026-08-18 23:15:10 +02:00
|
|
|
"io"
|
2026-08-18 12:18:35 +02:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2026-08-18 15:05:10 +02:00
|
|
|
"strings"
|
2026-08-19 08:57:59 +02:00
|
|
|
"sync"
|
2026-08-18 12:18:35 +02:00
|
|
|
"testing"
|
2026-08-18 15:05:10 +02:00
|
|
|
"time"
|
2026-08-18 12:18:35 +02:00
|
|
|
|
|
|
|
|
"github.com/kristianwind/verdande/internal/config"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-18 15:05:10 +02:00
|
|
|
// A panel that never answers must not become this server's problem. The container
|
|
|
|
|
// is being asked to stop, so the reply may legitimately never arrive — but the
|
|
|
|
|
// request has to end either way, or whatever sits in front answers for it.
|
|
|
|
|
func TestAPanelThatNeverAnswersStillEndsTheRequest(t *testing.T) {
|
2026-08-18 12:18:35 +02:00
|
|
|
panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2026-08-18 23:15:10 +02:00
|
|
|
// Drained first. A real server reads what it was sent, and one that does not
|
|
|
|
|
// never learns the caller has hung up — httptest.Server.Close then waits for
|
|
|
|
|
// a handler that will never return, and the whole binary times out instead
|
|
|
|
|
// of the one request.
|
|
|
|
|
_, _ = io.Copy(io.Discard, r.Body)
|
2026-08-18 15:05:10 +02:00
|
|
|
<-r.Context().Done()
|
2026-08-18 12:18:35 +02:00
|
|
|
}))
|
|
|
|
|
defer panel.Close()
|
|
|
|
|
|
|
|
|
|
ts := newTestServerWith(t, func(cfg *config.Config) {
|
|
|
|
|
cfg.PanelURL = panel.URL
|
2026-08-18 15:05:10 +02:00
|
|
|
cfg.PanelToken = "test-token"
|
|
|
|
|
cfg.PanelServerID = "server-1"
|
2026-08-18 12:18:35 +02:00
|
|
|
})
|
|
|
|
|
ts.bootstrap(t)
|
|
|
|
|
|
2026-08-18 15:05:10 +02:00
|
|
|
// Its own client: the shared one gives up at ten seconds, which is the very
|
|
|
|
|
// budget under test, so it could not tell a pass from a failure.
|
|
|
|
|
req, _ := http.NewRequest("POST", ts.URL+"/api/v1/panel/restart", nil)
|
|
|
|
|
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
|
|
|
|
for _, c := range ts.client.Jar.Cookies(mustParse(t, ts.URL)) {
|
|
|
|
|
req.AddCookie(c)
|
2026-08-18 12:18:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-18 15:05:10 +02:00
|
|
|
started := time.Now()
|
|
|
|
|
resp, err := (&http.Client{Timeout: 90 * time.Second}).Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("the request never came back (%v after %s)", err, time.Since(started).Round(time.Second))
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
took := time.Since(started)
|
|
|
|
|
|
|
|
|
|
if took > 20*time.Second {
|
|
|
|
|
t.Errorf("the restart took %s; that is long enough for a proxy to answer first",
|
|
|
|
|
took.Round(time.Second))
|
|
|
|
|
}
|
2026-08-18 12:18:35 +02:00
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
2026-08-18 15:05:10 +02:00
|
|
|
t.Errorf("status %d, want 202 — a panel that went quiet is the expected case, "+
|
|
|
|
|
"not a failure", resp.StatusCode)
|
2026-08-18 12:18:35 +02:00
|
|
|
}
|
2026-08-18 15:05:10 +02:00
|
|
|
t.Logf("came back in %s with %d", took.Round(time.Millisecond), resp.StatusCode)
|
2026-08-18 12:18:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-18 15:05:10 +02:00
|
|
|
// And a panel that says no must say so in this server's own words, not in a proxy's.
|
|
|
|
|
func TestAPanelThatRefusesSaysSoAsJSON(t *testing.T) {
|
2026-08-18 12:18:35 +02:00
|
|
|
panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2026-08-18 23:15:10 +02:00
|
|
|
_, _ = io.Copy(io.Discard, r.Body)
|
2026-08-18 15:05:10 +02:00
|
|
|
w.WriteHeader(http.StatusForbidden)
|
2026-08-18 12:18:35 +02:00
|
|
|
}))
|
|
|
|
|
defer panel.Close()
|
|
|
|
|
|
|
|
|
|
ts := newTestServerWith(t, func(cfg *config.Config) {
|
|
|
|
|
cfg.PanelURL = panel.URL
|
2026-08-18 15:05:10 +02:00
|
|
|
cfg.PanelToken = "wrong-token"
|
|
|
|
|
cfg.PanelServerID = "server-1"
|
2026-08-18 12:18:35 +02:00
|
|
|
})
|
|
|
|
|
ts.bootstrap(t)
|
|
|
|
|
|
|
|
|
|
resp, body := ts.do(t, "POST", "/api/v1/panel/restart", nil)
|
2026-08-18 19:17:49 +02:00
|
|
|
if resp.StatusCode != StatusUpstreamRefused {
|
2026-08-18 15:05:10 +02:00
|
|
|
t.Errorf("status %d, want 502: %s", resp.StatusCode, body)
|
2026-08-18 12:18:35 +02:00
|
|
|
}
|
2026-08-18 15:05:10 +02:00
|
|
|
if msg, _ := body["error"].(string); !strings.Contains(msg, "403") {
|
|
|
|
|
t.Errorf("the reply does not say what the panel answered: %v", body)
|
2026-08-18 12:18:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-19 08:57:59 +02:00
|
|
|
|
|
|
|
|
// Forty copies of one mail arrived in a single second because two runs read the
|
|
|
|
|
// same marker before either wrote a new one — somebody pressing "Fetch now" while
|
|
|
|
|
// the ten-minute sweep was already inside the same mailbox.
|
|
|
|
|
//
|
|
|
|
|
// The lock is what stops it, and a lock is only worth having if something tries
|
|
|
|
|
// the door: this runs the two concurrently on purpose.
|
|
|
|
|
func TestTwoRunsOfOneMailboxDoNotOverlap(t *testing.T) {
|
|
|
|
|
// A bare server: the lock needs nothing but its own zero value, and building a
|
|
|
|
|
// database and a session around it would only make the test slower to read.
|
|
|
|
|
srv := &Server{}
|
|
|
|
|
|
|
|
|
|
var inside, most int
|
|
|
|
|
var mu sync.Mutex
|
|
|
|
|
release := make(chan struct{})
|
|
|
|
|
|
|
|
|
|
// Stands in for a run: takes the lock, notes how many are inside, and holds it
|
|
|
|
|
// until told to let go.
|
|
|
|
|
run := func(done chan<- struct{}) {
|
2026-08-20 07:01:14 +02:00
|
|
|
unlock := srv.lockSync("box-1")
|
2026-08-19 08:57:59 +02:00
|
|
|
mu.Lock()
|
|
|
|
|
inside++
|
|
|
|
|
if inside > most {
|
|
|
|
|
most = inside
|
|
|
|
|
}
|
|
|
|
|
mu.Unlock()
|
|
|
|
|
|
|
|
|
|
<-release
|
|
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
|
inside--
|
|
|
|
|
mu.Unlock()
|
|
|
|
|
unlock()
|
|
|
|
|
done <- struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
first, second := make(chan struct{}), make(chan struct{})
|
|
|
|
|
go run(first)
|
|
|
|
|
go run(second)
|
|
|
|
|
|
|
|
|
|
// Long enough that both would be inside if nothing kept them apart.
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
close(release)
|
|
|
|
|
<-first
|
|
|
|
|
<-second
|
|
|
|
|
|
|
|
|
|
if most != 1 {
|
|
|
|
|
t.Errorf("%d runs were inside the same mailbox at once; that is how one mail "+
|
|
|
|
|
"becomes forty tasks", most)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// And two different mailboxes must not wait for each other: one slow host is
|
|
|
|
|
// not everybody's problem.
|
2026-08-20 07:01:14 +02:00
|
|
|
a := srv.lockSync("box-a")
|
2026-08-19 08:57:59 +02:00
|
|
|
done := make(chan struct{})
|
|
|
|
|
go func() {
|
2026-08-20 07:01:14 +02:00
|
|
|
srv.lockSync("box-b")()
|
2026-08-19 08:57:59 +02:00
|
|
|
close(done)
|
|
|
|
|
}()
|
|
|
|
|
select {
|
|
|
|
|
case <-done:
|
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
|
t.Error("a second mailbox waited for the first")
|
|
|
|
|
}
|
|
|
|
|
a()
|
|
|
|
|
}
|