Unit tests for DEK cache: - Put/Get operations, TTL expiry, LRU eviction - Tenant isolation, flush/clear, stats, empty DEK rejection Unit tests for KMS protocol: - Encrypt/decrypt round-trip with MemoryProvider - Multi-tenant isolation (wrong key fails MAC verification) - Cache hit verification, key rotation flow - Health check protocol Integration tests with testcontainers: - VaultProvider with hashicorp/vault:1.15 container - AWSProvider with localstack/localstack container - TenantKeyManager end-to-end with MemoryProvider
254 lines
7.2 KiB
Go
254 lines
7.2 KiB
Go
package kms_test
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/crypto/kms"
|
|
)
|
|
|
|
// TestDEKCache_PutAndGet tests basic cache put and get operations.
|
|
func TestDEKCache_PutAndGet(t *testing.T) {
|
|
cache := kms.NewDEKCache(kms.DefaultCacheConfig())
|
|
defer cache.Clear()
|
|
|
|
tenantID := "tenant-1"
|
|
artifactID := "artifact-1"
|
|
dek := []byte("test-dek-data-12345678901234567890123456789012")
|
|
|
|
// Put DEK in cache
|
|
if err := cache.Put(tenantID, artifactID, "kms-key-1", dek); err != nil {
|
|
t.Fatalf("Put failed: %v", err)
|
|
}
|
|
|
|
// Get DEK from cache (KMS available)
|
|
retrieved, ok := cache.Get(tenantID, artifactID, "kms-key-1", false)
|
|
if !ok {
|
|
t.Fatal("Get returned false, expected true")
|
|
}
|
|
|
|
if !bytes.Equal(retrieved, dek) {
|
|
t.Error("Retrieved DEK doesn't match original")
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_GetNonexistent tests getting a non-existent entry.
|
|
func TestDEKCache_GetNonexistent(t *testing.T) {
|
|
cache := kms.NewDEKCache(kms.DefaultCacheConfig())
|
|
defer cache.Clear()
|
|
|
|
_, ok := cache.Get("nonexistent", "nonexistent", "kms-key-1", false)
|
|
if ok {
|
|
t.Error("Get for non-existent key should return false")
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_TTLExpiry tests that entries expire after TTL.
|
|
func TestDEKCache_TTLExpiry(t *testing.T) {
|
|
// Use very short TTL for testing
|
|
config := kms.CacheConfig{
|
|
TTL: 50 * time.Millisecond,
|
|
MaxEntries: 100,
|
|
GraceWindow: 100 * time.Millisecond,
|
|
}
|
|
cache := kms.NewDEKCache(config)
|
|
defer cache.Clear()
|
|
|
|
tenantID := "tenant-1"
|
|
artifactID := "artifact-1"
|
|
dek := []byte("test-dek-data-12345678901234567890123456789012")
|
|
|
|
// Put DEK
|
|
cache.Put(tenantID, artifactID, "kms-key-1", dek)
|
|
|
|
// Should be available immediately
|
|
_, ok := cache.Get(tenantID, artifactID, "kms-key-1", false)
|
|
if !ok {
|
|
t.Error("DEK should be available immediately after put")
|
|
}
|
|
|
|
// Wait for TTL to expire
|
|
time.Sleep(60 * time.Millisecond)
|
|
|
|
// Should not be available after TTL (KMS available)
|
|
_, ok = cache.Get(tenantID, artifactID, "kms-key-1", false)
|
|
if ok {
|
|
t.Error("DEK should not be available after TTL expires")
|
|
}
|
|
|
|
// Should be available in grace window (KMS unavailable)
|
|
_, ok = cache.Get(tenantID, artifactID, "kms-key-1", true)
|
|
if !ok {
|
|
t.Error("DEK should be available in grace window when KMS is unavailable")
|
|
}
|
|
|
|
// Wait for grace window to expire
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Should not be available after grace window
|
|
_, ok = cache.Get(tenantID, artifactID, "kms-key-1", true)
|
|
if ok {
|
|
t.Error("DEK should not be available after grace window expires")
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_LRUeviction tests LRU eviction when cache is full.
|
|
func TestDEKCache_LRUeviction(t *testing.T) {
|
|
config := kms.CacheConfig{
|
|
TTL: 1 * time.Hour, // Long TTL so eviction is due to size
|
|
MaxEntries: 3,
|
|
GraceWindow: 1 * time.Hour,
|
|
}
|
|
cache := kms.NewDEKCache(config)
|
|
defer cache.Clear()
|
|
|
|
// Add 3 entries (at capacity)
|
|
for i := 0; i < 3; i++ {
|
|
dek := []byte("dek-data-12345678901234567890123456789012-" + string(rune('0'+i)))
|
|
cache.Put("tenant-1", string(rune('a'+i)), "kms-key-1", dek)
|
|
}
|
|
|
|
// Access first entry to make it recently used
|
|
cache.Get("tenant-1", "a", "kms-key-1", false)
|
|
|
|
// Add 4th entry (should evict 'b' as it's the oldest unaccessed)
|
|
dek4 := []byte("dek-data-12345678901234567890123456789012-4")
|
|
cache.Put("tenant-1", "d", "kms-key-1", dek4)
|
|
|
|
// 'a' should still exist (was accessed)
|
|
_, ok := cache.Get("tenant-1", "a", "kms-key-1", false)
|
|
if !ok {
|
|
t.Error("Entry 'a' should still exist after eviction")
|
|
}
|
|
|
|
// 'b' should be evicted
|
|
_, ok = cache.Get("tenant-1", "b", "kms-key-1", false)
|
|
if ok {
|
|
t.Error("Entry 'b' should have been evicted")
|
|
}
|
|
|
|
// 'c' and 'd' should exist
|
|
_, ok = cache.Get("tenant-1", "c", "kms-key-1", false)
|
|
if !ok {
|
|
t.Error("Entry 'c' should still exist")
|
|
}
|
|
_, ok = cache.Get("tenant-1", "d", "kms-key-1", false)
|
|
if !ok {
|
|
t.Error("Entry 'd' should exist")
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_Flush tests flushing entries for a specific tenant.
|
|
func TestDEKCache_Flush(t *testing.T) {
|
|
cache := kms.NewDEKCache(kms.DefaultCacheConfig())
|
|
defer cache.Clear()
|
|
|
|
// Add entries for two tenants
|
|
cache.Put("tenant-1", "artifact-1", "kms-key-1", []byte("dek-1"))
|
|
cache.Put("tenant-1", "artifact-2", "kms-key-1", []byte("dek-2"))
|
|
cache.Put("tenant-2", "artifact-1", "kms-key-2", []byte("dek-3"))
|
|
|
|
// Flush tenant-1
|
|
cache.Flush("tenant-1")
|
|
|
|
// tenant-1 entries should be gone
|
|
_, ok := cache.Get("tenant-1", "artifact-1", "kms-key-1", false)
|
|
if ok {
|
|
t.Error("tenant-1 artifact-1 should be flushed")
|
|
}
|
|
_, ok = cache.Get("tenant-1", "artifact-2", "kms-key-1", false)
|
|
if ok {
|
|
t.Error("tenant-1 artifact-2 should be flushed")
|
|
}
|
|
|
|
// tenant-2 entry should still exist
|
|
_, ok = cache.Get("tenant-2", "artifact-1", "kms-key-2", false)
|
|
if !ok {
|
|
t.Error("tenant-2 artifact-1 should still exist")
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_Clear tests clearing all entries.
|
|
func TestDEKCache_Clear(t *testing.T) {
|
|
cache := kms.NewDEKCache(kms.DefaultCacheConfig())
|
|
|
|
// Add entries
|
|
cache.Put("tenant-1", "artifact-1", "kms-key-1", []byte("dek-1"))
|
|
cache.Put("tenant-2", "artifact-1", "kms-key-2", []byte("dek-2"))
|
|
|
|
// Clear
|
|
cache.Clear()
|
|
|
|
// All entries should be gone
|
|
_, ok := cache.Get("tenant-1", "artifact-1", "kms-key-1", false)
|
|
if ok {
|
|
t.Error("All entries should be cleared")
|
|
}
|
|
_, ok = cache.Get("tenant-2", "artifact-1", "kms-key-2", false)
|
|
if ok {
|
|
t.Error("All entries should be cleared")
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_Stats tests cache statistics.
|
|
func TestDEKCache_Stats(t *testing.T) {
|
|
config := kms.DefaultCacheConfig()
|
|
cache := kms.NewDEKCache(config)
|
|
defer cache.Clear()
|
|
|
|
stats := cache.Stats()
|
|
|
|
if stats.Size != 0 {
|
|
t.Errorf("Initial size should be 0, got %d", stats.Size)
|
|
}
|
|
if stats.MaxSize != config.MaxEntries {
|
|
t.Errorf("MaxSize should be %d, got %d", config.MaxEntries, stats.MaxSize)
|
|
}
|
|
if stats.TTL != config.TTL {
|
|
t.Errorf("TTL should be %v, got %v", config.TTL, stats.TTL)
|
|
}
|
|
if stats.GraceWindow != config.GraceWindow {
|
|
t.Errorf("GraceWindow should be %v, got %v", config.GraceWindow, stats.GraceWindow)
|
|
}
|
|
|
|
// Add entry
|
|
cache.Put("tenant-1", "artifact-1", "kms-key-1", []byte("dek-1"))
|
|
|
|
stats = cache.Stats()
|
|
if stats.Size != 1 {
|
|
t.Errorf("Size should be 1 after put, got %d", stats.Size)
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_EmptyDEK tests that empty DEK is rejected.
|
|
func TestDEKCache_EmptyDEK(t *testing.T) {
|
|
cache := kms.NewDEKCache(kms.DefaultCacheConfig())
|
|
defer cache.Clear()
|
|
|
|
err := cache.Put("tenant-1", "artifact-1", "kms-key-1", []byte{})
|
|
if err == nil {
|
|
t.Error("Should reject empty DEK")
|
|
}
|
|
}
|
|
|
|
// TestDEKCache_Isolation tests that DEKs are isolated between tenants.
|
|
func TestDEKCache_Isolation(t *testing.T) {
|
|
cache := kms.NewDEKCache(kms.DefaultCacheConfig())
|
|
defer cache.Clear()
|
|
|
|
// Same artifact ID, different tenants
|
|
cache.Put("tenant-1", "shared-artifact", "kms-key-1", []byte("dek-for-tenant-1"))
|
|
cache.Put("tenant-2", "shared-artifact", "kms-key-2", []byte("dek-for-tenant-2"))
|
|
|
|
// Each tenant should get their own DEK
|
|
d1, ok := cache.Get("tenant-1", "shared-artifact", "kms-key-1", false)
|
|
if !ok || string(d1) != "dek-for-tenant-1" {
|
|
t.Error("tenant-1 should get their own DEK")
|
|
}
|
|
|
|
d2, ok := cache.Get("tenant-2", "shared-artifact", "kms-key-2", false)
|
|
if !ok || string(d2) != "dek-for-tenant-2" {
|
|
t.Error("tenant-2 should get their own DEK")
|
|
}
|
|
}
|