2025-11-21 10:21:08 +01:00
|
|
|
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"
|
|
|
|
|
|
2026-01-17 22:53:46 +01:00
|
|
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
|
|
|
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
2025-11-21 10:21:08 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2025-12-08 21:06:02 +01:00
|
|
|
// Setup test data - use legacy hash to avoid slow bcrypt
|
2025-11-21 10:21:08 +01:00
|
|
|
orgID := uuid.New().String()
|
2025-12-08 21:06:02 +01:00
|
|
|
userSub := "test-user"
|
2025-11-21 10:21:08 +01:00
|
|
|
|
|
|
|
|
org := domain.Organization{
|
|
|
|
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
|
|
|
|
Name: "Concurrent Test Org",
|
|
|
|
|
}
|
|
|
|
|
c.organizations[orgID] = org
|
2025-12-08 21:06:02 +01:00
|
|
|
c.users[userSub] = []string{orgID}
|
2025-11-21 10:21:08 +01:00
|
|
|
|
2025-12-08 21:06:02 +01:00
|
|
|
// Run concurrent reads using fast OrganizationsByUser
|
2025-11-21 10:21:08 +01:00
|
|
|
var wg sync.WaitGroup
|
2025-11-21 11:06:36 +01:00
|
|
|
numGoroutines := 20
|
2025-11-21 10:21:08 +01:00
|
|
|
|
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
2025-12-08 21:06:02 +01:00
|
|
|
orgs := c.OrganizationsByUser(userSub)
|
|
|
|
|
assert.NotEmpty(t, orgs)
|
|
|
|
|
assert.Equal(t, "Concurrent Test Org", orgs[0].Name)
|
2025-11-21 10:21:08 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCache_ConcurrentWrites(t *testing.T) {
|
|
|
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
|
|
|
c := New(logger)
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
2025-11-21 11:06:36 +01:00
|
|
|
numGoroutines := 10 // Reduced for race detector
|
2025-11-21 10:21:08 +01:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
2025-12-05 08:47:11 +01:00
|
|
|
// Setup initial data - use legacy hash to avoid slow bcrypt in concurrent test
|
2025-11-21 10:21:08 +01:00
|
|
|
orgID := uuid.New().String()
|
2025-12-05 08:47:11 +01:00
|
|
|
legacyKey := "test-rw-key" // gitleaks:allow
|
|
|
|
|
legacyHash := hash.String(legacyKey)
|
2025-11-21 10:21:08 +01:00
|
|
|
|
|
|
|
|
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,
|
2025-12-05 08:47:11 +01:00
|
|
|
Key: legacyHash,
|
2025-11-21 10:21:08 +01:00
|
|
|
}
|
|
|
|
|
c.users["user-initial"] = []string{orgID}
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
2025-12-05 08:47:11 +01:00
|
|
|
numReaders := 5
|
|
|
|
|
numWriters := 3
|
2025-11-21 10:21:08 +01:00
|
|
|
|
2025-12-05 08:47:11 +01:00
|
|
|
// Concurrent readers - use OrganizationsByUser which is fast
|
2025-11-21 10:21:08 +01:00
|
|
|
for i := 0; i < numReaders; i++ {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
2025-12-05 08:47:11 +01:00
|
|
|
orgs := c.OrganizationsByUser("user-initial")
|
|
|
|
|
assert.NotEmpty(t, orgs)
|
2025-11-21 10:21:08 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2025-11-22 18:37:07 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|