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) }