ffcf41b85a
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.
578 lines
15 KiB
Go
578 lines
15 KiB
Go
package domain
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
|
|
|
"gitlab.com/unboundsoftware/schemas/hash"
|
|
)
|
|
|
|
// AddOrganization tests
|
|
|
|
func TestAddOrganization_Validate_Success(t *testing.T) {
|
|
cmd := AddOrganization{
|
|
Name: "Test Org",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{} // New organization with no identity
|
|
err := cmd.Validate(context.Background(), org)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestAddOrganization_Validate_AlreadyExists(t *testing.T) {
|
|
cmd := AddOrganization{
|
|
Name: "Test Org",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("existing-org-id"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already exists")
|
|
}
|
|
|
|
func TestAddOrganization_Validate_EmptyName(t *testing.T) {
|
|
cmd := AddOrganization{
|
|
Name: "",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{}
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "name is required")
|
|
}
|
|
|
|
func TestAddOrganization_Event(t *testing.T) {
|
|
cmd := AddOrganization{
|
|
Name: "Test Org",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
event := cmd.Event(context.Background())
|
|
require.NotNil(t, event)
|
|
|
|
orgEvent, ok := event.(*OrganizationAdded)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "Test Org", orgEvent.Name)
|
|
assert.Equal(t, "user@example.com", orgEvent.Initiator)
|
|
}
|
|
|
|
// AddAPIKey tests
|
|
|
|
func TestAddAPIKey_Event(t *testing.T) {
|
|
type fields struct {
|
|
Name string
|
|
Key string
|
|
Refs []string
|
|
Read bool
|
|
Publish bool
|
|
Initiator string
|
|
}
|
|
type args struct {
|
|
in0 context.Context
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
}{
|
|
{
|
|
name: "event",
|
|
fields: fields{
|
|
Name: "test",
|
|
Key: "us_ak_1234567890123456",
|
|
Refs: []string{"Example@dev"},
|
|
Read: true,
|
|
Publish: true,
|
|
Initiator: "jim@example.org",
|
|
},
|
|
args: args{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
a := AddAPIKey{
|
|
Name: tt.fields.Name,
|
|
Key: tt.fields.Key,
|
|
Refs: tt.fields.Refs,
|
|
Read: tt.fields.Read,
|
|
Publish: tt.fields.Publish,
|
|
Initiator: tt.fields.Initiator,
|
|
}
|
|
event := a.Event(tt.args.in0)
|
|
require.NotNil(t, event)
|
|
|
|
// Cast to APIKeyAdded to verify fields
|
|
apiKeyEvent, ok := event.(*APIKeyAdded)
|
|
require.True(t, ok, "Event should be *APIKeyAdded")
|
|
|
|
// Verify non-key fields match exactly
|
|
assert.Equal(t, tt.fields.Name, apiKeyEvent.Name)
|
|
assert.Equal(t, tt.fields.Refs, apiKeyEvent.Refs)
|
|
assert.Equal(t, tt.fields.Read, apiKeyEvent.Read)
|
|
assert.Equal(t, tt.fields.Publish, apiKeyEvent.Publish)
|
|
assert.Equal(t, tt.fields.Initiator, apiKeyEvent.Initiator)
|
|
|
|
// Verify the key is hashed correctly (bcrypt format)
|
|
assert.True(t, strings.HasPrefix(apiKeyEvent.Key, "$2"), "Key should be bcrypt hashed")
|
|
assert.NotEqual(t, tt.fields.Key, apiKeyEvent.Key, "Key should be hashed, not plaintext")
|
|
|
|
// Verify the hash matches the original key
|
|
assert.True(t, hash.CompareAPIKey(apiKeyEvent.Key, tt.fields.Key), "Hashed key should match original")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddAPIKey_Validate_Success(t *testing.T) {
|
|
cmd := AddAPIKey{
|
|
Name: "production-key",
|
|
Key: "us_ak_1234567890123456",
|
|
Refs: []string{"main"},
|
|
Read: true,
|
|
Publish: false,
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
|
APIKeys: []APIKey{},
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), org)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestAddAPIKey_Validate_OrganizationNotExists(t *testing.T) {
|
|
cmd := AddAPIKey{
|
|
Name: "production-key",
|
|
Key: "us_ak_1234567890123456",
|
|
Refs: []string{"main"},
|
|
Read: true,
|
|
Publish: false,
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{} // No identity means it doesn't exist
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "does not exist")
|
|
}
|
|
|
|
func TestAddAPIKey_Validate_DuplicateKeyName(t *testing.T) {
|
|
cmd := AddAPIKey{
|
|
Name: "existing-key",
|
|
Key: "us_ak_1234567890123456",
|
|
Refs: []string{"main"},
|
|
Read: true,
|
|
Publish: false,
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
|
APIKeys: []APIKey{
|
|
{
|
|
Name: "existing-key",
|
|
Key: "hashed-key",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already exist")
|
|
assert.Contains(t, err.Error(), "existing-key")
|
|
}
|
|
|
|
// UpdateSubGraph tests
|
|
|
|
func TestUpdateSubGraph_Validate_Success(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: &url,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_MissingRef(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "",
|
|
Service: "users",
|
|
Url: &url,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "ref is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_RefWhitespaceOnly(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: " ",
|
|
Service: "users",
|
|
Url: &url,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "ref is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_MissingService(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "",
|
|
Url: &url,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "service is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_ServiceWhitespaceOnly(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: " ",
|
|
Url: &url,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "service is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_MissingSDL(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: &url,
|
|
Sdl: "",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "SDL is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_SDLWhitespaceOnly(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: &url,
|
|
Sdl: " \n\t ",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "SDL is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_MissingURL_NoExistingURL(t *testing.T) {
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: nil,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
Url: nil, // No existing URL
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "url is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_MissingURL_HasExistingURL(t *testing.T) {
|
|
existingURL := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: nil,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
Url: &existingURL, // Has existing URL, so nil is OK
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_EmptyURL_NoExistingURL(t *testing.T) {
|
|
emptyURL := ""
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: &emptyURL,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
Url: nil,
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "url is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_URLWhitespaceOnly_NoExistingURL(t *testing.T) {
|
|
whitespaceURL := " "
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: &whitespaceURL,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
subGraph := &SubGraph{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
|
Url: nil,
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), subGraph)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "url is missing")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Validate_WrongAggregateType(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: &url,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
// Pass wrong aggregate type
|
|
org := &Organization{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not a SubGraph")
|
|
}
|
|
|
|
func TestUpdateSubGraph_Event(t *testing.T) {
|
|
url := "http://example.com/graphql"
|
|
wsURL := "ws://example.com/graphql"
|
|
cmd := UpdateSubGraph{
|
|
OrganizationId: "org-123",
|
|
Ref: "main",
|
|
Service: "users",
|
|
Url: &url,
|
|
WSUrl: &wsURL,
|
|
Sdl: "type Query { hello: String }",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
event := cmd.Event(context.Background())
|
|
require.NotNil(t, event)
|
|
|
|
subGraphEvent, ok := event.(*SubGraphUpdated)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "org-123", subGraphEvent.OrganizationId)
|
|
assert.Equal(t, "main", subGraphEvent.Ref)
|
|
assert.Equal(t, "users", subGraphEvent.Service)
|
|
assert.Equal(t, url, *subGraphEvent.Url)
|
|
assert.Equal(t, wsURL, *subGraphEvent.WSUrl)
|
|
assert.Equal(t, "type Query { hello: String }", subGraphEvent.Sdl)
|
|
assert.Equal(t, "user@example.com", subGraphEvent.Initiator)
|
|
}
|
|
|
|
// RemoveAPIKey tests
|
|
|
|
func TestRemoveAPIKey_Validate_Success(t *testing.T) {
|
|
cmd := RemoveAPIKey{
|
|
KeyName: "production-key",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
|
APIKeys: []APIKey{
|
|
{
|
|
Name: "production-key",
|
|
Key: "hashed-key",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), org)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestRemoveAPIKey_Validate_OrganizationNotExists(t *testing.T) {
|
|
cmd := RemoveAPIKey{
|
|
KeyName: "production-key",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{} // No identity means it doesn't exist
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "does not exist")
|
|
}
|
|
|
|
func TestRemoveAPIKey_Validate_KeyNotFound(t *testing.T) {
|
|
cmd := RemoveAPIKey{
|
|
KeyName: "non-existent-key",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
|
APIKeys: []APIKey{
|
|
{
|
|
Name: "production-key",
|
|
Key: "hashed-key",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
assert.Contains(t, err.Error(), "non-existent-key")
|
|
}
|
|
|
|
func TestRemoveAPIKey_Event(t *testing.T) {
|
|
cmd := RemoveAPIKey{
|
|
KeyName: "production-key",
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
event := cmd.Event(context.Background())
|
|
require.NotNil(t, event)
|
|
|
|
keyEvent, ok := event.(*APIKeyRemoved)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "production-key", keyEvent.KeyName)
|
|
assert.Equal(t, "user@example.com", keyEvent.Initiator)
|
|
}
|
|
|
|
// RemoveOrganization tests
|
|
|
|
func TestRemoveOrganization_Validate_Success(t *testing.T) {
|
|
cmd := RemoveOrganization{
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{
|
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
|
Name: "Test Org",
|
|
}
|
|
|
|
err := cmd.Validate(context.Background(), org)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestRemoveOrganization_Validate_OrganizationNotExists(t *testing.T) {
|
|
cmd := RemoveOrganization{
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
org := &Organization{} // No identity means it doesn't exist
|
|
err := cmd.Validate(context.Background(), org)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "does not exist")
|
|
}
|
|
|
|
func TestRemoveOrganization_Event(t *testing.T) {
|
|
cmd := RemoveOrganization{
|
|
Initiator: "user@example.com",
|
|
}
|
|
|
|
event := cmd.Event(context.Background())
|
|
require.NotNil(t, event)
|
|
|
|
orgEvent, ok := event.(*OrganizationRemoved)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "user@example.com", orgEvent.Initiator)
|
|
}
|