Files
schemas/cache/cache_test.go
T
argoyle ffcf41b85a feat: add commands for managing organizations and users
Introduce `AddUserToOrganization`, `RemoveAPIKey`, and 
`RemoveOrganization` commands to enhance organization 
management. Implement validation for user addition and 
API key removal. Update GraphQL schema to support new 
mutations and add caching for the new events, ensuring 
that organizations and their relationships are accurately 
represented in the cache.
2025-11-22 18:37:07 +01:00

658 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
orgID := uuid.New().String()
apiKey := "test-concurrent-key" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Concurrent Test Org",
}
c.organizations[orgID] = org
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: hashedKey,
}
// Run concurrent reads (reduced for race detector)
var wg sync.WaitGroup
numGoroutines := 20
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
org := c.OrganizationByAPIKey(apiKey)
assert.NotNil(t, org)
assert.Equal(t, "Concurrent Test Org", org.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
orgID := uuid.New().String()
apiKey := "test-rw-key" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
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: hashedKey,
}
c.users["user-initial"] = []string{orgID}
var wg sync.WaitGroup
numReaders := 10 // Reduced for race detector
numWriters := 5 // Reduced for race detector
iterations := 3 // Reduced for race detector
// Concurrent readers
for i := 0; i < numReaders; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
org := c.OrganizationByAPIKey(apiKey)
assert.NotNil(t, org)
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)
}