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
10 changes: 9 additions & 1 deletion pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ type Request struct {
Body any
BodyContentType string
Headers map[string]string
// NoAccountID, when true, omits the accountIdentifier query param for this request.
NoAccountID bool
}

// Client makes authenticated HTTP requests to the Harness API.
Expand All @@ -82,6 +84,10 @@ type Client struct {
resolved *auth.ResolvedAuth
http *http.Client
cliCommand string // value for X-CLI-Command header; "completion" for completion requests
// NoAccountID, when true, omits the accountIdentifier query param from every
// request made by this Client. Set by callers that build requests via the
// Get/Post/Put/Delete/etc. shorthand methods, which don't accept per-request options.
NoAccountID bool
}

// New creates a Client from a command context.
Expand Down Expand Up @@ -243,7 +249,9 @@ func (c *Client) buildRequest(r Request) (*http.Request, *url.URL, error) {
return nil, nil, fmt.Errorf("building URL: %w", err)
}
q := u.Query()
q.Set("accountIdentifier", c.resolved.AccountID)
if !r.NoAccountID && !c.NoAccountID {
q.Set("accountIdentifier", c.resolved.AccountID)
}
for k, v := range r.QueryParams {
if v != "" {
q.Set(k, v)
Expand Down
74 changes: 74 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright © 2026 Harness Inc.
// SPDX-License-Identifier: Apache-2.0

package client

import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/harness/cli/v3/pkg/auth"
)

func testClient(apiURL string) *Client {
return &Client{
ctx: context.Background(),
resolved: &auth.ResolvedAuth{
APIUrl: apiURL,
AccountID: "acct",
AuthType: auth.AuthTypePAT,
PATToken: "pat.test",
},
http: &http.Client{},
}
}

func TestBuildRequest_AccountIdentifier(t *testing.T) {
tests := []struct {
name string
reqNoAccountID bool
clientNoAcctID bool
wantPresent bool
}{
{name: "default_present", wantPresent: true},
{name: "request_suppressed", reqNoAccountID: true, wantPresent: false},
{name: "client_suppressed", clientNoAcctID: true, wantPresent: false},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
c := testClient("https://example.test")
c.NoAccountID = tc.clientNoAcctID
req, u, err := c.buildRequest(Request{Method: "GET", Path: "/items", NoAccountID: tc.reqNoAccountID})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_ = req
got := u.Query().Get("accountIdentifier") != ""
if got != tc.wantPresent {
t.Fatalf("accountIdentifier present = %v, want %v (query=%q)", got, tc.wantPresent, u.RawQuery)
}
})
}
}

func TestDoRequest_AccountIdentifierOmittedOnWire(t *testing.T) {
var gotQuery url.Values
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotQuery = r.URL.Query()
w.Write([]byte(`{}`))
}))
defer srv.Close()

c := testClient(srv.URL)
c.NoAccountID = true
if _, _, err := c.DoRequest(Request{Method: "GET", Path: "/items"}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := gotQuery.Get("accountIdentifier"); got != "" {
t.Fatalf("accountIdentifier = %q, want empty", got)
}
}
1 change: 1 addition & 0 deletions pkg/endpoint/paging.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func BuildRequest(ctx *cmdctx.Ctx, ep *spec.EndpointSpec) (*client.Request, erro
Method: method,
Path: path,
QueryParams: qp,
NoAccountID: ep.NoAccountID,
}
if len(ep.BodyParams) > 0 {
req.Body = buildBody(ep, exprEnv)
Expand Down
44 changes: 44 additions & 0 deletions pkg/endpoint/paging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright © 2026 Harness Inc.
// SPDX-License-Identifier: Apache-2.0

package endpoint

import (
"context"
"testing"

"github.com/harness/cli/v3/pkg/auth"
"github.com/harness/cli/v3/pkg/cmdctx"
"github.com/harness/cli/v3/pkg/spec"
)

func TestBuildRequest_NoAccountID(t *testing.T) {
tests := []struct {
name string
ep *spec.EndpointSpec
want bool
}{
{name: "default_false", ep: &spec.EndpointSpec{Path: "/items"}, want: false},
{name: "propagated_true", ep: &spec.EndpointSpec{Path: "/items", NoAccountID: true}, want: true},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := &cmdctx.Ctx{
Context: context.Background(),
Auth: &auth.ResolvedAuth{
AccountID: "acct",
OrgID: "org",
ProjectID: "proj",
},
}
req, err := BuildRequest(ctx, tc.ep)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if req.NoAccountID != tc.want {
t.Fatalf("req.NoAccountID = %v, want %v", req.NoAccountID, tc.want)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/registry/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func callEndpointFull(ctx *cmdctx.Ctx, ep *spec.EndpointSpec, extraQueryParams m
}

c := client.New(ctx)
c.NoAccountID = ep.NoAccountID
method := ep.Method
if method == "" {
method = "GET"
Expand Down
30 changes: 30 additions & 0 deletions pkg/registry/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,36 @@ func TestCallEndpointFull_Priority3_DefaultDispatch(t *testing.T) {
}
}

// TestCallEndpointFull_NoAccountID verifies accountIdentifier is set by default
// and omitted when the endpoint opts out via no_account_id.
func TestCallEndpointFull_NoAccountID(t *testing.T) {
tests := []struct {
name string
ep *spec.EndpointSpec
want bool // want accountIdentifier present
}{
{name: "default_present", ep: &spec.EndpointSpec{Path: "/items"}, want: true},
{name: "suppressed", ep: &spec.EndpointSpec{Path: "/items", NoAccountID: true}, want: false},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
srv, cap := captureServer(t, `{}`)
r := New()
ctx := testCtx(srv.URL, nil)
ctx.Resolver = r

if _, _, err := callEndpointFull(ctx, tc.ep, nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := qv(t, cap.rawQuery).Get("accountIdentifier") != ""
if got != tc.want {
t.Fatalf("accountIdentifier present = %v, want %v (query=%q)", got, tc.want, cap.rawQuery)
}
})
}
}

// TestCallEndpointFull_Priority3_BodyFn — body_fn supplies the POST body.
func TestCallEndpointFull_Priority3_BodyFn(t *testing.T) {
srv, cap := captureServer(t, `{}`)
Expand Down
3 changes: 3 additions & 0 deletions pkg/spec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ type EndpointSpec struct {
// NoFields, when true, suppresses all field rendering (noun fields and fields_extra).
// Use with text_header/text_footer for commands whose response has no displayable fields.
NoFields bool `yaml:"no_fields,omitempty"`
// NoAccountID, when true, omits the accountIdentifier query param that is otherwise
// set on every request. Use for endpoints that reject or don't expect it.
NoAccountID bool `yaml:"no_account_id,omitempty"`
// FieldsSubset lists field IDs from the noun that this command's API actually returns.
// When set, --list-columns only advertises these IDs.
FieldsSubset []string `yaml:"fields_subset,omitempty"`
Expand Down
Loading