package auth import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestInMemoryPairingStore_CreateAndValidate(t *testing.T) { ctx := context.Background() s := NewInMemoryPairingStore() plain, info, err := s.Create(ctx, "user-1", 15*time.Minute) require.NoError(t, err) require.NotEmpty(t, plain) require.NotNil(t, info) assert.True(t, IsValidPairingToken(plain)) assert.Equal(t, "user-1", info.UserID) assert.Equal(t, KindPairing, info.Kind) assert.NotNil(t, info.ExpiresAt) assert.Nil(t, info.UsedAt) got, err := s.Validate(ctx, plain) require.NoError(t, err) assert.Equal(t, "user-1", got.UserID) } func TestInMemoryPairingStore_Validate_UnknownToken(t *testing.T) { s := NewInMemoryPairingStore() _, err := s.Validate(context.Background(), "vAc_unknown0000000000000000000000") assert.ErrorIs(t, err, ErrInvalidToken) } func TestInMemoryPairingStore_MarkUsed_IsOneTime(t *testing.T) { ctx := context.Background() s := NewInMemoryPairingStore() plain, _, err := s.Create(ctx, "user-1", 15*time.Minute) require.NoError(t, err) require.NoError(t, s.MarkUsed(ctx, plain, "device-1")) // Validate 必須失敗(一次性 token 已消費)。 _, err = s.Validate(ctx, plain) assert.ErrorIs(t, err, ErrTokenUsed) // 再次 MarkUsed 應為 no-op(冪等)。 assert.NoError(t, s.MarkUsed(ctx, plain, "another-device")) } func TestInMemoryPairingStore_Revoke(t *testing.T) { ctx := context.Background() s := NewInMemoryPairingStore() plain, _, err := s.Create(ctx, "user-1", 15*time.Minute) require.NoError(t, err) require.NoError(t, s.Revoke(ctx, plain)) _, err = s.Validate(ctx, plain) assert.ErrorIs(t, err, ErrTokenRevoked) // 撤銷不存在的 token → ErrInvalidToken assert.ErrorIs(t, s.Revoke(ctx, "vAc_abcdef00000000000000000000000000"), ErrInvalidToken) } func TestInMemoryPairingStore_CleanupExpired(t *testing.T) { ctx := context.Background() s := NewInMemoryPairingStore() // 產生一個已過期的 token(ttl = 1ms) expired, _, err := s.Create(ctx, "user-1", 1*time.Millisecond) require.NoError(t, err) // 另一個尚未過期 fresh, _, err := s.Create(ctx, "user-1", 1*time.Hour) require.NoError(t, err) // 等 10ms 確保第一個過期 time.Sleep(10 * time.Millisecond) removed, err := s.CleanupExpired(ctx, time.Now().UTC()) require.NoError(t, err) assert.Equal(t, 1, removed) _, err = s.Validate(ctx, expired) assert.ErrorIs(t, err, ErrInvalidToken, "過期的 token 應被清掉") _, err = s.Validate(ctx, fresh) assert.NoError(t, err, "未過期的 token 不應被清") } func TestInMemoryPairingStore_List_ByUser(t *testing.T) { ctx := context.Background() s := NewInMemoryPairingStore() _, _, err := s.Create(ctx, "user-A", time.Hour) require.NoError(t, err) _, _, err = s.Create(ctx, "user-A", time.Hour) require.NoError(t, err) _, _, err = s.Create(ctx, "user-B", time.Hour) require.NoError(t, err) listA, err := s.List(ctx, "user-A") require.NoError(t, err) assert.Len(t, listA, 2) listB, err := s.List(ctx, "user-B") require.NoError(t, err) assert.Len(t, listB, 1) listNone, err := s.List(ctx, "user-X") require.NoError(t, err) assert.Empty(t, listNone) } func TestInMemoryPairingStore_Validate_Expired(t *testing.T) { ctx := context.Background() s := NewInMemoryPairingStore() plain, _, err := s.Create(ctx, "user-1", 1*time.Millisecond) require.NoError(t, err) time.Sleep(5 * time.Millisecond) _, err = s.Validate(ctx, plain) assert.ErrorIs(t, err, ErrTokenExpired) } // TestInMemoryPairingStore_RevokeByDevice 驗證 cascade 撤銷(塊 5.2): // 只撤指定 device 名下未撤銷的 token,回傳撤銷數。 func TestInMemoryPairingStore_RevokeByDevice(t *testing.T) { ctx := context.Background() s := NewInMemoryPairingStore() // 綁 dev-1 的兩個 token(其中一個先撤) p1, _, err := s.Create(ctx, "user-1", 15*time.Minute) require.NoError(t, err) require.NoError(t, s.MarkUsed(ctx, p1, "dev-1")) p2, _, err := s.Create(ctx, "user-1", 15*time.Minute) require.NoError(t, err) require.NoError(t, s.MarkUsed(ctx, p2, "dev-1")) require.NoError(t, s.Revoke(ctx, p2)) // 已撤,不應重複計數 // 綁 dev-2(不應被撤) p3, _, err := s.Create(ctx, "user-1", 15*time.Minute) require.NoError(t, err) require.NoError(t, s.MarkUsed(ctx, p3, "dev-2")) revoked, err := s.RevokeByDevice(ctx, "dev-1") require.NoError(t, err) assert.Equal(t, 1, revoked, "只有 p1(未撤)被撤;p2 已撤不計") // p1 已撤 _, err = s.Validate(ctx, p1) assert.ErrorIs(t, err, ErrTokenRevoked) // p3(dev-2)不受影響 _, err = s.Validate(ctx, p3) assert.ErrorIs(t, err, ErrTokenUsed) // 已 used 但未 revoke // 空 deviceID 不撤任何 token n, err := s.RevokeByDevice(ctx, "") require.NoError(t, err) assert.Equal(t, 0, n) }