Files
wangjia 3527537c96 fix(connect): 4 处 500 记 slog.Error;加连接路径 schema 绑定测试
ConnectNode 四条 ErrInternal 路径(entitlement/配额/节点/配置渲染)此前只写
500 不记 err,导致「no such column: p.daily_mb」这种 SQL 错全静默 → 难排查。
改为各记 slog.Error(含 user/node/err)。

新增 store 层测试 TestSQLite_ConnectPathSchema:在跑过完整迁移(含 000015)的
真 SQLite 库上,跑连接路径依赖 015 schema 的三条查询——EntitlementForUser
(断言 DailyMB 从 plans.daily_mb 取到 102400)、EnsureDeviceDpUUID(devices.dp_uuid)、
AccountDayBytes——任一引用了没有迁移建的列即 SQL 报错、测试变红,守住本次
「查询引用了未迁移列」一类回归。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 08:11:32 +08:00

72 lines
2.6 KiB
Go

package store_test
import (
"context"
"testing"
"time"
"github.com/wangjia/pangolin/server/internal/nodes"
)
// TestSQLite_ConnectPathSchema guards the "no such column: p.daily_mb" class of
// regression that 500'd /v1/nodes/{id}/connect: the connect handler's store
// queries must bind to the schema the migrations actually produce. Each
// 000015-dependent query runs against a fully-migrated real SQLite DB — if any
// references a column/table no migration creates, the call fails with a SQL
// error and this test goes red. (The live outage was operational — binary
// deployed ahead of the migration — but this also catches a query that names a
// column no migration adds.)
func TestSQLite_ConnectPathSchema(t *testing.T) {
ctx := context.Background()
db := openSQLite(t)
ns := nodes.NewSQLNodeStore(db)
seedUser(t, db, 1)
// EntitlementForUser: the exact query that broke (SELECT ... p.daily_mb ...).
// With a PRO subscription, DailyMB must come back from plans.daily_mb (102400,
// seeded by migration 000015).
var proID int64
if err := db.QueryRow(`SELECT id FROM plans WHERE code='pro'`).Scan(&proID); err != nil {
t.Fatalf("lookup pro plan: %v", err)
}
if _, err := db.Exec(
`INSERT INTO subscriptions (user_id, plan_id, expires_at, source)
VALUES (1, ?, ?, 'trial')`,
proID, time.Now().UTC().Add(24*time.Hour)); err != nil {
t.Fatalf("seed subscription: %v", err)
}
ent, err := ns.EntitlementForUser(ctx, 1)
if err != nil {
t.Fatalf("EntitlementForUser: %v", err) // a missing daily_mb column lands here
}
if ent == nil {
t.Fatal("EntitlementForUser returned nil entitlement")
}
if ent.PlanCode != "pro" {
t.Errorf("PlanCode = %q, want pro", ent.PlanCode)
}
if !ent.DailyMB.Valid || ent.DailyMB.Int64 != 102400 {
t.Errorf("DailyMB = %+v, want valid 102400", ent.DailyMB)
}
// EnsureDeviceDpUUID: exercises devices.dp_uuid (000015 ADD COLUMN). Mints a
// per-device data-plane credential into the new column.
if _, err := db.Exec(
`INSERT INTO devices (id, uuid, user_id, name, platform)
VALUES (1, 'dev-uuid', 1, 'dev', 'windows')`); err != nil {
t.Fatalf("seed device: %v", err)
}
dp, devID, err := ns.EnsureDeviceDpUUID(ctx, 1, "dev-uuid")
if err != nil {
t.Fatalf("EnsureDeviceDpUUID: %v", err) // missing devices.dp_uuid lands here
}
if dp == "" || devID != 1 {
t.Errorf("EnsureDeviceDpUUID = (%q, %d), want non-empty dp + devID 1", dp, devID)
}
// AccountDayBytes: GB-quota query run right after entitlement; must bind cleanly.
if _, err := ns.AccountDayBytes(ctx, 1, time.Now().UTC()); err != nil {
t.Fatalf("AccountDayBytes: %v", err)
}
}