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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ go install github.com/linuxfoundation/lfx-cli/cmd/lfx@latest
# Log in via the Auth0 Device Code flow.
lfx auth login

# Log in against a non-production environment (aliases accepted).
lfx auth login --env staging # also: stage, stg
lfx auth login --env dev # also: develop, development

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Show canonical --env names in the README and point to lfx auth environments

Issue: The staging example leads with the canonical name; the development example leads with the alias dev and lists development under "also". The README also never mentions the new lfx auth environments command, even though --backend already points at lfx auth backends.

Proof: Canonical names are production / staging / development (environment.go:21-25). After lfx auth login --env dev, auth status prints Environment: development because login persists the normalized value. AGENTS.md asks to update the README for user-facing changes.

Why it matters: The README teaches an alias as the primary spelling, which fights this PR's move to full-word canonical names and what auth status / auth environments print.

Fix: Lead with the canonical names and point at the new command, e.g.

# Aliases are accepted; run `lfx auth environments` for the full list.
lfx auth login --env staging       # aliases: stage, stg
lfx auth login --env development   # aliases: dev, develop


# Show the current authentication status.
lfx auth status

Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ require (
github.com/99designs/keyring v1.2.2
github.com/tidwall/gjson v1.19.0
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.11.0
github.com/urfave/cli/v3 v3.13.0
golang.org/x/oauth2 v0.36.0
)

require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/dvsekhvalnov/jose2go v1.10.0 // indirect
github.com/dvsekhvalnov/jose2go v1.11.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
)
28 changes: 12 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.10.0 h1:5RmEnUoQBMBURnk346hX3dKqG60Jkf9qkp6dkLsFx60=
github.com/dvsekhvalnov/jose2go v1.10.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/dvsekhvalnov/jose2go v1.11.0 h1:9pnsV1/CLWFYSxMilqtiY1Fb4JZOxfDk/q0/CQiUMYw=
github.com/dvsekhvalnov/jose2go v1.11.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
Expand All @@ -21,24 +19,24 @@ github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli/v3 v3.11.0 h1:P/euJp99kb9p0tlVY+iYTLYYTAQlfl0hR2gUO1Img1Q=
github.com/urfave/cli/v3 v3.11.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY=
github.com/urfave/cli/v3 v3.13.0/go.mod h1:vXn6HxPNccJSzQr2QvwVncOKrgYGIHU0HY5h8B2nQj4=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
Expand All @@ -47,5 +45,3 @@ golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 1 addition & 1 deletion internal/commands/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func NewAPICommand() *cli.Command {
},
&cli.StringFlag{
Name: apiHostnameFlagName,
Usage: "Override the LFX API base URL (advanced; independent of the IdP domain). Requires a development-environment login (`lfx auth login --env=development`).",
Usage: "Override the LFX API base URL (advanced; independent of the IdP domain). Requires a development-environment login ('lfx auth login --env=development').",
},
},
Action: runAPI,
Expand Down
44 changes: 35 additions & 9 deletions internal/commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ var CredentialStoreFlags = []cli.Flag{
},
&cli.StringFlag{
Name: backendFlagName,
Usage: "Pin credential storage to a specific system backend (see `lfx auth backends`); mutually exclusive with --insecure-storage",
Usage: "Pin credential storage to a specific system backend (see 'lfx auth backends'); mutually exclusive with --insecure-storage",
},
}

Expand Down Expand Up @@ -84,6 +84,7 @@ func NewAuthCommand() *cli.Command {
newAuthStatusCommand(),
newAuthLogoutCommand(),
newAuthBackendsCommand(),
newAuthEnvironmentsCommand(),
},
}
}
Expand Down Expand Up @@ -116,12 +117,12 @@ func newAuthLoginCommand() *cli.Command {
},
&cli.StringFlag{
Name: envFlagName,
Usage: "Target environment: prod, staging, or development",
Value: string(envProd),
Usage: "Target environment; see 'lfx auth environments' for accepted values",
Value: string(envProduction),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[question] New logins persist "production", which released v1.0.x binaries reject

Issue: The renamed constant is also the value saved to DeviceState.Environment. A default lfx auth login (and --env=prod, after normalize) now writes "environment": "production" to state.json. Read-side normalize fixes the upgrade path; it does not help an older binary reading the new file.

Proof: --env defaults to string(envProduction) here. runAuthLogin normalizes to envProduction, then loginWithToken / loginWithDeviceCode persist Environment: string(env) (auth.go:179, auth.go:282). v1.0.1 (envProd = "prod", Latest) keys authDomains by "prod" and calls resolveEnvironment(authEnvironment(state.Environment)) with no alias step, so it errors invalid environment: "production".

Why it matters: Downgrading, or running a v1.0.x binary alongside this build against the same state dir, breaks lfx auth token / lfx api until the user logs in again with the older binary. Credentials stay in the keyring; only state.json becomes unreadable to v1.0.x.

Fix: If downgrade compatibility matters, keep writing the legacy production value ("prod") while still displaying production — normalizeEnvironment already reads both. If it does not, call that out in the release notes (going back to v1.0.x requires lfx auth login again).

},
&cli.StringFlag{
Name: audienceFlagName,
Usage: "Auth0 API audience to request tokens for (defaults to the selected environment's LFX API audience)",
Usage: "Auth0 API audience to request tokens for; defaults to --env's audience (see 'lfx auth environments')",
},
},
Action: runAuthLogin,
Expand All @@ -134,7 +135,7 @@ func runAuthLogin(ctx context.Context, cmd *cli.Command) error {
return err
}

env := authEnvironment(cmd.String(envFlagName))
env := normalizeEnvironment(cmd.String(envFlagName))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[minor] No test exercises alias handling through lfx auth login

Issue: This is the only place aliases are applied to user input. The new tests cover normalizeEnvironment / resolveEnvironment in isolation. Dropping this call, or passing the raw flag into defaultAudienceForEnvironment, would break lfx auth login --env=dev while every test still passed.

Proof: No *_test.go references runAuthLogin or loginWithToken. The --with-token path does not hit the network: it reads stdin and saves through credStoreFromCommand. --insecure-storage plus t.Setenv("XDG_STATE_HOME", t.TempDir()) is already a tested seam elsewhere (api_test.go swaps stdin).

Why it matters: Accepting aliases in lfx auth login is the user-facing feature. Helper tests can stay green while the command itself is unwired.

Fix: Table-test a root command (Flags: CredentialStoreFlags, Commands: NewAuthCommand()) with --insecure-storage auth login --with-token --env=<alias> for prod / stg / dev, piping a dummy refresh token on stdin. Assert the saved DeviceState has the canonical Environment, matching IDPDomain, and that environment's default audience.

domain, clientID, err := resolveEnvironment(env)
if err != nil {
return err
Expand Down Expand Up @@ -336,7 +337,7 @@ func loadDeviceStateForBackend(store credstore.Store, cmd *cli.Command) (state c
)
}

domain, clientID, err = resolveEnvironment(authEnvironment(state.Environment))
domain, clientID, err = resolveEnvironment(normalizeEnvironment(state.Environment))
if err != nil {
return credstore.DeviceState{}, "", "", err
}
Expand Down Expand Up @@ -488,7 +489,7 @@ func resolveAccessToken(ctx context.Context, cmd *cli.Command) (token, audience
}

if creds.ValidAccessToken() {
return creds.AccessToken, state.Audience, authEnvironment(state.Environment), nil
return creds.AccessToken, state.Audience, normalizeEnvironment(state.Environment), nil
}

if creds.RefreshToken == "" {
Expand Down Expand Up @@ -527,7 +528,7 @@ func resolveAccessToken(ctx context.Context, cmd *cli.Command) (token, audience
return "", "", "", fmt.Errorf("save refreshed credentials: %w", err)
}

return refreshed.AccessToken, state.Audience, authEnvironment(state.Environment), nil
return refreshed.AccessToken, state.Audience, normalizeEnvironment(state.Environment), nil
}

func newAuthStatusCommand() *cli.Command {
Expand Down Expand Up @@ -563,7 +564,7 @@ func newAuthStatusCommand() *cli.Command {
)
}
if state.Environment != "" {
fmt.Printf(" %-22s %s\n", "Environment:", state.Environment)
fmt.Printf(" %-22s %s\n", "Environment:", normalizeEnvironment(state.Environment))
}
if state.IDPDomain != "" {
fmt.Printf(" %-22s %s\n", "IdP domain:", state.IDPDomain)
Expand Down Expand Up @@ -680,6 +681,31 @@ func newAuthBackendsCommand() *cli.Command {
}
}

// newAuthEnvironmentsCommand builds `lfx auth environments`, which lists
// every canonical --env value accepted by `lfx auth login`, along with
// its aliases and default API audience. Kept out of --env's own usage
// text (and out of the main help listing, aside from the command itself)
// to keep that terser; full detail lives here for anyone who needs to
// look it up.
func newAuthEnvironmentsCommand() *cli.Command {
return &cli.Command{
Name: "environments",
Usage: "List --env values accepted by `lfx auth login`, including aliases and default audiences",
Comment thread
emsearcy marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Use single quotes in the environments command Usage, like the rest of the help text

Issue: This PR switched flag Usage that names a command to single quotes ('lfx auth backends', 'lfx auth environments'). This new command Usage still wraps lfx auth login in backticks — it is now the only Usage string in internal/commands that still has them.

Proof: Flag Usage runs through urfave/cli's unquoteUsage (the original reason backticks were removed). Command Usage is printed as-is, so these backticks show up literally next to sibling commands that use no quoting, and next to --env's single-quoted 'lfx auth environments'.

Why it matters: Help output mixes quoting styles for the same kind of command reference. No behavioral break; consistency only.

Fix: Usage: "List --env values accepted by 'lfx auth login', including aliases and default audiences"

Action: func(_ context.Context, _ *cli.Command) error {
fmt.Println("Accepted --env values:")
for _, env := range []authEnvironment{envProduction, envStaging, envDevelopment} {
line := " " + string(env)
if aliases := environmentAliases(env); len(aliases) > 0 {
line += fmt.Sprintf(" (aliases: %s)", strings.Join(aliases, ", "))
}
fmt.Println(line)
fmt.Printf(" default audience: %s\n", defaultAudiences[env])
}
return nil
},
}
}

// lfidClaimsNamespace prefixes the custom LFID claims Auth0 adds to the ID
// token, namely username. Distinct from the shorter "http://lfx.dev/claims"
// LFX claims namespace used elsewhere.
Expand Down
96 changes: 94 additions & 2 deletions internal/commands/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ func TestDefaultAudienceForEnvironment(t *testing.T) {
want string
}{
{
name: "prod",
env: envProd,
name: "production",
env: envProduction,
want: "https://lfx-api.v2.cluster.lfx.dev/",
},
{
Expand Down Expand Up @@ -177,6 +177,34 @@ func TestLoadDeviceStateForBackendDomainMismatch(t *testing.T) {
})
}

func TestLoadDeviceStateForBackendLegacyProdEnvironment(t *testing.T) {
// state.json files written before "prod" was renamed to "production"
// persist the old name; loadDeviceStateForBackend must still resolve
// them via normalizeEnvironment rather than erroring or requiring a
// fresh `lfx auth login`.
store := newInsecureStore(t)
if err := store.SaveDeviceState(credstore.DeviceState{
IDPDomain: "sso.linuxfoundation.org",
Environment: "prod",
Insecure: true,
}); err != nil {
t.Fatalf("SaveDeviceState: %v", err)
}

newTestCommand(t, []string{"--insecure-storage"}, func(cmd *cli.Command) {
state, domain, _, err := loadDeviceStateForBackend(store, cmd)
if err != nil {
t.Fatalf("loadDeviceStateForBackend: %v", err)
}
if domain != "sso.linuxfoundation.org" {
t.Errorf("domain = %q, want sso.linuxfoundation.org", domain)
}
if state.Environment != "prod" {
t.Errorf("state.Environment = %q, want the raw persisted value %q", state.Environment, "prod")
}
})
}

func TestLoadDeviceStateForBackendKeyringBackendMismatch(t *testing.T) {
store := newInsecureStore(t)
if err := store.SaveDeviceState(credstore.DeviceState{
Expand Down Expand Up @@ -343,3 +371,67 @@ func TestStateMismatchReasonBackendPinMismatch(t *testing.T) {
}
})
}

func TestNormalizeEnvironment(t *testing.T) {
tests := []struct {
input string
want authEnvironment
}{
// Canonical names pass through unchanged.
{"production", envProduction},
{"staging", envStaging},
{"development", envDevelopment},
// Aliases.
{"prod", envProduction},
{"stage", envStaging},
{"stg", envStaging},
{"develop", envDevelopment},
{"dev", envDevelopment},
// Unrecognized inputs are returned as-is (error surfaces in resolveEnvironment).
{"unknown", authEnvironment("unknown")},
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
if got := normalizeEnvironment(tc.input); got != tc.want {
t.Errorf("normalizeEnvironment(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}

func TestResolveEnvironmentAliases(t *testing.T) {
tests := []struct {
input string
wantDomain string
}{
{"prod", "sso.linuxfoundation.org"},
{"stage", "linuxfoundation-staging.auth0.com"},
{"stg", "linuxfoundation-staging.auth0.com"},
{"dev", "linuxfoundation-dev.auth0.com"},
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
env := normalizeEnvironment(tc.input)
domain, clientID, err := resolveEnvironment(env)
if err != nil {
t.Fatalf("resolveEnvironment(%q): %v", tc.input, err)
}
if domain != tc.wantDomain {
t.Errorf("domain = %q, want %q", domain, tc.wantDomain)
}
if clientID == "" {
t.Error("clientID = \"\", want a non-empty compiled-in client ID")
}
})
}
}

func TestResolveEnvironmentUnknown(t *testing.T) {
_, _, err := resolveEnvironment(authEnvironment("bogus"))
if err == nil {
t.Fatal("resolveEnvironment(\"bogus\"): got nil error, want errInvalidEnvironment")
}
if !errors.Is(err, errInvalidEnvironment) {
t.Errorf("resolveEnvironment(\"bogus\") error = %v, want wrapping errInvalidEnvironment", err)
}
}
Loading
Loading