From 5c432205ad1d0e9c33e6748d9594c6cfe925ed49 Mon Sep 17 00:00:00 2001 From: Muse Date: Thu, 22 Oct 2026 14:44:53 +0000 Subject: [PATCH] =?UTF-8?q?test(agent)=EF=BC=9A=E5=9B=9E=E5=A1=AB=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E6=B5=8B=E8=AF=95=E2=80=94=E2=80=94=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=20SQL=20=E9=80=A0=20NULL=20=E8=80=81=E8=A1=8C=E8=B5=B0?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E5=BF=83=E8=B7=B3+install=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/agent_token_backfill_test.go | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/internal/web/controller/agent_token_backfill_test.go b/internal/web/controller/agent_token_backfill_test.go index c9ac27a..46874b0 100644 --- a/internal/web/controller/agent_token_backfill_test.go +++ b/internal/web/controller/agent_token_backfill_test.go @@ -123,6 +123,62 @@ func TestAgentTokenBackfillOnAuthenticatedRequest(t *testing.T) { } } +// Regression for the production failure of the first V1.0.0 deployment: +// rows that existed before item 19 got token_plain from AutoMigrate as +// SQL NULL, not ”, and the original guard `token_plain = ”` never +// matches NULL — the backfill silently never fired on any legacy node. +// Seed the row exactly as production has it (raw SQL, explicit NULL) +// instead of through GORM, which would write ”. +func TestAgentTokenBackfillFromNullLegacyRow(t *testing.T) { + engine, node := setupBackfillTest(t) + const token = "legacy-token-backfill-00000000000000000004" + db := database.GetDB() + if err := db.Exec( + "INSERT INTO agent_nodes (token_hash, token_prefix, token_plain, name, node_id, last_seen, created_at, updated_at) VALUES (?, ?, NULL, ?, ?, 0, 0, 0)", + hashAgentToken(token), token[:12], "backfill-node", node.Id, + ).Error; err != nil { + t.Fatalf("seed NULL legacy row: %v", err) + } + var seeded model.AgentNode + if err := db.Where("token_hash = ?", hashAgentToken(token)).First(&seeded).Error; err != nil { + t.Fatalf("load seeded row: %v", err) + } + var nullRows int + if err := db.Raw("SELECT COUNT(*) FROM agent_nodes WHERE id = ? AND token_plain IS NULL", seeded.Id).Scan(&nullRows).Error; err != nil { + t.Fatalf("NULL check: %v", err) + } + if nullRows != 1 { + t.Fatalf("seeded row is not SQL NULL as intended (null rows = %d)", nullRows) + } + + if code := heartbeatWith(engine, token); code != http.StatusOK { + t.Fatalf("heartbeat with valid token: status %d", code) + } + if row := reloadAgent(t, seeded.Id); row.TokenPlain != token { + t.Fatalf("token not backfilled from NULL legacy row: %q", row.TokenPlain) + } + + req := httptest.NewRequest(http.MethodGet, "/panel/api/agent/install/"+strconv.Itoa(node.Id), nil) + rec := httptest.NewRecorder() + engine.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("install endpoint: status %d", rec.Code) + } + var resp struct { + Success bool `json:"success"` + Obj struct { + Token string `json:"token"` + HasToken bool `json:"hasToken"` + } `json:"obj"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("install response not JSON: %v", err) + } + if !resp.Success || !resp.Obj.HasToken || resp.Obj.Token != token { + t.Fatalf("install endpoint did not return the backfilled token: %+v", resp.Obj) + } +} + func TestAgentTokenNoBackfillOnFailedAuth(t *testing.T) { engine, node := setupBackfillTest(t) const token = "legacy-token-backfill-00000000000000000002"