Skip to content

Commit 4ff78c2

Browse files
CLOUDP-333521: [AtlasCLI] [Mac] Write e2e test for migration to secure storage (#4135)
1 parent c2c302c commit 4ff78c2

File tree

5 files changed

+211
-5
lines changed

5 files changed

+211
-5
lines changed

build/ci/evergreen.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,25 @@ tasks:
13651365
-e SNYK_CFG_ORG=${SNYK_ORG} \
13661366
-v ${workdir}/src/github.com/mongodb/mongodb-atlas-cli:/app \
13671367
snyk/snyk:golang snyk monitor
1368+
#
1369+
- name: atlas_config_migration_e2e
1370+
tags: ["e2e","config-migration"]
1371+
must_have_test_results: true
1372+
depends_on:
1373+
- name: compile
1374+
variant: "code_health"
1375+
patch_optional: true
1376+
commands:
1377+
- func: "install gotestsum"
1378+
- func: "e2e test"
1379+
vars:
1380+
MONGODB_ATLAS_ORG_ID: ${atlas_org_id}
1381+
MONGODB_ATLAS_PROJECT_ID: ${atlas_project_id}
1382+
MONGODB_ATLAS_PRIVATE_API_KEY: ${atlas_private_api_key}
1383+
MONGODB_ATLAS_PUBLIC_API_KEY: ${atlas_public_api_key}
1384+
MONGODB_ATLAS_OPS_MANAGER_URL: ${mcli_ops_manager_url}
1385+
MONGODB_ATLAS_SERVICE: cloud
1386+
E2E_TEST_PACKAGES: ./test/e2e/config/migration/...
13681387
task_groups:
13691388
- name: atlas_deployments_windows_group
13701389
setup_task:
@@ -1683,6 +1702,18 @@ buildvariants:
16831702
- ubuntu2204-small
16841703
tasks:
16851704
- name: ".snyk"
1705+
- name: e2e_config_migration_macos_14
1706+
display_name: "E2E Config Migration Tests"
1707+
allowed_requesters: ["patch", "ad_hoc", "github_pr"]
1708+
tags:
1709+
- config-migration
1710+
- foliage_health
1711+
run_on:
1712+
- macos-14-arm64-docker
1713+
expansions:
1714+
<<: *go_linux_version
1715+
tasks:
1716+
- name: ".e2e .config-migration"
16861717
patch_aliases:
16871718
- alias: "localdev"
16881719
variant_tags: ["localdev cron"]

cmd/atlas/atlas.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ func loadConfig() (*config.Profile, error) {
6565
return nil, fmt.Errorf("error loading config: %w. Please run `atlas auth login` to reconfigure your profile", initErr)
6666
}
6767

68+
if !configStore.IsSecure() {
69+
fmt.Fprintf(os.Stderr, "Warning: Secure storage is not available, falling back to insecure storage\n")
70+
}
71+
6872
profile := config.NewProfile(config.DefaultProfile, configStore)
6973
config.SetProfile(profile)
7074

test/e2e/atlas/search_nodes/searchnodes/search_nodes_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,9 @@ func TestSearchNodes(t *testing.T) {
6666
internal.ProfileName(),
6767
)
6868

69-
resp, err := cmd.CombinedOutput()
70-
respStr := string(resp)
71-
72-
require.NoError(t, err, respStr)
73-
require.Equal(t, "{}\n", respStr)
69+
stdout, stderr, err := internal.RunAndGetSeparateStdOutAndErr(cmd)
70+
require.NoError(t, err, string(stderr))
71+
require.Equal(t, "{}\n", string(stdout))
7472
})
7573

7674
g.Run("Create search node", func(t *testing.T) { //nolint:thelper // g.Run replaces t.Run
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2020 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package migration
16+
17+
import (
18+
"os"
19+
"os/exec"
20+
"path"
21+
"runtime"
22+
"testing"
23+
24+
"github.com/mongodb/mongodb-atlas-cli/atlascli/test/internal"
25+
"github.com/pelletier/go-toml"
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
"github.com/zalando/go-keyring"
29+
)
30+
31+
func TestConfigMigration(t *testing.T) {
32+
if testing.Short() {
33+
t.Skip("skipping test in short mode")
34+
}
35+
36+
if runtime.GOOS != "darwin" {
37+
t.Skip("skipping test on non-macOS")
38+
}
39+
40+
// Create a temporary config folder
41+
dir := internal.TempConfigFolder(t)
42+
t.Cleanup(func() {
43+
os.RemoveAll(dir)
44+
})
45+
46+
// Initialize the keychain
47+
require.NoError(t, internal.InitKeychain(t))
48+
49+
// Write the old config format that needs to be migrated
50+
configPath := path.Join(dir, "config.toml")
51+
err := os.WriteFile(configPath, []byte(`[e2e-migration]
52+
org_id = "test_id"
53+
public_api_key = "test_pub"
54+
private_api_key = "test_priv"
55+
service = "cloud"
56+
`), 0600)
57+
require.NoError(t, err)
58+
59+
// Get the CLI path
60+
cliPath, err := internal.AtlasCLIBin()
61+
require.NoError(t, err)
62+
63+
// Run the CLI, any command will trigger the migration
64+
cmd := exec.Command(cliPath)
65+
cmd.Env = os.Environ()
66+
67+
// Get the output
68+
outputBytes, err := cmd.CombinedOutput()
69+
output := string(outputBytes)
70+
if err != nil {
71+
t.Fatalf("failed to run command: %v, output: %s", err, output)
72+
}
73+
74+
// Ensure we're not falling back to insecure storage
75+
assert.NotContains(t, output, "Warning: Secure storage is not available, falling back to insecure storage")
76+
77+
// Read the config file and check that it contains the expected migrated structure
78+
configContent, err := os.ReadFile(configPath)
79+
if err != nil {
80+
t.Fatalf("failed to read config file: %v", err)
81+
}
82+
83+
// Parse the config file as TOML
84+
var config map[string]any
85+
require.NoError(t, toml.Unmarshal(configContent, &config))
86+
87+
// Get the profile that we expect to be migrated
88+
migrationProfile, ok := config["e2e-migration"].(map[string]any)
89+
if !ok {
90+
t.Fatalf("e2e-migration not found in config")
91+
}
92+
93+
// Check that the profile was migrated correctly
94+
// Secrets should be removed from the profile
95+
assert.Equal(t, "test_id", migrationProfile["org_id"])
96+
assert.Nil(t, migrationProfile["public_api_key"])
97+
assert.Nil(t, migrationProfile["private_api_key"])
98+
assert.Equal(t, "cloud", migrationProfile["service"])
99+
100+
// Check that the secrets are stored in the keychain
101+
publicAPIKey, err := keyring.Get("atlascli_e2e-migration", "public_api_key")
102+
require.NoError(t, err)
103+
assert.Equal(t, "test_pub", publicAPIKey)
104+
105+
privateAPIKey, err := keyring.Get("atlascli_e2e-migration", "private_api_key")
106+
require.NoError(t, err)
107+
assert.Equal(t, "test_priv", privateAPIKey)
108+
}

test/internal/helper.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"os"
2727
"os/exec"
2828
"path/filepath"
29+
"runtime"
2930
"strconv"
3031
"strings"
3132
"testing"
@@ -614,6 +615,70 @@ func TempConfigFolder(t *testing.T) string {
614615
return dir
615616
}
616617

618+
func InitKeychain(t *testing.T) error {
619+
t.Helper()
620+
621+
if runtime.GOOS == "darwin" {
622+
return InitKeychainMac(t)
623+
}
624+
625+
return fmt.Errorf("keychain initialization not supported on %s", runtime.GOOS)
626+
}
627+
628+
func InitKeychainMac(t *testing.T) error {
629+
t.Helper()
630+
631+
// Run the following command to initialize the keychain
632+
//
633+
// Create the preferences directory, expected by the security command to exist
634+
// HOME=dir mkdir -p $dir/Library/Preferences
635+
//
636+
// Create the keychain:
637+
// HOME=dir /usr/bin/security create-keychain -p "" default.keychain-db
638+
//
639+
// Add the keychain to the search list:
640+
// HOME=dir /usr/bin/security list-keychains -d user -s default.keychain-db
641+
//
642+
// Set the default keychain:
643+
// HOME=dir /usr/bin/security default-keychain -s default.keychain-db
644+
//
645+
// Unlock the keychain:
646+
// HOME=dir /usr/bin/security unlock-keychain -p "" default.keychain-db
647+
//
648+
649+
home := os.Getenv("HOME")
650+
651+
if err := os.MkdirAll(filepath.Join(home, "Library", "Preferences"), os.ModePerm); err != nil {
652+
return fmt.Errorf("error creating preferences directory: %w", err)
653+
}
654+
655+
createCmd := exec.Command("/usr/bin/security", "create-keychain", "-p", "", "default.keychain-db")
656+
createCmd.Env = os.Environ()
657+
if err := createCmd.Run(); err != nil {
658+
return fmt.Errorf("error creating keychain: %w", err)
659+
}
660+
661+
listCmd := exec.Command("/usr/bin/security", "list-keychains", "-d", "user", "-s", "default.keychain-db")
662+
listCmd.Env = os.Environ()
663+
if err := listCmd.Run(); err != nil {
664+
return fmt.Errorf("error listing keychains: %w", err)
665+
}
666+
667+
defaultCmd := exec.Command("/usr/bin/security", "default-keychain", "-s", "default.keychain-db")
668+
defaultCmd.Env = os.Environ()
669+
if err := defaultCmd.Run(); err != nil {
670+
return fmt.Errorf("error setting default keychain: %w", err)
671+
}
672+
673+
unlockCmd := exec.Command("/usr/bin/security", "unlock-keychain", "-p", "", "default.keychain-db")
674+
unlockCmd.Env = os.Environ()
675+
if err := unlockCmd.Run(); err != nil {
676+
return fmt.Errorf("error unlocking keychain: %w", err)
677+
}
678+
679+
return nil
680+
}
681+
617682
func createProject(projectName string) (string, error) {
618683
cliPath, err := AtlasCLIBin()
619684
if err != nil {

0 commit comments

Comments
 (0)