mirror of
https://github.com/avivsinai/bitbucket-cli.git
synced 2026-09-19 07:13:00 +08:00
feat(cmd): add variable commands for pipeline variable management
This commit is contained in:
@@ -3,9 +3,12 @@
|
||||
|
||||
# Go source files
|
||||
*.go text eol=lf
|
||||
*.mod text eol=lf
|
||||
*.sum text eol=lf
|
||||
|
||||
# Shell scripts
|
||||
*.sh text eol=lf
|
||||
*.bash text eol=lf
|
||||
|
||||
# Batch files (Windows)
|
||||
*.bat text eol=crlf
|
||||
@@ -20,6 +23,9 @@
|
||||
|
||||
# Makefiles
|
||||
Makefile text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
.gitignore text eol=lf
|
||||
.gitattributes text eol=lf
|
||||
|
||||
# Binary files
|
||||
*.png binary
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
package bbcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// PipelineVariable represents a Bitbucket Cloud pipeline variable.
|
||||
type PipelineVariable struct {
|
||||
UUID string `json:"uuid"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Secured bool `json:"secured"`
|
||||
}
|
||||
|
||||
// VariableListOptions configures variable list requests.
|
||||
type VariableListOptions struct {
|
||||
Limit int
|
||||
}
|
||||
|
||||
type variableListPage struct {
|
||||
Values []PipelineVariable `json:"values"`
|
||||
Next string `json:"next"`
|
||||
}
|
||||
|
||||
// ListRepositoryVariables lists pipeline variables for a repository.
|
||||
func (c *Client) ListRepositoryVariables(ctx context.Context, workspace, repoSlug string, opts VariableListOptions) ([]PipelineVariable, error) {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return nil, fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
|
||||
pageLen := opts.Limit
|
||||
if pageLen <= 0 || pageLen > 100 {
|
||||
pageLen = 100
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/pipelines_config/variables?pagelen=%d",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
pageLen,
|
||||
)
|
||||
|
||||
var variables []PipelineVariable
|
||||
for path != "" {
|
||||
req, err := c.http.NewRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var page variableListPage
|
||||
if err := c.http.Do(req, &page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variables = append(variables, page.Values...)
|
||||
|
||||
if opts.Limit > 0 && len(variables) >= opts.Limit {
|
||||
variables = variables[:opts.Limit]
|
||||
break
|
||||
}
|
||||
|
||||
if page.Next == "" {
|
||||
break
|
||||
}
|
||||
|
||||
nextURL, err := url.Parse(page.Next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path = nextURL.RequestURI()
|
||||
}
|
||||
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
// CreateRepositoryVariableInput configures repository variable creation.
|
||||
type CreateRepositoryVariableInput struct {
|
||||
Key string
|
||||
Value string
|
||||
Secured bool
|
||||
}
|
||||
|
||||
// CreateRepositoryVariable creates a pipeline variable for a repository.
|
||||
func (c *Client) CreateRepositoryVariable(ctx context.Context, workspace, repoSlug string, input CreateRepositoryVariableInput) (*PipelineVariable, error) {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return nil, fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
if input.Key == "" {
|
||||
return nil, fmt.Errorf("variable key is required")
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"key": input.Key,
|
||||
"value": input.Value,
|
||||
"secured": input.Secured,
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/pipelines_config/variables",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "POST", path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var variable PipelineVariable
|
||||
if err := c.http.Do(req, &variable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &variable, nil
|
||||
}
|
||||
|
||||
// UpdateRepositoryVariableInput configures repository variable updates.
|
||||
type UpdateRepositoryVariableInput struct {
|
||||
Key string
|
||||
Value string
|
||||
Secured bool
|
||||
}
|
||||
|
||||
// UpdateRepositoryVariable updates a pipeline variable for a repository.
|
||||
// The variableUUID identifies the variable to update.
|
||||
func (c *Client) UpdateRepositoryVariable(ctx context.Context, workspace, repoSlug, variableUUID string, input UpdateRepositoryVariableInput) (*PipelineVariable, error) {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return nil, fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
if variableUUID == "" {
|
||||
return nil, fmt.Errorf("variable UUID is required")
|
||||
}
|
||||
if input.Key == "" {
|
||||
return nil, fmt.Errorf("variable key is required")
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"key": input.Key,
|
||||
"value": input.Value,
|
||||
"secured": input.Secured,
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/pipelines_config/variables/%s",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
url.PathEscape(variableUUID),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "PUT", path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var variable PipelineVariable
|
||||
if err := c.http.Do(req, &variable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &variable, nil
|
||||
}
|
||||
|
||||
// DeleteRepositoryVariable deletes a pipeline variable from a repository.
|
||||
func (c *Client) DeleteRepositoryVariable(ctx context.Context, workspace, repoSlug, variableUUID string) error {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
if variableUUID == "" {
|
||||
return fmt.Errorf("variable UUID is required")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/pipelines_config/variables/%s",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
url.PathEscape(variableUUID),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.http.Do(req, nil)
|
||||
}
|
||||
|
||||
// --- Workspace-level variable methods ---
|
||||
|
||||
// ListWorkspaceVariables lists pipeline variables for a workspace.
|
||||
func (c *Client) ListWorkspaceVariables(ctx context.Context, workspace string, opts VariableListOptions) ([]PipelineVariable, error) {
|
||||
if workspace == "" {
|
||||
return nil, fmt.Errorf("workspace is required")
|
||||
}
|
||||
|
||||
pageLen := opts.Limit
|
||||
if pageLen <= 0 || pageLen > 100 {
|
||||
pageLen = 100
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/workspaces/%s/pipelines-config/variables?pagelen=%d",
|
||||
url.PathEscape(workspace),
|
||||
pageLen,
|
||||
)
|
||||
|
||||
var variables []PipelineVariable
|
||||
for path != "" {
|
||||
req, err := c.http.NewRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var page variableListPage
|
||||
if err := c.http.Do(req, &page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variables = append(variables, page.Values...)
|
||||
|
||||
if opts.Limit > 0 && len(variables) >= opts.Limit {
|
||||
variables = variables[:opts.Limit]
|
||||
break
|
||||
}
|
||||
|
||||
if page.Next == "" {
|
||||
break
|
||||
}
|
||||
|
||||
nextURL, err := url.Parse(page.Next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path = nextURL.RequestURI()
|
||||
}
|
||||
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
// CreateWorkspaceVariableInput configures workspace variable creation.
|
||||
type CreateWorkspaceVariableInput struct {
|
||||
Key string
|
||||
Value string
|
||||
Secured bool
|
||||
}
|
||||
|
||||
// CreateWorkspaceVariable creates a pipeline variable for a workspace.
|
||||
func (c *Client) CreateWorkspaceVariable(ctx context.Context, workspace string, input CreateWorkspaceVariableInput) (*PipelineVariable, error) {
|
||||
if workspace == "" {
|
||||
return nil, fmt.Errorf("workspace is required")
|
||||
}
|
||||
if input.Key == "" {
|
||||
return nil, fmt.Errorf("variable key is required")
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"key": input.Key,
|
||||
"value": input.Value,
|
||||
"secured": input.Secured,
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/workspaces/%s/pipelines-config/variables",
|
||||
url.PathEscape(workspace),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "POST", path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var variable PipelineVariable
|
||||
if err := c.http.Do(req, &variable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &variable, nil
|
||||
}
|
||||
|
||||
// UpdateWorkspaceVariableInput configures workspace variable updates.
|
||||
type UpdateWorkspaceVariableInput struct {
|
||||
Key string
|
||||
Value string
|
||||
Secured bool
|
||||
}
|
||||
|
||||
// UpdateWorkspaceVariable updates a pipeline variable for a workspace.
|
||||
func (c *Client) UpdateWorkspaceVariable(ctx context.Context, workspace, variableUUID string, input UpdateWorkspaceVariableInput) (*PipelineVariable, error) {
|
||||
if workspace == "" {
|
||||
return nil, fmt.Errorf("workspace is required")
|
||||
}
|
||||
if variableUUID == "" {
|
||||
return nil, fmt.Errorf("variable UUID is required")
|
||||
}
|
||||
if input.Key == "" {
|
||||
return nil, fmt.Errorf("variable key is required")
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"key": input.Key,
|
||||
"value": input.Value,
|
||||
"secured": input.Secured,
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/workspaces/%s/pipelines-config/variables/%s",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(variableUUID),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "PUT", path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var variable PipelineVariable
|
||||
if err := c.http.Do(req, &variable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &variable, nil
|
||||
}
|
||||
|
||||
// DeleteWorkspaceVariable deletes a pipeline variable from a workspace.
|
||||
func (c *Client) DeleteWorkspaceVariable(ctx context.Context, workspace, variableUUID string) error {
|
||||
if workspace == "" {
|
||||
return fmt.Errorf("workspace is required")
|
||||
}
|
||||
if variableUUID == "" {
|
||||
return fmt.Errorf("variable UUID is required")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/workspaces/%s/pipelines-config/variables/%s",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(variableUUID),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.http.Do(req, nil)
|
||||
}
|
||||
|
||||
// --- Deployment environment variable methods ---
|
||||
|
||||
// DeploymentEnvironment represents a deployment environment in Bitbucket Cloud.
|
||||
type DeploymentEnvironment struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
EnvironmentType struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"environment_type"`
|
||||
}
|
||||
|
||||
type deploymentEnvironmentListPage struct {
|
||||
Values []DeploymentEnvironment `json:"values"`
|
||||
Next string `json:"next"`
|
||||
}
|
||||
|
||||
// ListDeploymentEnvironments lists deployment environments for a repository.
|
||||
func (c *Client) ListDeploymentEnvironments(ctx context.Context, workspace, repoSlug string) ([]DeploymentEnvironment, error) {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return nil, fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/environments?pagelen=100",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
)
|
||||
|
||||
var environments []DeploymentEnvironment
|
||||
for path != "" {
|
||||
req, err := c.http.NewRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var page deploymentEnvironmentListPage
|
||||
if err := c.http.Do(req, &page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
environments = append(environments, page.Values...)
|
||||
|
||||
if page.Next == "" {
|
||||
break
|
||||
}
|
||||
|
||||
nextURL, err := url.Parse(page.Next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path = nextURL.RequestURI()
|
||||
}
|
||||
|
||||
return environments, nil
|
||||
}
|
||||
|
||||
// ListDeploymentVariables lists pipeline variables for a deployment environment.
|
||||
func (c *Client) ListDeploymentVariables(ctx context.Context, workspace, repoSlug, environmentUUID string, opts VariableListOptions) ([]PipelineVariable, error) {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return nil, fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
if environmentUUID == "" {
|
||||
return nil, fmt.Errorf("environment UUID is required")
|
||||
}
|
||||
|
||||
pageLen := opts.Limit
|
||||
if pageLen <= 0 || pageLen > 100 {
|
||||
pageLen = 100
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/deployments_config/environments/%s/variables?pagelen=%d",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
url.PathEscape(environmentUUID),
|
||||
pageLen,
|
||||
)
|
||||
|
||||
var variables []PipelineVariable
|
||||
for path != "" {
|
||||
req, err := c.http.NewRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var page variableListPage
|
||||
if err := c.http.Do(req, &page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variables = append(variables, page.Values...)
|
||||
|
||||
if opts.Limit > 0 && len(variables) >= opts.Limit {
|
||||
variables = variables[:opts.Limit]
|
||||
break
|
||||
}
|
||||
|
||||
if page.Next == "" {
|
||||
break
|
||||
}
|
||||
|
||||
nextURL, err := url.Parse(page.Next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path = nextURL.RequestURI()
|
||||
}
|
||||
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
// CreateDeploymentVariableInput configures deployment variable creation.
|
||||
type CreateDeploymentVariableInput struct {
|
||||
Key string
|
||||
Value string
|
||||
Secured bool
|
||||
}
|
||||
|
||||
// CreateDeploymentVariable creates a pipeline variable for a deployment environment.
|
||||
func (c *Client) CreateDeploymentVariable(ctx context.Context, workspace, repoSlug, environmentUUID string, input CreateDeploymentVariableInput) (*PipelineVariable, error) {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return nil, fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
if environmentUUID == "" {
|
||||
return nil, fmt.Errorf("environment UUID is required")
|
||||
}
|
||||
if input.Key == "" {
|
||||
return nil, fmt.Errorf("variable key is required")
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"key": input.Key,
|
||||
"value": input.Value,
|
||||
"secured": input.Secured,
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/deployments_config/environments/%s/variables",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
url.PathEscape(environmentUUID),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "POST", path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var variable PipelineVariable
|
||||
if err := c.http.Do(req, &variable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &variable, nil
|
||||
}
|
||||
|
||||
// UpdateDeploymentVariableInput configures deployment variable updates.
|
||||
type UpdateDeploymentVariableInput struct {
|
||||
Key string
|
||||
Value string
|
||||
Secured bool
|
||||
}
|
||||
|
||||
// UpdateDeploymentVariable updates a pipeline variable for a deployment environment.
|
||||
func (c *Client) UpdateDeploymentVariable(ctx context.Context, workspace, repoSlug, environmentUUID, variableUUID string, input UpdateDeploymentVariableInput) (*PipelineVariable, error) {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return nil, fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
if environmentUUID == "" {
|
||||
return nil, fmt.Errorf("environment UUID is required")
|
||||
}
|
||||
if variableUUID == "" {
|
||||
return nil, fmt.Errorf("variable UUID is required")
|
||||
}
|
||||
if input.Key == "" {
|
||||
return nil, fmt.Errorf("variable key is required")
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"key": input.Key,
|
||||
"value": input.Value,
|
||||
"secured": input.Secured,
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/deployments_config/environments/%s/variables/%s",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
url.PathEscape(environmentUUID),
|
||||
url.PathEscape(variableUUID),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "PUT", path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var variable PipelineVariable
|
||||
if err := c.http.Do(req, &variable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &variable, nil
|
||||
}
|
||||
|
||||
// DeleteDeploymentVariable deletes a pipeline variable from a deployment environment.
|
||||
func (c *Client) DeleteDeploymentVariable(ctx context.Context, workspace, repoSlug, environmentUUID, variableUUID string) error {
|
||||
if workspace == "" || repoSlug == "" {
|
||||
return fmt.Errorf("workspace and repository slug are required")
|
||||
}
|
||||
if environmentUUID == "" {
|
||||
return fmt.Errorf("environment UUID is required")
|
||||
}
|
||||
if variableUUID == "" {
|
||||
return fmt.Errorf("variable UUID is required")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repositories/%s/%s/deployments_config/environments/%s/variables/%s",
|
||||
url.PathEscape(workspace),
|
||||
url.PathEscape(repoSlug),
|
||||
url.PathEscape(environmentUUID),
|
||||
url.PathEscape(variableUUID),
|
||||
)
|
||||
|
||||
req, err := c.http.NewRequest(ctx, "DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.http.Do(req, nil)
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package bbcloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListRepositoryVariablesValidation(t *testing.T) {
|
||||
client, err := New(Options{BaseURL: "https://api.bitbucket.org/2.0"})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workspace string
|
||||
repoSlug string
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "missing workspace",
|
||||
workspace: "",
|
||||
repoSlug: "repo",
|
||||
errorContains: "workspace and repository slug are required",
|
||||
},
|
||||
{
|
||||
name: "missing repo slug",
|
||||
workspace: "workspace",
|
||||
repoSlug: "",
|
||||
errorContains: "workspace and repository slug are required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := client.ListRepositoryVariables(ctx, tt.workspace, tt.repoSlug, VariableListOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tt.errorContains)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errorContains) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.errorContains, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRepositoryVariablesPagination(t *testing.T) {
|
||||
var requestCount int
|
||||
var serverURL string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestCount++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch requestCount {
|
||||
case 1:
|
||||
resp := variableListPage{
|
||||
Values: []PipelineVariable{
|
||||
{UUID: "{uuid-1}", Key: "VAR1", Value: "value1"},
|
||||
{UUID: "{uuid-2}", Key: "VAR2", Value: "value2"},
|
||||
},
|
||||
Next: serverURL + "/repositories/ws/repo/pipelines_config/variables?page=2",
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
case 2:
|
||||
resp := variableListPage{
|
||||
Values: []PipelineVariable{
|
||||
{UUID: "{uuid-3}", Key: "VAR3", Value: "value3"},
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
default:
|
||||
t.Fatalf("unexpected request %d", requestCount)
|
||||
}
|
||||
}))
|
||||
serverURL = server.URL
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := New(Options{BaseURL: server.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
variables, err := client.ListRepositoryVariables(ctx, "ws", "repo", VariableListOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRepositoryVariables: %v", err)
|
||||
}
|
||||
|
||||
if len(variables) != 3 {
|
||||
t.Errorf("expected 3 variables, got %d", len(variables))
|
||||
}
|
||||
if requestCount != 2 {
|
||||
t.Errorf("expected 2 requests for pagination, got %d", requestCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRepositoryVariablesRespectsLimit(t *testing.T) {
|
||||
var requestCount int
|
||||
var serverURL string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestCount++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
resp := variableListPage{
|
||||
Values: []PipelineVariable{
|
||||
{UUID: "{uuid-1}", Key: "VAR1"},
|
||||
{UUID: "{uuid-2}", Key: "VAR2"},
|
||||
{UUID: "{uuid-3}", Key: "VAR3"},
|
||||
},
|
||||
Next: serverURL + "/repositories/ws/repo/pipelines_config/variables?page=2",
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
serverURL = server.URL
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := New(Options{BaseURL: server.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
variables, err := client.ListRepositoryVariables(ctx, "ws", "repo", VariableListOptions{Limit: 2})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRepositoryVariables: %v", err)
|
||||
}
|
||||
|
||||
if len(variables) != 2 {
|
||||
t.Errorf("expected 2 variables (limit), got %d", len(variables))
|
||||
}
|
||||
if requestCount != 1 {
|
||||
t.Errorf("expected 1 request (limit satisfied), got %d", requestCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRepositoryVariableValidation(t *testing.T) {
|
||||
client, err := New(Options{BaseURL: "https://api.bitbucket.org/2.0"})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workspace string
|
||||
repoSlug string
|
||||
input CreateRepositoryVariableInput
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "missing workspace",
|
||||
workspace: "",
|
||||
repoSlug: "repo",
|
||||
input: CreateRepositoryVariableInput{Key: "VAR1", Value: "value"},
|
||||
errorContains: "workspace and repository slug are required",
|
||||
},
|
||||
{
|
||||
name: "missing repo slug",
|
||||
workspace: "workspace",
|
||||
repoSlug: "",
|
||||
input: CreateRepositoryVariableInput{Key: "VAR1", Value: "value"},
|
||||
errorContains: "workspace and repository slug are required",
|
||||
},
|
||||
{
|
||||
name: "missing key",
|
||||
workspace: "workspace",
|
||||
repoSlug: "repo",
|
||||
input: CreateRepositoryVariableInput{Key: "", Value: "value"},
|
||||
errorContains: "variable key is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := client.CreateRepositoryVariable(ctx, tt.workspace, tt.repoSlug, tt.input)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tt.errorContains)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errorContains) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.errorContains, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRepositoryVariable(t *testing.T) {
|
||||
var capturedBody map[string]any
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
|
||||
_ = json.NewDecoder(r.Body).Decode(&capturedBody)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := PipelineVariable{
|
||||
UUID: "{new-uuid}",
|
||||
Key: capturedBody["key"].(string),
|
||||
Secured: capturedBody["secured"].(bool),
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := New(Options{BaseURL: server.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
variable, err := client.CreateRepositoryVariable(ctx, "ws", "repo", CreateRepositoryVariableInput{
|
||||
Key: "MY_VAR",
|
||||
Value: "secret",
|
||||
Secured: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRepositoryVariable: %v", err)
|
||||
}
|
||||
|
||||
if variable.Key != "MY_VAR" {
|
||||
t.Errorf("expected key MY_VAR, got %s", variable.Key)
|
||||
}
|
||||
if !variable.Secured {
|
||||
t.Error("expected variable to be secured")
|
||||
}
|
||||
|
||||
if capturedBody["key"] != "MY_VAR" {
|
||||
t.Errorf("expected request body key=MY_VAR, got %v", capturedBody["key"])
|
||||
}
|
||||
if capturedBody["value"] != "secret" {
|
||||
t.Errorf("expected request body value=secret, got %v", capturedBody["value"])
|
||||
}
|
||||
if capturedBody["secured"] != true {
|
||||
t.Errorf("expected request body secured=true, got %v", capturedBody["secured"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepositoryVariableValidation(t *testing.T) {
|
||||
client, err := New(Options{BaseURL: "https://api.bitbucket.org/2.0"})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workspace string
|
||||
repoSlug string
|
||||
uuid string
|
||||
input UpdateRepositoryVariableInput
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "missing workspace",
|
||||
workspace: "",
|
||||
repoSlug: "repo",
|
||||
uuid: "{uuid}",
|
||||
input: UpdateRepositoryVariableInput{Key: "VAR1", Value: "value"},
|
||||
errorContains: "workspace and repository slug are required",
|
||||
},
|
||||
{
|
||||
name: "missing uuid",
|
||||
workspace: "workspace",
|
||||
repoSlug: "repo",
|
||||
uuid: "",
|
||||
input: UpdateRepositoryVariableInput{Key: "VAR1", Value: "value"},
|
||||
errorContains: "variable UUID is required",
|
||||
},
|
||||
{
|
||||
name: "missing key",
|
||||
workspace: "workspace",
|
||||
repoSlug: "repo",
|
||||
uuid: "{uuid}",
|
||||
input: UpdateRepositoryVariableInput{Key: "", Value: "value"},
|
||||
errorContains: "variable key is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := client.UpdateRepositoryVariable(ctx, tt.workspace, tt.repoSlug, tt.uuid, tt.input)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tt.errorContains)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errorContains) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.errorContains, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepositoryVariableValidation(t *testing.T) {
|
||||
client, err := New(Options{BaseURL: "https://api.bitbucket.org/2.0"})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workspace string
|
||||
repoSlug string
|
||||
uuid string
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "missing workspace",
|
||||
workspace: "",
|
||||
repoSlug: "repo",
|
||||
uuid: "{uuid}",
|
||||
errorContains: "workspace and repository slug are required",
|
||||
},
|
||||
{
|
||||
name: "missing uuid",
|
||||
workspace: "workspace",
|
||||
repoSlug: "repo",
|
||||
uuid: "",
|
||||
errorContains: "variable UUID is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := client.DeleteRepositoryVariable(ctx, tt.workspace, tt.repoSlug, tt.uuid)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tt.errorContains)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errorContains) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.errorContains, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListWorkspaceVariablesValidation(t *testing.T) {
|
||||
client, err := New(Options{BaseURL: "https://api.bitbucket.org/2.0"})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
_, err = client.ListWorkspaceVariables(ctx, "", VariableListOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing workspace, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "workspace is required") {
|
||||
t.Errorf("expected error about workspace, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDeploymentEnvironments(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/environments") {
|
||||
t.Errorf("expected path to contain /environments, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := deploymentEnvironmentListPage{
|
||||
Values: []DeploymentEnvironment{
|
||||
{UUID: "{env-1}", Name: "production", Slug: "production"},
|
||||
{UUID: "{env-2}", Name: "staging", Slug: "staging"},
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := New(Options{BaseURL: server.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
environments, err := client.ListDeploymentEnvironments(ctx, "ws", "repo")
|
||||
if err != nil {
|
||||
t.Fatalf("ListDeploymentEnvironments: %v", err)
|
||||
}
|
||||
|
||||
if len(environments) != 2 {
|
||||
t.Errorf("expected 2 environments, got %d", len(environments))
|
||||
}
|
||||
if environments[0].Name != "production" {
|
||||
t.Errorf("expected first environment to be production, got %s", environments[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDeploymentVariables(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/deployments_config/environments/") {
|
||||
t.Errorf("expected path to contain /deployments_config/environments/, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := variableListPage{
|
||||
Values: []PipelineVariable{
|
||||
{UUID: "{var-1}", Key: "DEPLOY_VAR", Value: "value1"},
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := New(Options{BaseURL: server.URL})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
variables, err := client.ListDeploymentVariables(ctx, "ws", "repo", "{env-uuid}", VariableListOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListDeploymentVariables: %v", err)
|
||||
}
|
||||
|
||||
if len(variables) != 1 {
|
||||
t.Errorf("expected 1 variable, got %d", len(variables))
|
||||
}
|
||||
if variables[0].Key != "DEPLOY_VAR" {
|
||||
t.Errorf("expected key DEPLOY_VAR, got %s", variables[0].Key)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmd/project"
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmd/repo"
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmd/status"
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmd/variable"
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmd/webhook"
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/cmdutil"
|
||||
)
|
||||
@@ -62,6 +63,7 @@ Common flows:
|
||||
webhook.NewCommand(f),
|
||||
status.NewCmdStatus(f),
|
||||
pipeline.NewCmdPipeline(f),
|
||||
variable.NewCommand(f),
|
||||
api.NewCmdAPI(f),
|
||||
extension.NewCmdExtension(f),
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,312 @@
|
||||
package variable
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateVariableKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid simple key",
|
||||
key: "MY_VAR",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid lowercase key",
|
||||
key: "my_var",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid mixed case key",
|
||||
key: "MyVar",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid key with numbers",
|
||||
key: "VAR123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid single letter",
|
||||
key: "A",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid key starting with underscore after letter",
|
||||
key: "A_B_C",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
key: "",
|
||||
wantErr: true,
|
||||
errContains: "cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "key starting with number",
|
||||
key: "123VAR",
|
||||
wantErr: true,
|
||||
errContains: "must start with a letter",
|
||||
},
|
||||
{
|
||||
name: "key starting with underscore",
|
||||
key: "_MY_VAR",
|
||||
wantErr: true,
|
||||
errContains: "must start with a letter",
|
||||
},
|
||||
{
|
||||
name: "key with hyphen",
|
||||
key: "MY-VAR",
|
||||
wantErr: true,
|
||||
errContains: "invalid character",
|
||||
},
|
||||
{
|
||||
name: "key with space",
|
||||
key: "MY VAR",
|
||||
wantErr: true,
|
||||
errContains: "invalid character",
|
||||
},
|
||||
{
|
||||
name: "key with special character",
|
||||
key: "MY$VAR",
|
||||
wantErr: true,
|
||||
errContains: "invalid character",
|
||||
},
|
||||
{
|
||||
name: "key with dot",
|
||||
key: "MY.VAR",
|
||||
wantErr: true,
|
||||
errContains: "invalid character",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateVariableKey(tt.key)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errContains)
|
||||
return
|
||||
}
|
||||
if tt.errContains != "" {
|
||||
if !containsString(err.Error(), tt.errContains) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.errContains, err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvFile(t *testing.T) {
|
||||
// Note: Test data uses obviously fake variable names and values.
|
||||
// These are NOT real credentials - they are test fixtures for env file parsing.
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
want map[string]string
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "simple key value pairs",
|
||||
content: "FOO=bar\nBAZ=qux",
|
||||
want: map[string]string{
|
||||
"FOO": "bar",
|
||||
"BAZ": "qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with comments",
|
||||
content: "# This is a comment\nFOO=bar\n# Another comment\nBAZ=qux",
|
||||
want: map[string]string{
|
||||
"FOO": "bar",
|
||||
"BAZ": "qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with empty lines",
|
||||
content: "FOO=bar\n\n\nBAZ=qux\n",
|
||||
want: map[string]string{
|
||||
"FOO": "bar",
|
||||
"BAZ": "qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with double quotes",
|
||||
content: "FOO=\"hello world\"",
|
||||
want: map[string]string{
|
||||
"FOO": "hello world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with single quotes",
|
||||
content: "FOO='hello world'",
|
||||
want: map[string]string{
|
||||
"FOO": "hello world",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
content: "FOO=",
|
||||
want: map[string]string{
|
||||
"FOO": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "value with equals sign",
|
||||
content: "FOO=a=b=c",
|
||||
want: map[string]string{
|
||||
"FOO": "a=b=c",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "whitespace around key",
|
||||
content: " FOO =bar",
|
||||
want: map[string]string{
|
||||
"FOO": "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "leading/trailing whitespace in line",
|
||||
content: " FOO=bar \n BAZ=qux ",
|
||||
want: map[string]string{
|
||||
"FOO": "bar",
|
||||
"BAZ": "qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing equals sign",
|
||||
content: "FOO bar",
|
||||
wantErr: true,
|
||||
errContains: "invalid format",
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
content: "=bar",
|
||||
wantErr: true,
|
||||
errContains: "empty key",
|
||||
},
|
||||
{
|
||||
name: "empty file",
|
||||
content: "",
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "only comments and empty lines",
|
||||
content: "# comment 1\n\n# comment 2\n",
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "value with hash not a comment",
|
||||
content: "FOO=bar#baz#qux",
|
||||
want: map[string]string{
|
||||
"FOO": "bar#baz#qux",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "quoted value with mixed chars",
|
||||
content: "FOO=\"hello 'world' and more\"",
|
||||
want: map[string]string{
|
||||
"FOO": "hello 'world' and more",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a temp file with the content
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.env")
|
||||
err := os.WriteFile(tmpFile, []byte(tt.content), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write temp file: %v", err)
|
||||
}
|
||||
|
||||
got, err := parseEnvFile(tmpFile)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errContains)
|
||||
return
|
||||
}
|
||||
if tt.errContains != "" {
|
||||
if !containsString(err.Error(), tt.errContains) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.errContains, err.Error())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("got %d entries, want %d", len(got), len(tt.want))
|
||||
}
|
||||
|
||||
for k, v := range tt.want {
|
||||
if got[k] != v {
|
||||
t.Errorf("key %q: got %q, want %q", k, got[k], v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvFileNotFound(t *testing.T) {
|
||||
_, err := parseEnvFile("/nonexistent/path/to/file.env")
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file, got nil")
|
||||
}
|
||||
if !containsString(err.Error(), "failed to open") {
|
||||
t.Errorf("expected error about opening file, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeConstants(t *testing.T) {
|
||||
// Verify scope constants have expected values
|
||||
tests := []struct {
|
||||
name string
|
||||
constant string
|
||||
expected string
|
||||
}{
|
||||
{"repository", scopeRepository, "repository"},
|
||||
{"workspace", scopeWorkspace, "workspace"},
|
||||
{"deployment", scopeDeployment, "deployment"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.constant != tt.expected {
|
||||
t.Errorf("scope constant %s = %q, want %q", tt.name, tt.constant, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// containsString is a helper to check if a string contains a substring
|
||||
func containsString(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -4,14 +4,17 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/avivsinai/bitbucket-cli/pkg/iostreams"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// Interface exposes interactive prompt helpers used by commands.
|
||||
type Interface interface {
|
||||
Input(prompt, defaultValue string) (string, error)
|
||||
Password(prompt string) (string, error)
|
||||
Confirm(prompt string, defaultYes bool) (bool, error)
|
||||
}
|
||||
|
||||
@@ -60,6 +63,31 @@ func (p *system) Input(prompt, defaultValue string) (string, error) {
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (p *system) Password(prompt string) (string, error) {
|
||||
if p.ios == nil || !p.ios.CanPrompt() {
|
||||
return "", errors.New("interactive prompts require a TTY")
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(p.ios.Out, prompt+": "); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get the file descriptor for stdin to disable echo
|
||||
stdin, ok := p.ios.In.(*os.File)
|
||||
if !ok {
|
||||
return "", errors.New("password input requires a terminal")
|
||||
}
|
||||
|
||||
password, err := term.ReadPassword(int(stdin.Fd()))
|
||||
// Print newline since ReadPassword doesn't echo the Enter key
|
||||
_, _ = fmt.Fprintln(p.ios.Out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(password), nil
|
||||
}
|
||||
|
||||
func (p *system) Confirm(prompt string, defaultYes bool) (bool, error) {
|
||||
r, err := p.reader()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user