From e52ac62b0a4f671620b2f674eab0b5bb5ed43c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Artturi=20Sipil=C3=A4?= Date: Fri, 25 Oct 2024 12:53:35 +0300 Subject: [PATCH] Refactor to using modular checks per resource type --- .github/workflows/codeql.yml | 81 +++----------- checks.go | 195 +++++++++++++++++++++++++++++++++ checks_test.go | 207 +++++++++++++++++++++++++++++++++++ go.sum | 1 + main.go | 63 +++++++---- topicvalidator.go | 126 --------------------- topicvalidator_test.go | 123 --------------------- 7 files changed, 459 insertions(+), 337 deletions(-) create mode 100644 checks.go create mode 100644 checks_test.go delete mode 100644 topicvalidator.go delete mode 100644 topicvalidator_test.go diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 25a4332..2c683dd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,8 +1,7 @@ -name: "CodeQL Advanced" +name: CodeQL on: push: - branches: [ "main" ] pull_request: types: - opened @@ -10,73 +9,25 @@ on: - reopened - labeled - unlabeled - schedule: - - cron: '20 19 * * 6' jobs: - analyze: - name: Analyze (${{ matrix.language }}) + codeql: + name: Run codeql runs-on: ubuntu-latest - permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read - strategy: fail-fast: false matrix: - include: - - language: go - build-mode: autobuild - # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + language: + - go steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: github/codeql-action/init@v3 + with: + languages: '${{ matrix.language }}' + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 diff --git a/checks.go b/checks.go new file mode 100644 index 0000000..00f4f22 --- /dev/null +++ b/checks.go @@ -0,0 +1,195 @@ +package main + +import ( + "slices" +) + +type CheckResult struct { + ok bool + errors []ResultError +} + +func changeIsRequestedByOwner( + resourceChange ResourceChange, + requester *StateResource, + _ []*StateResource, + plan *Plan, +) CheckResult { + checkResult := CheckResult{ok: true, errors: []ResultError{}} + + // If the owner is defined but it's a new group it's in the state post-apply so we have to use config to check it + if resourceChange.Change.AfterUnknown.OwnerUserGroupID { + if !isUserGroupMemberInConfig(resourceChange, requester, plan) { + checkResult.ok = false + checkResult.errors = append(checkResult.errors, + newRequestError(resourceChange.Address, resourceChange.Change.After.Tag), + ) + } + + // There is an error in validating topic owner so return the errors immediately + return checkResult + } + + // When the resource is created, the requester must be a member of the owner group after the change + if slices.Contains(resourceChange.Change.Actions, "create") { + checkResult.errors = append(checkResult.errors, + validateRequesterFromState(resourceChange.Address, resourceChange.Change.After, requester, plan)...) + } + // When the resource is updated, the requester must be a member of the owner group before and after the change + if slices.Contains(resourceChange.Change.Actions, "update") { + checkResult.errors = append(checkResult.errors, + validateRequesterFromState(resourceChange.Address, resourceChange.Change.Before, requester, plan)...) + checkResult.errors = append(checkResult.errors, + validateRequesterFromState(resourceChange.Address, resourceChange.Change.After, requester, plan)...) + } + // When the resource is deleted, the requester must be a member of the owner group before the change + if slices.Contains(resourceChange.Change.Actions, "delete") { + checkResult.errors = append(checkResult.errors, + validateRequesterFromState(resourceChange.Address, resourceChange.Change.Before, requester, plan)...) + } + + if len(checkResult.errors) > 0 { + checkResult.ok = false + } + return checkResult +} + +func changeIsApprovedByOwner( + resourceChange ResourceChange, + _ *StateResource, + approvers []*StateResource, + plan *Plan, +) CheckResult { + checkResult := CheckResult{ok: true, errors: []ResultError{}} + + // If the owner is defined but it's a new group it's in the state post-apply so we have to use config to check it + if resourceChange.Change.AfterUnknown.OwnerUserGroupID { + foundApprover := false + for _, approver := range approvers { + if isUserGroupMemberInConfig(resourceChange, approver, plan) { + foundApprover = true // one known approver is enough + } + } + + if !foundApprover { + checkResult.ok = false + checkResult.errors = append(checkResult.errors, + newApproveError(resourceChange.Address, resourceChange.Change.After.Tag), + ) + + // There is an error in validating topic owner so return the errors immediately + return checkResult + } + } + + // When the resource is created, the approvers must be a member of the owner group after the change + if slices.Contains(resourceChange.Change.Actions, "create") { + checkResult.errors = append( + checkResult.errors, + validateApproversFromState(resourceChange.Address, resourceChange.Change.After, approvers, plan)..., + ) + } + + if slices.Contains(resourceChange.Change.Actions, "update") { + // updating owner requires approvals from both old and the new owner + // in other cases checking Change.After would be redundant + checkResult.errors = append( + checkResult.errors, + validateApproversFromState(resourceChange.Address, resourceChange.Change.Before, approvers, plan)..., + ) + checkResult.errors = append( + checkResult.errors, + validateApproversFromState(resourceChange.Address, resourceChange.Change.After, approvers, plan)..., + ) + } + + // When the resource is deleted, the approvers must be a member of the owner group before the change + if slices.Contains(resourceChange.Change.Actions, "delete") { + checkResult.errors = append( + checkResult.errors, + validateApproversFromState(resourceChange.Address, resourceChange.Change.Before, approvers, plan)..., + ) + } + + if len(checkResult.errors) > 0 { + checkResult.ok = false + } + return checkResult +} + +func validateApproversFromState( + address string, + resource *ChangeResource, + approvers []*StateResource, + plan *Plan, +) []ResultError { + resultErrors := []ResultError{} + + // if the resource in state is missing or doesn't have an owner, return immediately + if resource == nil { + return resultErrors + } + if resource.OwnerUserGroupID == nil { + return resultErrors + } + if *resource.OwnerUserGroupID == "" { + return resultErrors + } + + // At least one approver is required + for _, approver := range approvers { + if isUserGroupMemberInState(resource, approver, plan) { + // found a member, short circuit the function + return resultErrors + } + } + + // did not find a member, add an approve error + resultErrors = append(resultErrors, newApproveError(address, resource.Tag)) + return resultErrors +} + +func validateRequesterFromState( + address string, + resource *ChangeResource, + requester *StateResource, + plan *Plan, +) []ResultError { + resultErrors := []ResultError{} + + // if the resource in state is missing or doesn't have an owner, return immediately + if resource == nil { + return resultErrors + } + if resource.OwnerUserGroupID == nil { + return resultErrors + } + if *resource.OwnerUserGroupID == "" { + return resultErrors + } + + // Requester is required + if requester == nil || !isUserGroupMemberInState(resource, requester, plan) { + resultErrors = append(resultErrors, newRequestError(address, resource.Tag)) + return resultErrors + } + + // did not find any member, add an approve error + return resultErrors +} + +func newRequestError(address string, tag []Tag) ResultError { + return ResultError{ + Error: "requesting user is not a member of the owner group", + Address: address, + Tags: tag, + } +} + +func newApproveError(address string, tag []Tag) ResultError { + return ResultError{ + Error: "approval is required from a member of the owner group", + Address: address, + Tags: tag, + } +} diff --git a/checks_test.go b/checks_test.go new file mode 100644 index 0000000..2940915 --- /dev/null +++ b/checks_test.go @@ -0,0 +1,207 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnit_ValidateApproversFromStateSimpleCases(t *testing.T) { + tests := []struct { + name string + address string + resource *ChangeResource + approvers []*StateResource + plan *Plan + expectedErrors int + }{ + { + name: "Resource has no owner group ID", + address: "resource1", + resource: &ChangeResource{OwnerUserGroupID: nil}, + approvers: []*StateResource{{}}, + expectedErrors: 0, + }, + { + name: "Resource owner group ID is empty", + address: "resource2", + resource: &ChangeResource{OwnerUserGroupID: stringPtr("")}, + approvers: []*StateResource{{}}, + expectedErrors: 0, + }, + { + name: "Resource is nil", + address: "resource3", + resource: nil, + approvers: []*StateResource{{}}, + expectedErrors: 0, + }, + { + name: "No approvers", + address: "resource4", + resource: &ChangeResource{OwnerUserGroupID: stringPtr("owner-group")}, + approvers: []*StateResource{}, + expectedErrors: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resultErrors := validateApproversFromState(tt.address, tt.resource, tt.approvers, tt.plan) + if len(resultErrors) != tt.expectedErrors { + t.Errorf("expected %d errors, got %d", tt.expectedErrors, len(resultErrors)) + } + }) + } +} +func TestUnit_ValidateRequesterFromStateSimpleCases(t *testing.T) { + tests := []struct { + name string + address string + resource *ChangeResource + requester *StateResource + plan *Plan + expectedErrors int + }{ + { + name: "Resource has no owner group ID", + address: "resource3", + resource: &ChangeResource{OwnerUserGroupID: nil}, + requester: &StateResource{}, + expectedErrors: 0, + }, + { + name: "Resource owner group ID is empty", + address: "resource4", + resource: &ChangeResource{OwnerUserGroupID: stringPtr("")}, + requester: &StateResource{}, + expectedErrors: 0, + }, + { + name: "Requester is nil", + address: "resource5", + resource: &ChangeResource{OwnerUserGroupID: stringPtr("owner-group")}, + + requester: nil, + expectedErrors: 1, + }, + { + name: "Resource is nil", + address: "resource6", + resource: nil, + requester: &StateResource{}, + expectedErrors: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resultErrors := validateRequesterFromState(tt.address, tt.resource, tt.requester, tt.plan) + if len(resultErrors) != tt.expectedErrors { + t.Errorf("expected %d errors, got %d", tt.expectedErrors, len(resultErrors)) + } + }) + } +} + +func TestUnit_NewRequestError(t *testing.T) { + tests := []struct { + name string + address string + tag []Tag + expected ResultError + }{ + { + name: "Single tag", + address: "resource1", + tag: []Tag{{Key: "env", Value: "prod"}}, + expected: ResultError{ + Error: "requesting user is not a member of the owner group", + Address: "resource1", + Tags: []Tag{{Key: "env", Value: "prod"}}, + }, + }, + { + name: "Multiple tags", + address: "resource2", + tag: []Tag{{Key: "env", Value: "prod"}, {Key: "team", Value: "devops"}}, + expected: ResultError{ + Error: "requesting user is not a member of the owner group", + Address: "resource2", + Tags: []Tag{{Key: "env", Value: "prod"}, {Key: "team", Value: "devops"}}, + }, + }, + { + name: "No tags", + address: "resource3", + tag: []Tag{}, + expected: ResultError{ + Error: "requesting user is not a member of the owner group", + Address: "resource3", + Tags: []Tag{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := newRequestError(tt.address, tt.tag) + if !assert.ObjectsAreEqual(tt.expected, result) { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestUnit_NewApproveError(t *testing.T) { + tests := []struct { + name string + address string + tag []Tag + expected ResultError + }{ + { + name: "Single tag", + address: "resource1", + tag: []Tag{{Key: "env", Value: "prod"}}, + expected: ResultError{ + Error: "approval is required from a member of the owner group", + Address: "resource1", + Tags: []Tag{{Key: "env", Value: "prod"}}, + }, + }, + { + name: "Multiple tags", + address: "resource2", + tag: []Tag{{Key: "env", Value: "prod"}, {Key: "team", Value: "devops"}}, + expected: ResultError{ + Error: "approval is required from a member of the owner group", + Address: "resource2", + Tags: []Tag{{Key: "env", Value: "prod"}, {Key: "team", Value: "devops"}}, + }, + }, + { + name: "No tags", + address: "resource3", + tag: []Tag{}, + expected: ResultError{ + Error: "approval is required from a member of the owner group", + Address: "resource3", + Tags: []Tag{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := newApproveError(tt.address, tt.tag) + if !assert.ObjectsAreEqual(tt.expected, result) { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +func stringPtr(s string) *string { + return &s +} diff --git a/go.sum b/go.sum index e20fa14..60ce688 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 9640020..ae95450 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,9 @@ import ( "encoding/json" "flag" "log" + "maps" "os" + "slices" "strings" ) @@ -57,7 +59,7 @@ type StateResource struct { Values struct { InternalUserID string `json:"internal_user_id"` ExternalUserID string `json:"external_user_id"` - Tag []Tag `json:"tag"` + Tag []Tag `json:"tag"` // if supported by the resource OwnerUserGroupID *string `json:"owner_user_group_id"` GroupID *string `json:"group_id"` UserID *string `json:"user_id"` @@ -110,6 +112,19 @@ const ( AivenOrganizationUserGroupMember ResourceType = "aiven_organization_user_group_member" ) +type Check func(ResourceChange, *StateResource, []*StateResource, *Plan) CheckResult + +type ResourceErrorKey struct { + resource string + error string +} + +var checks = map[ResourceType][]Check{ + AivenKafkaTopic: {changeIsRequestedByOwner, changeIsApprovedByOwner}, + AivenExternalIdentity: {}, + AivenOrganizationUserGroupMember: {}, +} + func main() { path := flag.String("plan", "", "path to a file with terraform plan output in json format") requesterID := flag.String("requester", "", "user identified as the requester of the change") @@ -157,18 +172,35 @@ func validateResourceChange( approvers []*StateResource, plan *Plan, ) []ResultError { - var validator Validator - switch resourceChange.Type { - case AivenKafkaTopic: - validator = TopicValidator{} - default: + + resourceChecks, ok := checks[resourceChange.Type] + if !ok { + // no checks for this resource type return []ResultError{} } - return validator.ValidateResourceChange(resourceChange, requester, approvers, plan) + var checkErrors = make([]ResultError, 0) + + // run the checks and collect errors + for _, check := range resourceChecks { + singleCheckResult := check(resourceChange, requester, approvers, plan) + if !singleCheckResult.ok { + checkErrors = append(checkErrors, singleCheckResult.errors...) + } + } + + // Remove duplicate errors + errorMap := make(map[ResourceErrorKey]ResultError) + for _, err := range checkErrors { + errorMap[ResourceErrorKey{resource: resourceChange.Name, error: err.Error}] = err + } + + // Convert the map back into a slice + return slices.Collect(maps.Values(errorMap)) } +// Finds external identity resource for a given user ID from the current (prior) state func findExternalIdentity(userID string, plan *Plan) *StateResource { for _, resource := range plan.State.Values.RootModule.Resources { if resource.Type == AivenExternalIdentity && userID == resource.Values.ExternalUserID { @@ -182,6 +214,7 @@ func findApprovers(approverIDs []string, requesterID string, plan *Plan) []*Stat var approvers []*StateResource for _, approverID := range approverIDs { approver := findExternalIdentity(approverID, plan) + // requester can't approve their own request if approver != nil && requesterID != approverID { approvers = append(approvers, approver) } @@ -248,19 +281,3 @@ func isUserGroupMemberInState(resourceWithOwner *ChangeResource, user *StateReso } return false } - -func newRequestError(address string, tag []Tag) ResultError { - return ResultError{ - Error: "requesting user is not a member of the owner group", - Address: address, - Tags: tag, - } -} - -func newApproveError(address string, tag []Tag) ResultError { - return ResultError{ - Error: "approval is required from a member of the owner group", - Address: address, - Tags: tag, - } -} diff --git a/topicvalidator.go b/topicvalidator.go deleted file mode 100644 index b9bdbb7..0000000 --- a/topicvalidator.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "maps" - "slices" -) - -type TopicErrorKey struct { - topic string - error string -} - -type TopicValidator struct{} - -// ValidateResourceChange implements Validator. -func (tv TopicValidator) ValidateResourceChange( - resource ResourceChange, // topic - requester *StateResource, - approvers []*StateResource, - plan *Plan, -) []ResultError { - var topicErrors = make([]ResultError, 0) - if resource.Change.AfterUnknown.OwnerUserGroupID { - topicErrors = append(topicErrors, validateKafkaTopicOwnerFromConfig(resource, requester, approvers, plan)...) - - // There is an error in validating topic owner so return the errors immediately - return topicErrors - } - if slices.Contains(resource.Change.Actions, "create") { - topicErrors = append( - topicErrors, - validateKafkaTopicOwnerFromState(resource.Address, resource.Change.After, requester, approvers, plan)..., - ) - } - if slices.Contains(resource.Change.Actions, "update") { - // updating topic owner requires approvals from both old and the new owner - // in other cases checking Change.After is redundant - topicErrors = append( - topicErrors, - validateKafkaTopicOwnerFromState(resource.Address, resource.Change.Before, requester, approvers, plan)..., - ) - topicErrors = append( - topicErrors, - validateKafkaTopicOwnerFromState(resource.Address, resource.Change.After, requester, approvers, plan)..., - ) - } - if slices.Contains(resource.Change.Actions, "delete") { - topicErrors = append( - topicErrors, - validateKafkaTopicOwnerFromState(resource.Address, resource.Change.Before, requester, approvers, plan)..., - ) - } - - // Convert the topicErrors into a map to remove duplicates - topicErrorMap := make(map[TopicErrorKey]ResultError) - for _, topicError := range topicErrors { - topicErrorMap[TopicErrorKey{topic: resource.Name, error: topicError.Error}] = topicError - } - - // Convert the map back into a slice - return slices.Collect(maps.Values(topicErrorMap)) -} - -func validateKafkaTopicOwnerFromState( - address string, - topic *ChangeResource, - requester *StateResource, - approvers []*StateResource, - plan *Plan, -) []ResultError { - resultErrors := []ResultError{} - - // if the topic in state is missing or doesn't have an owner, return immediately - if topic == nil { - return resultErrors - } - if topic.OwnerUserGroupID == nil { - return resultErrors - } - if *topic.OwnerUserGroupID == "" { - return resultErrors - } - - // Requester is required - if requester == nil { - resultErrors = append(resultErrors, newRequestError(address, topic.Tag)) - return resultErrors - } - - if !isUserGroupMemberInState(topic, requester, plan) { - resultErrors = append(resultErrors, newRequestError(address, topic.Tag)) - return resultErrors - } - - // At least one approver is required - for _, approver := range approvers { - if isUserGroupMemberInState(topic, approver, plan) { - // found a member, short circuit the function - return resultErrors - } - } - - // did not find a member, add approve error - resultErrors = append(resultErrors, newApproveError(address, topic.Tag)) - return resultErrors -} - -func validateKafkaTopicOwnerFromConfig( - resourceChange ResourceChange, - requester *StateResource, - approvers []*StateResource, - plan *Plan, -) []ResultError { - validationErrors := []ResultError{} - if !isUserGroupMemberInConfig(resourceChange, requester, plan) { - validationErrors = append(validationErrors, newRequestError(resourceChange.Address, resourceChange.Change.After.Tag)) - } - for _, approver := range approvers { - if isUserGroupMemberInConfig(resourceChange, approver, plan) { - return validationErrors - } - } - - validationErrors = append(validationErrors, newApproveError(resourceChange.Address, resourceChange.Change.After.Tag)) - return validationErrors -} diff --git a/topicvalidator_test.go b/topicvalidator_test.go deleted file mode 100644 index 0f0dff3..0000000 --- a/topicvalidator_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -// These are very basic sad path tests cases, other cases are covered by the "e2e" tests of main.go -func TestUnit_ValidateKafkaTopicOwnerFromState(t *testing.T) { - tests := []struct { - name string - address string - topic *ChangeResource - requester *StateResource - approvers []*StateResource - plan *Plan - wantErrors []ResultError - }{ - { - name: "Topic is nil", - address: "test-address", - topic: nil, - requester: &StateResource{}, - approvers: []*StateResource{}, - plan: &Plan{}, - wantErrors: []ResultError{}, - }, - { - name: "Topic owner is nil", - address: "test-address", - topic: &ChangeResource{ - OwnerUserGroupID: nil, - }, - requester: &StateResource{}, - approvers: []*StateResource{}, - plan: &Plan{}, - wantErrors: []ResultError{}, - }, - { - name: "Topic owner is empty", - address: "test-address", - topic: &ChangeResource{ - OwnerUserGroupID: new(string), - }, - requester: &StateResource{}, - approvers: []*StateResource{}, - plan: &Plan{}, - wantErrors: []ResultError{}, - }, - { - name: "Requester is nil", - address: "test-address", - topic: &ChangeResource{ - OwnerUserGroupID: stringPtr("owner-id"), - }, - requester: nil, - approvers: []*StateResource{}, - plan: &Plan{}, - wantErrors: []ResultError{newRequestError("test-address", []Tag(nil))}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotErrors := validateKafkaTopicOwnerFromState(tt.address, tt.topic, tt.requester, tt.approvers, tt.plan) - assert.Equal(t, tt.wantErrors, gotErrors) - }) - } -} - -func TestUnit_ValidateKafkaTopicOwnerFromConfig(t *testing.T) { - tests := []struct { - name string - resourceChange ResourceChange - requester *StateResource - approvers []*StateResource - plan *Plan - wantErrors []ResultError - }{ - { - name: "Requester is not a member", - resourceChange: ResourceChange{ - Address: "test-address", - Change: Change{ - After: &ChangeResource{ - Tag: []Tag{}, - }, - }, - }, - requester: &StateResource{}, - approvers: []*StateResource{}, - plan: &Plan{}, - wantErrors: []ResultError{newRequestError("test-address", []Tag{}), newApproveError("test-address", []Tag{})}, - }, - { - name: "Approver is not a member", - resourceChange: ResourceChange{ - Address: "test-address", - Change: Change{ - After: &ChangeResource{ - Tag: []Tag{}, - }, - }, - }, - requester: &StateResource{}, - approvers: []*StateResource{}, - plan: &Plan{}, - wantErrors: []ResultError{newRequestError("test-address", []Tag{}), newApproveError("test-address", []Tag{})}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotErrors := validateKafkaTopicOwnerFromConfig(tt.resourceChange, tt.requester, tt.approvers, tt.plan) - assert.Equal(t, tt.wantErrors, gotErrors) - }) - } -} - -func stringPtr(s string) *string { - return &s -}