Files
schemas/cache/cache_test.go
T
argoyle 129cd8aad1 test(cache): update tests to use legacy hash for speed
Update test setup to leverage legacy hash for API keys in order  
to avoid the performance impact of bcrypt. Replace the API key  
based test with a user subscription-based test to enhance  
concurrency and reliability. Replace `OrganizationByAPIKey`  
with `OrganizationsByUser` to optimize the retrieval process.
2025-12-08 21:06:02 +01:00

646 lines
17 KiB
Go

package cache
import (
"log/slog"
"os"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/hash"
)
func TestCache_OrganizationByAPIKey(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
apiKey := "test-api-key-123" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
// Add organization to cache
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
}
c.organizations[orgID] = org
// Add API key to cache
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: hashedKey,
Refs: []string{"main"},
Read: true,
Publish: true,
}
// Test finding organization by plaintext API key
foundOrg := c.OrganizationByAPIKey(apiKey)
require.NotNil(t, foundOrg)
assert.Equal(t, org.Name, foundOrg.Name)
assert.Equal(t, orgID, foundOrg.ID.String())
// Test with wrong API key
notFoundOrg := c.OrganizationByAPIKey("wrong-key")
assert.Nil(t, notFoundOrg)
}
func TestCache_OrganizationByAPIKey_Legacy(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
apiKey := "legacy-api-key-456" // gitleaks:allow
legacyHash := hash.String(apiKey)
// Add organization to cache
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Legacy Org",
}
c.organizations[orgID] = org
// Add API key with legacy SHA256 hash
c.apiKeys[apiKeyId(orgID, "legacy-key")] = domain.APIKey{
Name: "legacy-key",
OrganizationId: orgID,
Key: legacyHash,
Refs: []string{"main"},
Read: true,
Publish: false,
}
// Test finding organization with legacy hash
foundOrg := c.OrganizationByAPIKey(apiKey)
require.NotNil(t, foundOrg)
assert.Equal(t, org.Name, foundOrg.Name)
}
func TestCache_OrganizationsByUser(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
userSub := "user-123"
org1ID := uuid.New().String()
org2ID := uuid.New().String()
org1 := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(org1ID),
Name: "Org 1",
}
org2 := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(org2ID),
Name: "Org 2",
}
c.organizations[org1ID] = org1
c.organizations[org2ID] = org2
c.users[userSub] = []string{org1ID, org2ID}
orgs := c.OrganizationsByUser(userSub)
assert.Len(t, orgs, 2)
assert.Contains(t, []string{orgs[0].Name, orgs[1].Name}, "Org 1")
assert.Contains(t, []string{orgs[0].Name, orgs[1].Name}, "Org 2")
}
func TestCache_ApiKeyByKey(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
apiKey := "test-api-key-789" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
expectedKey := domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: hashedKey,
Refs: []string{"main", "dev"},
Read: true,
Publish: true,
}
c.apiKeys[apiKeyId(orgID, "test-key")] = expectedKey
foundKey := c.ApiKeyByKey(apiKey)
require.NotNil(t, foundKey)
assert.Equal(t, expectedKey.Name, foundKey.Name)
assert.Equal(t, expectedKey.OrganizationId, foundKey.OrganizationId)
assert.Equal(t, expectedKey.Refs, foundKey.Refs)
// Test with wrong key
notFoundKey := c.ApiKeyByKey("wrong-key")
assert.Nil(t, notFoundKey)
}
func TestCache_Services(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
ref := "main"
service1 := "service-1"
service2 := "service-2"
lastUpdate := "2024-01-01T12:00:00Z"
c.services[orgID] = map[string]map[string]struct{}{
ref: {
service1: {},
service2: {},
},
}
c.lastUpdate[refKey(orgID, ref)] = lastUpdate
// Test getting services with empty lastUpdate
services, returnedLastUpdate := c.Services(orgID, ref, "")
assert.Len(t, services, 2)
assert.Contains(t, services, service1)
assert.Contains(t, services, service2)
assert.Equal(t, lastUpdate, returnedLastUpdate)
// Test with older lastUpdate (should return services)
services, returnedLastUpdate = c.Services(orgID, ref, "2023-12-31T12:00:00Z")
assert.Len(t, services, 2)
assert.Equal(t, lastUpdate, returnedLastUpdate)
// Test with newer lastUpdate (should return empty)
services, returnedLastUpdate = c.Services(orgID, ref, "2024-01-02T12:00:00Z")
assert.Len(t, services, 0)
assert.Equal(t, lastUpdate, returnedLastUpdate)
}
func TestCache_SubGraphId(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
ref := "main"
service := "test-service"
subGraphID := uuid.New().String()
c.subGraphs[subGraphKey(orgID, ref, service)] = subGraphID
foundID := c.SubGraphId(orgID, ref, service)
assert.Equal(t, subGraphID, foundID)
// Test with non-existent key
notFoundID := c.SubGraphId("wrong-org", ref, service)
assert.Empty(t, notFoundID)
}
func TestCache_Update_OrganizationAdded(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
event := &domain.OrganizationAdded{
Name: "New Org",
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify organization was added
org, exists := c.organizations[orgID]
assert.True(t, exists)
assert.Equal(t, "New Org", org.Name)
// Verify user was added
assert.Contains(t, c.users["user-123"], orgID)
}
func TestCache_Update_APIKeyAdded(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
keyName := "test-key"
hashedKey := "hashed-key-value"
// Add organization first
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
APIKeys: []domain.APIKey{},
}
c.organizations[orgID] = org
event := &domain.APIKeyAdded{
OrganizationId: orgID,
Name: keyName,
Key: hashedKey,
Refs: []string{"main"},
Read: true,
Publish: false,
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(uuid.New().String())
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify API key was added to cache
key, exists := c.apiKeys[apiKeyId(orgID, keyName)]
assert.True(t, exists)
assert.Equal(t, keyName, key.Name)
assert.Equal(t, hashedKey, key.Key)
assert.Equal(t, []string{"main"}, key.Refs)
// Verify API key was added to organization
updatedOrg := c.organizations[orgID]
assert.Len(t, updatedOrg.APIKeys, 1)
assert.Equal(t, keyName, updatedOrg.APIKeys[0].Name)
}
func TestCache_Update_SubGraphUpdated(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
ref := "main"
service := "test-service"
subGraphID := uuid.New().String()
event := &domain.SubGraphUpdated{
OrganizationId: orgID,
Ref: ref,
Service: service,
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(subGraphID)
event.SetWhen(time.Now())
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify subgraph was added to services
assert.Contains(t, c.services[orgID][ref], subGraphID)
// Verify subgraph ID was stored
assert.Equal(t, subGraphID, c.subGraphs[subGraphKey(orgID, ref, service)])
// Verify lastUpdate was set
assert.NotEmpty(t, c.lastUpdate[refKey(orgID, ref)])
}
func TestCache_AddUser_NoDuplicates(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
userSub := "user-123"
orgID := uuid.New().String()
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
}
// Add user first time
c.addUser(userSub, org)
assert.Len(t, c.users[userSub], 1)
assert.Equal(t, orgID, c.users[userSub][0])
// Add same user/org again - should not create duplicate
c.addUser(userSub, org)
assert.Len(t, c.users[userSub], 1, "Should not add duplicate organization")
assert.Equal(t, orgID, c.users[userSub][0])
}
func TestCache_ConcurrentReads(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
// Setup test data - use legacy hash to avoid slow bcrypt
orgID := uuid.New().String()
userSub := "test-user"
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Concurrent Test Org",
}
c.organizations[orgID] = org
c.users[userSub] = []string{orgID}
// Run concurrent reads using fast OrganizationsByUser
var wg sync.WaitGroup
numGoroutines := 20
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
orgs := c.OrganizationsByUser(userSub)
assert.NotEmpty(t, orgs)
assert.Equal(t, "Concurrent Test Org", orgs[0].Name)
}()
}
wg.Wait()
}
func TestCache_ConcurrentWrites(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
var wg sync.WaitGroup
numGoroutines := 10 // Reduced for race detector
// Concurrent organization additions
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
orgID := uuid.New().String()
event := &domain.OrganizationAdded{
Name: "Org " + string(rune(index)),
Initiator: "user-" + string(rune(index)),
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
assert.NoError(t, err)
}(i)
}
wg.Wait()
// Verify all organizations were added
assert.Equal(t, numGoroutines, len(c.organizations))
}
func TestCache_ConcurrentReadsAndWrites(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
// Setup initial data - use legacy hash to avoid slow bcrypt in concurrent test
orgID := uuid.New().String()
legacyKey := "test-rw-key" // gitleaks:allow
legacyHash := hash.String(legacyKey)
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "RW Test Org",
}
c.organizations[orgID] = org
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: legacyHash,
}
c.users["user-initial"] = []string{orgID}
var wg sync.WaitGroup
numReaders := 5
numWriters := 3
// Concurrent readers - use OrganizationsByUser which is fast
for i := 0; i < numReaders; i++ {
wg.Add(1)
go func() {
defer wg.Done()
orgs := c.OrganizationsByUser("user-initial")
assert.NotEmpty(t, orgs)
}()
}
// Concurrent writers
for i := 0; i < numWriters; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
newOrgID := uuid.New().String()
event := &domain.OrganizationAdded{
Name: "New Org " + string(rune(index)),
Initiator: "user-new-" + string(rune(index)),
}
event.ID = *eventsourced.IdFromString(newOrgID)
_, err := c.Update(event, nil)
assert.NoError(t, err)
}(i)
}
wg.Wait()
// Verify cache is in consistent state
assert.GreaterOrEqual(t, len(c.organizations), numWriters)
}
func TestCache_Update_APIKeyRemoved(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
keyName := "test-key"
hashedKey := "hashed-key-value"
// Add organization with API key
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
APIKeys: []domain.APIKey{
{
Name: keyName,
OrganizationId: orgID,
Key: hashedKey,
Refs: []string{"main"},
Read: true,
Publish: false,
},
},
}
c.organizations[orgID] = org
c.apiKeys[apiKeyId(orgID, keyName)] = org.APIKeys[0]
// Verify key exists before removal
_, exists := c.apiKeys[apiKeyId(orgID, keyName)]
assert.True(t, exists)
// Remove the API key
event := &domain.APIKeyRemoved{
KeyName: keyName,
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify API key was removed from cache
_, exists = c.apiKeys[apiKeyId(orgID, keyName)]
assert.False(t, exists, "API key should be removed from cache")
// Verify API key was removed from organization
updatedOrg := c.organizations[orgID]
assert.Len(t, updatedOrg.APIKeys, 0, "API key should be removed from organization")
}
func TestCache_Update_APIKeyRemoved_MultipleKeys(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
// Add organization with multiple API keys
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
APIKeys: []domain.APIKey{
{
Name: "key1",
OrganizationId: orgID,
Key: "hash1",
},
{
Name: "key2",
OrganizationId: orgID,
Key: "hash2",
},
{
Name: "key3",
OrganizationId: orgID,
Key: "hash3",
},
},
}
c.organizations[orgID] = org
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
c.apiKeys[apiKeyId(orgID, "key2")] = org.APIKeys[1]
c.apiKeys[apiKeyId(orgID, "key3")] = org.APIKeys[2]
// Remove the middle key
event := &domain.APIKeyRemoved{
KeyName: "key2",
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify only key2 was removed
_, exists := c.apiKeys[apiKeyId(orgID, "key1")]
assert.True(t, exists, "key1 should still exist")
_, exists = c.apiKeys[apiKeyId(orgID, "key2")]
assert.False(t, exists, "key2 should be removed")
_, exists = c.apiKeys[apiKeyId(orgID, "key3")]
assert.True(t, exists, "key3 should still exist")
// Verify organization has 2 keys remaining
updatedOrg := c.organizations[orgID]
assert.Len(t, updatedOrg.APIKeys, 2)
assert.Equal(t, "key1", updatedOrg.APIKeys[0].Name)
assert.Equal(t, "key3", updatedOrg.APIKeys[1].Name)
}
func TestCache_Update_OrganizationRemoved(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
userSub := "user-123"
// Add organization with API keys, users, and subgraphs
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
APIKeys: []domain.APIKey{
{
Name: "key1",
OrganizationId: orgID,
Key: "hash1",
},
},
}
c.organizations[orgID] = org
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
c.users[userSub] = []string{orgID}
c.services[orgID] = map[string]map[string]struct{}{
"main": {
"service1": {},
},
}
c.subGraphs[subGraphKey(orgID, "main", "service1")] = "subgraph-id"
c.lastUpdate[refKey(orgID, "main")] = "2024-01-01T12:00:00Z"
// Remove the organization
event := &domain.OrganizationRemoved{
Initiator: userSub,
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify organization was removed
_, exists := c.organizations[orgID]
assert.False(t, exists, "Organization should be removed from cache")
// Verify API keys were removed
_, exists = c.apiKeys[apiKeyId(orgID, "key1")]
assert.False(t, exists, "API keys should be removed from cache")
// Verify user association was removed
userOrgs := c.users[userSub]
assert.NotContains(t, userOrgs, orgID, "User should not be associated with removed organization")
// Verify services were removed
_, exists = c.services[orgID]
assert.False(t, exists, "Services should be removed from cache")
// Verify subgraphs were removed
_, exists = c.subGraphs[subGraphKey(orgID, "main", "service1")]
assert.False(t, exists, "Subgraphs should be removed from cache")
// Verify lastUpdate was removed
_, exists = c.lastUpdate[refKey(orgID, "main")]
assert.False(t, exists, "LastUpdate should be removed from cache")
}
func TestCache_Update_OrganizationRemoved_MultipleUsers(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
user1 := "user-1"
user2 := "user-2"
otherOrgID := uuid.New().String()
// Add organization
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
}
c.organizations[orgID] = org
// Add users with multiple org associations
c.users[user1] = []string{orgID, otherOrgID}
c.users[user2] = []string{orgID}
// Remove the organization
event := &domain.OrganizationRemoved{
Initiator: user1,
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify user1 still has otherOrgID but not removed orgID
assert.Len(t, c.users[user1], 1)
assert.Equal(t, otherOrgID, c.users[user1][0])
// Verify user2 has no organizations
assert.Len(t, c.users[user2], 0)
}