Ctrl K
ComunidadGuías…

cli/secret_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/cli/clitest"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
	"github.com/coder/coder/v2/testutil/expecter"
)

func TestSecretCreate(t *testing.T) {
	t.Parallel()

	t.Run("MissingValue", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(t, "secret", "create", "api-key")
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "secret value must be provided by exactly one of --value or non-interactive stdin (pipe or redirect)")
	})

	t.Run("MissingValueOnTTY", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(t, "--force-tty", "secret", "create", "api-key")
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "secret value must be provided with --value or stdin via pipe or redirect")
	})

	t.Run("SuccessWithValueFlag", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(
			t,
			"secret",
			"create",
			"api-key",
			"--value", "super-secret-value",
			"--description", "API key for workspace tools",
			"--env", "API_KEY",
			"--file", "~/.api-key",
		)
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "api-key")

		secret, err := client.UserSecretByName(ctx, codersdk.Me, "api-key")
		require.NoError(t, err)
		require.Equal(t, "api-key", secret.Name)
		require.Equal(t, "API key for workspace tools", secret.Description)
		require.Equal(t, "API_KEY", secret.EnvName)
		require.Equal(t, "~/.api-key", secret.FilePath)
	})

	t.Run("ValueFlagConflictsWithStdin", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(
			t,
			"secret",
			"create",
			"api-key",
			"--value", "super-secret-value",
		)
		clitest.SetupConfig(t, client, root)
		inv.Stdin = strings.NewReader("different-value")

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "secret value may be provided by only one source, got --value, stdin")
	})

	t.Run("SuccessWithStdin", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(
			t,
			"secret",
			"create",
			"api-key",
			"--description", "API key for workspace tools",
			"--env", "API_KEY",
		)
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)
		inv.Stdin = strings.NewReader("super-secret-value")

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "api-key")

		secret, err := client.UserSecretByName(ctx, codersdk.Me, "api-key")
		require.NoError(t, err)
		require.Equal(t, "api-key", secret.Name)
		require.Equal(t, "API key for workspace tools", secret.Description)
		require.Equal(t, "API_KEY", secret.EnvName)
	})

	t.Run("StdinTrailingNewlineWarnsAndPreservesValue", func(t *testing.T) {
		t.Parallel()

		ownerClient, db := coderdtest.NewWithDatabase(t, nil)
		firstUser := coderdtest.CreateFirstUser(t, ownerClient)
		client, user := coderdtest.CreateAnotherUser(t, ownerClient, firstUser.OrganizationID)

		inv, root := clitest.New(
			t,
			"secret",
			"create",
			"api-key",
			"--description", "API key for workspace tools",
			"--env", "API_KEY",
		)
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)
		inv.Stdin = strings.NewReader("super-secret-value\n")

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "api-key")
		require.Contains(t, output.Stderr(), "secret value from stdin ends with a trailing newline")

		secret, err := db.GetUserSecretByUserIDAndName(
			dbauthz.AsSystemRestricted(ctx),
			database.GetUserSecretByUserIDAndNameParams{
				UserID: user.ID,
				Name:   "api-key",
			},
		)
		require.NoError(t, err)
		require.Equal(t, "super-secret-value\n", secret.Value)
	})

	t.Run("EmptyStdinIsNotProvided", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(t, "secret", "create", "api-key")
		clitest.SetupConfig(t, client, root)
		inv.Stdin = strings.NewReader("")

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "secret value must be provided by exactly one of --value or non-interactive stdin (pipe or redirect)")
	})
}

func TestSecretUpdate(t *testing.T) {
	t.Parallel()

	t.Run("ServerValidationError", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "my-secret",
			Value:   "original-value",
			EnvName: "MY_SECRET",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "update", "my-secret")
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "At least one field must be provided")
	})

	t.Run("AllowsClearingFields", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:        "my-secret",
			Value:       "original-value",
			Description: "original description",
			EnvName:     "MY_SECRET",
			FilePath:    "~/.my-secret",
		})
		require.NoError(t, err)

		// Clearing env_name and description while leaving file_path
		// keeps the secret well-formed (still has an injection
		// target). Trying to clear both env_name and file_path is
		// covered by the server-side test below.
		inv, root := clitest.New(
			t,
			"secret",
			"update",
			"my-secret",
			"--value", "rotated-secret",
			"--description", "",
			"--env", "",
		)
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "my-secret")

		secret, err := client.UserSecretByName(ctx, codersdk.Me, "my-secret")
		require.NoError(t, err)
		require.Equal(t, "", secret.Description)
		require.Equal(t, "", secret.EnvName)
		require.Equal(t, "~/.my-secret", secret.FilePath)
	})

	t.Run("ClearingBothTargetsRejected", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "my-secret",
			Value:   "original-value",
			EnvName: "MY_SECRET",
		})
		require.NoError(t, err)

		inv, root := clitest.New(
			t,
			"secret",
			"update",
			"my-secret",
			"--env", "",
		)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.Error(t, err)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
		require.Len(t, sdkErr.Validations, 1)
		require.Equal(t, "env_name", sdkErr.Validations[0].Field)
		require.Contains(t, sdkErr.Validations[0].Detail, "at least one of env_name or file_path")
	})

	t.Run("UpdatesValueFromEmptyFlag", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "my-secret",
			Value:   "original-value",
			EnvName: "MY_SECRET",
		})
		require.NoError(t, err)

		inv, root := clitest.New(
			t,
			"secret",
			"update",
			"my-secret",
			"--value", "",
		)
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "my-secret")
	})

	t.Run("UpdatesValueFromStdin", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "my-secret",
			Value:   "original-value",
			EnvName: "MY_SECRET",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "update", "my-secret")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)
		inv.Stdin = strings.NewReader("rotated-secret")

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "my-secret")
	})

	t.Run("ValueFlagConflictsWithStdin", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "my-secret",
			Value:   "original-value",
			EnvName: "MY_SECRET",
		})
		require.NoError(t, err)

		inv, root := clitest.New(
			t,
			"secret",
			"update",
			"my-secret",
			"--value", "rotated-secret",
		)
		clitest.SetupConfig(t, client, root)
		inv.Stdin = strings.NewReader("different-value")

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "secret value may be provided by only one source, got --value, stdin")
	})
}

func TestSecretList(t *testing.T) {
	t.Parallel()

	t.Run("TableOutput", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:        "tool-config",
			Value:       "config-value",
			Description: "Tool configuration",
			FilePath:    "~/.config/tool/config.json",
		})
		require.NoError(t, err)
		_, err = client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:        "service-token",
			Value:       "service-token-value",
			Description: "Service access token",
			EnvName:     "SERVICE_TOKEN",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "list")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)

		out := output.Stdout()
		assert.Contains(t, out, "NAME")
		assert.Contains(t, out, "CREATED")
		assert.Contains(t, out, "UPDATED")
		assert.Contains(t, out, "ENV")
		assert.Contains(t, out, "FILE")
		assert.Contains(t, out, "DESCRIPTION")
		assert.Contains(t, out, "service-token")
		assert.Contains(t, out, "SERVICE_TOKEN")
		assert.Contains(t, out, "tool-config")
		assert.Contains(t, out, "~/.config/tool/config.json")
	})

	t.Run("JSONOutput", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:        "service-token",
			Value:       "service-token-value",
			Description: "Service access token",
			EnvName:     "SERVICE_TOKEN",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "list", "--output=json")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)

		var got []codersdk.UserSecret
		require.NoError(t, json.Unmarshal([]byte(output.Stdout()), &got))
		require.Len(t, got, 1)
		require.Equal(t, created, got[0])
	})

	t.Run("SingleSecretTableOutput", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:        "tool-config",
			Value:       "config-value",
			Description: "Tool configuration",
			FilePath:    "~/.config/tool/config.json",
		})
		require.NoError(t, err)
		_, err = client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:        "service-token",
			Value:       "service-token-value",
			Description: "Service access token",
			EnvName:     "SERVICE_TOKEN",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "list", "service-token")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)

		out := output.Stdout()
		assert.Contains(t, out, "NAME")
		assert.Contains(t, out, "CREATED")
		assert.Contains(t, out, "UPDATED")
		assert.Contains(t, out, "ENV")
		assert.Contains(t, out, "FILE")
		assert.Contains(t, out, "DESCRIPTION")
		assert.Contains(t, out, "service-token")
		assert.Contains(t, out, "SERVICE_TOKEN")
		assert.NotContains(t, out, "tool-config")
		assert.NotContains(t, out, "~/.config/tool/config.json")
	})

	t.Run("SingleSecretJSONOutput", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:        "service-token",
			Value:       "service-token-value",
			Description: "Service access token",
			EnvName:     "SERVICE_TOKEN",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "list", "service-token", "--output=json")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)

		var got []codersdk.UserSecret
		require.NoError(t, json.Unmarshal([]byte(output.Stdout()), &got))
		require.Len(t, got, 1)
		require.Equal(t, created, got[0])
	})

	t.Run("EmptyState", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(t, "secret", "list")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.NoError(t, err)
		assert.Contains(t, output.Stderr(), "No secrets found.")
	})
}

func TestSecretListWorkspaceBuild(t *testing.T) {
	t.Parallel()

	client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
	owner := coderdtest.CreateFirstUser(t, client)
	version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
	coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
	template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

	setupCtx := testutil.Context(t, testutil.WaitMedium)
	_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
		Name: "user-token", Value: "u", EnvName: "TOKEN",
	})
	require.NoError(t, err)
	// A user secret and the build secret that replaces it may share a name.
	_, err = client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
		Name: "API_KEY", Value: "u", EnvName: "API_KEY",
	})
	require.NoError(t, err)
	workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
		req.Secrets = []codersdk.WorkspaceSecretInput{
			{Name: "build-token", Value: new("b"), EnvName: "TOKEN"},
			{Name: "API_KEY", Value: new("b"), EnvName: "API_KEY"},
		}
	})
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

	// A workspace owned by another user, which the logged-in owner can read.
	member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	memberWorkspace := coderdtest.CreateWorkspace(t, member, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
		req.Secrets = []codersdk.WorkspaceSecretInput{
			{Name: "member-token", Value: new("m"), EnvName: "TOKEN"},
		}
	})
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, memberWorkspace.LatestBuild.ID)

	cases := []struct {
		name string
		args []string
		env  map[string]string
	}{
		{name: "InsideWorkspace", env: map[string]string{"CODER_WORKSPACE_BUILD_ID": workspace.LatestBuild.ID.String()}},
		{name: "InsideWorkspaceOlderAgent", env: map[string]string{"CODER_WORKSPACE_ID": workspace.ID.String()}},
		{name: "WorkspaceFlag", args: []string{"--workspace", workspace.Name}},
		{name: "BuildIDFlag", args: []string{"--build-id", workspace.LatestBuild.ID.String()}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			inv, root := clitest.New(t, append([]string{"secret", "list"}, tc.args...)...)
			for k, v := range tc.env {
				inv.Environ.Set(k, v)
			}
			output := clitest.Capture(inv)
			clitest.SetupConfig(t, client, root)

			ctx := testutil.Context(t, testutil.WaitMedium)
			require.NoError(t, inv.WithContext(ctx).Run())

			out := output.Stdout()
			assert.Contains(t, out, "SOURCE")
			assert.Contains(t, out, "REPLACED BY")
			// Rows start with the name and source columns, so a build row is
			// distinct from the user row that names it under REPLACED BY.
			assert.Regexp(t, `(?m)^build-token\s+build\s`, out)
			assert.Regexp(t, `(?m)^API_KEY\s+build\s`, out)
			assert.Regexp(t, `(?m)^user-token\s+user\s.*env: build/build-token`, out)
			assert.Regexp(t, `(?m)^API_KEY\s+user\s.*env: build/API_KEY`, out)
			assert.NotContains(t, out, "EPHEMERAL", "ephemeral is hidden by default")
		})
	}

	// Without a build, the build-only columns are not shown.
	t.Run("NoBuildColumns", func(t *testing.T) {
		t.Parallel()

		inv, root := clitest.New(t, "secret", "list")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		require.NoError(t, inv.WithContext(ctx).Run())
		assert.Contains(t, output.Stdout(), "user-token")
		assert.NotContains(t, output.Stdout(), "SOURCE")
		assert.NotContains(t, output.Stdout(), "REPLACED BY")
	})

	t.Run("EphemeralColumn", func(t *testing.T) {
		t.Parallel()

		inv, root := clitest.New(t, "secret", "list", "--build-id", workspace.LatestBuild.ID.String(), "-c", "name,source,ephemeral")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		require.NoError(t, inv.WithContext(ctx).Run())
		assert.Contains(t, output.Stdout(), "EPHEMERAL")
		assert.NotContains(t, output.Stdout(), "REPLACED BY", "explicit columns are kept")
	})

	// Inside a workspace that the logged-in deployment or user cannot see,
	// only user secrets are listed.
	for _, envName := range []string{"CODER_WORKSPACE_BUILD_ID", "CODER_WORKSPACE_ID"} {
		t.Run("UnknownCurrentWorkspace/"+envName, func(t *testing.T) {
			t.Parallel()

			inv, root := clitest.New(t, "secret", "list")
			inv.Environ.Set(envName, uuid.NewString())
			output := clitest.Capture(inv)
			clitest.SetupConfig(t, client, root)

			ctx := testutil.Context(t, testutil.WaitMedium)
			require.NoError(t, inv.WithContext(ctx).Run())
			assert.Contains(t, output.Stderr(), "only that user's secrets are listed")
			assert.Contains(t, output.Stdout(), "user-token")
			assert.NotContains(t, output.Stdout(), "build-token")
		})
	}

	t.Run("UnknownBuildIDFlag", func(t *testing.T) {
		t.Parallel()

		inv, root := clitest.New(t, "secret", "list", "--build-id", uuid.NewString())
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		var apiErr *codersdk.Error
		require.ErrorAs(t, err, &apiErr)
		require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
	})

	// Another user's build from the workspace environment is skipped
	// silently, so only the logged-in user's secrets are listed.
	t.Run("OtherOwnerFromEnv", func(t *testing.T) {
		t.Parallel()

		inv, root := clitest.New(t, "secret", "list")
		inv.Environ.Set("CODER_WORKSPACE_BUILD_ID", memberWorkspace.LatestBuild.ID.String())
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		require.NoError(t, inv.WithContext(ctx).Run())
		assert.Empty(t, output.Stderr())
		assert.Contains(t, output.Stdout(), "user-token")
		assert.NotContains(t, output.Stdout(), "member-token")
	})

	// An explicitly requested build of another user's workspace fails.
	t.Run("OtherOwnerBuildIDFlag", func(t *testing.T) {
		t.Parallel()

		inv, root := clitest.New(t, "secret", "list", "--build-id", memberWorkspace.LatestBuild.ID.String())
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "belongs to")
	})

	t.Run("NameWithFlag", func(t *testing.T) {
		t.Parallel()

		inv, root := clitest.New(t, "secret", "list", "user-token", "--build-id", workspace.LatestBuild.ID.String())
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		require.ErrorContains(t, inv.WithContext(ctx).Run(), "cannot be used with a secret name")
	})
}

func TestSecretDelete(t *testing.T) {
	t.Parallel()

	t.Run("Success", func(t *testing.T) {
		t.Parallel()

		logger := testutil.Logger(t)
		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "service-token",
			Value:   "service-token-value",
			EnvName: "SERVICE_TOKEN",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "delete", "service-token")
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		inv = inv.WithContext(ctx)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		waiter := clitest.StartWithWaiter(t, inv)
		stdout.ExpectMatch(ctx, "Delete secret")
		stdout.ExpectMatch(ctx, "service-token")
		stdin.WriteLine("yes")
		stdout.ExpectMatch(ctx, "Deleted secret")

		require.NoError(t, waiter.Wait())

		_, err = client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
		require.Error(t, err)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
	})

	t.Run("YesSkipsPrompt", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "service-token",
			Value:   "service-token-value",
			EnvName: "SERVICE_TOKEN",
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "secret", "delete", "service-token", "--yes")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "Deleted secret")
		require.NotContains(t, output.Stdout(), "Delete secret")
		require.Empty(t, output.Stderr())

		_, err = client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
		require.Error(t, err)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
	})

	t.Run("NotFound", func(t *testing.T) {
		t.Parallel()

		logger := testutil.Logger(t)
		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(t, "secret", "delete", "missing-secret")
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		inv = inv.WithContext(ctx)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		waiter := clitest.StartWithWaiter(t, inv)
		stdout.ExpectMatch(ctx, "Delete secret")
		stdout.ExpectMatch(ctx, "missing-secret")
		stdin.WriteLine("yes")

		err := waiter.Wait()
		require.ErrorContains(t, err, `delete secret "missing-secret"`)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
	})
}

func TestSecretImport(t *testing.T) {
	t.Parallel()

	writeSecretsFile := func(t *testing.T, name, content string) string {
		t.Helper()

		path := filepath.Join(t.TempDir(), name)
		require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
		return path
	}

	t.Run("InfersFormatFromExtension", func(t *testing.T) {
		t.Parallel()

		tests := []struct {
			name    string
			file    string
			content string
		}{
			{name: "Env", file: "secrets.env", content: "ALPHA=a\nBETA=b\n"},
			{name: "JSON", file: "secrets.json", content: `{"ALPHA":"a","BETA":"b"}`},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				t.Parallel()

				client := coderdtest.New(t, nil)
				_ = coderdtest.CreateFirstUser(t, client)

				inv, root := clitest.New(t, "secret", "import", writeSecretsFile(t, tt.file, tt.content))
				output := clitest.Capture(inv)
				clitest.SetupConfig(t, client, root)

				ctx := testutil.Context(t, testutil.WaitMedium)
				require.NoError(t, inv.WithContext(ctx).Run())
				require.Contains(t, output.Stdout(), "Imported 2 secrets.")
				require.NotContains(t, output.Stderr(), "without an environment variable name")

				secret, err := client.UserSecretByName(ctx, codersdk.Me, "ALPHA")
				require.NoError(t, err)
				require.Equal(t, "ALPHA", secret.EnvName)
			})
		}
	})

	// The flag wins over the extension, and its value is matched
	// case-insensitively.
	t.Run("InputFormatOverridesExtension", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		path := writeSecretsFile(t, "secrets.json", "ALPHA=a\n")
		inv, root := clitest.New(t, "secret", "import", path, "--input-format", "ENV")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		require.NoError(t, inv.WithContext(ctx).Run())
		require.Contains(t, output.Stdout(), "Imported 1 secret.")

		_, err := client.UserSecretByName(ctx, codersdk.Me, "ALPHA")
		require.NoError(t, err)
	})

	t.Run("Stdin", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(t, "secret", "import", "-", "--input-format", "env")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)
		inv.Stdin = strings.NewReader("ALPHA=a\n")

		ctx := testutil.Context(t, testutil.WaitMedium)
		require.NoError(t, inv.WithContext(ctx).Run())
		require.Contains(t, output.Stdout(), "Imported 1 secret.")

		_, err := client.UserSecretByName(ctx, codersdk.Me, "ALPHA")
		require.NoError(t, err)
	})

	t.Run("UnknownExtension", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		path := writeSecretsFile(t, "secrets.txt", "ALPHA=a\n")
		inv, root := clitest.New(t, "secret", "import", path)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.ErrorContains(t, err, "set --input-format to one of: env, json, yaml")
	})

	t.Run("RejectsLocalErrorsBeforeSending", func(t *testing.T) {
		t.Parallel()

		tests := []struct {
			name    string
			content []byte
			wantErr string
		}{
			{name: "Malformed", content: []byte("ALPHA=a\nNOEQUALS\n"), wantErr: "line 2: expected KEY=VALUE"},
			{name: "InvalidUTF8", content: []byte{'A', 'L', 'P', 'H', 'A', '=', 0xff}, wantErr: "must contain valid UTF-8"},
			{name: "InvalidEntry", content: []byte("ALPHA=a\nBETA=\n"), wantErr: `secret 2 ("BETA") value: Value is required.`},
		}
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				t.Parallel()

				var requests atomic.Int64
				server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					requests.Add(1)
					w.WriteHeader(http.StatusInternalServerError)
				}))
				defer server.Close()

				client := codersdk.New(must(url.Parse(server.URL)))
				client.SetSessionToken("test-token")
				path := filepath.Join(t.TempDir(), "secrets.env")
				require.NoError(t, os.WriteFile(path, tt.content, 0o600))
				inv, root := clitest.New(t, "secret", "import", path)
				clitest.SetupConfig(t, client, root)

				err := inv.Run()
				require.ErrorContains(t, err, tt.wantErr)
				require.Zero(t, requests.Load(), "expected no API request")
			})
		}
	})

	t.Run("RejectsTTYStdin", func(t *testing.T) {
		t.Parallel()

		var requests atomic.Int64
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			requests.Add(1)
			w.WriteHeader(http.StatusInternalServerError)
		}))
		defer server.Close()

		client := codersdk.New(must(url.Parse(server.URL)))
		client.SetSessionToken("test-token")
		inv, root := clitest.New(t, "--force-tty", "secret", "import", "-", "--input-format", "env")
		clitest.SetupConfig(t, client, root)

		err := inv.Run()
		require.ErrorContains(t, err, "secrets file must be provided via non-interactive stdin (pipe or redirect)")
		require.Zero(t, requests.Load(), "expected no API request")
	})

	t.Run("WarnsAboutKeysWithoutEnvNames", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		// PATH is reserved and MY-TOKEN is not a valid identifier, so both are
		// imported without an env name.
		require.Error(t, codersdk.UserSecretEnvNameValid("PATH"))
		path := writeSecretsFile(t, "secrets.env", "ALPHA=a\nPATH=b\nMY-TOKEN=c\n")
		inv, root := clitest.New(t, "secret", "import", path)
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		require.NoError(t, inv.WithContext(ctx).Run())
		require.Contains(t, output.Stdout(), "Imported 3 secrets.")
		require.Contains(t, output.Stderr(), `2 secrets imported without an environment variable name: "PATH", "MY-TOKEN"`)

		secret, err := client.UserSecretByName(ctx, codersdk.Me, "PATH")
		require.NoError(t, err)
		require.Empty(t, secret.EnvName)
	})
}

func TestSecretEnableDisable(t *testing.T) {
	t.Parallel()

	t.Run("Disable", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "service-token",
			Value:   "service-token-value",
			EnvName: "SERVICE_TOKEN",
		})
		require.NoError(t, err)
		require.True(t, created.Enabled)

		inv, root := clitest.New(t, "secret", "disable", "service-token")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "Disabled secret")
		require.Contains(t, output.Stdout(), "service-token")

		got, err := client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
		require.NoError(t, err)
		assert.False(t, got.Enabled)
	})

	t.Run("Enable", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		setupCtx := testutil.Context(t, testutil.WaitMedium)
		disabled := false
		created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
			Name:    "service-token",
			Value:   "service-token-value",
			EnvName: "SERVICE_TOKEN",
			Enabled: &disabled,
		})
		require.NoError(t, err)
		require.False(t, created.Enabled)

		inv, root := clitest.New(t, "secret", "enable", "service-token")
		output := clitest.Capture(inv)
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)
		require.Contains(t, output.Stdout(), "Enabled secret")
		require.Contains(t, output.Stdout(), "service-token")

		got, err := client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
		require.NoError(t, err)
		assert.True(t, got.Enabled)
	})

	t.Run("NotFound", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, nil)
		_ = coderdtest.CreateFirstUser(t, client)

		inv, root := clitest.New(t, "secret", "disable", "missing-secret")
		clitest.SetupConfig(t, client, root)

		ctx := testutil.Context(t, testutil.WaitMedium)
		err := inv.WithContext(ctx).Run()
		require.Error(t, err)
		var sdkErr *codersdk.Error
		require.ErrorAs(t, err, &sdkErr)
		require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
	})
}