From 862060875bdfbff19f6839baae3cc61c9a7a9f0a Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Fri, 21 Nov 2025 12:49:23 +0100 Subject: [PATCH] test: add validation and event tests for organization and API key Adds unit tests for the `AddOrganization` and `AddAPIKey` commands. These tests validate various scenarios, including success cases, handling of already existing organizations or keys, and ensuring required fields are checked. The changes enhance test coverage and ensure robustness of the command logic. --- .gitignore | 1 + domain/commands_test.go | 390 ++++++++++++++++++++++++++++++++++ sdlmerge/sdlmerge_test.go | 434 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 825 insertions(+) create mode 100644 sdlmerge/sdlmerge_test.go diff --git a/.gitignore b/.gitignore index b20db27..8cffe7a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .testCoverage.txt .testCoverage.txt.tmp coverage.html +coverage.out /exported /release /schemactl diff --git a/domain/commands_test.go b/domain/commands_test.go index aec95ac..4fa063e 100644 --- a/domain/commands_test.go +++ b/domain/commands_test.go @@ -7,10 +7,68 @@ import ( "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 @@ -74,3 +132,335 @@ func TestAddAPIKey_Event(t *testing.T) { }) } } + +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) +} diff --git a/sdlmerge/sdlmerge_test.go b/sdlmerge/sdlmerge_test.go new file mode 100644 index 0000000..1d74584 --- /dev/null +++ b/sdlmerge/sdlmerge_test.go @@ -0,0 +1,434 @@ +package sdlmerge + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMergeSDLs_Success(t *testing.T) { + // Both types need to be in the same subgraph or properly federated + sdl1 := ` + type User { + id: ID! + name: String! + } + + type Post { + id: ID! + title: String! + } + ` + + result, err := MergeSDLs(sdl1) + require.NoError(t, err) + assert.Contains(t, result, "User") + assert.Contains(t, result, "Post") + assert.Contains(t, result, "id") + assert.Contains(t, result, "name") + assert.Contains(t, result, "title") +} + +func TestMergeSDLs_SingleSchema(t *testing.T) { + sdl := ` + type Query { + hello: String + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "Query") + assert.Contains(t, result, "hello") +} + +func TestMergeSDLs_EmptySchemas(t *testing.T) { + result, err := MergeSDLs() + require.NoError(t, err) + // With no schemas, result will be empty after processing + // This is valid - just verifies no crash + _ = result +} + +func TestMergeSDLs_InvalidSyntax(t *testing.T) { + invalidSDL := ` + type User { + id: ID! + name: String! + // Missing closing brace + ` + + _, err := MergeSDLs(invalidSDL) + require.Error(t, err) + assert.Contains(t, err.Error(), "parse graphql document string") +} + +func TestMergeSDLs_UnknownType(t *testing.T) { + sdl := ` + type User { + id: ID! + profile: UnknownType + } + ` + + _, err := MergeSDLs(sdl) + require.Error(t, err) + assert.Contains(t, err.Error(), "validate schema") +} + +func TestMergeSDLs_DuplicateTypes_DifferentFields(t *testing.T) { + // Same type with different fields in different subgraphs - should fail + // In federation, shared types must be identical + sdl1 := ` + type User { + id: ID! + } + ` + sdl2 := ` + type User { + name: String! + } + ` + + _, err := MergeSDLs(sdl1, sdl2) + require.Error(t, err) + assert.Contains(t, err.Error(), "shared type") +} + +func TestMergeSDLs_ExtendType(t *testing.T) { + sdl1 := ` + type User { + id: ID! + } + ` + sdl2 := ` + extend type User { + email: String! + } + ` + + result, err := MergeSDLs(sdl1, sdl2) + require.NoError(t, err) + assert.Contains(t, result, "User") + assert.Contains(t, result, "id") + assert.Contains(t, result, "email") +} + +func TestMergeSDLs_Scalars(t *testing.T) { + sdl := ` + scalar DateTime + + type Event { + id: ID! + createdAt: DateTime! + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "DateTime") + assert.Contains(t, result, "Event") +} + +func TestMergeSDLs_Enums(t *testing.T) { + sdl := ` + enum Role { + ADMIN + USER + GUEST + } + + type User { + id: ID! + role: Role! + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "Role") + assert.Contains(t, result, "ADMIN") + assert.Contains(t, result, "USER") +} + +func TestMergeSDLs_Interfaces(t *testing.T) { + sdl := ` + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String! + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "Node") + assert.Contains(t, result, "implements") +} + +func TestMergeSDLs_Unions(t *testing.T) { + sdl := ` + type User { + id: ID! + name: String! + } + + type Bot { + id: ID! + version: String! + } + + union Actor = User | Bot + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "Actor") + assert.Contains(t, result, "User") + assert.Contains(t, result, "Bot") +} + +func TestMergeSDLs_InputTypes(t *testing.T) { + sdl := ` + input CreateUserInput { + name: String! + email: String! + } + + type Mutation { + createUser(input: CreateUserInput!): User + } + + type User { + id: ID! + name: String! + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "CreateUserInput") + assert.Contains(t, result, "createUser") +} + +func TestMergeSDLs_Directives(t *testing.T) { + sdl := ` + type User { + id: ID! + name: String! @deprecated(reason: "Use fullName instead") + fullName: String! + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "User") + assert.Contains(t, result, "name") + assert.Contains(t, result, "fullName") +} + +func TestMergeSDLs_FederationKeys(t *testing.T) { + // Federation @key directive + sdl := ` + type User @key(fields: "id") { + id: ID! + name: String! + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "User") + // @key directive should be removed during merge + assert.NotContains(t, result, "@key") +} + +func TestMergeSDLs_ExternalFields(t *testing.T) { + // Federation @external directive + sdl1 := ` + type User @key(fields: "id") { + id: ID! + name: String! + } + ` + sdl2 := ` + extend type User @key(fields: "id") { + id: ID! @external + posts: [Post!]! + } + + type Post { + id: ID! + title: String! + } + ` + + result, err := MergeSDLs(sdl1, sdl2) + require.NoError(t, err) + assert.Contains(t, result, "User") + assert.Contains(t, result, "Post") + // @external fields should be removed + assert.NotContains(t, result, "@external") +} + +func TestMergeSDLs_ComplexSchema(t *testing.T) { + // Multiple subgraphs with various types - simplified to avoid cross-references + users := ` + type User @key(fields: "id") { + id: ID! + username: String! + email: String! + } + + type Query { + user(id: ID!): User + users: [User!]! + } + ` + + posts := ` + extend type User @key(fields: "id") { + id: ID! @external + posts: [Post!]! + } + + type Post @key(fields: "id") { + id: ID! + title: String! + content: String! + } + + extend type Query { + post(id: ID!): Post + posts: [Post!]! + } + ` + + comments := ` + extend type Post @key(fields: "id") { + id: ID! @external + comments: [Comment!]! + } + + type Comment { + id: ID! + text: String! + } + + extend type Query { + comment(id: ID!): Comment + } + ` + + result, err := MergeSDLs(users, posts, comments) + require.NoError(t, err) + + // Verify all types are present + assert.Contains(t, result, "User") + assert.Contains(t, result, "Post") + assert.Contains(t, result, "Comment") + assert.Contains(t, result, "Query") + + // Verify fields from all subgraphs + assert.Contains(t, result, "username") + assert.Contains(t, result, "posts") + assert.Contains(t, result, "comments") +} + +func TestMergeSDLs_EmptyTypeDefinition(t *testing.T) { + sdl := ` + type Empty {} + ` + + _, err := MergeSDLs(sdl) + require.Error(t, err) + // Empty types are invalid in GraphQL + assert.Contains(t, err.Error(), "empty body") +} + +func TestMergeSDLs_MultipleValidationErrors(t *testing.T) { + // Schema with multiple errors + sdl := ` + type User { + id: ID! + profile: NonExistentType1 + settings: NonExistentType2 + } + ` + + _, err := MergeSDLs(sdl) + require.Error(t, err) +} + +func TestMergeSDLs_ListTypes(t *testing.T) { + sdl := ` + type User { + id: ID! + tags: [String!]! + friends: [User!] + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "User") + assert.Contains(t, result, "tags") + assert.Contains(t, result, "friends") +} + +func TestMergeSDLs_NonNullTypes(t *testing.T) { + sdl := ` + type User { + id: ID! + name: String! + email: String + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "User") + assert.Contains(t, result, "id") + assert.Contains(t, result, "name") + assert.Contains(t, result, "email") +} + +func TestMergeSDLs_Comments(t *testing.T) { + sdl := ` + # This is a user type + type User { + # User ID + id: ID! + # User name + name: String! + } + ` + + result, err := MergeSDLs(sdl) + require.NoError(t, err) + assert.Contains(t, result, "User") +} + +func TestMergeSDLs_LargeSchema(t *testing.T) { + // Test with a reasonably large schema to ensure performance + var sdlBuilder strings.Builder + for i := 0; i < 50; i++ { + sdlBuilder.WriteString("type Type") + sdlBuilder.WriteString(strings.Repeat(string(rune('A'+i%26)), 1)) + sdlBuilder.WriteString(string(rune('0' + i/26))) + sdlBuilder.WriteString(" { id: ID }\n") + } + + result, err := MergeSDLs(sdlBuilder.String()) + require.NoError(t, err) + + // Verify some types are present + assert.Contains(t, result, "TypeA0") + assert.Contains(t, result, "TypeB0") + assert.Contains(t, result, "TypeC0") +}