package auth import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestInMemorySessionTokenStore_CreateAndGet 驗證一次完整的建立 → 查詢循環。 func TestInMemorySessionTokenStore_CreateAndGet(t *testing.T) { s := NewInMemorySessionTokenStore() ctx := context.Background() plain, info, err := s.Create(ctx, "user-1", "dev-1", "parent-hash", SessionTokenTTL) require.NoError(t, err) assert.True(t, IsValidSessionToken(plain), "產出 token 應通過格式驗證:%s", plain) require.NotNil(t, info) assert.Equal(t, "user-1", info.UserID) assert.Equal(t, "dev-1", info.DeviceID) assert.Equal(t, "parent-hash", info.ParentTokenHash) require.NotNil(t, info.ExpiresAt) assert.WithinDuration(t, time.Now().UTC().Add(SessionTokenTTL), *info.ExpiresAt, 2*time.Second) got, err := s.Get(ctx, plain) require.NoError(t, err) assert.Equal(t, "user-1", got.UserID) assert.Equal(t, info.TokenHash, got.TokenHash) } // TestInMemorySessionTokenStore_Get_NotFound 驗證查詢不存在 token 回 ErrInvalidToken。 func TestInMemorySessionTokenStore_Get_NotFound(t *testing.T) { s := NewInMemorySessionTokenStore() _, err := s.Get(context.Background(), "vAs_deadbeef") assert.ErrorIs(t, err, ErrInvalidToken) } // TestInMemorySessionTokenStore_Get_Expired 驗證過期 token 回 ErrTokenExpired。 func TestInMemorySessionTokenStore_Get_Expired(t *testing.T) { s := NewInMemorySessionTokenStore() ctx := context.Background() // TTL 設 1ns 確保立即過期 plain, _, err := s.Create(ctx, "u", "d", "", 1*time.Nanosecond) require.NoError(t, err) time.Sleep(5 * time.Millisecond) _, err = s.Get(ctx, plain) assert.True(t, errors.Is(err, ErrTokenExpired), "應回 ErrTokenExpired,實際:%v", err) } // TestInMemorySessionTokenStore_Revoke 驗證撤銷後 Get 回 ErrTokenRevoked。 func TestInMemorySessionTokenStore_Revoke(t *testing.T) { s := NewInMemorySessionTokenStore() ctx := context.Background() plain, _, err := s.Create(ctx, "u", "d", "", SessionTokenTTL) require.NoError(t, err) require.NoError(t, s.Revoke(ctx, plain)) _, err = s.Get(ctx, plain) assert.ErrorIs(t, err, ErrTokenRevoked) // 冪等:再撤一次不該報錯 assert.NoError(t, s.Revoke(ctx, plain)) } // TestInMemorySessionTokenStore_Revoke_NotFound 驗證撤銷不存在 token 回 ErrInvalidToken。 func TestInMemorySessionTokenStore_Revoke_NotFound(t *testing.T) { s := NewInMemorySessionTokenStore() err := s.Revoke(context.Background(), "vAs_nope") assert.ErrorIs(t, err, ErrInvalidToken) } // TestInMemorySessionTokenStore_CleanupExpired 驗證過期 token 會被清掉。 func TestInMemorySessionTokenStore_CleanupExpired(t *testing.T) { s := NewInMemorySessionTokenStore() ctx := context.Background() // 一個會過期、一個長效 expiredTok, _, err := s.Create(ctx, "u1", "d1", "", 1*time.Nanosecond) require.NoError(t, err) freshTok, _, err := s.Create(ctx, "u2", "d2", "", SessionTokenTTL) require.NoError(t, err) time.Sleep(5 * time.Millisecond) removed, err := s.CleanupExpired(ctx, time.Now().UTC()) require.NoError(t, err) assert.Equal(t, 1, removed) // 過期的應查不到 _, err = s.Get(ctx, expiredTok) assert.ErrorIs(t, err, ErrInvalidToken) // 新鮮的仍在 _, err = s.Get(ctx, freshTok) assert.NoError(t, err) } // TestInMemorySessionTokenStore_NeverExpires 驗證 ttl <= 0 時 ExpiresAt 為 nil。 func TestInMemorySessionTokenStore_NeverExpires(t *testing.T) { s := NewInMemorySessionTokenStore() _, info, err := s.Create(context.Background(), "u", "d", "", 0) require.NoError(t, err) assert.Nil(t, info.ExpiresAt, "ttl=0 時 ExpiresAt 應為 nil") } // TestInMemorySessionTokenStore_RevokeByDevice 驗證 cascade 撤銷(塊 5.2): // 只撤指定 device 名下未撤銷的 session token,回傳撤銷數。 func TestInMemorySessionTokenStore_RevokeByDevice(t *testing.T) { ctx := context.Background() s := NewInMemorySessionTokenStore() s1, _, err := s.Create(ctx, "user-1", "dev-1", "", SessionTokenTTL) require.NoError(t, err) s2, _, err := s.Create(ctx, "user-1", "dev-1", "", SessionTokenTTL) require.NoError(t, err) require.NoError(t, s.Revoke(ctx, s2)) // 已撤,不重複計 // dev-2 不應被撤 o1, _, err := s.Create(ctx, "user-1", "dev-2", "", SessionTokenTTL) require.NoError(t, err) revoked, err := s.RevokeByDevice(ctx, "dev-1") require.NoError(t, err) assert.Equal(t, 1, revoked) _, err = s.Get(ctx, s1) assert.ErrorIs(t, err, ErrTokenRevoked) _, err = s.Get(ctx, o1) assert.NoError(t, err) n, err := s.RevokeByDevice(ctx, "") require.NoError(t, err) assert.Equal(t, 0, n) }