From 6ec4d8c9aae09f036dc04f3ace7a939c01930c98 Mon Sep 17 00:00:00 2001 From: Ethan Blackburn Date: Sun, 7 Apr 2024 14:54:43 -0400 Subject: [PATCH] Add test directory --- .github/workflows/tests.yml | 16 ++ tests/end2end_test.go | 289 ++++++++++++++++++++++++++++++++++++ tests/main_test.go | 122 +++++++++++++++ tests/setup.sh | 35 +++++ tests/teardown.sh | 6 + 5 files changed, 468 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 tests/end2end_test.go create mode 100644 tests/main_test.go create mode 100755 tests/setup.sh create mode 100644 tests/teardown.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e854ea1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,16 @@ +# .github/workflows/tests.yml +name: tests + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + - name: Run Tests \ No newline at end of file diff --git a/tests/end2end_test.go b/tests/end2end_test.go new file mode 100644 index 0000000..7e3e468 --- /dev/null +++ b/tests/end2end_test.go @@ -0,0 +1,289 @@ +package tests + +import ( + "context" + "fmt" + "io/ioutil" + "os/exec" + "testing" + + "github.com/santiago-labs/telophasecli/lib/awsorgs" + "github.com/santiago-labs/telophasecli/lib/ymlparser" + "github.com/santiago-labs/telophasecli/resource" + "github.com/stretchr/testify/assert" +) + +type E2ETestCase struct { + Name string + OrgYaml string + Expected *resource.OrganizationUnit +} + +var tests = []E2ETestCase{ + { + Name: "Test that we can create OUs", + OrgYaml: ` +Organization: + Name: root + OrganizationUnits: + - Name: ProductionTenants + - Name: Development + Accounts: + - AccountName: master + Email: master@example.com +`, + Expected: &resource.OrganizationUnit{ + OUName: "root", + ChildOUs: []*resource.OrganizationUnit{ + { + OUName: "ProductionTenants", + }, + { + OUName: "Development", + }, + }, + Accounts: []*resource.Account{ + { + AccountName: "master", + Email: "master@example.com", + }, + }, + }, + }, + { + Name: "Test that we can create nested OUs", + OrgYaml: ` +Organization: + Name: root + OrganizationUnits: + - Name: ProductionTenants + OrganizationUnits: + - Name: ProductionEU + - Name: ProductionUS + - Name: Development + OrganizationUnits: + - Name: DevEU + - Name: DevUS + Accounts: + - AccountName: master + Email: master@example.com +`, + Expected: &resource.OrganizationUnit{ + OUName: "root", + ChildOUs: []*resource.OrganizationUnit{ + { + OUName: "ProductionTenants", + ChildOUs: []*resource.OrganizationUnit{ + { + OUName: "ProductionEU", + }, + { + OUName: "ProductionUS", + }, + }, + }, + { + OUName: "Development", + ChildOUs: []*resource.OrganizationUnit{ + { + OUName: "DevEU", + }, + { + OUName: "DevUS", + }, + }, + }, + }, + Accounts: []*resource.Account{ + { + AccountName: "master", + Email: "master@example.com", + }, + }, + }, + }, + { + Name: "Test that we can create accounts", + OrgYaml: ` +Organization: + Name: root + Accounts: + - AccountName: master + Email: master@example.com + - AccountName: test1 + Email: test1@example.com + - AccountName: test2 + Email: test2@example.com +`, + Expected: &resource.OrganizationUnit{ + OUName: "root", + Accounts: []*resource.Account{ + { + AccountName: "master", + Email: "master@example.com", + }, + { + AccountName: "test1", + Email: "test1@example.com", + }, + { + AccountName: "test2", + Email: "test2@example.com", + }, + }, + }, + }, + { + Name: "Test that we can create accounts in OUs", + OrgYaml: ` +Organization: + Name: root + OrganizationUnits: + - Name: ProductionTenants + Accounts: + - AccountName: test1 + Email: test1@example.com + OrganizationUnits: + - Name: ProductionEU + - Name: ProductionUS + Accounts: + - AccountName: test2 + Email: test2@example.com + - Name: Development + OrganizationUnits: + - Name: DevEU + Accounts: + - AccountName: test3 + Email: test3@example.com + - AccountName: test4 + Email: test4@example.com + - AccountName: test5 + Email: test5@example.com + - Name: DevUS + Accounts: + - AccountName: test6 + Email: test6@example.com + - AccountName: test7 + Email: test7@example.com + Accounts: + - AccountName: master + Email: master@example.com + - AccountName: test8 + Email: test8@example.com + - AccountName: test9 + Email: test9@example.com +`, + Expected: &resource.OrganizationUnit{ + OUName: "root", + ChildOUs: []*resource.OrganizationUnit{ + { + OUName: "ProductionTenants", + Accounts: []*resource.Account{ + { + AccountName: "test1", + Email: "test1@example.com", + }, + }, + ChildOUs: []*resource.OrganizationUnit{ + { + OUName: "ProductionEU", + }, + { + OUName: "ProductionUS", + Accounts: []*resource.Account{ + { + AccountName: "test2", + Email: "test2@example.com", + }, + }, + }, + }, + }, + { + OUName: "Development", + ChildOUs: []*resource.OrganizationUnit{ + { + OUName: "DevEU", + Accounts: []*resource.Account{ + { + AccountName: "test3", + Email: "test3@example.com", + }, + { + AccountName: "test4", + Email: "test4@example.com", + }, + { + AccountName: "test5", + Email: "test5@example.com", + }, + }, + }, + { + OUName: "DevUS", + Accounts: []*resource.Account{ + { + AccountName: "test6", + Email: "test6@example.com", + }, + { + AccountName: "test7", + Email: "test7@example.com", + }, + }, + }, + }, + }, + }, + Accounts: []*resource.Account{ + { + AccountName: "master", + Email: "master@example.com", + }, + { + AccountName: "test8", + Email: "test8@example.com", + }, + { + AccountName: "test9", + Email: "test9@example.com", + }, + }, + }, + }, +} + +func TestEndToEnd(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Recovered from panic: %v", r) + } + }() + + for _, test := range tests { + setupTest() + + fmt.Printf("Running test: %s\n", test.Name) + err := ioutil.WriteFile("organization.yml", []byte(test.OrgYaml), 0644) + assert.NoError(t, err, "Failed to write organization.yml") + + parsedOrg, err := ymlparser.ParseOrganizationV2("organization.yml") + assert.NoError(t, err, "Failed to parse organization.yml") + + compareOrganizationUnits(t, test.Expected, parsedOrg) + + cmd := exec.Command("bash", "-c", "../telophasecli deploy") + _, stderr, err := runCmd(cmd) + assert.NoError(t, err, fmt.Sprintf("Failed to run telophasecli deploy. STDERR \n %s \n", stderr)) + + ctx := context.Background() + orgClient := awsorgs.New() + rootId, err := orgClient.GetRootId() + assert.NoError(t, err, "Failed to fetch rootId") + + fetchedOrg, err := orgClient.FetchOUAndDescendents(ctx, rootId, "000000000000") + assert.NoError(t, err, "Failed to fetch rootOU") + + compareOrganizationUnits(t, test.Expected, &fetchedOrg) + } +} diff --git a/tests/main_test.go b/tests/main_test.go new file mode 100644 index 0000000..2807a38 --- /dev/null +++ b/tests/main_test.go @@ -0,0 +1,122 @@ +package tests + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/santiago-labs/telophasecli/resource" + "github.com/stretchr/testify/assert" +) + +func setup() error { + fmt.Println("Running setup") + + os.Setenv("LOCALSTACK", "true") + os.Setenv("AWS_REGION", "us-east-1") + + cmd := exec.Command("bash", "setup.sh") + if _, stderr, err := runCmd(cmd); err != nil { + return fmt.Errorf("Failed to run setup: %v\n %s \n", err, stderr) + } + return nil +} + +func setupTest() { + resetCmd := exec.Command("curl", "-v", "--request", "POST", "http://localhost:4566/_localstack/state/reset") + if _, stderr, err := runCmd(resetCmd); err != nil { + fmt.Printf("Failed to reset localstack state: %v\n %s \n ", err, stderr) + os.Exit(1) + } + + cmd := exec.Command("awslocal", "organizations", "create-organization", "--feature-set", "ALL") + if _, stderr, err := runCmd(cmd); err != nil { + fmt.Printf("Failed to create localstack org: %v\n %s \n ", err, stderr) + os.Exit(1) + } +} + +func teardown() error { + fmt.Println("Running teardown") + cmd := exec.Command("bash", "teardown.sh") + if _, stderr, err := runCmd(cmd); err != nil { + return fmt.Errorf("Failed to run teardown: %v\n %s \n", err, stderr) + } + return nil +} + +func TestMain(m *testing.M) { + err := setup() + if err != nil { + fmt.Println(err) + teardown() + os.Exit(1) + } + code := m.Run() + teardown() + os.Exit(code) +} + +func compareOrganizationUnits(t *testing.T, expected, actual *resource.OrganizationUnit) { + assert.Equal(t, expected.OUName, actual.OUName, "OU Name not equal") + + sort.Slice(expected.ChildOUs, func(i, j int) bool { + return expected.ChildOUs[i].OUName < expected.ChildOUs[j].OUName + }) + sort.Slice(actual.ChildOUs, func(i, j int) bool { + return actual.ChildOUs[i].OUName < actual.ChildOUs[j].OUName + }) + diff := cmp.Diff(expected.ChildOUs, actual.ChildOUs) + assert.Equal(t, len(expected.ChildOUs), len(actual.ChildOUs), "Child OUs not equal: %v", diff) + + sort.Slice(expected.Accounts, func(i, j int) bool { + return expected.Accounts[i].AccountName < expected.Accounts[j].AccountName + }) + sort.Slice(actual.Accounts, func(i, j int) bool { + return actual.Accounts[i].AccountName < actual.Accounts[j].AccountName + }) + + acctDiff := cmp.Diff(expected.Accounts, actual.Accounts) + assert.Equal(t, len(expected.Accounts), len(actual.Accounts), "Accounts not equal: %v", acctDiff) + assert.Equal(t, expected.BaselineStacks, actual.BaselineStacks) + assert.Equal(t, expected.ServiceControlPolicies, actual.ServiceControlPolicies) + + for i, childOU := range expected.ChildOUs { + compareOrganizationUnits(t, childOU, actual.ChildOUs[i]) + } + + for i, account := range expected.Accounts { + compareAccounts(t, account, actual.Accounts[i]) + } +} + +func compareAccounts(t *testing.T, expected, actual *resource.Account) { + assert.Equal(t, expected.Email, actual.Email, "Account Emails not equal") + assert.Equal(t, expected.AccountName, actual.AccountName, "Account Name not equal") + assert.Equal(t, expected.State, actual.State, "Account State not equal") + assert.Equal(t, expected.AssumeRoleName, actual.AssumeRoleName, "Account AssumeRoleName not equal") + assert.Equal(t, expected.ManagementAccount, actual.ManagementAccount, "Account ManagementAccount not equal") + assert.Equal(t, expected.Tags, actual.Tags, "Account Tags not equal") + assert.Equal(t, expected.BaselineStacks, actual.BaselineStacks, "Account BaselineStacks not equal") + assert.Equal(t, expected.ServiceControlPolicies, actual.ServiceControlPolicies, "Account ServiceControlPolicies not equal") +} + +func runCmd(cmd *exec.Cmd) (string, string, error) { + var stdoutBuf, stderrBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + + if err := cmd.Start(); err != nil { + return "", "", fmt.Errorf("[ERROR] %v", err) + } + + if err := cmd.Wait(); err != nil { + return stdoutBuf.String(), stderrBuf.String(), fmt.Errorf("[ERROR] %v", err) + } + + return stdoutBuf.String(), stderrBuf.String(), nil +} diff --git a/tests/setup.sh b/tests/setup.sh new file mode 100755 index 0000000..04d0027 --- /dev/null +++ b/tests/setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -eux + +OS=$(uname -s) + +if [ "$OS" == "Linux" ]; then + sudo apt-get update + sudo apt-get install -y awscli + npm install -g aws-cdk + curl -Lo localstack-cli-3.3.0-linux-amd64-onefile.tar.gz https://github.com/localstack/localstack-cli/releases/download/v3.3.0/localstack-cli-3.3.0-linux-amd64-onefile.tar.gz + sudo tar xvzf localstack-cli-3.3.0-linux-*-onefile.tar.gz -C /usr/local/bin +elif [ "$OS" == "Darwin" ]; then + brew install awscli + npm install -g aws-cdk + brew install localstack/tap/localstack-cli +else + echo "Unsupported operating system: $OS" + exit 1 +fi + +cd .. +go build . +cd tests + +localstack start -d + +sleep 5 + +# Pre-create tf-test-state table to avoid concurrency bug in localstack. +aws dynamodb create-table --table-name tf-test-state \ +--attribute-definitions AttributeName=id,AttributeType=S \ +--key-schema AttributeName=id,KeyType=HASH \ +--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \ +--endpoint-url http://localhost:4566 diff --git a/tests/teardown.sh b/tests/teardown.sh new file mode 100644 index 0000000..eecb717 --- /dev/null +++ b/tests/teardown.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -eux + +localstack stop +rm organization.yml \ No newline at end of file