Skip to content
Merged
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
17 changes: 17 additions & 0 deletions pkg/cmd/secret/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ func actionRun(opts *Options) error {
if err != nil {
return err
}
if opts.ID != "" {
id := strings.TrimSpace(opts.ID)
if id == "" {
return fmt.Errorf("secret provider id is required; use a positional arg or --id")
}
Comment on lines +82 to +86
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In file mode you now trim/validate opts.ID, but in the non-file path the only check is opts.ID == "". That means a7 secret create " " --uri ... (or --id " ") will pass local validation and fail later at the API, which is inconsistent with the stricter behavior added for file-based create. Consider normalizing/validating the ID (trim + non-empty) once and reusing it for both file and non-file modes (and ideally rejecting non-string payload["id"] rather than fmt.Sprint-coercing it).

Copilot uses AI. Check for mistakes.
payload["id"] = id
} else {
id, ok := payload["id"]
if !ok || id == nil {
return fmt.Errorf("secret provider id is required; use a positional arg or --id")
}
trimmedID := strings.TrimSpace(fmt.Sprint(id))
if trimmedID == "" {
return fmt.Errorf("secret provider id is required; use a positional arg or --id")
}
payload["id"] = trimmedID
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +89 to +98
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the new ID enforcement/normalization, payload["id"] is always set for --file runs (either from opts.ID or validated from the file). The later if id, ok := payload["id"]; ok { ... } else { ... } means the POST branch is now effectively unreachable in file mode. Consider simplifying this path to always issue the PUT-by-id request once the ID is guaranteed, to reduce dead/duplicated control flow.

Copilot uses AI. Check for mistakes.

httpClient, err := opts.Client()
if err != nil {
Expand Down
88 changes: 88 additions & 0 deletions pkg/cmd/secret/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package create
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"testing"

"github.com/api7/a7/internal/config"
Expand Down Expand Up @@ -64,6 +66,92 @@ func TestCreateSecret_JSON(t *testing.T) {
registry.Verify(t)
}

func TestCreateSecret_FileUsesPositionalID(t *testing.T) {
ios, _, out, _ := iostreams.Test()
registry := &httpmock.Registry{}
registry.Register(http.MethodPut, "/apisix/admin/secret_providers/vault/s1", httpmock.JSONResponse(`{"id":"vault/s1","uri":"http://vault","prefix":"kv"}`))

file := filepath.Join(t.TempDir(), "secret.json")
if err := os.WriteFile(file, []byte(`{"uri":"http://vault","prefix":"kv","token":"tok"}`), 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}

opts := &Options{
IO: ios,
Client: func() (*http.Client, error) { return registry.GetClient(), nil },
Config: func() (config.Config, error) {
return &mockConfig{baseURL: "http://api.local", gatewayGroup: "gg1"}, nil
},
GatewayGroup: "gg1",
ID: "vault/s1",
File: file,
}

if err := actionRun(opts); err != nil {
t.Fatalf("actionRun failed: %v", err)
}

var item api.Secret
if err := json.Unmarshal(out.Bytes(), &item); err != nil {
t.Fatalf("failed to parse output: %v", err)
}
if item.ID != "vault/s1" {
t.Fatalf("expected positional id in output, got: %+v", item)
}

registry.Verify(t)
}

func TestCreateSecret_FileRequiresID(t *testing.T) {
ios, _, _, _ := iostreams.Test()
registry := &httpmock.Registry{}

file := filepath.Join(t.TempDir(), "secret.json")
if err := os.WriteFile(file, []byte(`{"uri":"http://vault","prefix":"kv","token":"tok"}`), 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}

err := actionRun(&Options{
IO: ios,
Client: func() (*http.Client, error) { return registry.GetClient(), nil },
Config: func() (config.Config, error) {
return &mockConfig{baseURL: "http://api.local", gatewayGroup: "gg1"}, nil
},
GatewayGroup: "gg1",
File: file,
})
if err == nil || err.Error() != "secret provider id is required; use a positional arg or --id" {
t.Fatalf("expected --id required error, got: %v", err)
}

registry.Verify(t)
}

func TestCreateSecret_FileRejectsWhitespaceID(t *testing.T) {
ios, _, _, _ := iostreams.Test()
registry := &httpmock.Registry{}

file := filepath.Join(t.TempDir(), "secret.json")
if err := os.WriteFile(file, []byte(`{"id":" ","uri":"http://vault","prefix":"kv","token":"tok"}`), 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}

err := actionRun(&Options{
IO: ios,
Client: func() (*http.Client, error) { return registry.GetClient(), nil },
Config: func() (config.Config, error) {
return &mockConfig{baseURL: "http://api.local", gatewayGroup: "gg1"}, nil
},
GatewayGroup: "gg1",
File: file,
})
if err == nil || err.Error() != "secret provider id is required; use a positional arg or --id" {
t.Fatalf("expected --id required error, got: %v", err)
}

registry.Verify(t)
}

func TestCreateSecret_RequiresID(t *testing.T) {
ios, _, _, _ := iostreams.Test()
registry := &httpmock.Registry{}
Expand Down
8 changes: 4 additions & 4 deletions test/e2e/secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ func TestSecret_CRUD(t *testing.T) {

// Create
stdout, stderr, err := runA7WithEnv(env, "secret", "create", secretID, "-f", tmpFile, "-g", gatewayGroup)
if err != nil {
t.Skip("secret create failed (vault may not be configured)")
}
require.NoError(t, err, "secret create failed: stdout=%s stderr=%s", stdout, stderr)

// Get
stdout, stderr, err = runA7WithEnv(env, "secret", "get", secretID, "-g", gatewayGroup)
Expand All @@ -77,7 +75,9 @@ func TestSecret_CRUD(t *testing.T) {
// Get JSON
var secret map[string]interface{}
runA7JSON(t, env, &secret, "secret", "get", secretID, "-g", gatewayGroup, "-o", "json")
assert.Equal(t, secretID, secret["id"])
// The runtime secret provider API returns the provider-local ID; the CLI
// still uses the compound ID for addressing resources.
assert.Equal(t, "e2e-secret-crud", secret["id"])
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The E2E JSON assertion now expects only the suffix ("e2e-secret-crud"), but the CLI/API elsewhere (e.g., pkg/cmd/secret/get tests) treats id as the full compound ID ("vault/"). Since secretID is "vault/e2e-secret-crud", this will likely fail or mask regressions. Consider asserting against secretID (or explicitly document/justify why the API returns a stripped id here).

Suggested change
assert.Equal(t, "e2e-secret-crud", secret["id"])
assert.Equal(t, secretID, secret["id"])

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +80
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion hard-codes the provider-local ID ("e2e-secret-crud") while the test defines secretID as the compound manager/id. To avoid the test becoming inconsistent if secretID changes, derive the expected provider-local ID from secretID (e.g., split on the first '/').

Copilot uses AI. Check for mistakes.
assert.Equal(t, "https://vault.example.com", secret["uri"])
assert.Equal(t, "kv/apisix", secret["prefix"])
Comment on lines +80 to 82
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Expect the full secret id here, or normalize it in the command first.

pkg/cmd/secret/get/get.go returns item.ID as the API sent it, and pkg/api/types_secret.go unmarshals id unchanged. Without a separate normalization layer, this assertion should expect vault/e2e-secret-crud, not just e2e-secret-crud.

Suggested fix
-	assert.Equal(t, "e2e-secret-crud", secret["id"])
+	assert.Equal(t, "vault/e2e-secret-crud", secret["id"])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert.Equal(t, "e2e-secret-crud", secret["id"])
assert.Equal(t, "https://vault.example.com", secret["uri"])
assert.Equal(t, "kv/apisix", secret["prefix"])
assert.Equal(t, "vault/e2e-secret-crud", secret["id"])
assert.Equal(t, "https://vault.example.com", secret["uri"])
assert.Equal(t, "kv/apisix", secret["prefix"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/secret_test.go` around lines 78 - 80, The test asserts the short
secret id but the command returns the raw API id (prefixed); update the
expectation in test/e2e/secret_test.go to assert the full id
"vault/e2e-secret-crud" (or alternatively add normalization in
pkg/cmd/secret/get/get.go to strip the "vault/" prefix before printing),
referencing the code path that uses item.ID and the unmarshaling in
pkg/api/types_secret.go so the test matches the actual output.


Expand Down
Loading