package store import ( "errors" "testing" "oneblog/internal/model" ) func TestEnsureOwner(t *testing.T) { s := openTestStore(t) owner, err := s.EnsureOwner("admin") if err != nil { t.Fatal(err) } if owner.Role != model.RoleOwner || owner.Provider != "admin" { t.Fatalf("owner row wrong: %+v", owner) } // 幂等:再取一次是同一行 again, err := s.EnsureOwner("admin") if err != nil { t.Fatal(err) } if again.ID != owner.ID { t.Fatalf("重复 EnsureOwner 建了两行: %d vs %d", owner.ID, again.ID) } // 改了环境变量里的用户名,handle 要跟上 got, err := s.EnsureOwner("newname") if err != nil { t.Fatal(err) } if got.ID != owner.ID || got.Handle != "newname" { t.Fatalf("handle 未同步: %+v", got) } // 全库只应有一个 owner all, err := s.ListReaders() if err != nil { t.Fatal(err) } n := 0 for _, r := range all { if r.Role == model.RoleOwner { n++ } } if n != 1 { t.Fatalf("owner 行数 = %d, want 1", n) } } func TestOwnerNotClobberedByReaderUpsert(t *testing.T) { // 站主行是 provider=admin,OAuth 登录建的是 provider=github 行, // 两者不能互相覆盖 —— 这是「站主能自定义资料」成立的前提。 s := openTestStore(t) owner, err := s.EnsureOwner("admin") if err != nil { t.Fatal(err) } if _, err := s.UpsertReader(model.Reader{Provider: "github", Handle: "someone", Name: "路人"}); err != nil { t.Fatal(err) } got, err := s.GetOwner() if err != nil { t.Fatal(err) } if got.ID != owner.ID || got.Provider != "admin" { t.Fatalf("站主行被改动: %+v", got) } } func TestBindIdentityGuards(t *testing.T) { s := openTestStore(t) owner, _ := s.EnsureOwner("admin") other, err := s.UpsertReader(model.Reader{Provider: "github", Handle: "intruder", Name: "n"}) if err != nil { t.Fatal(err) } if err := s.BindIdentity(owner.ID, "github", "12345", "littleckin"); err != nil { t.Fatal(err) } // 重复绑同一个:幂等 if err := s.BindIdentity(owner.ID, "github", "12345", "littleckin"); err != nil { t.Fatalf("重复绑定应幂等,got %v", err) } // 关键护栏:同一个外部账号不能被第二个用户占走 if err := s.BindIdentity(other.ID, "github", "12345", "hijack"); !errors.Is(err, ErrConflict) { t.Fatalf("抢占他人身份应 ErrConflict, got %v", err) } // 换平台可以(UNIQUE 是 user_id+provider) if err := s.BindIdentity(owner.ID, "telegram", "999", "tg"); err != nil { t.Fatal(err) } ids, err := s.ListIdentities(owner.ID) if err != nil { t.Fatal(err) } if len(ids) != 2 { t.Fatalf("绑定数 = %d, want 2", len(ids)) } // 登录查找:按外部身份找到站主,且 role 正确 u, err := s.GetUserByIdentity("github", "12345") if err != nil { t.Fatal(err) } if u.ID != owner.ID || u.Role != model.RoleOwner { t.Fatalf("身份反查结果错: %+v", u) } if _, err := s.GetUserByIdentity("github", "nope"); !errors.Is(err, ErrNotFound) { t.Fatalf("未绑定应 ErrNotFound, got %v", err) } // 解绑 if err := s.UnbindIdentity(owner.ID, "github"); err != nil { t.Fatal(err) } if _, err := s.GetUserByIdentity("github", "12345"); !errors.Is(err, ErrNotFound) { t.Fatal("解绑后仍能查到") } if err := s.UnbindIdentity(owner.ID, "github"); !errors.Is(err, ErrNotFound) { t.Fatalf("重复解绑应 ErrNotFound, got %v", err) } } func TestPasskeyLifecycle(t *testing.T) { s := openTestStore(t) owner, _ := s.EnsureOwner("admin") other, _ := s.UpsertReader(model.Reader{Provider: "github", Handle: "x", Name: "x"}) p, err := s.AddPasskey(model.Passkey{UserID: owner.ID, CredentialID: "cred-1", PublicKey: "base64key", Name: "MacBook", SignCount: 3}) if err != nil { t.Fatal(err) } if p.ID == 0 { t.Fatal("没拿到自增 id") } // 同一把凭据不能注册两次 if _, err := s.AddPasskey(model.Passkey{UserID: other.ID, CredentialID: "cred-1", PublicKey: "k", Name: "dup"}); err == nil { t.Fatal("重复 credential_id 应被拒") } list, err := s.ListPasskeys(owner.ID) if err != nil { t.Fatal(err) } if len(list) != 1 || list[0].Name != "MacBook" { t.Fatalf("list=%+v", list) } if list[0].PublicKey != "" { t.Fatal("列表接口不该回传公钥") } got, err := s.GetPasskeyByCredentialID("cred-1") if err != nil { t.Fatal(err) } if got.PublicKey != "base64key" || got.SignCount != 3 { t.Fatalf("got=%+v", got) } if err := s.TouchPasskey(got.ID, 9); err != nil { t.Fatal(err) } after, _ := s.GetPasskeyByCredentialID("cred-1") if after.SignCount != 9 || after.LastUsedAt == "" { t.Fatalf("TouchPasskey 未生效: %+v", after) } // 越权删除必须失败:id 是站主的,user_id 给别人 if err := s.DeletePasskey(after.ID, other.ID); !errors.Is(err, ErrNotFound) { t.Fatalf("跨用户删除应 ErrNotFound, got %v", err) } if err := s.DeletePasskey(after.ID, owner.ID); err != nil { t.Fatal(err) } if rest, _ := s.ListPasskeys(owner.ID); len(rest) != 0 { t.Fatalf("删除后仍有 %d 条", len(rest)) } }