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) } }) } }