feat: keep pairing codes and GitHub login states in Redis

This commit is contained in:
DiheChen committed 2026-11-11 00:11:43 +08:00
1 parent 240c8899a9
commit ad0d0900d8
8 files changed
+611 -10

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
package redis
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
goredis "github.com/redis/go-redis/v9"
"github.com/dihemoe/alisten/internal/app/port"
)
var (
_ port.PairingCodes = (*Codes)(nil)
_ port.GitHubLogins = (*Codes)(nil)
_ port.GitHubVerifications = (*Codes)(nil)
)
const (
pairingPrefix = "pairing:"
githubRequestPrefix = "github:request:"
githubGrantPrefix = "github:grant:"
githubVerifiedPrefix = "github:verified:"
)
// 配对码、GitHub 登录的 state 和兑换码、用 GitHub 确认身份的记录,键中只有它们的哈希。丢失时用户重新生成、重新登录或重新确认即可。
type Codes struct {
client *goredis.Client
}
func NewCodes(client *goredis.Client) *Codes {
return &Codes{client: client}
}
func codeKey(prefix string, hash []byte) string {
return prefix + hex.EncodeToString(hash)
}
type pairingRecord struct {
MemberID string `json:"memberId"`
Session []byte `json:"session"`
}
type githubRequestRecord struct {
Intent string `json:"intent"`
Client string `json:"client"`
MemberID string `json:"memberId"`
Nonce []byte `json:"nonce"`
Verifier string `json:"verifier"`
}
type githubGrantRecord struct {
githubRequestRecord
GitHubID int64 `json:"githubId"`
}
var (
intentNames = map[port.GitHubIntent]string{port.GitHubLogin: "login", port.GitHubBind: "bind", port.GitHubVerify: "verify"}
clientNames = map[port.GitHubClient]string{port.GitHubWeb: "web", port.GitHubApp: "app"}
)
func requestRecord(r port.GitHubRequest) githubRequestRecord {
return githubRequestRecord{Intent: intentNames[r.Intent], Client: clientNames[r.Client], MemberID: r.MemberID, Nonce: r.NonceHash, Verifier: r.CodeVerifier}
}
func (r githubRequestRecord) request() (port.GitHubRequest, error) {
intent, ok := keyOf(intentNames, r.Intent)
if !ok {
return port.GitHubRequest{}, fmt.Errorf("不认识的意图 %q", r.Intent)
}
client, ok := keyOf(clientNames, r.Client)
if !ok {
return port.GitHubRequest{}, fmt.Errorf("不认识的客户端 %q", r.Client)
}
return port.GitHubRequest{Intent: intent, Client: client, MemberID: r.MemberID, NonceHash: r.Nonce, CodeVerifier: r.Verifier}, nil
}
func keyOf[K comparable](names map[K]string, name string) (K, bool) {
for k, n := range names {
if n == name {
return k, true
}
}
var zero K
return zero, false
}
func (c *Codes) CreatePairing(ctx context.Context, codeHash []byte, p port.Pairing, ttl time.Duration) (bool, error) {
value, err := json.Marshal(pairingRecord{MemberID: p.MemberID, Session: p.SessionTokenHash})
if err != nil {
return false, err
}
created, err := c.client.SetNX(ctx, codeKey(pairingPrefix, codeHash), value, ttl).Result()
if err != nil {
return false, fmt.Errorf("保存配对码:%w", err)
}
return created, nil
}
func (c *Codes) TakePairing(ctx context.Context, codeHash []byte) (port.Pairing, error) {
var record pairingRecord
if err := c.take(ctx, codeKey(pairingPrefix, codeHash), &record); err != nil {
return port.Pairing{}, fmt.Errorf("取出配对码:%w", err)
}
return port.Pairing{MemberID: record.MemberID, SessionTokenHash: record.Session}, nil
}
func (c *Codes) SaveRequest(ctx context.Context, stateHash []byte, r port.GitHubRequest, ttl time.Duration) error {
if err := c.save(ctx, codeKey(githubRequestPrefix, stateHash), requestRecord(r), ttl); err != nil {
return fmt.Errorf("保存 GitHub 授权请求:%w", err)
}
return nil
}
func (c *Codes) TakeRequest(ctx context.Context, stateHash []byte) (port.GitHubRequest, error) {
var record githubRequestRecord
if err := c.take(ctx, codeKey(githubRequestPrefix, stateHash), &record); err != nil {
return port.GitHubRequest{}, fmt.Errorf("取出 GitHub 授权请求:%w", err)
}
r, err := record.request()
if err != nil {
return port.GitHubRequest{}, fmt.Errorf("取出 GitHub 授权请求:%w", err)
}
return r, nil
}
func (c *Codes) SaveGrant(ctx context.Context, codeHash []byte, g port.GitHubGrant, ttl time.Duration) error {
record := githubGrantRecord{githubRequestRecord: requestRecord(g.Request), GitHubID: g.GitHubID}
if err := c.save(ctx, codeKey(githubGrantPrefix, codeHash), record, ttl); err != nil {
return fmt.Errorf("保存 GitHub 登录的兑换码:%w", err)
}
return nil
}
func (c *Codes) TakeGrant(ctx context.Context, codeHash []byte) (port.GitHubGrant, error) {
var record githubGrantRecord
if err := c.take(ctx, codeKey(githubGrantPrefix, codeHash), &record); err != nil {
return port.GitHubGrant{}, fmt.Errorf("取出 GitHub 登录的兑换码:%w", err)
}
r, err := record.request()
if err != nil {
return port.GitHubGrant{}, fmt.Errorf("取出 GitHub 登录的兑换码:%w", err)
}
return port.GitHubGrant{Request: r, GitHubID: record.GitHubID}, nil
}
func (c *Codes) SaveVerification(ctx context.Context, tokenHash []byte, ttl time.Duration) error {
if err := c.client.Set(ctx, codeKey(githubVerifiedPrefix, tokenHash), "", ttl).Err(); err != nil {
return fmt.Errorf("保存用 GitHub 确认身份的记录:%w", err)
}
return nil
}
func (c *Codes) Verified(ctx context.Context, tokenHash []byte) (bool, error) {
n, err := c.client.Exists(ctx, codeKey(githubVerifiedPrefix, tokenHash)).Result()
if err != nil {
return false, fmt.Errorf("读取用 GitHub 确认身份的记录:%w", err)
}
return n == 1, nil
}
func (c *Codes) DeleteVerification(ctx context.Context, tokenHash []byte) error {
if err := c.client.Del(ctx, codeKey(githubVerifiedPrefix, tokenHash)).Err(); err != nil {
return fmt.Errorf("删除用 GitHub 确认身份的记录:%w", err)
}
return nil
}
func (c *Codes) save(ctx context.Context, key string, record any, ttl time.Duration) error {
value, err := json.Marshal(record)
if err != nil {
return err
}
return c.client.Set(ctx, key, value, ttl).Err()
}
// GETDEL 让取出和删除是一个原子操作:并发的两次兑换只有一次能拿到。
func (c *Codes) take(ctx context.Context, key string, record any) error {
value, err := c.client.GetDel(ctx, key).Bytes()
if errors.Is(err, goredis.Nil) {
return port.ErrCodeNotFound
}
if err != nil {
return err
}
if err := json.Unmarshal(value, record); err != nil {
return fmt.Errorf("解码存储的值:%w", err)
}
return nil
}
+137
View File
@@ -0,0 +1,137 @@
package redis
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"testing"
"time"
"github.com/dihemoe/alisten/internal/app/port"
)
func hashOf(s string) []byte {
sum := sha256.Sum256([]byte(s))
return sum[:]
}
func TestPairingCodes(t *testing.T) {
client := newClient(t)
codes := NewCodes(client)
p := port.Pairing{MemberID: "00000000-0000-7000-8000-000000000001", SessionTokenHash: hashOf("session")}
created, err := codes.CreatePairing(t.Context(), hashOf("ABCD2345"), p, 5*time.Minute)
if err != nil || !created {
t.Fatalf("CreatePairing = %v, %v", created, err)
}
key := "pairing:" + hex.EncodeToString(hashOf("ABCD2345"))
if ttl := client.PTTL(t.Context(), key).Val(); ttl < 4*time.Minute || ttl > 5*time.Minute {
t.Errorf("配对码的有效期 = %v,期望 5 分钟", ttl)
}
other := port.Pairing{MemberID: "00000000-0000-7000-8000-000000000002", SessionTokenHash: hashOf("other")}
if created, err := codes.CreatePairing(t.Context(), hashOf("ABCD2345"), other, time.Minute); err != nil || created {
t.Errorf("哈希已经有配对码时 CreatePairing = %v, %v,期望不覆盖", created, err)
}
got, err := codes.TakePairing(t.Context(), hashOf("ABCD2345"))
if err != nil || got.MemberID != p.MemberID || !bytes.Equal(got.SessionTokenHash, p.SessionTokenHash) {
t.Errorf("TakePairing = %#v, %v,期望 %#v", got, err, p)
}
if _, err := codes.TakePairing(t.Context(), hashOf("ABCD2345")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("第二次取 = %v,期望 ErrCodeNotFound", err)
}
if _, err := codes.TakePairing(t.Context(), hashOf("ZZZZ2345")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("不存在的配对码 = %v,期望 ErrCodeNotFound", err)
}
if err := client.Set(t.Context(), "pairing:"+hex.EncodeToString(hashOf("BROKEN22")), "not json", time.Minute).Err(); err != nil {
t.Fatal(err)
}
if _, err := codes.TakePairing(t.Context(), hashOf("BROKEN22")); err == nil || errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("存储中的值无法解码时 = %v,期望报错", err)
}
}
func TestGitHubLogins(t *testing.T) {
client := newClient(t)
codes := NewCodes(client)
r := port.GitHubRequest{Intent: port.GitHubBind, Client: port.GitHubApp, MemberID: "00000000-0000-7000-8000-000000000001", NonceHash: hashOf("nonce"), CodeVerifier: "verifier-1"}
if err := codes.SaveRequest(t.Context(), hashOf("state"), r, 10*time.Minute); err != nil {
t.Fatal(err)
}
if ttl := client.PTTL(t.Context(), "github:request:"+hex.EncodeToString(hashOf("state"))).Val(); ttl < 9*time.Minute || ttl > 10*time.Minute {
t.Errorf("state 的有效期 = %v,期望 10 分钟", ttl)
}
if _, err := codes.TakeGrant(t.Context(), hashOf("state")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("用 state 取兑换码 = %v,期望 ErrCodeNotFound:两种键互不相通", err)
}
got, err := codes.TakeRequest(t.Context(), hashOf("state"))
if err != nil || got.Intent != r.Intent || got.Client != r.Client || got.MemberID != r.MemberID || !bytes.Equal(got.NonceHash, r.NonceHash) || got.CodeVerifier != r.CodeVerifier {
t.Errorf("TakeRequest = %#v, %v,期望 %#v", got, err, r)
}
if _, err := codes.TakeRequest(t.Context(), hashOf("state")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("第二次取 state = %v,期望 ErrCodeNotFound", err)
}
g := port.GitHubGrant{Request: port.GitHubRequest{Intent: port.GitHubLogin, Client: port.GitHubWeb, MemberID: r.MemberID, NonceHash: r.NonceHash}, GitHubID: 9007199254740993}
if err := codes.SaveGrant(t.Context(), hashOf("code"), g, time.Minute); err != nil {
t.Fatal(err)
}
if ttl := client.PTTL(t.Context(), "github:grant:"+hex.EncodeToString(hashOf("code"))).Val(); ttl < 50*time.Second || ttl > time.Minute {
t.Errorf("兑换码的有效期 = %v,期望 1 分钟", ttl)
}
gotGrant, err := codes.TakeGrant(t.Context(), hashOf("code"))
if err != nil || gotGrant.GitHubID != g.GitHubID || gotGrant.Request.Intent != port.GitHubLogin || gotGrant.Request.Client != port.GitHubWeb || gotGrant.Request.MemberID != r.MemberID || !bytes.Equal(gotGrant.Request.NonceHash, r.NonceHash) {
t.Errorf("TakeGrant = %#v, %v,期望 %#v", gotGrant, err, g)
}
if _, err := codes.TakeGrant(t.Context(), hashOf("code")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("第二次取兑换码 = %v,期望 ErrCodeNotFound", err)
}
for name, record := range map[string]string{
"意图": `{"intent":"steal","client":"web","memberId":"x","nonce":""}`,
"客户端": `{"intent":"login","client":"evil","memberId":"x","nonce":""}`,
} {
if err := client.Set(t.Context(), "github:request:"+hex.EncodeToString(hashOf("odd")), record, time.Minute).Err(); err != nil {
t.Fatal(err)
}
if _, err := codes.TakeRequest(t.Context(), hashOf("odd")); err == nil || errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("存储中的%s不认识时 = %v,期望报错", name, err)
}
}
}
func TestGitHubVerifications(t *testing.T) {
client := newClient(t)
codes := NewCodes(client)
if err := codes.SaveVerification(t.Context(), hashOf("session"), 10*time.Minute); err != nil {
t.Fatal(err)
}
if ttl := client.PTTL(t.Context(), "github:verified:"+hex.EncodeToString(hashOf("session"))).Val(); ttl < 9*time.Minute || ttl > 10*time.Minute {
t.Errorf("确认的有效期 = %v,期望 10 分钟", ttl)
}
for range 2 {
if ok, err := codes.Verified(t.Context(), hashOf("session")); err != nil || !ok {
t.Errorf("Verified = %v, %v,期望读取不删除", ok, err)
}
}
if err := codes.DeleteVerification(t.Context(), hashOf("session")); err != nil {
t.Fatal(err)
}
if ok, err := codes.Verified(t.Context(), hashOf("session")); err != nil || ok {
t.Errorf("删除之后 Verified = %v, %v,期望 false", ok, err)
}
if err := codes.DeleteVerification(t.Context(), hashOf("session")); err != nil {
t.Errorf("删除不存在的确认 = %v,期望什么也不做", err)
}
g := port.GitHubGrant{Request: port.GitHubRequest{Intent: port.GitHubVerify, Client: port.GitHubWeb, MemberID: "00000000-0000-7000-8000-000000000001", NonceHash: hashOf("nonce")}, GitHubID: 42}
if err := codes.SaveGrant(t.Context(), hashOf("other"), g, time.Minute); err != nil {
t.Fatal(err)
}
if ok, err := codes.Verified(t.Context(), hashOf("other")); err != nil || ok {
t.Errorf("同一个哈希的兑换码当作确认 = %v, %v,期望 false:两种键互不相通", ok, err)
}
if got, err := codes.TakeGrant(t.Context(), hashOf("other")); err != nil || got.Request.Intent != port.GitHubVerify {
t.Errorf("TakeGrant = %#v, %v,期望意图 verify 原样取回", got, err)
}
}
+2 -3
View File
@@ -2,7 +2,6 @@ package redis
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -36,14 +35,14 @@ func (d *Devices) SaveDevice(ctx context.Context, deviceHash []byte, memberID st
if err != nil {
return err
}
if err := d.client.Set(ctx, devicePrefix+hex.EncodeToString(deviceHash), value, ttl).Err(); err != nil {
if err := d.client.Set(ctx, codeKey(devicePrefix, deviceHash), value, ttl).Err(); err != nil {
return fmt.Errorf("保存登录设备凭证:%w", err)
}
return nil
}
func (d *Devices) DeviceMember(ctx context.Context, deviceHash []byte) (string, []byte, error) {
value, err := d.client.Get(ctx, devicePrefix+hex.EncodeToString(deviceHash)).Bytes()
value, err := d.client.Get(ctx, codeKey(devicePrefix, deviceHash)).Bytes()
if errors.Is(err, goredis.Nil) {
return "", nil, port.ErrDeviceNotFound
}
@@ -2,7 +2,6 @@ package redis
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"testing"
@@ -11,11 +10,6 @@ import (
"github.com/dihemoe/alisten/internal/app/port"
)
func hashOf(s string) []byte {
sum := sha256.Sum256([]byte(s))
return sum[:]
}
func TestLoginDevices(t *testing.T) {
client := newClient(t)
devices := NewDevices(client)
+62 -1
View File
@@ -1,6 +1,47 @@
package port
import "context"
import (
"context"
"time"
)
type GitHubIntent int
const (
// 游客用 GitHub 登录:GitHub 账号没有绑定过时,就是注册。
GitHubLogin GitHubIntent = iota + 1
// 已注册的成员绑定 GitHub。
GitHubBind
// 只绑定了 GitHub 的账号补设用户名和密码之前,用账号绑定的 GitHub 确认身份。
GitHubVerify
)
// 发起授权的客户端,GitHub 授权结束之后跳回它。
type GitHubClient int
const (
GitHubWeb GitHubClient = iota + 1
// App 用系统浏览器授权,授权结束之后要跳回 App。
GitHubApp
)
// 发起 GitHub 授权时记下的请求,以 state 为键。
type GitHubRequest struct {
Intent GitHubIntent
Client GitHubClient
// 发起授权的成员。
MemberID string
// 发起授权的客户端保存的 nonce 的 SHA-256。
NonceHash []byte
// PKCE 的 code verifier:GitHub 签发的授权码只能配合它换取令牌,别人的授权码放进这次授权的回调里换不到令牌。
CodeVerifier string
}
// GitHub 确认了用户的身份,等待发起授权的客户端兑换,以兑换码为键。
type GitHubGrant struct {
Request GitHubRequest
GitHubID int64
}
// 后台填写的 GitHub OAuth App。
type GitHubOAuthApp struct {
@@ -16,3 +57,23 @@ type GitHubOAuthApps interface {
// 没有配置时什么也不做。
DeleteGitHubOAuthApp(ctx context.Context) error
}
// state 和兑换码只以 SHA-256 哈希的形式出现在端口中,所有实例共用。
type GitHubLogins interface {
SaveRequest(ctx context.Context, stateHash []byte, r GitHubRequest, ttl time.Duration) error
// 取出并删除,同一个 state 只能取出一次。不存在或已过期时返回 ErrCodeNotFound。
TakeRequest(ctx context.Context, stateHash []byte) (GitHubRequest, error)
SaveGrant(ctx context.Context, codeHash []byte, g GitHubGrant, ttl time.Duration) error
// 与 TakeRequest 相同。
TakeGrant(ctx context.Context, codeHash []byte) (GitHubGrant, error)
}
// 只绑定了 GitHub 的账号补设用户名和密码之前,要用账号绑定的 GitHub 确认身份。确认记在发出请求的会话上,以会话的成员 token
// 的 SHA-256 哈希为键,所有实例共用。丢失时用户重新确认即可。
type GitHubVerifications interface {
SaveVerification(ctx context.Context, tokenHash []byte, ttl time.Duration) error
// 不存在或已过期时返回 false。
Verified(ctx context.Context, tokenHash []byte) (bool, error)
// 不存在时什么也不做。
DeleteVerification(ctx context.Context, tokenHash []byte) error
}
+25
View File
@@ -0,0 +1,25 @@
package port
import (
"context"
"errors"
"time"
)
// 配对码、GitHub 登录的 state 和兑换码都只能用一次:取出即删除,到期自动失效。
var ErrCodeNotFound = errors.New("不存在或已经过期")
// 已注册成员生成的配对码:新设备兑换之后登录这个账号。
type Pairing struct {
MemberID string
// 生成配对码的会话:它被删除之后(退出登录、修改或重置密码),配对码随之作废。
SessionTokenHash []byte
}
// 配对码只以 SHA-256 哈希的形式出现在端口中,所有实例共用。
type PairingCodes interface {
// 这个哈希已经有配对码时返回 false,不覆盖它。
CreatePairing(ctx context.Context, codeHash []byte, p Pairing, ttl time.Duration) (bool, error)
// 取出并删除,同一个配对码只能取出一次。不存在或已过期时返回 ErrCodeNotFound。
TakePairing(ctx context.Context, codeHash []byte) (Pairing, error)
}
+97
View File
@@ -0,0 +1,97 @@
package porttest
import (
"context"
"sync"
"time"
"github.com/dihemoe/alisten/internal/app/port"
)
var (
_ port.PairingCodes = (*Codes)(nil)
_ port.GitHubLogins = (*Codes)(nil)
_ port.GitHubVerifications = (*Codes)(nil)
)
// 时间取自 time.Now,可以在 synctest 中使用。
type Codes struct {
mu sync.Mutex
entries map[string]codeEntry
}
type codeEntry struct {
value any
expires time.Time
}
func NewCodes() *Codes {
return &Codes{entries: map[string]codeEntry{}}
}
func (c *Codes) put(key string, value any, ttl time.Duration, overwrite bool) bool {
c.mu.Lock()
defer c.mu.Unlock()
if e, ok := c.entries[key]; ok && !overwrite && time.Now().Before(e.expires) {
return false
}
c.entries[key] = codeEntry{value: value, expires: time.Now().Add(ttl)}
return true
}
func take[T any](c *Codes, key string) (T, error) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.entries[key]
delete(c.entries, key)
if !ok || !time.Now().Before(e.expires) {
var zero T
return zero, port.ErrCodeNotFound
}
return e.value.(T), nil
}
func (c *Codes) CreatePairing(_ context.Context, codeHash []byte, p port.Pairing, ttl time.Duration) (bool, error) {
return c.put("pairing:"+string(codeHash), p, ttl, false), nil
}
func (c *Codes) TakePairing(_ context.Context, codeHash []byte) (port.Pairing, error) {
return take[port.Pairing](c, "pairing:"+string(codeHash))
}
func (c *Codes) SaveRequest(_ context.Context, stateHash []byte, r port.GitHubRequest, ttl time.Duration) error {
c.put("github:request:"+string(stateHash), r, ttl, true)
return nil
}
func (c *Codes) TakeRequest(_ context.Context, stateHash []byte) (port.GitHubRequest, error) {
return take[port.GitHubRequest](c, "github:request:"+string(stateHash))
}
func (c *Codes) SaveGrant(_ context.Context, codeHash []byte, g port.GitHubGrant, ttl time.Duration) error {
c.put("github:grant:"+string(codeHash), g, ttl, true)
return nil
}
func (c *Codes) TakeGrant(_ context.Context, codeHash []byte) (port.GitHubGrant, error) {
return take[port.GitHubGrant](c, "github:grant:"+string(codeHash))
}
func (c *Codes) SaveVerification(_ context.Context, tokenHash []byte, ttl time.Duration) error {
c.put("github:verified:"+string(tokenHash), struct{}{}, ttl, true)
return nil
}
func (c *Codes) Verified(_ context.Context, tokenHash []byte) (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.entries["github:verified:"+string(tokenHash)]
return ok && time.Now().Before(e.expires), nil
}
func (c *Codes) DeleteVerification(_ context.Context, tokenHash []byte) error {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, "github:verified:"+string(tokenHash))
return nil
}
+95
View File
@@ -0,0 +1,95 @@
package porttest
import (
"errors"
"testing"
"testing/synctest"
"time"
"github.com/dihemoe/alisten/internal/app/port"
)
// 与 Redis 适配器的 TestPairingCodes、TestGitHubLogins 相同的语义。
func TestCodes(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
codes := NewCodes()
p := port.Pairing{MemberID: "m1", SessionTokenHash: []byte("session")}
if created, err := codes.CreatePairing(t.Context(), []byte("code"), p, time.Minute); err != nil || !created {
t.Fatalf("CreatePairing = %v, %v", created, err)
}
if created, _ := codes.CreatePairing(t.Context(), []byte("code"), port.Pairing{MemberID: "m2"}, time.Minute); created {
t.Error("哈希已经有配对码时不应覆盖")
}
if got, err := codes.TakePairing(t.Context(), []byte("code")); err != nil || got.MemberID != "m1" {
t.Errorf("TakePairing = %#v, %v", got, err)
}
if _, err := codes.TakePairing(t.Context(), []byte("code")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("第二次取 = %v,期望 ErrCodeNotFound", err)
}
if _, err := codes.CreatePairing(t.Context(), []byte("late"), p, time.Minute); err != nil {
t.Fatal(err)
}
r := port.GitHubRequest{Intent: port.GitHubLogin, MemberID: "m1", NonceHash: []byte("nonce")}
if err := codes.SaveRequest(t.Context(), []byte("late"), r, 2*time.Minute); err != nil {
t.Fatal(err)
}
if err := codes.SaveGrant(t.Context(), []byte("late"), port.GitHubGrant{Request: r, GitHubID: 42}, 3*time.Minute); err != nil {
t.Fatal(err)
}
time.Sleep(time.Minute)
if _, err := codes.TakePairing(t.Context(), []byte("late")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("过期之后取配对码 = %v,期望 ErrCodeNotFound", err)
}
if created, _ := codes.CreatePairing(t.Context(), []byte("late"), p, time.Minute); !created {
t.Error("过期的配对码不应挡住新的")
}
if got, err := codes.TakeRequest(t.Context(), []byte("late")); err != nil || got.Intent != port.GitHubLogin {
t.Errorf("同一个哈希的 state = %#v, %v,三种键互不相通", got, err)
}
time.Sleep(2 * time.Minute)
if _, err := codes.TakeGrant(t.Context(), []byte("late")); !errors.Is(err, port.ErrCodeNotFound) {
t.Errorf("过期之后取兑换码 = %v,期望 ErrCodeNotFound", err)
}
})
}
// 与 Redis 适配器的 TestGitHubVerifications 相同的语义。
func TestGitHubVerifications(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
codes := NewCodes()
if err := codes.SaveVerification(t.Context(), []byte("session"), 10*time.Minute); err != nil {
t.Fatal(err)
}
for range 2 {
if ok, err := codes.Verified(t.Context(), []byte("session")); err != nil || !ok {
t.Errorf("Verified = %v, %v,期望读取不删除", ok, err)
}
}
if err := codes.DeleteVerification(t.Context(), []byte("session")); err != nil {
t.Fatal(err)
}
if ok, err := codes.Verified(t.Context(), []byte("session")); err != nil || ok {
t.Errorf("删除之后 Verified = %v, %v,期望 false", ok, err)
}
if err := codes.DeleteVerification(t.Context(), []byte("session")); err != nil {
t.Errorf("删除不存在的确认 = %v,期望什么也不做", err)
}
r := port.GitHubRequest{Intent: port.GitHubVerify, MemberID: "m1", NonceHash: []byte("nonce")}
if err := codes.SaveGrant(t.Context(), []byte("other"), port.GitHubGrant{Request: r, GitHubID: 42}, time.Minute); err != nil {
t.Fatal(err)
}
if ok, err := codes.Verified(t.Context(), []byte("other")); err != nil || ok {
t.Errorf("同一个哈希的兑换码当作确认 = %v, %v,期望 false:两种键互不相通", ok, err)
}
if err := codes.SaveVerification(t.Context(), []byte("late"), 10*time.Minute); err != nil {
t.Fatal(err)
}
time.Sleep(10 * time.Minute)
if ok, err := codes.Verified(t.Context(), []byte("late")); err != nil || ok {
t.Errorf("过期之后 Verified = %v, %v,期望 false", ok, err)
}
})
}