Skip to content

Commit 0c89454

Browse files
committed
fix(project): resolve -e/--env for all Admin-API commands
Only the executor-based commands resolved the -e/--env flag. Every Admin-API command (extension list/install/uninstall/activate/ deactivate/update/delete/outdated/upload, admin-api, clear-cache, upgrade-check) built its client straight from the base config, so -e staging silently targeted the default shop and an unknown environment name was not rejected. A mutating command could act on the wrong (possibly production) shop. Add Config.WithEnvironment, which applies the selected environment's URL and admin_api over the base config, and a readConfigWithEnvironment helper that every Admin-API command now uses. Unknown environment names fail with the same error the executor path already returns.
1 parent 3eaa8af commit 0c89454

17 files changed

Lines changed: 312 additions & 12 deletions

cmd/project/executor.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ import (
77
"github.com/shopware/shopware-cli/internal/shop"
88
)
99

10+
// readConfigWithEnvironment loads the project config with the -e/--env
11+
// environment applied, so Admin API commands target the selected environment.
12+
// Without -e the config is returned as-is: Admin API commands historically
13+
// used the base url/admin_api, and an existing environments.local entry must
14+
// not silently retarget them.
15+
func readConfigWithEnvironment(cmd *cobra.Command, allowFallback bool) (*shop.Config, error) {
16+
cfg, err := shop.ReadConfig(cmd.Context(), projectConfigPath, allowFallback)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
if environmentName == "" {
22+
return cfg, nil
23+
}
24+
25+
return cfg.WithEnvironment(environmentName)
26+
}
27+
1028
// resolveExecutor returns the Executor for the current environment.
1129
func resolveExecutor(cmd *cobra.Command, projectRoot string) (executor.Executor, error) {
1230
cfg, err := shop.ReadConfig(cmd.Context(), projectConfigPath, true)

cmd/project/project_admin_api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var projectAdminApiCmd = &cobra.Command{
2121
var cfg *shop.Config
2222
var err error
2323

24-
if cfg, err = shop.ReadConfig(cobraCmd.Context(), projectConfigPath, false); err != nil {
24+
if cfg, err = readConfigWithEnvironment(cobraCmd, false); err != nil {
2525
return err
2626
}
2727

cmd/project/project_clear_cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var projectClearCacheCmd = &cobra.Command{
1818
var cfg *shop.Config
1919
var err error
2020

21-
if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, false); err != nil {
21+
if cfg, err = readConfigWithEnvironment(cmd, false); err != nil {
2222
return err
2323
}
2424

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package project
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// writeMinimalPlugin creates a minimal platform plugin the upload command can
14+
// load, so its RunE reaches the environment-resolution step.
15+
func writeMinimalPlugin(t *testing.T) string {
16+
t.Helper()
17+
18+
dir := t.TempDir()
19+
require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte(`{
20+
"name": "frosh/frosh-test",
21+
"type": "shopware-platform-plugin",
22+
"license": "MIT",
23+
"version": "1.0.0",
24+
"require": { "shopware/core": "~6.6.0" },
25+
"autoload": { "psr-4": { "FroshTest\\": "src/" } },
26+
"extra": {
27+
"shopware-plugin-class": "FroshTest\\FroshTest",
28+
"label": { "de-DE": "Test", "en-GB": "Test" }
29+
}
30+
}`), 0o644))
31+
require.NoError(t, os.WriteFile(filepath.Join(dir, ".shopware-extension.yml"), []byte(
32+
"build:\n zip:\n composer:\n enabled: false\n assets:\n enabled: false\n",
33+
), 0o644))
34+
require.NoError(t, os.MkdirAll(filepath.Join(dir, "src"), 0o755))
35+
require.NoError(t, os.WriteFile(filepath.Join(dir, "src", "FroshTest.php"),
36+
[]byte("<?php\nnamespace FroshTest;\nuse Shopware\\Core\\Framework\\Plugin;\nclass FroshTest extends Plugin {}\n"), 0o644))
37+
38+
return dir
39+
}
40+
41+
// TestReadConfigWithEnvironmentPropagatesConfigError ensures a config-read
42+
// failure is surfaced rather than swallowed by the environment resolution.
43+
func TestReadConfigWithEnvironmentPropagatesConfigError(t *testing.T) {
44+
previousConfigPath := projectConfigPath
45+
previousEnvironmentName := environmentName
46+
t.Cleanup(func() {
47+
projectConfigPath = previousConfigPath
48+
environmentName = previousEnvironmentName
49+
})
50+
51+
projectConfigPath = filepath.Join(t.TempDir(), "does-not-exist.yml")
52+
environmentName = "staging"
53+
54+
cmd := &cobra.Command{}
55+
cmd.SetContext(t.Context())
56+
57+
_, err := readConfigWithEnvironment(cmd, false)
58+
require.Error(t, err)
59+
assert.Contains(t, err.Error(), "cannot find project configuration file")
60+
}
61+
62+
// TestNoEnvFlagKeepsBaseConfig ensures that without -e the base url and
63+
// credentials are used even when an environments.local entry exists, so the
64+
// fix does not silently retarget existing setups.
65+
func TestNoEnvFlagKeepsBaseConfig(t *testing.T) {
66+
configPath := filepath.Join(t.TempDir(), ".shopware-project.yml")
67+
require.NoError(t, os.WriteFile(configPath, []byte(`
68+
url: http://127.0.0.1:9
69+
compatibility_date: "2026-01-01"
70+
admin_api:
71+
client_id: base-id
72+
client_secret: base-secret
73+
environments:
74+
local:
75+
url: http://127.0.0.1:7
76+
`), 0o644))
77+
78+
previousConfigPath := projectConfigPath
79+
previousEnvironmentName := environmentName
80+
projectConfigPath = configPath
81+
environmentName = ""
82+
t.Cleanup(func() {
83+
projectConfigPath = previousConfigPath
84+
environmentName = previousEnvironmentName
85+
})
86+
87+
projectExtensionListCmd.SetContext(t.Context())
88+
89+
err := projectExtensionListCmd.RunE(projectExtensionListCmd, []string{})
90+
require.Error(t, err)
91+
assert.Contains(t, err.Error(), "127.0.0.1:9", "without -e the base URL must be used")
92+
assert.NotContains(t, err.Error(), "127.0.0.1:7", "environments.local must not silently retarget commands")
93+
}
94+
95+
// TestAdminAPICommandsResolveEnvironment verifies every project command that
96+
// talks to a shop honors the -e/--env flag: an unknown environment is rejected,
97+
// and a known environment's URL is the one contacted.
98+
func TestAdminAPICommandsResolveEnvironment(t *testing.T) {
99+
pluginDir := writeMinimalPlugin(t)
100+
101+
cases := []struct {
102+
name string
103+
cmd *cobra.Command
104+
args []string
105+
}{
106+
{"admin-api", projectAdminApiCmd, []string{"GET", "/_info/config"}},
107+
{"clear-cache", projectClearCacheCmd, nil},
108+
{"extension activate", projectExtensionActivateCmd, []string{"Foo"}},
109+
{"extension deactivate", projectExtensionDeactivateCmd, []string{"Foo"}},
110+
{"extension delete", projectExtensionDeleteCmd, []string{"Foo"}},
111+
{"extension install", projectExtensionInstallCmd, []string{"Foo"}},
112+
{"extension list", projectExtensionListCmd, nil},
113+
{"extension outdated", projectExtensionOutdatedCmd, nil},
114+
{"extension uninstall", projectExtensionUninstallCmd, []string{"Foo"}},
115+
{"extension update", projectExtensionUpdateCmd, []string{"Foo"}},
116+
{"extension upload", projectExtensionUploadCmd, []string{pluginDir}},
117+
{"upgrade-check", projectUpgradeCheckCmd, nil},
118+
}
119+
120+
for _, tc := range cases {
121+
t.Run(tc.name+"/rejects unknown environment", func(t *testing.T) {
122+
setupEnvironmentConfig(t)
123+
environmentName = "nonexistent"
124+
tc.cmd.SetContext(t.Context())
125+
126+
err := tc.cmd.RunE(tc.cmd, tc.args)
127+
require.Error(t, err)
128+
assert.Contains(t, err.Error(), `environment "nonexistent" not found`,
129+
"command must reject an unknown environment instead of silently using the base config")
130+
})
131+
132+
t.Run(tc.name+"/targets selected environment", func(t *testing.T) {
133+
setupEnvironmentConfig(t)
134+
environmentName = "staging"
135+
tc.cmd.SetContext(t.Context())
136+
137+
err := tc.cmd.RunE(tc.cmd, tc.args)
138+
require.Error(t, err)
139+
assert.Contains(t, err.Error(), "127.0.0.1:29",
140+
"command must contact the staging environment URL")
141+
assert.NotContains(t, err.Error(), "127.0.0.1:9/",
142+
"command must not contact the base URL when -e staging is given")
143+
})
144+
}
145+
}

cmd/project/project_extension_activate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var projectExtensionActivateCmd = &cobra.Command{
1818
var cfg *shop.Config
1919
var err error
2020

21-
if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, false); err != nil {
21+
if cfg, err = readConfigWithEnvironment(cmd, false); err != nil {
2222
return err
2323
}
2424

cmd/project/project_extension_deactivate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var projectExtensionDeactivateCmd = &cobra.Command{
1818
var cfg *shop.Config
1919
var err error
2020

21-
if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
21+
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
2222
return err
2323
}
2424

cmd/project/project_extension_delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var projectExtensionDeleteCmd = &cobra.Command{
1818
var cfg *shop.Config
1919
var err error
2020

21-
if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
21+
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
2222
return err
2323
}
2424

cmd/project/project_extension_install.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var projectExtensionInstallCmd = &cobra.Command{
1818
var cfg *shop.Config
1919
var err error
2020

21-
if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
21+
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
2222
return err
2323
}
2424

cmd/project/project_extension_list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var projectExtensionListCmd = &cobra.Command{
2222

2323
outputAsJson, _ := cmd.PersistentFlags().GetBool("json")
2424

25-
if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
25+
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
2626
return err
2727
}
2828

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package project
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func setupEnvironmentConfig(t *testing.T) {
13+
t.Helper()
14+
15+
configPath := filepath.Join(t.TempDir(), ".shopware-project.yml")
16+
require.NoError(t, os.WriteFile(configPath, []byte(`
17+
url: http://127.0.0.1:9
18+
compatibility_date: "2026-01-01"
19+
admin_api:
20+
client_id: base-id
21+
client_secret: base-secret
22+
environments:
23+
staging:
24+
url: http://127.0.0.1:29
25+
admin_api:
26+
client_id: staging-id
27+
client_secret: staging-secret
28+
`), 0o644))
29+
30+
previousConfigPath := projectConfigPath
31+
previousEnvironmentName := environmentName
32+
projectConfigPath = configPath
33+
t.Cleanup(func() {
34+
projectConfigPath = previousConfigPath
35+
environmentName = previousEnvironmentName
36+
})
37+
}
38+
39+
func TestExtensionListTargetsSelectedEnvironment(t *testing.T) {
40+
setupEnvironmentConfig(t)
41+
42+
environmentName = "staging"
43+
projectExtensionListCmd.SetContext(t.Context())
44+
45+
err := projectExtensionListCmd.RunE(projectExtensionListCmd, []string{})
46+
require.Error(t, err)
47+
assert.Contains(t, err.Error(), "127.0.0.1:29", "command must dial the staging environment URL")
48+
assert.NotContains(t, err.Error(), "127.0.0.1:9/", "command must not dial the base URL")
49+
}
50+
51+
func TestExtensionListRejectsUnknownEnvironment(t *testing.T) {
52+
setupEnvironmentConfig(t)
53+
54+
environmentName = "nonexistent"
55+
projectExtensionListCmd.SetContext(t.Context())
56+
57+
err := projectExtensionListCmd.RunE(projectExtensionListCmd, []string{})
58+
require.Error(t, err)
59+
assert.Contains(t, err.Error(), `environment "nonexistent" not found`)
60+
}

0 commit comments

Comments
 (0)