Files
dsh-web/internal/api/reconcile_test.go
T

142 lines
5.4 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"git.misaka.ren/m1saka/dsh-web/internal/store"
"net/http"
"testing"
"time"
)
func TestReconcileIdentityAndMissing(t *testing.T) {
t.Setenv("PANEL_CONTAINER_ID", "")
a, _ := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("unexpected destructive request %s", r.Method)
}
w.Write([]byte(`[{"Id":"c1","State":"running","Labels":{"dsh-mgr.instance":"one","dsh-mgr.owner":"owner"}},{"Id":"c3","State":"running","Labels":{"dsh-mgr.instance":"orphan","dsh-mgr.owner":"owner"}}]`))
})
for _, i := range []store.Instance{{ID: "one", ContainerID: "c1", Status: "exited"}, {ID: "missing", ContainerID: "c2", Status: "running"}, {ID: "locked", ContainerID: "c4", Status: "error"}} {
i.UID = "owner"
i.Created = time.Now()
if err := a.s.AddInstance(i); err != nil {
t.Fatal(err)
}
}
if err := a.Reconcile(context.Background()); err != nil {
t.Fatal(err)
}
for id, want := range map[string]string{"one": "running", "missing": "error", "locked": "error"} {
i, err := a.s.GetInstance(id)
if err != nil || i.Status != want {
t.Fatalf("%s: %+v %v", id, i, err)
}
}
var count int
if err := a.s.DB.QueryRow("SELECT count(*) FROM audit WHERE action='reconcile.orphan'").Scan(&count); err != nil || count != 1 {
t.Fatalf("orphan audit: %d %v", count, err)
}
}
func TestReconcilePanelNetwork(t *testing.T) {
for _, tc := range []struct {
name string
connected, race, connectFail, missing, foreign, wrongDriver, panelFail bool
wantConnect int
wantError bool
}{
{name: "already connected", connected: true},
{name: "reconnect", wantConnect: 1},
{name: "concurrent connection", race: true, wantConnect: 1},
{name: "connect failure", connectFail: true, wantConnect: 1, wantError: true},
{name: "missing network", missing: true, wantError: true},
{name: "foreign network", foreign: true, wantError: true},
{name: "not bridge", wrongDriver: true, wantError: true},
{name: "panel inspect failure", panelFail: true, wantError: true},
} {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("PANEL_CONTAINER_ID", "configured-panel")
connects := 0
connected := tc.connected
a, _ := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
switch r.Method + " " + r.URL.Path {
case "GET /v1.47/containers/json":
fmt.Fprint(w, `[{"Id":"c1","State":"running","Labels":{"dsh-mgr.instance":"one","dsh-mgr.owner":"owner"}},{"Id":"foreign","State":"running","Labels":{"dsh-mgr.instance":"foreign","dsh-mgr.owner":"other"}},{"Id":"unknown","State":"running","Labels":{"dsh-mgr.instance":"unknown","dsh-mgr.owner":"owner"}}]`)
case "GET /v1.47/networks/" + runtimeNetwork("one"):
if tc.missing {
http.Error(w, "network not found", http.StatusNotFound)
return
}
owner, driver := "one", "bridge"
if tc.foreign {
owner = "other"
}
if tc.wrongDriver {
driver = "host"
}
json.NewEncoder(w).Encode(map[string]any{"Id": "network-id", "Name": runtimeNetwork("one"), "Driver": driver, "Labels": map[string]string{"dsh-mgr.instance": owner}})
case "GET /v1.47/containers/configured-panel/json", "GET /v1.47/containers/panel-full-id/json":
if tc.panelFail {
http.Error(w, "panel missing", http.StatusNotFound)
return
}
networkID := "unrelated-network"
if connected {
networkID = "network-id"
}
fmt.Fprintf(w, `{"Id":"panel-full-id","NetworkSettings":{"Networks":{"bridge":{"NetworkID":%q}}}}`, networkID)
case "POST /v1.47/networks/network-id/connect":
connects++
var body map[string]string
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body) != 1 || body["Container"] != "panel-full-id" {
t.Errorf("unsafe connection body: %v, %v", body, err)
}
if tc.connectFail || tc.race {
connected = tc.race
http.Error(w, "connection failed", http.StatusForbidden)
return
}
connected = true
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
http.Error(w, "unexpected request", http.StatusBadRequest)
}
})
for _, i := range []store.Instance{
{ID: "one", UID: "owner", ContainerID: "c1", Status: "exited", Created: time.Now()},
{ID: "foreign", UID: "owner", ContainerID: "foreign", Status: "running", Created: time.Now()},
} {
if err := a.s.AddInstance(i); err != nil {
t.Fatal(err)
}
}
if err := a.Reconcile(context.Background()); (err != nil) != tc.wantError {
t.Fatalf("Reconcile error = %v, want error %v", err, tc.wantError)
}
if !tc.wantError {
if err := a.Reconcile(context.Background()); err != nil {
t.Fatal(err)
}
}
if connects != tc.wantConnect {
t.Fatalf("connections = %d, want %d", connects, tc.wantConnect)
}
wantStatus, wantAudits := "running", 0
if tc.wantError {
wantStatus, wantAudits = "error", 1
}
i, err := a.s.GetInstance("one")
if err != nil || i.Status != wantStatus {
t.Fatalf("instance: %+v %v", i, err)
}
var audits int
if err := a.s.DB.QueryRow("SELECT count(*) FROM audit WHERE action='reconcile.network_error'").Scan(&audits); err != nil || audits != wantAudits {
t.Fatalf("network audits: %d %v", audits, err)
}
})
}
}