Files
Cyrene_v2/cmd/cyrene/wire_frontend_test.go
AskaEth 79b1ec36aa
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(auth): API Key 落盘(修「每次重启 key 全失效」)
- 缺陷:internal/auth/apikey.go 的 key 表是纯内存 map(注释自认 production should persist)⇒ 一次 restart 后所有 key 一律 401;实测挡住了大附件真机 e2e,也与「key 用于生产救火」的长凭据语义矛盾。

- 迁移 0016_api_keys:表 api_keys(id/name/key_hash/created_at/last_used_at/revoked_at)+ 唯一索引 idx_api_keys_key_hash;**只存 SHA-256 hash,无明文入口**。

- 纪律:Generate 先落库后改内存(失败 ⇒ 内存不变 + 返回错误);AttachKeyStore 启动时把库中未吊销 hash 载入内存(Load 失败仍接上 store ⇒ Generate 会响亮报错,而不是静默退回「不落盘」);Revoke/RevokeAll/RevokeByMask 先落库再移出内存(落库失败 ⇒ WARN + 内存不变,fail-closed:绝不让被吊销的 key 重启后复活)。

- 内存表也改为存 hash(不再存明文)⇒ 面板 id/掩码由 hash 派生:不再泄露明文首尾、且跨重启稳定(重启前记下的 id 重启后仍可吊销)。

- 契约零变化:HTTP 请求/响应形状、网关代码、既有内存用例断言全部未动(网关构造注入点只在组合根 wireAPIKeys)。

- 主会话独立验收:隔离 worktree(K1 的 7 文件)build/vet 绿、全树 59 包 0 FAIL、key 用例 24 PASS、双平台交叉编译绿;变异独立复现(跳过落库 ⇒ 重启存活用例红)。

- 取舍与遗留:吊销落库失败在 HTTP 层仍表现为 404/revoked:0(签名零改动的代价,日志可辨);last_used_at 列已建但恒 NULL(避免把读路径变写路径);旧内存 key 已被此前重启消灭,需重签一次后即长期有效。
2026-10-04 19:55:18 +08:00

72 lines
2.5 KiB
Go

package main
import (
"context"
"database/sql"
"path/filepath"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
)
// 本文件锁定组合根接线(K1):wireFrontend 调用的 wireAPIKeys 必须真的把 SQLite
// 载体接到 key 管理器,否则签发的 key 又回到「重启即失效」。
//
// 为什么在组合根测这件事:newApp 无法在单元测试里实例化(要开库、迁移、拉起全部
// 子系统,见 wire_context_test.go 的说明),所以这里直接对 wireAPIKeys 这一步
// 接线取证 —— 它与 newApp 里调用的是同一个函数。
// openMigratedTestDB 打开一个临时文件库并跑完全部迁移。用文件而非 :memory:,
// 因为「重启仍在」必须跨连接读同一份数据。
func openMigratedTestDB(t *testing.T, path string) *sql.DB {
t.Helper()
ctx := context.Background()
conn, err := db.Open(ctx, path, testLog())
if err != nil {
t.Fatalf("db.Open(%s): %v", path, err)
}
if err := db.MigrateUp(ctx, conn, testLog()); err != nil {
_ = conn.Close()
t.Fatalf("db.MigrateUp: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
return conn
}
// TestWireAPIKeys_KeySurvivesRestart 断言组合根接的线真的落盘:签发 → 关连接
// (进程退出)→ 用同一个库重新接线 → 同一把 key 仍能认证。
func TestWireAPIKeys_KeySurvivesRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "cyrene.db")
first := &app{db: openMigratedTestDB(t, path), cfg: &config.Config{}, log: testLog()}
keys := first.wireAPIKeys()
key, err := keys.Generate()
if err != nil {
t.Fatalf("Generate: %v", err)
}
if err := first.db.Close(); err != nil {
t.Fatalf("close first connection: %v", err)
}
second := &app{db: openMigratedTestDB(t, path), cfg: &config.Config{}, log: testLog()}
restarted := second.wireAPIKeys()
if !restarted.Verify(key) {
t.Fatal("重启后 key 无法认证:组合根没有把落盘载体接上")
}
}
// TestWireAPIKeys_NilDBFallsBackToMemory 断言没有数据库句柄时(纯内存测试/
// 构建路径)接线是空操作且不 panic:key 照旧可签发可校验,只是不跨重启。
func TestWireAPIKeys_NilDBFallsBackToMemory(t *testing.T) {
a := &app{db: nil, cfg: &config.Config{}, log: testLog()}
keys := a.wireAPIKeys() // 必须不 panic
key, err := keys.Generate()
if err != nil {
t.Fatalf("Generate: %v", err)
}
if !keys.Verify(key) {
t.Fatal("未接库路径的 key 无法认证")
}
}