Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmd/project/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ import (
"github.com/shopware/shopware-cli/internal/shop"
)

// readConfigWithEnvironment loads the project config with the -e/--env
// environment applied, so Admin API commands target the selected environment.
// Without -e the config is returned as-is: Admin API commands historically
// used the base url/admin_api, and an existing environments.local entry must
// not silently retarget them.
func readConfigWithEnvironment(cmd *cobra.Command, allowFallback bool) (*shop.Config, error) {
cfg, err := shop.ReadConfig(cmd.Context(), projectConfigPath, allowFallback)
if err != nil {
return nil, err
}

if environmentName == "" {
return cfg, nil
}

return cfg.WithEnvironment(environmentName)
}

// resolveExecutor returns the Executor for the current environment.
func resolveExecutor(cmd *cobra.Command, projectRoot string) (executor.Executor, error) {
cfg, err := shop.ReadConfig(cmd.Context(), projectConfigPath, true)
Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var projectAdminApiCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cobraCmd.Context(), projectConfigPath, false); err != nil {
if cfg, err = readConfigWithEnvironment(cobraCmd, false); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_clear_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var projectClearCacheCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, false); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, false); err != nil {
return err
}

Expand Down
145 changes: 145 additions & 0 deletions cmd/project/project_env_resolution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package project

import (
"os"
"path/filepath"
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// writeMinimalPlugin creates a minimal platform plugin the upload command can
// load, so its RunE reaches the environment-resolution step.
func writeMinimalPlugin(t *testing.T) string {
t.Helper()

dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte(`{
"name": "frosh/frosh-test",
"type": "shopware-platform-plugin",
"license": "MIT",
"version": "1.0.0",
"require": { "shopware/core": "~6.6.0" },
"autoload": { "psr-4": { "FroshTest\\": "src/" } },
"extra": {
"shopware-plugin-class": "FroshTest\\FroshTest",
"label": { "de-DE": "Test", "en-GB": "Test" }
}
}`), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".shopware-extension.yml"), []byte(
"build:\n zip:\n composer:\n enabled: false\n assets:\n enabled: false\n",
), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "src"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "src", "FroshTest.php"),
[]byte("<?php\nnamespace FroshTest;\nuse Shopware\\Core\\Framework\\Plugin;\nclass FroshTest extends Plugin {}\n"), 0o644))

return dir
}

// TestReadConfigWithEnvironmentPropagatesConfigError ensures a config-read
// failure is surfaced rather than swallowed by the environment resolution.
func TestReadConfigWithEnvironmentPropagatesConfigError(t *testing.T) {
previousConfigPath := projectConfigPath
previousEnvironmentName := environmentName
t.Cleanup(func() {
projectConfigPath = previousConfigPath
environmentName = previousEnvironmentName
})

projectConfigPath = filepath.Join(t.TempDir(), "does-not-exist.yml")
environmentName = "staging"

cmd := &cobra.Command{}
cmd.SetContext(t.Context())

_, err := readConfigWithEnvironment(cmd, false)
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot find project configuration file")
}

// TestNoEnvFlagKeepsBaseConfig ensures that without -e the base url and
// credentials are used even when an environments.local entry exists, so the
// fix does not silently retarget existing setups.
func TestNoEnvFlagKeepsBaseConfig(t *testing.T) {
configPath := filepath.Join(t.TempDir(), ".shopware-project.yml")
require.NoError(t, os.WriteFile(configPath, []byte(`
url: http://127.0.0.1:9
compatibility_date: "2026-01-01"
admin_api:
client_id: base-id
client_secret: base-secret
environments:
local:
url: http://127.0.0.1:7
`), 0o644))

previousConfigPath := projectConfigPath
previousEnvironmentName := environmentName
projectConfigPath = configPath
environmentName = ""
t.Cleanup(func() {
projectConfigPath = previousConfigPath
environmentName = previousEnvironmentName
})

projectExtensionListCmd.SetContext(t.Context())

err := projectExtensionListCmd.RunE(projectExtensionListCmd, []string{})
require.Error(t, err)
assert.Contains(t, err.Error(), "127.0.0.1:9", "without -e the base URL must be used")
assert.NotContains(t, err.Error(), "127.0.0.1:7", "environments.local must not silently retarget commands")
}

// TestAdminAPICommandsResolveEnvironment verifies every project command that
// talks to a shop honors the -e/--env flag: an unknown environment is rejected,
// and a known environment's URL is the one contacted.
func TestAdminAPICommandsResolveEnvironment(t *testing.T) {
pluginDir := writeMinimalPlugin(t)

cases := []struct {
name string
cmd *cobra.Command
args []string
}{
{"admin-api", projectAdminApiCmd, []string{"GET", "/_info/config"}},
{"clear-cache", projectClearCacheCmd, nil},
{"extension activate", projectExtensionActivateCmd, []string{"Foo"}},
{"extension deactivate", projectExtensionDeactivateCmd, []string{"Foo"}},
{"extension delete", projectExtensionDeleteCmd, []string{"Foo"}},
{"extension install", projectExtensionInstallCmd, []string{"Foo"}},
{"extension list", projectExtensionListCmd, nil},
{"extension outdated", projectExtensionOutdatedCmd, nil},
{"extension uninstall", projectExtensionUninstallCmd, []string{"Foo"}},
{"extension update", projectExtensionUpdateCmd, []string{"Foo"}},
{"extension upload", projectExtensionUploadCmd, []string{pluginDir}},
{"upgrade-check", projectUpgradeCheckCmd, nil},
}

for _, tc := range cases {
t.Run(tc.name+"/rejects unknown environment", func(t *testing.T) {
setupEnvironmentConfig(t)
environmentName = "nonexistent"
tc.cmd.SetContext(t.Context())

err := tc.cmd.RunE(tc.cmd, tc.args)
require.Error(t, err)
assert.Contains(t, err.Error(), `environment "nonexistent" not found`,
"command must reject an unknown environment instead of silently using the base config")
})

t.Run(tc.name+"/targets selected environment", func(t *testing.T) {
setupEnvironmentConfig(t)
environmentName = "staging"
tc.cmd.SetContext(t.Context())

err := tc.cmd.RunE(tc.cmd, tc.args)
require.Error(t, err)
assert.Contains(t, err.Error(), "127.0.0.1:29",
"command must contact the staging environment URL")
assert.NotContains(t, err.Error(), "127.0.0.1:9/",
"command must not contact the base URL when -e staging is given")
})
}
}
2 changes: 1 addition & 1 deletion cmd/project/project_extension_activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var projectExtensionActivateCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, false); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, false); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_extension_deactivate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var projectExtensionDeactivateCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_extension_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var projectExtensionDeleteCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_extension_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var projectExtensionInstallCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_extension_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var projectExtensionListCmd = &cobra.Command{

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

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
60 changes: 60 additions & 0 deletions cmd/project/project_extension_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package project

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func setupEnvironmentConfig(t *testing.T) {
t.Helper()

configPath := filepath.Join(t.TempDir(), ".shopware-project.yml")
require.NoError(t, os.WriteFile(configPath, []byte(`
url: http://127.0.0.1:9
compatibility_date: "2026-01-01"
admin_api:
client_id: base-id
client_secret: base-secret
environments:
staging:
url: http://127.0.0.1:29
admin_api:
client_id: staging-id
client_secret: staging-secret
`), 0o644))

previousConfigPath := projectConfigPath
previousEnvironmentName := environmentName
projectConfigPath = configPath
t.Cleanup(func() {
projectConfigPath = previousConfigPath
environmentName = previousEnvironmentName
})
}

func TestExtensionListTargetsSelectedEnvironment(t *testing.T) {
setupEnvironmentConfig(t)

environmentName = "staging"
projectExtensionListCmd.SetContext(t.Context())

err := projectExtensionListCmd.RunE(projectExtensionListCmd, []string{})
require.Error(t, err)
assert.Contains(t, err.Error(), "127.0.0.1:29", "command must dial the staging environment URL")
assert.NotContains(t, err.Error(), "127.0.0.1:9/", "command must not dial the base URL")
}

func TestExtensionListRejectsUnknownEnvironment(t *testing.T) {
setupEnvironmentConfig(t)

environmentName = "nonexistent"
projectExtensionListCmd.SetContext(t.Context())

err := projectExtensionListCmd.RunE(projectExtensionListCmd, []string{})
require.Error(t, err)
assert.Contains(t, err.Error(), `environment "nonexistent" not found`)
}
2 changes: 1 addition & 1 deletion cmd/project/project_extension_outdated.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var projectExtensionOutdatedCmd = &cobra.Command{

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

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_extension_uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var projectExtensionUninstallCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_extension_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var projectExtensionUpdateCmd = &cobra.Command{
var cfg *shop.Config
var err error

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_extension_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ var projectExtensionUploadCmd = &cobra.Command{
}
}

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/project/project_upgrade_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var projectUpgradeCheckCmd = &cobra.Command{
var shopwareVersion *version.Version
var extensions map[string]string

if cfg, err = shop.ReadConfig(cmd.Context(), projectConfigPath, true); err != nil {
if cfg, err = readConfigWithEnvironment(cmd, true); err != nil {
return err
}

Expand Down
22 changes: 22 additions & 0 deletions internal/shop/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ func (c *Config) ResolveEnvironment(name string) (*EnvironmentConfig, error) {
}, nil
}

// WithEnvironment returns a copy of the config whose URL and Admin API
// credentials come from the named environment, falling back to the base
// values for anything the environment does not override.
func (c *Config) WithEnvironment(name string) (*Config, error) {
env, err := c.ResolveEnvironment(name)
if err != nil {
return nil, err
}

cfg := *c

if env.URL != "" {
cfg.URL = env.URL
}

if env.AdminApi != nil {
cfg.AdminApi = env.AdminApi
}

return &cfg, nil
}

func (c *Config) IsAdminAPIConfigured() bool {
if c.AdminApi == nil {
return false
Expand Down
Loading
Loading