diff --git a/HAND_EDITS.md b/HAND_EDITS.md new file mode 100644 index 0000000..d8f098c --- /dev/null +++ b/HAND_EDITS.md @@ -0,0 +1,241 @@ +# Hand-Edits to Generated SDK Code + +## Scope + +This file catalogs commits that modified files inside **`v24/api/keyfactor/v{1,2}/`** after +their initial generation (baseline: `2ed41db`, "Generate V24 client"). These files carry +generator-output "DO NOT EDIT" headers, but the project's `.openapi-generator-ignore` files +are 1040 bytes of generator boilerplate comments with no active ignore patterns (verified by +byte count and content inspection) — no protection mechanism is in place. **Without the right +templates and swagger patches, naive regeneration of v24 would silently drop every hand-edit +listed below.** + +**This document does NOT currently cover `v25/api/keyfactor/v{1,2}/`, `v2/api/keyfactor/`, or +the repo-root `api/keyfactor/`.** `v25` has its own hand-edit history starting from its own +generation baseline (`536f3e2`, "feat(api): Add support for Keyfactor API Command up to +25.1.1") that has not been fully audited against this convention — see the "v25 (partial)" +section below for the one edit ported there as part of this change set. Treat any claim of +completeness in this file as scoped to v24 only; do not assume v25/v2/root are safe to +regenerate just because they aren't listed here. + +## Conventions + +For each file, hand-edits are listed in commit order (oldest first). Each entry notes: + +- **Commit SHA + subject** — recover the full diff with `git show -- `. +- **What it changed** — brief description. +- **Reproduced by upstream swagger?** — this repo has no swagger/OpenAPI spec file checked in + at any commit in its history (`find . -iname '*swagger*' -o -iname '*openapi*'` under + `v24/` returns nothing but generated output and `.openapi-generator-ignore`), so the answer + is **No** for every entry below: none of these edits can be verified against a source spec + we can regenerate from. "No" here does not mean "wrong," only "pure Go logic (or a + hand-authored file matching generator conventions) with no committed spec counterpart." +- **Regression test pins this?** — Yes if a `*_test.go` test would fail (or fail to compile) + without the hand-edit; "compile-only" if removal would only break a build elsewhere in this + repo without a test asserting behavior. +- **Action on regen** — `preserve` (must be re-applied post-regen), `verify` (re-check whether + reproduced), `obsolete` (intentional removal), `docs-only` (no behavioral impact). + +--- + +## v24 + +### `v24/api/keyfactor/v1/client.go` + `v24/api/keyfactor/v2/client.go` + +1. **`af6340b`** — *Ab#82568 (#30)* — adds `NewAPIClientWithAuth(auth AuthConfig) *APIClient` + to both files: constructs an `APIClient` with a pre-built `AuthConfig`, bypassing the + network call inside `Authenticate()`. This is the entry point the VCR-cassette-based unit + test harness (in this repo and in consumers such as the Terraform provider) uses to inject + a fake or replay-mode `AuthConfig` without hitting a real Command server. + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **Compile-only** — `v24/client.go` (the top-level wrapper) + calls `v1.NewAPIClientWithAuth` / `v2.NewAPIClientWithAuth` directly, so `go build ./...` + fails if either is removed or its signature changes, but no test in *this* repo asserts + its runtime behavior. Downstream consumers' VCR test suites depend on it at compile time + too. + - Action: **preserve**. + +2. **`229db7d`** — *Feat/ca cleanup enrollment fields (#32)* — restores `AccessToken`, + `Audience`, and `Scopes` to the `auth_providers.CommandConfigOauth{...}` struct literal + inside `buildHttpClientV2`'s OAuth branch. Commit `2b88eb2` (2026-03-18) had silently + dropped these three fields in an earlier refactor, which broke pre-fetched + `access_token`-only authentication (callers supplying just hostname + access token, no + `client_id`/`client_secret`/`token_url`). + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **Yes** — `TestBuildHttpClientV2_OAuthAccessTokenPropagation` + drives `buildHttpClientV2` itself end-to-end against a fake Command server (the same + `newFakeCommandServer`-style harness `TestBuildHttpClientV2_ClientTimeoutPropagation` uses + for entry #4 below) with a `Server{AccessToken: ...}` and no `client_id`/`client_secret`/ + `token_url`, then asserts the request that reaches the fake server carries the token as a + `Bearer` credential. Deleting `AccessToken`/`Audience`/`Scopes` from this file's literal + makes that test fail (verified by reproduction against the pre-fix code). Note: + `TestCommandConfigOauth_AccessTokenFieldPropagation`, despite its name and despite living + in this file, does **not** exercise `buildHttpClientV2` — it only builds its own local + `auth_providers.CommandConfigOauth{}` literal and would keep passing even if this file's + literal dropped the three fields. That test still has value (it compile-pins the upstream + `auth_providers` field names existing at all), but it is + `TestBuildHttpClientV2_OAuthAccessTokenPropagation`, not it, that protects this specific + hand-edit. + - Action: **preserve**. (This is the second time these fields were silently dropped and + restored — see the near-identical prior loss at `2b88eb2` — so it is a high-value + preserve.) + +3. **`229db7d`** (same commit) — changes `prepareRequest`'s port guard from + `serverConfig.Port > 0 && serverConfig.Port <= 65535` to + `serverConfig.Port > 0 && serverConfig.Port <= 65535 && serverConfig.Port != 443`, so a + `Server` configured with the default HTTPS port no longer produces + `https://host:443/...` request URLs (some servers/proxies that match on an exact, + port-suffix-free `Host` header rejected the explicit `:443`). + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **Yes, as of this change set** — + `TestPrepareRequest_Port443Guard` (added alongside this document update) asserts both + that port 443 is omitted and that a non-443 port (8443) is still appended. Prior to this + change, reverting the guard failed nothing in CI. + - Action: **preserve**. + +4. **`2a6c5b4`** — *fix(v24): plumb Server.ClientTimeout into rebuilt auth config* — inside + `buildHttpClientV2()`, adds `HttpClientTimeout: cfg.ClientTimeout` to the + `baseConfig := auth_providers.CommandAuthConfig{...}` struct literal in both files. Without + it, `Server.ClientTimeout` (added upstream by `keyfactor-auth-client-go` v1.6.0-rc.2 to fix + [issue #51](https://github.com/Keyfactor/keyfactor-auth-client-go/issues/51)) was silently + dropped when this SDK rebuilt its own `CommandAuthConfig`, so every caller — including the + Terraform provider's `request_timeout` setting — fell back to + `auth_providers.DefaultClientTimeout` (60s) regardless of what was configured. This + surfaced as `net/http: timeout awaiting response headers` on long-running calls such as PFX + enrollment. + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **Yes** — `TestBuildHttpClientV2_ClientTimeoutPropagation` in + both `v1/client_test.go` and `v2/client_test.go` calls `buildHttpClientV2()` against a + fake Command server and asserts the resulting `CommandAuthConfigBasic.HttpClientTimeout` + and derived `BuildTransport().ResponseHeaderTimeout` reflect the configured value. That + test is itself hermetic against ambient `KEYFACTOR_SKIP_VERIFY` / + `KEYFACTOR_CA_CERT` / `KEYFACTOR_CLIENT_TIMEOUT` env values (see the `unsetEnvForTest` + helper in the same file) so it can't be spuriously broken by a caller's shell + environment. + - Action: **preserve**. + +### `v24/api/keyfactor/v1/model_certificate_authorities_certificate_authority_request.go` + `..._response.go` + +5. **`229db7d`** — adds `UseForEnrollment *bool`, `CertificateCleanupEnabled NullableBool`, + `DeleteWithArchivedKey NullableBool`, `TimeAfterExpiration NullableInt32`, and + `TimeAfterExpirationUnits *CSSCMSDataModelEnumsCertificateCleanupTimeUnits` fields (with + generator-style getters/setters/Has*) to both the CA request and response models. + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **Yes** — `TestCARequestFields_CleanupAndEnrollment`, + `TestCAResponseFields_CleanupAndEnrollment`, `TestCARequestFields_NilSafety`, and + `TestCAResponseFields_NilSafety` in `model_certificate_authorities_test.go` exercise every + setter/getter/Has* and nil-receiver safety for all five fields on both structs. + - Action: **preserve**. + +### `v24/api/keyfactor/v1/model_css_cms_core_enums_enrollment_type.go` + +6. **`af6340b`** — adds enum value `3` and the bitmask-combination values `5`, `6`, `7` to + `AllowedCSSCMSCoreEnumsEnrollmentTypeEnumValues` (the type is a bitmask: 1=PFX, 2=CSR, so + combined values are valid), with a comment documenting the bitmask semantics. + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **No** — no test in this repo references + `CSSCMSCoreEnumsEnrollmentType`'s allowed-values list. + - Action: **preserve**. + +### `v24/api/keyfactor/v1/model_css_cms_core_enums_key_retention_policy.go` + +7. **`af6340b`** — rewrites `UnmarshalJSON` to try integer form first (original generated + behavior), then fall back to a new `keyRetentionPolicyStringToInt` map for EJBCA's + string-form responses (e.g. `"None"`, `"ShortTerm"`), defaulting unknown strings to `0` + (`None`) rather than erroring, to avoid breaking reads of new/unknown values. + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **No** — no test in this repo exercises + `CSSCMSCoreEnumsKeyRetentionPolicy.UnmarshalJSON`'s string-form or unknown-value paths. + - Action: **preserve**. + +### `v24/api/keyfactor/v1/model_css_cms_data_model_enums_certificate_cleanup_time_units.go` + +8. **`af6340b`** — new file (does not exist before this commit). Hand-authored in the exact + style of a generated enum file (including the generator's `DO NOT EDIT` header and the + known openapi-generator template artifact where `Parse()` unconditionally returns an error + for enums generated with no string-value mapping, before checking a `stringsToEnum` map + that is always empty — this is not new breakage, it reproduces a real upstream generator + quirk found on comparable enum files elsewhere in this SDK). Defines + `CSSCMSDataModelEnumsCertificateCleanupTimeUnits` (0=Days, 1=Weeks, 2=Months), consumed by + the CA and template cleanup fields added in the same commit / in `229db7d`. + - Reproduced by upstream swagger: **No** — hand-created, not generated. + - Regression test pins this: **Compile-only** — `TestCARequestFields_CleanupAndEnrollment` + references the `CSSCMSDATAMODELENUMSCERTIFICATECLEANUPTIMEUNITS__1` constant, so removing + the type breaks compilation, but no test exercises its `UnmarshalJSON`/`Parse` behavior. + - Action: **preserve**. + +### `v24/api/keyfactor/v1/model_system_day_of_week.go` + `v24/api/keyfactor/v2/model_system_day_of_week.go` + +9. **`96dd817`** — rewrites `SystemDayOfWeek.UnmarshalJSON` to try the integer form first + (preserving the original generated validation against + `AllowedSystemDayOfWeekEnumValues`), then fall back to the existing `Parse()` day-name + mapping (e.g. `"Monday"`) when the payload is a JSON string, since Keyfactor Command + serializes `WeeklyModel.Days` as day-name strings in some API responses. Applied + identically to both v1 and v2 packages. + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **Yes** — `TestSystemDayOfWeek_UnmarshalJSON_IntForm`, + `_StringForm`, `_InvalidString`, `_OutOfRangeInt`, `TestWeeklyModel_UnmarshalJSON_DayNameStrings`, + and `_DayIndexInts` in `model_system_day_of_week_test.go` cover both forms and error + cases. + - Action: **preserve**. + +### `v24/api/keyfactor/v1/model_templates_template_retrieval_response.go` + `model_templates_template_update_request.go` + +10. **`af6340b`** — adds `CertificateCleanupEnabled NullableBool`, `TimeAfterExpiration + NullableInt32`, `TimeAfterExpirationUnits + *CSSCMSDataModelEnumsCertificateCleanupTimeUnits`, `DeleteWithArchivedKey NullableBool`, + and (retrieval response only) `Manageability NullableInt32` fields, with + generator-style getters/setters/Has*/`ToMap` entries. `Manageability` was present in + actual JSON responses but missing from the Go struct, so it always deserialized as the + zero value before this fix. + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **No** — no test in this repo references + `TemplatesTemplateRetrievalResponse`, `TemplatesTemplateUpdateRequest`, or + `Manageability`. + - Action: **preserve**. + +--- + +## v25 (partial — not a full audit) + +The v25 module (baseline `536f3e2`) independently carries its own OAuth-field-restoration +(`0c1df4d`) and port-443-guard (`b374f3d`) hand-edits, predating this document and this +branch. Those have **not** been audited against this document's conventions and are +deliberately **not** claimed as covered here — see "Scope" above. + +The one v25 edit made as part of this change set: + +1. **`db2be75`** — *fix(v25): plumb Server.ClientTimeout into rebuilt auth config* — the same + edit as v24 entry #4 above, applied to `v25/api/keyfactor/v1/client.go` and + `v25/api/keyfactor/v2/client.go`. Required bumping v25's `keyfactor-auth-client-go` + dependency from `v1.3.0` to `v1.6.0-rc.2` (the first version with `Server.ClientTimeout`); + verified no API compatibility breaks (`go mod tidy && go build ./...` clean). + - Reproduced by upstream swagger: **No**. + - Regression test pins this: **Yes** — `TestBuildHttpClientV2_ClientTimeoutPropagation`, + ported verbatim from v24, in both v25 v1 and v2 `client_test.go`. + - Action: **preserve**. + +2. Also ported to v25 as part of this change set: `TestPrepareRequest_Port443Guard` (see v24 + entry #3), protecting v25's pre-existing `b374f3d` port-443 guard, which had the same + "unprotected hand-edit" gap as v24's copy. + +3. Also ported to v25 as part of this change set: `TestBuildHttpClientV2_OAuthAccessTokenPropagation` + (see v24 entry #2), protecting v25's pre-existing `0c1df4d` OAuth-field-restoration, which had + the same "unprotected hand-edit" gap as v24's copy before this change set (the only test that + named the fields, `TestCommandConfigOauth_AccessTokenFieldPropagation`, never called + `buildHttpClientV2`). This does not constitute a full audit of `0c1df4d` against this + document's conventions — see "Scope" above — only this one test gap is closed. + +**Not fixed in this change set:** the repo-root `api/keyfactor/` module and the `v2/` +(`github.com/Keyfactor/keyfactor-go-client-sdk/v2`) module contain the identical +`buildHttpClientV2` `ClientTimeout`-drop bug. A dependency bump to `keyfactor-auth-client-go +v1.6.0-rc.2` was verified to build cleanly in both (`go mod tidy && go build ./...`, +`GOWORK=off`), so the bug is technically fixable the same way. It was deliberately left +unfixed here because: (a) neither module has a single `*_test.go` file today — fixing "with +equivalent tests" per the standard set by v24/v25 would mean authoring first-ever test +scaffolding for these modules, a materially larger and separate undertaking than the +one-line v24/v25 fix; (b) both appear to be legacy/superseded major-API-version modules (the +README states v25 is "the latest available SDK"; `CHANGELOG.md` has no recent root/v2-module +entries); (c) the known primary consumer (terraform-provider-keyfactor) pins only the v24 +module. If an active consumer of the root or `v2` module surfaces, file a follow-up issue +rather than assuming this gap is safe to ignore indefinitely. diff --git a/v24/api/keyfactor/v1/client.go b/v24/api/keyfactor/v1/client.go index ba208f4..667586d 100644 --- a/v24/api/keyfactor/v1/client.go +++ b/v24/api/keyfactor/v1/client.go @@ -317,11 +317,12 @@ func buildHttpClientV2(cfg *auth_providers.Server) (AuthConfig, error) { clientAuthType := cfg.GetAuthType() baseConfig := auth_providers.CommandAuthConfig{ - CommandHostName: cfg.Host, - CommandPort: cfg.Port, - CommandAPIPath: cfg.APIPath, - CommandCACert: cfg.CACertPath, - SkipVerify: cfg.SkipTLSVerify, + CommandHostName: cfg.Host, + CommandPort: cfg.Port, + CommandAPIPath: cfg.APIPath, + CommandCACert: cfg.CACertPath, + SkipVerify: cfg.SkipTLSVerify, + HttpClientTimeout: cfg.ClientTimeout, } if clientAuthType == "basic" { diff --git a/v24/api/keyfactor/v1/client_test.go b/v24/api/keyfactor/v1/client_test.go index fadf205..8fced1e 100644 --- a/v24/api/keyfactor/v1/client_test.go +++ b/v24/api/keyfactor/v1/client_test.go @@ -1,12 +1,37 @@ package v1 import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "os" "reflect" "testing" + "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) +// unsetEnvForTest removes an environment variable for the duration of the +// test and restores its original value (or absence) on cleanup. Unlike +// t.Setenv(key, ""), which leaves the variable "present" with an empty +// value (still visible to os.LookupEnv), this genuinely unsets it so code +// that branches on presence -- e.g. ValidateAuthConfig's +// KEYFACTOR_CLIENT_TIMEOUT handling, which treats a present-but-unparseable +// value as "leave HttpClientTimeout at its current value" rather than +// falling through to the 60s default -- behaves as if the caller's shell +// never exported it at all. +func unsetEnvForTest(t *testing.T, key string) { + t.Helper() + if orig, ok := os.LookupEnv(key); ok { + t.Cleanup(func() { + _ = os.Setenv(key, orig) + }) + _ = os.Unsetenv(key) + } +} + // TestCommandConfigOauth_AccessTokenFieldPropagation is a compilation + correctness // regression test for the v2.8.0 bug where AccessToken, Audience, and Scopes were // silently dropped when constructing CommandConfigOauth from auth_providers.Server @@ -97,3 +122,242 @@ func TestCommandConfigOauth_AccessTokenOnlyNoClientCreds(t *testing.T) { t.Errorf("ClientSecret = %q, want empty", oauthCfg.ClientSecret) } } + +// newFakeCommandServer stands in for a Keyfactor Command instance for +// CommandAuthConfigBasic.Authenticate(), which performs a real GET against +// {host}/{apiPath}/Status/Endpoints as part of authentication. It always +// returns 200 with a valid JSON string array, regardless of credentials. +func newFakeCommandServer(t *testing.T) *httptest.Server { + t.Helper() + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server +} + +// TestBuildHttpClientV2_ClientTimeoutPropagation is a regression test for the +// bug where Server.ClientTimeout was silently dropped when buildHttpClientV2 +// rebuilt its own CommandAuthConfig, causing every caller (including the +// Terraform provider's request_timeout setting) to fall back to +// auth_providers.DefaultClientTimeout (60s) regardless of what was +// configured -- surfacing as "net/http: timeout awaiting response headers" on +// long-running calls such as PFX enrollment. Unlike +// TestCommandConfigOauth_AccessTokenFieldPropagation above (which mirrors +// buildHttpClientV2's lines to avoid the network call inside Authenticate()), +// this test exercises buildHttpClientV2 itself against a fake Command server. +func TestBuildHttpClientV2_ClientTimeoutPropagation(t *testing.T) { + // Hermetic against the ambient environment: ValidateAuthConfig (in + // keyfactor-auth-client-go's auth_core.go) unconditionally overwrites + // SkipVerify from KEYFACTOR_SKIP_VERIFY whenever the variable is merely + // *present*, regardless of its value -- so anything other than exactly + // "true"/"1" (e.g. "TRUE", "false", "0", or an empty string sourced from + // a lab env file) flips SkipTLSVerify back to false and this test fails + // against its own self-signed httptest server with "x509: certificate + // signed by unknown authority". Pin it explicitly rather than relying on + // it being unset in whatever shell runs `go test`. (The upstream + // unconditional-overwrite behavior itself is tracked and fixed + // separately in keyfactor-auth-client-go; this only needs to make our + // test hermetic against it.) + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + // A stale/bad KEYFACTOR_CA_CERT path in the ambient environment would + // make BuildTransport() below treat the value as literal PEM bytes and + // fail with "failed to append custom CA cert to pool". Neutralize it. + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + // KEYFACTOR_CLIENT_TIMEOUT only matters when HttpClientTimeout is <= 0 + // going in (it isn't here -- srv.ClientTimeout is 300 below), but pin it + // too for defense-in-depth. t.Setenv(..., "") would NOT achieve this: an + // empty value is still "present" to os.LookupEnv, so ValidateAuthConfig + // would see ok=true, fail to strconv.Atoi(""), and leave + // HttpClientTimeout at whatever it currently is instead of falling + // through to the 60s default -- the "zero-timeout case". + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 300, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + basicCfg, ok := authCfg.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandAuthConfigBasic, got %T", authCfg) + } + + if basicCfg.HttpClientTimeout != 300 { + t.Errorf("CommandAuthConfigBasic.HttpClientTimeout = %d, want %d", basicCfg.HttpClientTimeout, 300) + } + + transport, tErr := basicCfg.CommandAuthConfig.BuildTransport() + if tErr != nil { + t.Fatalf("BuildTransport() returned unexpected error: %v", tErr) + } + + expected := 300 * time.Second + if transport.ResponseHeaderTimeout != expected { + t.Errorf("ResponseHeaderTimeout = %v, want %v", transport.ResponseHeaderTimeout, expected) + } +} + +// newFakeCommandServerCapturingAuth is like newFakeCommandServer but also +// records the Authorization header of the last request it received, so a +// test can assert which credential actually reached the wire (rather than +// only asserting the shape of a struct literal that never leaves the test). +func newFakeCommandServerCapturingAuth(t *testing.T) (*httptest.Server, *string) { + t.Helper() + var gotAuth string + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server, &gotAuth +} + +// TestBuildHttpClientV2_OAuthAccessTokenPropagation is a regression test for +// the hand-edit cataloged in HAND_EDITS.md (v24 entry #2) that restores +// AccessToken, Audience, and Scopes to the auth_providers.CommandConfigOauth{} +// literal inside buildHttpClientV2's OAuth branch. Those three fields were +// silently dropped once already (2b88eb2) and restored (229db7d); this test +// -- unlike TestCommandConfigOauth_AccessTokenFieldPropagation above, which +// only constructs its own local CommandConfigOauth literal and would keep +// passing even if buildHttpClientV2's actual literal dropped these fields -- +// drives buildHttpClientV2 itself end-to-end against a fake Command server +// and asserts the pre-fetched access token actually reaches the wire as a +// Bearer credential. +func TestBuildHttpClientV2_OAuthAccessTokenPropagation(t *testing.T) { + // See TestBuildHttpClientV2_ClientTimeoutPropagation for why each of + // these is pinned/unset: makes the test hermetic against the ambient + // shell environment. + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + unsetEnvForTest(t, auth_providers.EnvKeyfactorAccessToken) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientID) + + server, gotAuth := newFakeCommandServerCapturingAuth(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + // Deliberately no ClientID/ClientSecret/OAuthTokenUrl: this is the + // pre-fetched access_token-only auth path that 2b88eb2 broke. + AccessToken: "mytoken-abc123", + Audience: "https://my.audience.example.com", + Scopes: []string{"read", "write"}, + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 60, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + oauthCfg, ok := authCfg.(*auth_providers.CommandConfigOauth) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandConfigOauth, got %T", authCfg) + } + + if oauthCfg.AccessToken != "mytoken-abc123" { + t.Errorf("CommandConfigOauth.AccessToken = %q, want %q", oauthCfg.AccessToken, "mytoken-abc123") + } + if oauthCfg.Audience != "https://my.audience.example.com" { + t.Errorf("CommandConfigOauth.Audience = %q, want %q", oauthCfg.Audience, "https://my.audience.example.com") + } + if !reflect.DeepEqual(oauthCfg.Scopes, []string{"read", "write"}) { + t.Errorf("CommandConfigOauth.Scopes = %v, want %v", oauthCfg.Scopes, []string{"read", "write"}) + } + + // The real assertion: buildHttpClientV2's internal Authenticate() call + // made an actual HTTP request to the fake Command server, and that + // request must carry the access token as a Bearer credential. If + // AccessToken were dropped from buildHttpClientV2's literal, the OAuth + // branch would fall through to the client-credentials grant with no + // ClientID/ClientSecret/TokenURL and buildHttpClientV2 would return an + // error above instead of ever reaching this assertion. + wantAuth := "Bearer mytoken-abc123" + if *gotAuth != wantAuth { + t.Errorf("request Authorization header = %q, want %q", *gotAuth, wantAuth) + } +} + +// TestPrepareRequest_Port443Guard is a regression test for the hand-edit in +// commit 229db7d that added "&& serverConfig.Port != 443" to prepareRequest's +// port guard. Without it, a Server configured with Port: 443 (the default +// HTTPS port, and what many callers -- including the Terraform provider -- +// set explicitly) produces request URLs like "https://host:443/..." instead +// of "https://host/...". Both are technically valid HTTPS URLs, but the +// explicit ":443" broke servers/proxies that match on Host header exactly +// (no port suffix) and was reported as a functional regression. This test +// was previously unprotected: reverting the guard would fail nothing in CI. +func TestPrepareRequest_Port443Guard(t *testing.T) { + // Constructed directly against the exported AuthClient field (rather + // than via NewAPIClientWithAuth) so this test exercises prepareRequest + // in isolation without depending on an unrelated hand-edit. + newClientWithPort := func(t *testing.T, port int) *APIClient { + t.Helper() + return &APIClient{ + AuthClient: &auth_providers.CommandAuthConfigBasic{ + CommandAuthConfig: auth_providers.CommandAuthConfig{ + CommandHostName: "command.example.com", + CommandPort: port, + }, + }, + } + } + + prepare := func(t *testing.T, c *APIClient) *http.Request { + t.Helper() + req, err := c.prepareRequest( + context.Background(), + "https://placeholder.invalid/api/Status/Endpoints", + "GET", + nil, + map[string]string{}, + nil, + nil, + nil, + ) + if err != nil { + t.Fatalf("prepareRequest() returned unexpected error: %v", err) + } + return req + } + + t.Run("port 443 is omitted from the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 443)) + if got, want := req.URL.Host, "command.example.com"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) + + t.Run("non-443 port is still appended to the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 8443)) + if got, want := req.URL.Host, "command.example.com:8443"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) +} diff --git a/v24/api/keyfactor/v1/model_system_day_of_week.go b/v24/api/keyfactor/v1/model_system_day_of_week.go index c137e17..be750a0 100644 --- a/v24/api/keyfactor/v1/model_system_day_of_week.go +++ b/v24/api/keyfactor/v1/model_system_day_of_week.go @@ -79,20 +79,31 @@ var AllowedSystemDayOfWeekEnumValues = []SystemDayOfWeek{ } func (v *SystemDayOfWeek) UnmarshalJSON(src []byte) error { + // Keyfactor Command has, across API versions, serialized this enum both + // as a JSON integer (e.g. 1) and as a day-name string (e.g. "Monday"). + // Try the integer form first to preserve the original generated + // validation against AllowedSystemDayOfWeekEnumValues. var value int32 - err := json.Unmarshal(src, &value) - if err != nil { - return err - } - enumTypeValue := SystemDayOfWeek(value) - for _, existing := range AllowedSystemDayOfWeekEnumValues { - if existing == enumTypeValue { - *v = enumTypeValue - return nil + if err := json.Unmarshal(src, &value); err == nil { + enumTypeValue := SystemDayOfWeek(value) + for _, existing := range AllowedSystemDayOfWeekEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } } + + return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + } + + // Fall back to the string form (e.g. "Monday") using the existing Parse + // helper, which maps day names to their enum values. + var strValue string + if err := json.Unmarshal(src, &strValue); err != nil { + return fmt.Errorf("SystemDayOfWeek must be a JSON integer or day-name string, got %s", string(src)) } - return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + return v.Parse(strValue) } // NewSystemDayOfWeekFromValue returns a pointer to a valid SystemDayOfWeek diff --git a/v24/api/keyfactor/v1/model_system_day_of_week_test.go b/v24/api/keyfactor/v1/model_system_day_of_week_test.go new file mode 100644 index 0000000..2201cd8 --- /dev/null +++ b/v24/api/keyfactor/v1/model_system_day_of_week_test.go @@ -0,0 +1,117 @@ +/* +Copyright 2025 Keyfactor +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain a +copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless +required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +OR CONDITIONS OF ANY KIND, either express or implied. See the License for +the specific language governing permissions and limitations under the +License. +*/ + +package v1 + +import ( + "encoding/json" + "testing" + "time" +) + +// TestSystemDayOfWeek_UnmarshalJSON_IntForm verifies that the original +// generated wire form (a JSON integer) still deserializes correctly. +func TestSystemDayOfWeek_UnmarshalJSON_IntForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`1`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling int form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_StringForm covers the regression from +// GitHub issue #185: Keyfactor Command v25.5 serializes WeeklyModel.Days as +// day-name strings (e.g. "Monday") rather than integers. +func TestSystemDayOfWeek_UnmarshalJSON_StringForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`"Monday"`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling string form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_InvalidString verifies a malformed day +// name still produces a clear error rather than silently defaulting. +func TestSystemDayOfWeek_UnmarshalJSON_InvalidString(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`"Funday"`), &got) + if err == nil { + t.Fatalf("expected error for invalid day-name string, got nil (value=%v)", got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt verifies an out-of-range +// integer still produces a clear error rather than silently accepting it. +func TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`42`), &got) + if err == nil { + t.Fatalf("expected error for out-of-range int, got nil (value=%v)", got) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayNameStrings is the full round-trip +// regression test for issue #185: a Weekly-shaped schedule payload as +// returned by GET /CertificateAuthority on a v25.5 Command instance must +// deserialize into KeyfactorCommonSchedulingModelsWeeklyModel without error. +func TestWeeklyModel_UnmarshalJSON_DayNameStrings(t *testing.T) { + payload := `{"Days":["Monday","Friday"],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with day-name strings: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } + + wantTime, err := time.Parse(time.RFC3339, "2000-01-01T07:00:00Z") + if err != nil { + t.Fatalf("failed to parse expected time: %v", err) + } + if model.Time == nil || !model.Time.Equal(wantTime) { + t.Errorf("Time: expected %v, got %v", wantTime, model.Time) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayIndexInts verifies the pre-v25.5 integer +// wire form of WeeklyModel.Days still round-trips correctly, guarding +// against a regression in the other direction. +func TestWeeklyModel_UnmarshalJSON_DayIndexInts(t *testing.T) { + payload := `{"Days":[1,5],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with int days: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } +} diff --git a/v24/api/keyfactor/v2/client.go b/v24/api/keyfactor/v2/client.go index 70f0a56..a65a230 100644 --- a/v24/api/keyfactor/v2/client.go +++ b/v24/api/keyfactor/v2/client.go @@ -129,11 +129,12 @@ func buildHttpClientV2(cfg *auth_providers.Server) (AuthConfig, error) { clientAuthType := cfg.GetAuthType() baseConfig := auth_providers.CommandAuthConfig{ - CommandHostName: cfg.Host, - CommandPort: cfg.Port, - CommandAPIPath: cfg.APIPath, - CommandCACert: cfg.CACertPath, - SkipVerify: cfg.SkipTLSVerify, + CommandHostName: cfg.Host, + CommandPort: cfg.Port, + CommandAPIPath: cfg.APIPath, + CommandCACert: cfg.CACertPath, + SkipVerify: cfg.SkipTLSVerify, + HttpClientTimeout: cfg.ClientTimeout, } if clientAuthType == "basic" { diff --git a/v24/api/keyfactor/v2/client_test.go b/v24/api/keyfactor/v2/client_test.go index 6a58a66..5df6091 100644 --- a/v24/api/keyfactor/v2/client_test.go +++ b/v24/api/keyfactor/v2/client_test.go @@ -1,12 +1,37 @@ package v2 import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "os" "reflect" "testing" + "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) +// unsetEnvForTest removes an environment variable for the duration of the +// test and restores its original value (or absence) on cleanup. Unlike +// t.Setenv(key, ""), which leaves the variable "present" with an empty +// value (still visible to os.LookupEnv), this genuinely unsets it so code +// that branches on presence -- e.g. ValidateAuthConfig's +// KEYFACTOR_CLIENT_TIMEOUT handling, which treats a present-but-unparseable +// value as "leave HttpClientTimeout at its current value" rather than +// falling through to the 60s default -- behaves as if the caller's shell +// never exported it at all. +func unsetEnvForTest(t *testing.T, key string) { + t.Helper() + if orig, ok := os.LookupEnv(key); ok { + t.Cleanup(func() { + _ = os.Setenv(key, orig) + }) + _ = os.Unsetenv(key) + } +} + // TestCommandConfigOauth_AccessTokenFieldPropagation is a compilation + correctness // regression test for the v2.8.0 bug where AccessToken, Audience, and Scopes were // silently dropped when constructing CommandConfigOauth from auth_providers.Server @@ -97,3 +122,242 @@ func TestCommandConfigOauth_AccessTokenOnlyNoClientCreds(t *testing.T) { t.Errorf("ClientSecret = %q, want empty", oauthCfg.ClientSecret) } } + +// newFakeCommandServer stands in for a Keyfactor Command instance for +// CommandAuthConfigBasic.Authenticate(), which performs a real GET against +// {host}/{apiPath}/Status/Endpoints as part of authentication. It always +// returns 200 with a valid JSON string array, regardless of credentials. +func newFakeCommandServer(t *testing.T) *httptest.Server { + t.Helper() + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server +} + +// TestBuildHttpClientV2_ClientTimeoutPropagation is a regression test for the +// bug where Server.ClientTimeout was silently dropped when buildHttpClientV2 +// rebuilt its own CommandAuthConfig, causing every caller (including the +// Terraform provider's request_timeout setting) to fall back to +// auth_providers.DefaultClientTimeout (60s) regardless of what was +// configured -- surfacing as "net/http: timeout awaiting response headers" on +// long-running calls such as PFX enrollment. Unlike +// TestCommandConfigOauth_AccessTokenFieldPropagation above (which mirrors +// buildHttpClientV2's lines to avoid the network call inside Authenticate()), +// this test exercises buildHttpClientV2 itself against a fake Command server. +func TestBuildHttpClientV2_ClientTimeoutPropagation(t *testing.T) { + // Hermetic against the ambient environment: ValidateAuthConfig (in + // keyfactor-auth-client-go's auth_core.go) unconditionally overwrites + // SkipVerify from KEYFACTOR_SKIP_VERIFY whenever the variable is merely + // *present*, regardless of its value -- so anything other than exactly + // "true"/"1" (e.g. "TRUE", "false", "0", or an empty string sourced from + // a lab env file) flips SkipTLSVerify back to false and this test fails + // against its own self-signed httptest server with "x509: certificate + // signed by unknown authority". Pin it explicitly rather than relying on + // it being unset in whatever shell runs `go test`. (The upstream + // unconditional-overwrite behavior itself is tracked and fixed + // separately in keyfactor-auth-client-go; this only needs to make our + // test hermetic against it.) + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + // A stale/bad KEYFACTOR_CA_CERT path in the ambient environment would + // make BuildTransport() below treat the value as literal PEM bytes and + // fail with "failed to append custom CA cert to pool". Neutralize it. + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + // KEYFACTOR_CLIENT_TIMEOUT only matters when HttpClientTimeout is <= 0 + // going in (it isn't here -- srv.ClientTimeout is 300 below), but pin it + // too for defense-in-depth. t.Setenv(..., "") would NOT achieve this: an + // empty value is still "present" to os.LookupEnv, so ValidateAuthConfig + // would see ok=true, fail to strconv.Atoi(""), and leave + // HttpClientTimeout at whatever it currently is instead of falling + // through to the 60s default -- the "zero-timeout case". + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 300, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + basicCfg, ok := authCfg.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandAuthConfigBasic, got %T", authCfg) + } + + if basicCfg.HttpClientTimeout != 300 { + t.Errorf("CommandAuthConfigBasic.HttpClientTimeout = %d, want %d", basicCfg.HttpClientTimeout, 300) + } + + transport, tErr := basicCfg.CommandAuthConfig.BuildTransport() + if tErr != nil { + t.Fatalf("BuildTransport() returned unexpected error: %v", tErr) + } + + expected := 300 * time.Second + if transport.ResponseHeaderTimeout != expected { + t.Errorf("ResponseHeaderTimeout = %v, want %v", transport.ResponseHeaderTimeout, expected) + } +} + +// newFakeCommandServerCapturingAuth is like newFakeCommandServer but also +// records the Authorization header of the last request it received, so a +// test can assert which credential actually reached the wire (rather than +// only asserting the shape of a struct literal that never leaves the test). +func newFakeCommandServerCapturingAuth(t *testing.T) (*httptest.Server, *string) { + t.Helper() + var gotAuth string + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server, &gotAuth +} + +// TestBuildHttpClientV2_OAuthAccessTokenPropagation is a regression test for +// the hand-edit cataloged in HAND_EDITS.md (v24 entry #2) that restores +// AccessToken, Audience, and Scopes to the auth_providers.CommandConfigOauth{} +// literal inside buildHttpClientV2's OAuth branch. Those three fields were +// silently dropped once already (2b88eb2) and restored (229db7d); this test +// -- unlike TestCommandConfigOauth_AccessTokenFieldPropagation above, which +// only constructs its own local CommandConfigOauth literal and would keep +// passing even if buildHttpClientV2's actual literal dropped these fields -- +// drives buildHttpClientV2 itself end-to-end against a fake Command server +// and asserts the pre-fetched access token actually reaches the wire as a +// Bearer credential. +func TestBuildHttpClientV2_OAuthAccessTokenPropagation(t *testing.T) { + // See TestBuildHttpClientV2_ClientTimeoutPropagation for why each of + // these is pinned/unset: makes the test hermetic against the ambient + // shell environment. + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + unsetEnvForTest(t, auth_providers.EnvKeyfactorAccessToken) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientID) + + server, gotAuth := newFakeCommandServerCapturingAuth(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + // Deliberately no ClientID/ClientSecret/OAuthTokenUrl: this is the + // pre-fetched access_token-only auth path that 2b88eb2 broke. + AccessToken: "mytoken-abc123", + Audience: "https://my.audience.example.com", + Scopes: []string{"read", "write"}, + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 60, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + oauthCfg, ok := authCfg.(*auth_providers.CommandConfigOauth) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandConfigOauth, got %T", authCfg) + } + + if oauthCfg.AccessToken != "mytoken-abc123" { + t.Errorf("CommandConfigOauth.AccessToken = %q, want %q", oauthCfg.AccessToken, "mytoken-abc123") + } + if oauthCfg.Audience != "https://my.audience.example.com" { + t.Errorf("CommandConfigOauth.Audience = %q, want %q", oauthCfg.Audience, "https://my.audience.example.com") + } + if !reflect.DeepEqual(oauthCfg.Scopes, []string{"read", "write"}) { + t.Errorf("CommandConfigOauth.Scopes = %v, want %v", oauthCfg.Scopes, []string{"read", "write"}) + } + + // The real assertion: buildHttpClientV2's internal Authenticate() call + // made an actual HTTP request to the fake Command server, and that + // request must carry the access token as a Bearer credential. If + // AccessToken were dropped from buildHttpClientV2's literal, the OAuth + // branch would fall through to the client-credentials grant with no + // ClientID/ClientSecret/TokenURL and buildHttpClientV2 would return an + // error above instead of ever reaching this assertion. + wantAuth := "Bearer mytoken-abc123" + if *gotAuth != wantAuth { + t.Errorf("request Authorization header = %q, want %q", *gotAuth, wantAuth) + } +} + +// TestPrepareRequest_Port443Guard is a regression test for the hand-edit in +// commit 229db7d that added "&& serverConfig.Port != 443" to prepareRequest's +// port guard. Without it, a Server configured with Port: 443 (the default +// HTTPS port, and what many callers -- including the Terraform provider -- +// set explicitly) produces request URLs like "https://host:443/..." instead +// of "https://host/...". Both are technically valid HTTPS URLs, but the +// explicit ":443" broke servers/proxies that match on Host header exactly +// (no port suffix) and was reported as a functional regression. This test +// was previously unprotected: reverting the guard would fail nothing in CI. +func TestPrepareRequest_Port443Guard(t *testing.T) { + // Constructed directly against the exported AuthClient field (rather + // than via NewAPIClientWithAuth) so this test exercises prepareRequest + // in isolation without depending on an unrelated hand-edit. + newClientWithPort := func(t *testing.T, port int) *APIClient { + t.Helper() + return &APIClient{ + AuthClient: &auth_providers.CommandAuthConfigBasic{ + CommandAuthConfig: auth_providers.CommandAuthConfig{ + CommandHostName: "command.example.com", + CommandPort: port, + }, + }, + } + } + + prepare := func(t *testing.T, c *APIClient) *http.Request { + t.Helper() + req, err := c.prepareRequest( + context.Background(), + "https://placeholder.invalid/api/Status/Endpoints", + "GET", + nil, + map[string]string{}, + nil, + nil, + nil, + ) + if err != nil { + t.Fatalf("prepareRequest() returned unexpected error: %v", err) + } + return req + } + + t.Run("port 443 is omitted from the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 443)) + if got, want := req.URL.Host, "command.example.com"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) + + t.Run("non-443 port is still appended to the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 8443)) + if got, want := req.URL.Host, "command.example.com:8443"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) +} diff --git a/v24/api/keyfactor/v2/model_system_day_of_week.go b/v24/api/keyfactor/v2/model_system_day_of_week.go index bf9ae58..d2a4988 100644 --- a/v24/api/keyfactor/v2/model_system_day_of_week.go +++ b/v24/api/keyfactor/v2/model_system_day_of_week.go @@ -79,20 +79,31 @@ var AllowedSystemDayOfWeekEnumValues = []SystemDayOfWeek{ } func (v *SystemDayOfWeek) UnmarshalJSON(src []byte) error { + // Keyfactor Command has, across API versions, serialized this enum both + // as a JSON integer (e.g. 1) and as a day-name string (e.g. "Monday"). + // Try the integer form first to preserve the original generated + // validation against AllowedSystemDayOfWeekEnumValues. var value int32 - err := json.Unmarshal(src, &value) - if err != nil { - return err - } - enumTypeValue := SystemDayOfWeek(value) - for _, existing := range AllowedSystemDayOfWeekEnumValues { - if existing == enumTypeValue { - *v = enumTypeValue - return nil + if err := json.Unmarshal(src, &value); err == nil { + enumTypeValue := SystemDayOfWeek(value) + for _, existing := range AllowedSystemDayOfWeekEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } } + + return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + } + + // Fall back to the string form (e.g. "Monday") using the existing Parse + // helper, which maps day names to their enum values. + var strValue string + if err := json.Unmarshal(src, &strValue); err != nil { + return fmt.Errorf("SystemDayOfWeek must be a JSON integer or day-name string, got %s", string(src)) } - return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + return v.Parse(strValue) } // NewSystemDayOfWeekFromValue returns a pointer to a valid SystemDayOfWeek diff --git a/v24/api/keyfactor/v2/model_system_day_of_week_test.go b/v24/api/keyfactor/v2/model_system_day_of_week_test.go new file mode 100644 index 0000000..eb2a288 --- /dev/null +++ b/v24/api/keyfactor/v2/model_system_day_of_week_test.go @@ -0,0 +1,117 @@ +/* +Copyright 2025 Keyfactor +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain a +copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless +required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +OR CONDITIONS OF ANY KIND, either express or implied. See the License for +the specific language governing permissions and limitations under the +License. +*/ + +package v2 + +import ( + "encoding/json" + "testing" + "time" +) + +// TestSystemDayOfWeek_UnmarshalJSON_IntForm verifies that the original +// generated wire form (a JSON integer) still deserializes correctly. +func TestSystemDayOfWeek_UnmarshalJSON_IntForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`1`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling int form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_StringForm covers the regression from +// GitHub issue #185: Keyfactor Command v25.5 serializes WeeklyModel.Days as +// day-name strings (e.g. "Monday") rather than integers. +func TestSystemDayOfWeek_UnmarshalJSON_StringForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`"Monday"`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling string form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_InvalidString verifies a malformed day +// name still produces a clear error rather than silently defaulting. +func TestSystemDayOfWeek_UnmarshalJSON_InvalidString(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`"Funday"`), &got) + if err == nil { + t.Fatalf("expected error for invalid day-name string, got nil (value=%v)", got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt verifies an out-of-range +// integer still produces a clear error rather than silently accepting it. +func TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`42`), &got) + if err == nil { + t.Fatalf("expected error for out-of-range int, got nil (value=%v)", got) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayNameStrings is the full round-trip +// regression test for issue #185: a Weekly-shaped schedule payload as +// returned by GET /CertificateAuthority on a v25.5 Command instance must +// deserialize into KeyfactorCommonSchedulingModelsWeeklyModel without error. +func TestWeeklyModel_UnmarshalJSON_DayNameStrings(t *testing.T) { + payload := `{"Days":["Monday","Friday"],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with day-name strings: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } + + wantTime, err := time.Parse(time.RFC3339, "2000-01-01T07:00:00Z") + if err != nil { + t.Fatalf("failed to parse expected time: %v", err) + } + if model.Time == nil || !model.Time.Equal(wantTime) { + t.Errorf("Time: expected %v, got %v", wantTime, model.Time) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayIndexInts verifies the pre-v25.5 integer +// wire form of WeeklyModel.Days still round-trips correctly, guarding +// against a regression in the other direction. +func TestWeeklyModel_UnmarshalJSON_DayIndexInts(t *testing.T) { + payload := `{"Days":[1,5],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with int days: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } +} diff --git a/v24/go.mod b/v24/go.mod index 216dadf..309020c 100644 --- a/v24/go.mod +++ b/v24/go.mod @@ -1,26 +1,31 @@ module github.com/Keyfactor/keyfactor-go-client-sdk/v24 -go 1.22 +go 1.24.0 -toolchain go1.24.0 - -require github.com/Keyfactor/keyfactor-auth-client-go v1.1.0-rc.8 +require github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/v24/go.sum b/v24/go.sum index ba2f2cd..57b346a 100644 --- a/v24/go.sum +++ b/v24/go.sum @@ -1,35 +1,49 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.0-rc.8 h1:JtA7UwqCSsqBUc0shlEBk+g2xNMorxwQHtknCY7hcUg= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.0-rc.8/go.mod h1:dUIVnqWpPiYkGqKMYQi6Z98fqzQdkZ1KHvJpCoSLQ2s= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 h1:nsp5hrG7EtGFOAaAIyRHt7FSLSWJSIDz66GrLaJU4yA= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -40,27 +54,64 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 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/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v25/api/keyfactor/v1/client.go b/v25/api/keyfactor/v1/client.go index 51c7350..fe885f2 100644 --- a/v25/api/keyfactor/v1/client.go +++ b/v25/api/keyfactor/v1/client.go @@ -257,11 +257,12 @@ func buildHttpClientV2(cfg *auth_providers.Server) (AuthConfig, error) { clientAuthType := cfg.GetAuthType() baseConfig := auth_providers.CommandAuthConfig{ - CommandHostName: cfg.Host, - CommandPort: cfg.Port, - CommandAPIPath: cfg.APIPath, - CommandCACert: cfg.CACertPath, - SkipVerify: cfg.SkipTLSVerify, + CommandHostName: cfg.Host, + CommandPort: cfg.Port, + CommandAPIPath: cfg.APIPath, + CommandCACert: cfg.CACertPath, + SkipVerify: cfg.SkipTLSVerify, + HttpClientTimeout: cfg.ClientTimeout, } if clientAuthType == "basic" { @@ -286,9 +287,9 @@ func buildHttpClientV2(cfg *auth_providers.Server) (AuthConfig, error) { ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, TokenURL: cfg.OAuthTokenUrl, - Audience: cfg.Audience, - Scopes: cfg.Scopes, - AccessToken: cfg.AccessToken, + Audience: cfg.Audience, + Scopes: cfg.Scopes, + AccessToken: cfg.AccessToken, } aErr := oauthCfg.Authenticate() if aErr != nil { diff --git a/v25/api/keyfactor/v1/client_test.go b/v25/api/keyfactor/v1/client_test.go index fadf205..8fced1e 100644 --- a/v25/api/keyfactor/v1/client_test.go +++ b/v25/api/keyfactor/v1/client_test.go @@ -1,12 +1,37 @@ package v1 import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "os" "reflect" "testing" + "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) +// unsetEnvForTest removes an environment variable for the duration of the +// test and restores its original value (or absence) on cleanup. Unlike +// t.Setenv(key, ""), which leaves the variable "present" with an empty +// value (still visible to os.LookupEnv), this genuinely unsets it so code +// that branches on presence -- e.g. ValidateAuthConfig's +// KEYFACTOR_CLIENT_TIMEOUT handling, which treats a present-but-unparseable +// value as "leave HttpClientTimeout at its current value" rather than +// falling through to the 60s default -- behaves as if the caller's shell +// never exported it at all. +func unsetEnvForTest(t *testing.T, key string) { + t.Helper() + if orig, ok := os.LookupEnv(key); ok { + t.Cleanup(func() { + _ = os.Setenv(key, orig) + }) + _ = os.Unsetenv(key) + } +} + // TestCommandConfigOauth_AccessTokenFieldPropagation is a compilation + correctness // regression test for the v2.8.0 bug where AccessToken, Audience, and Scopes were // silently dropped when constructing CommandConfigOauth from auth_providers.Server @@ -97,3 +122,242 @@ func TestCommandConfigOauth_AccessTokenOnlyNoClientCreds(t *testing.T) { t.Errorf("ClientSecret = %q, want empty", oauthCfg.ClientSecret) } } + +// newFakeCommandServer stands in for a Keyfactor Command instance for +// CommandAuthConfigBasic.Authenticate(), which performs a real GET against +// {host}/{apiPath}/Status/Endpoints as part of authentication. It always +// returns 200 with a valid JSON string array, regardless of credentials. +func newFakeCommandServer(t *testing.T) *httptest.Server { + t.Helper() + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server +} + +// TestBuildHttpClientV2_ClientTimeoutPropagation is a regression test for the +// bug where Server.ClientTimeout was silently dropped when buildHttpClientV2 +// rebuilt its own CommandAuthConfig, causing every caller (including the +// Terraform provider's request_timeout setting) to fall back to +// auth_providers.DefaultClientTimeout (60s) regardless of what was +// configured -- surfacing as "net/http: timeout awaiting response headers" on +// long-running calls such as PFX enrollment. Unlike +// TestCommandConfigOauth_AccessTokenFieldPropagation above (which mirrors +// buildHttpClientV2's lines to avoid the network call inside Authenticate()), +// this test exercises buildHttpClientV2 itself against a fake Command server. +func TestBuildHttpClientV2_ClientTimeoutPropagation(t *testing.T) { + // Hermetic against the ambient environment: ValidateAuthConfig (in + // keyfactor-auth-client-go's auth_core.go) unconditionally overwrites + // SkipVerify from KEYFACTOR_SKIP_VERIFY whenever the variable is merely + // *present*, regardless of its value -- so anything other than exactly + // "true"/"1" (e.g. "TRUE", "false", "0", or an empty string sourced from + // a lab env file) flips SkipTLSVerify back to false and this test fails + // against its own self-signed httptest server with "x509: certificate + // signed by unknown authority". Pin it explicitly rather than relying on + // it being unset in whatever shell runs `go test`. (The upstream + // unconditional-overwrite behavior itself is tracked and fixed + // separately in keyfactor-auth-client-go; this only needs to make our + // test hermetic against it.) + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + // A stale/bad KEYFACTOR_CA_CERT path in the ambient environment would + // make BuildTransport() below treat the value as literal PEM bytes and + // fail with "failed to append custom CA cert to pool". Neutralize it. + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + // KEYFACTOR_CLIENT_TIMEOUT only matters when HttpClientTimeout is <= 0 + // going in (it isn't here -- srv.ClientTimeout is 300 below), but pin it + // too for defense-in-depth. t.Setenv(..., "") would NOT achieve this: an + // empty value is still "present" to os.LookupEnv, so ValidateAuthConfig + // would see ok=true, fail to strconv.Atoi(""), and leave + // HttpClientTimeout at whatever it currently is instead of falling + // through to the 60s default -- the "zero-timeout case". + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 300, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + basicCfg, ok := authCfg.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandAuthConfigBasic, got %T", authCfg) + } + + if basicCfg.HttpClientTimeout != 300 { + t.Errorf("CommandAuthConfigBasic.HttpClientTimeout = %d, want %d", basicCfg.HttpClientTimeout, 300) + } + + transport, tErr := basicCfg.CommandAuthConfig.BuildTransport() + if tErr != nil { + t.Fatalf("BuildTransport() returned unexpected error: %v", tErr) + } + + expected := 300 * time.Second + if transport.ResponseHeaderTimeout != expected { + t.Errorf("ResponseHeaderTimeout = %v, want %v", transport.ResponseHeaderTimeout, expected) + } +} + +// newFakeCommandServerCapturingAuth is like newFakeCommandServer but also +// records the Authorization header of the last request it received, so a +// test can assert which credential actually reached the wire (rather than +// only asserting the shape of a struct literal that never leaves the test). +func newFakeCommandServerCapturingAuth(t *testing.T) (*httptest.Server, *string) { + t.Helper() + var gotAuth string + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server, &gotAuth +} + +// TestBuildHttpClientV2_OAuthAccessTokenPropagation is a regression test for +// the hand-edit cataloged in HAND_EDITS.md (v24 entry #2) that restores +// AccessToken, Audience, and Scopes to the auth_providers.CommandConfigOauth{} +// literal inside buildHttpClientV2's OAuth branch. Those three fields were +// silently dropped once already (2b88eb2) and restored (229db7d); this test +// -- unlike TestCommandConfigOauth_AccessTokenFieldPropagation above, which +// only constructs its own local CommandConfigOauth literal and would keep +// passing even if buildHttpClientV2's actual literal dropped these fields -- +// drives buildHttpClientV2 itself end-to-end against a fake Command server +// and asserts the pre-fetched access token actually reaches the wire as a +// Bearer credential. +func TestBuildHttpClientV2_OAuthAccessTokenPropagation(t *testing.T) { + // See TestBuildHttpClientV2_ClientTimeoutPropagation for why each of + // these is pinned/unset: makes the test hermetic against the ambient + // shell environment. + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + unsetEnvForTest(t, auth_providers.EnvKeyfactorAccessToken) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientID) + + server, gotAuth := newFakeCommandServerCapturingAuth(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + // Deliberately no ClientID/ClientSecret/OAuthTokenUrl: this is the + // pre-fetched access_token-only auth path that 2b88eb2 broke. + AccessToken: "mytoken-abc123", + Audience: "https://my.audience.example.com", + Scopes: []string{"read", "write"}, + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 60, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + oauthCfg, ok := authCfg.(*auth_providers.CommandConfigOauth) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandConfigOauth, got %T", authCfg) + } + + if oauthCfg.AccessToken != "mytoken-abc123" { + t.Errorf("CommandConfigOauth.AccessToken = %q, want %q", oauthCfg.AccessToken, "mytoken-abc123") + } + if oauthCfg.Audience != "https://my.audience.example.com" { + t.Errorf("CommandConfigOauth.Audience = %q, want %q", oauthCfg.Audience, "https://my.audience.example.com") + } + if !reflect.DeepEqual(oauthCfg.Scopes, []string{"read", "write"}) { + t.Errorf("CommandConfigOauth.Scopes = %v, want %v", oauthCfg.Scopes, []string{"read", "write"}) + } + + // The real assertion: buildHttpClientV2's internal Authenticate() call + // made an actual HTTP request to the fake Command server, and that + // request must carry the access token as a Bearer credential. If + // AccessToken were dropped from buildHttpClientV2's literal, the OAuth + // branch would fall through to the client-credentials grant with no + // ClientID/ClientSecret/TokenURL and buildHttpClientV2 would return an + // error above instead of ever reaching this assertion. + wantAuth := "Bearer mytoken-abc123" + if *gotAuth != wantAuth { + t.Errorf("request Authorization header = %q, want %q", *gotAuth, wantAuth) + } +} + +// TestPrepareRequest_Port443Guard is a regression test for the hand-edit in +// commit 229db7d that added "&& serverConfig.Port != 443" to prepareRequest's +// port guard. Without it, a Server configured with Port: 443 (the default +// HTTPS port, and what many callers -- including the Terraform provider -- +// set explicitly) produces request URLs like "https://host:443/..." instead +// of "https://host/...". Both are technically valid HTTPS URLs, but the +// explicit ":443" broke servers/proxies that match on Host header exactly +// (no port suffix) and was reported as a functional regression. This test +// was previously unprotected: reverting the guard would fail nothing in CI. +func TestPrepareRequest_Port443Guard(t *testing.T) { + // Constructed directly against the exported AuthClient field (rather + // than via NewAPIClientWithAuth) so this test exercises prepareRequest + // in isolation without depending on an unrelated hand-edit. + newClientWithPort := func(t *testing.T, port int) *APIClient { + t.Helper() + return &APIClient{ + AuthClient: &auth_providers.CommandAuthConfigBasic{ + CommandAuthConfig: auth_providers.CommandAuthConfig{ + CommandHostName: "command.example.com", + CommandPort: port, + }, + }, + } + } + + prepare := func(t *testing.T, c *APIClient) *http.Request { + t.Helper() + req, err := c.prepareRequest( + context.Background(), + "https://placeholder.invalid/api/Status/Endpoints", + "GET", + nil, + map[string]string{}, + nil, + nil, + nil, + ) + if err != nil { + t.Fatalf("prepareRequest() returned unexpected error: %v", err) + } + return req + } + + t.Run("port 443 is omitted from the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 443)) + if got, want := req.URL.Host, "command.example.com"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) + + t.Run("non-443 port is still appended to the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 8443)) + if got, want := req.URL.Host, "command.example.com:8443"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) +} diff --git a/v25/api/keyfactor/v1/model_system_day_of_week.go b/v25/api/keyfactor/v1/model_system_day_of_week.go index d8b8dc7..f29e86c 100644 --- a/v25/api/keyfactor/v1/model_system_day_of_week.go +++ b/v25/api/keyfactor/v1/model_system_day_of_week.go @@ -79,20 +79,31 @@ var AllowedSystemDayOfWeekEnumValues = []SystemDayOfWeek{ } func (v *SystemDayOfWeek) UnmarshalJSON(src []byte) error { + // Keyfactor Command has, across API versions, serialized this enum both + // as a JSON integer (e.g. 1) and as a day-name string (e.g. "Monday"). + // Try the integer form first to preserve the original generated + // validation against AllowedSystemDayOfWeekEnumValues. var value int32 - err := json.Unmarshal(src, &value) - if err != nil { - return err - } - enumTypeValue := SystemDayOfWeek(value) - for _, existing := range AllowedSystemDayOfWeekEnumValues { - if existing == enumTypeValue { - *v = enumTypeValue - return nil + if err := json.Unmarshal(src, &value); err == nil { + enumTypeValue := SystemDayOfWeek(value) + for _, existing := range AllowedSystemDayOfWeekEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } } + + return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + } + + // Fall back to the string form (e.g. "Monday") using the existing Parse + // helper, which maps day names to their enum values. + var strValue string + if err := json.Unmarshal(src, &strValue); err != nil { + return fmt.Errorf("SystemDayOfWeek must be a JSON integer or day-name string, got %s", string(src)) } - return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + return v.Parse(strValue) } // NewSystemDayOfWeekFromValue returns a pointer to a valid SystemDayOfWeek diff --git a/v25/api/keyfactor/v1/model_system_day_of_week_test.go b/v25/api/keyfactor/v1/model_system_day_of_week_test.go new file mode 100644 index 0000000..2201cd8 --- /dev/null +++ b/v25/api/keyfactor/v1/model_system_day_of_week_test.go @@ -0,0 +1,117 @@ +/* +Copyright 2025 Keyfactor +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain a +copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless +required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +OR CONDITIONS OF ANY KIND, either express or implied. See the License for +the specific language governing permissions and limitations under the +License. +*/ + +package v1 + +import ( + "encoding/json" + "testing" + "time" +) + +// TestSystemDayOfWeek_UnmarshalJSON_IntForm verifies that the original +// generated wire form (a JSON integer) still deserializes correctly. +func TestSystemDayOfWeek_UnmarshalJSON_IntForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`1`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling int form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_StringForm covers the regression from +// GitHub issue #185: Keyfactor Command v25.5 serializes WeeklyModel.Days as +// day-name strings (e.g. "Monday") rather than integers. +func TestSystemDayOfWeek_UnmarshalJSON_StringForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`"Monday"`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling string form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_InvalidString verifies a malformed day +// name still produces a clear error rather than silently defaulting. +func TestSystemDayOfWeek_UnmarshalJSON_InvalidString(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`"Funday"`), &got) + if err == nil { + t.Fatalf("expected error for invalid day-name string, got nil (value=%v)", got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt verifies an out-of-range +// integer still produces a clear error rather than silently accepting it. +func TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`42`), &got) + if err == nil { + t.Fatalf("expected error for out-of-range int, got nil (value=%v)", got) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayNameStrings is the full round-trip +// regression test for issue #185: a Weekly-shaped schedule payload as +// returned by GET /CertificateAuthority on a v25.5 Command instance must +// deserialize into KeyfactorCommonSchedulingModelsWeeklyModel without error. +func TestWeeklyModel_UnmarshalJSON_DayNameStrings(t *testing.T) { + payload := `{"Days":["Monday","Friday"],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with day-name strings: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } + + wantTime, err := time.Parse(time.RFC3339, "2000-01-01T07:00:00Z") + if err != nil { + t.Fatalf("failed to parse expected time: %v", err) + } + if model.Time == nil || !model.Time.Equal(wantTime) { + t.Errorf("Time: expected %v, got %v", wantTime, model.Time) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayIndexInts verifies the pre-v25.5 integer +// wire form of WeeklyModel.Days still round-trips correctly, guarding +// against a regression in the other direction. +func TestWeeklyModel_UnmarshalJSON_DayIndexInts(t *testing.T) { + payload := `{"Days":[1,5],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with int days: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } +} diff --git a/v25/api/keyfactor/v2/client.go b/v25/api/keyfactor/v2/client.go index 656c4a7..8390026 100644 --- a/v25/api/keyfactor/v2/client.go +++ b/v25/api/keyfactor/v2/client.go @@ -116,11 +116,12 @@ func buildHttpClientV2(cfg *auth_providers.Server) (AuthConfig, error) { clientAuthType := cfg.GetAuthType() baseConfig := auth_providers.CommandAuthConfig{ - CommandHostName: cfg.Host, - CommandPort: cfg.Port, - CommandAPIPath: cfg.APIPath, - CommandCACert: cfg.CACertPath, - SkipVerify: cfg.SkipTLSVerify, + CommandHostName: cfg.Host, + CommandPort: cfg.Port, + CommandAPIPath: cfg.APIPath, + CommandCACert: cfg.CACertPath, + SkipVerify: cfg.SkipTLSVerify, + HttpClientTimeout: cfg.ClientTimeout, } if clientAuthType == "basic" { @@ -145,9 +146,9 @@ func buildHttpClientV2(cfg *auth_providers.Server) (AuthConfig, error) { ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, TokenURL: cfg.OAuthTokenUrl, - Audience: cfg.Audience, - Scopes: cfg.Scopes, - AccessToken: cfg.AccessToken, + Audience: cfg.Audience, + Scopes: cfg.Scopes, + AccessToken: cfg.AccessToken, } aErr := oauthCfg.Authenticate() if aErr != nil { diff --git a/v25/api/keyfactor/v2/client_test.go b/v25/api/keyfactor/v2/client_test.go index 6a58a66..5df6091 100644 --- a/v25/api/keyfactor/v2/client_test.go +++ b/v25/api/keyfactor/v2/client_test.go @@ -1,12 +1,37 @@ package v2 import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "os" "reflect" "testing" + "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" ) +// unsetEnvForTest removes an environment variable for the duration of the +// test and restores its original value (or absence) on cleanup. Unlike +// t.Setenv(key, ""), which leaves the variable "present" with an empty +// value (still visible to os.LookupEnv), this genuinely unsets it so code +// that branches on presence -- e.g. ValidateAuthConfig's +// KEYFACTOR_CLIENT_TIMEOUT handling, which treats a present-but-unparseable +// value as "leave HttpClientTimeout at its current value" rather than +// falling through to the 60s default -- behaves as if the caller's shell +// never exported it at all. +func unsetEnvForTest(t *testing.T, key string) { + t.Helper() + if orig, ok := os.LookupEnv(key); ok { + t.Cleanup(func() { + _ = os.Setenv(key, orig) + }) + _ = os.Unsetenv(key) + } +} + // TestCommandConfigOauth_AccessTokenFieldPropagation is a compilation + correctness // regression test for the v2.8.0 bug where AccessToken, Audience, and Scopes were // silently dropped when constructing CommandConfigOauth from auth_providers.Server @@ -97,3 +122,242 @@ func TestCommandConfigOauth_AccessTokenOnlyNoClientCreds(t *testing.T) { t.Errorf("ClientSecret = %q, want empty", oauthCfg.ClientSecret) } } + +// newFakeCommandServer stands in for a Keyfactor Command instance for +// CommandAuthConfigBasic.Authenticate(), which performs a real GET against +// {host}/{apiPath}/Status/Endpoints as part of authentication. It always +// returns 200 with a valid JSON string array, regardless of credentials. +func newFakeCommandServer(t *testing.T) *httptest.Server { + t.Helper() + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server +} + +// TestBuildHttpClientV2_ClientTimeoutPropagation is a regression test for the +// bug where Server.ClientTimeout was silently dropped when buildHttpClientV2 +// rebuilt its own CommandAuthConfig, causing every caller (including the +// Terraform provider's request_timeout setting) to fall back to +// auth_providers.DefaultClientTimeout (60s) regardless of what was +// configured -- surfacing as "net/http: timeout awaiting response headers" on +// long-running calls such as PFX enrollment. Unlike +// TestCommandConfigOauth_AccessTokenFieldPropagation above (which mirrors +// buildHttpClientV2's lines to avoid the network call inside Authenticate()), +// this test exercises buildHttpClientV2 itself against a fake Command server. +func TestBuildHttpClientV2_ClientTimeoutPropagation(t *testing.T) { + // Hermetic against the ambient environment: ValidateAuthConfig (in + // keyfactor-auth-client-go's auth_core.go) unconditionally overwrites + // SkipVerify from KEYFACTOR_SKIP_VERIFY whenever the variable is merely + // *present*, regardless of its value -- so anything other than exactly + // "true"/"1" (e.g. "TRUE", "false", "0", or an empty string sourced from + // a lab env file) flips SkipTLSVerify back to false and this test fails + // against its own self-signed httptest server with "x509: certificate + // signed by unknown authority". Pin it explicitly rather than relying on + // it being unset in whatever shell runs `go test`. (The upstream + // unconditional-overwrite behavior itself is tracked and fixed + // separately in keyfactor-auth-client-go; this only needs to make our + // test hermetic against it.) + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + // A stale/bad KEYFACTOR_CA_CERT path in the ambient environment would + // make BuildTransport() below treat the value as literal PEM bytes and + // fail with "failed to append custom CA cert to pool". Neutralize it. + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + // KEYFACTOR_CLIENT_TIMEOUT only matters when HttpClientTimeout is <= 0 + // going in (it isn't here -- srv.ClientTimeout is 300 below), but pin it + // too for defense-in-depth. t.Setenv(..., "") would NOT achieve this: an + // empty value is still "present" to os.LookupEnv, so ValidateAuthConfig + // would see ok=true, fail to strconv.Atoi(""), and leave + // HttpClientTimeout at whatever it currently is instead of falling + // through to the 60s default -- the "zero-timeout case". + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 300, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + basicCfg, ok := authCfg.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandAuthConfigBasic, got %T", authCfg) + } + + if basicCfg.HttpClientTimeout != 300 { + t.Errorf("CommandAuthConfigBasic.HttpClientTimeout = %d, want %d", basicCfg.HttpClientTimeout, 300) + } + + transport, tErr := basicCfg.CommandAuthConfig.BuildTransport() + if tErr != nil { + t.Fatalf("BuildTransport() returned unexpected error: %v", tErr) + } + + expected := 300 * time.Second + if transport.ResponseHeaderTimeout != expected { + t.Errorf("ResponseHeaderTimeout = %v, want %v", transport.ResponseHeaderTimeout, expected) + } +} + +// newFakeCommandServerCapturingAuth is like newFakeCommandServer but also +// records the Authorization header of the last request it received, so a +// test can assert which credential actually reached the wire (rather than +// only asserting the shape of a struct literal that never leaves the test). +func newFakeCommandServerCapturingAuth(t *testing.T) (*httptest.Server, *string) { + t.Helper() + var gotAuth string + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server, &gotAuth +} + +// TestBuildHttpClientV2_OAuthAccessTokenPropagation is a regression test for +// the hand-edit cataloged in HAND_EDITS.md (v24 entry #2) that restores +// AccessToken, Audience, and Scopes to the auth_providers.CommandConfigOauth{} +// literal inside buildHttpClientV2's OAuth branch. Those three fields were +// silently dropped once already (2b88eb2) and restored (229db7d); this test +// -- unlike TestCommandConfigOauth_AccessTokenFieldPropagation above, which +// only constructs its own local CommandConfigOauth literal and would keep +// passing even if buildHttpClientV2's actual literal dropped these fields -- +// drives buildHttpClientV2 itself end-to-end against a fake Command server +// and asserts the pre-fetched access token actually reaches the wire as a +// Bearer credential. +func TestBuildHttpClientV2_OAuthAccessTokenPropagation(t *testing.T) { + // See TestBuildHttpClientV2_ClientTimeoutPropagation for why each of + // these is pinned/unset: makes the test hermetic against the ambient + // shell environment. + t.Setenv(auth_providers.EnvKeyfactorSkipVerify, "true") + unsetEnvForTest(t, auth_providers.EnvKeyfactorCACert) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientTimeout) + unsetEnvForTest(t, auth_providers.EnvKeyfactorAccessToken) + unsetEnvForTest(t, auth_providers.EnvKeyfactorClientID) + + server, gotAuth := newFakeCommandServerCapturingAuth(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + srv := &auth_providers.Server{ + Host: u.Host, + // Deliberately no ClientID/ClientSecret/OAuthTokenUrl: this is the + // pre-fetched access_token-only auth path that 2b88eb2 broke. + AccessToken: "mytoken-abc123", + Audience: "https://my.audience.example.com", + Scopes: []string{"read", "write"}, + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 60, + } + + authCfg, err := buildHttpClientV2(srv) + if err != nil { + t.Fatalf("buildHttpClientV2() returned unexpected error: %v", err) + } + + oauthCfg, ok := authCfg.(*auth_providers.CommandConfigOauth) + if !ok { + t.Fatalf("expected AuthConfig to be *auth_providers.CommandConfigOauth, got %T", authCfg) + } + + if oauthCfg.AccessToken != "mytoken-abc123" { + t.Errorf("CommandConfigOauth.AccessToken = %q, want %q", oauthCfg.AccessToken, "mytoken-abc123") + } + if oauthCfg.Audience != "https://my.audience.example.com" { + t.Errorf("CommandConfigOauth.Audience = %q, want %q", oauthCfg.Audience, "https://my.audience.example.com") + } + if !reflect.DeepEqual(oauthCfg.Scopes, []string{"read", "write"}) { + t.Errorf("CommandConfigOauth.Scopes = %v, want %v", oauthCfg.Scopes, []string{"read", "write"}) + } + + // The real assertion: buildHttpClientV2's internal Authenticate() call + // made an actual HTTP request to the fake Command server, and that + // request must carry the access token as a Bearer credential. If + // AccessToken were dropped from buildHttpClientV2's literal, the OAuth + // branch would fall through to the client-credentials grant with no + // ClientID/ClientSecret/TokenURL and buildHttpClientV2 would return an + // error above instead of ever reaching this assertion. + wantAuth := "Bearer mytoken-abc123" + if *gotAuth != wantAuth { + t.Errorf("request Authorization header = %q, want %q", *gotAuth, wantAuth) + } +} + +// TestPrepareRequest_Port443Guard is a regression test for the hand-edit in +// commit 229db7d that added "&& serverConfig.Port != 443" to prepareRequest's +// port guard. Without it, a Server configured with Port: 443 (the default +// HTTPS port, and what many callers -- including the Terraform provider -- +// set explicitly) produces request URLs like "https://host:443/..." instead +// of "https://host/...". Both are technically valid HTTPS URLs, but the +// explicit ":443" broke servers/proxies that match on Host header exactly +// (no port suffix) and was reported as a functional regression. This test +// was previously unprotected: reverting the guard would fail nothing in CI. +func TestPrepareRequest_Port443Guard(t *testing.T) { + // Constructed directly against the exported AuthClient field (rather + // than via NewAPIClientWithAuth) so this test exercises prepareRequest + // in isolation without depending on an unrelated hand-edit. + newClientWithPort := func(t *testing.T, port int) *APIClient { + t.Helper() + return &APIClient{ + AuthClient: &auth_providers.CommandAuthConfigBasic{ + CommandAuthConfig: auth_providers.CommandAuthConfig{ + CommandHostName: "command.example.com", + CommandPort: port, + }, + }, + } + } + + prepare := func(t *testing.T, c *APIClient) *http.Request { + t.Helper() + req, err := c.prepareRequest( + context.Background(), + "https://placeholder.invalid/api/Status/Endpoints", + "GET", + nil, + map[string]string{}, + nil, + nil, + nil, + ) + if err != nil { + t.Fatalf("prepareRequest() returned unexpected error: %v", err) + } + return req + } + + t.Run("port 443 is omitted from the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 443)) + if got, want := req.URL.Host, "command.example.com"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) + + t.Run("non-443 port is still appended to the request host", func(t *testing.T) { + req := prepare(t, newClientWithPort(t, 8443)) + if got, want := req.URL.Host, "command.example.com:8443"; got != want { + t.Errorf("URL.Host = %q, want %q", got, want) + } + }) +} diff --git a/v25/api/keyfactor/v2/model_system_day_of_week.go b/v25/api/keyfactor/v2/model_system_day_of_week.go index 65de2b2..5b07ab4 100644 --- a/v25/api/keyfactor/v2/model_system_day_of_week.go +++ b/v25/api/keyfactor/v2/model_system_day_of_week.go @@ -79,20 +79,31 @@ var AllowedSystemDayOfWeekEnumValues = []SystemDayOfWeek{ } func (v *SystemDayOfWeek) UnmarshalJSON(src []byte) error { + // Keyfactor Command has, across API versions, serialized this enum both + // as a JSON integer (e.g. 1) and as a day-name string (e.g. "Monday"). + // Try the integer form first to preserve the original generated + // validation against AllowedSystemDayOfWeekEnumValues. var value int32 - err := json.Unmarshal(src, &value) - if err != nil { - return err - } - enumTypeValue := SystemDayOfWeek(value) - for _, existing := range AllowedSystemDayOfWeekEnumValues { - if existing == enumTypeValue { - *v = enumTypeValue - return nil + if err := json.Unmarshal(src, &value); err == nil { + enumTypeValue := SystemDayOfWeek(value) + for _, existing := range AllowedSystemDayOfWeekEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } } + + return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + } + + // Fall back to the string form (e.g. "Monday") using the existing Parse + // helper, which maps day names to their enum values. + var strValue string + if err := json.Unmarshal(src, &strValue); err != nil { + return fmt.Errorf("SystemDayOfWeek must be a JSON integer or day-name string, got %s", string(src)) } - return fmt.Errorf("%+v is not a valid SystemDayOfWeek", value) + return v.Parse(strValue) } // NewSystemDayOfWeekFromValue returns a pointer to a valid SystemDayOfWeek diff --git a/v25/api/keyfactor/v2/model_system_day_of_week_test.go b/v25/api/keyfactor/v2/model_system_day_of_week_test.go new file mode 100644 index 0000000..eb2a288 --- /dev/null +++ b/v25/api/keyfactor/v2/model_system_day_of_week_test.go @@ -0,0 +1,117 @@ +/* +Copyright 2025 Keyfactor +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain a +copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless +required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +OR CONDITIONS OF ANY KIND, either express or implied. See the License for +the specific language governing permissions and limitations under the +License. +*/ + +package v2 + +import ( + "encoding/json" + "testing" + "time" +) + +// TestSystemDayOfWeek_UnmarshalJSON_IntForm verifies that the original +// generated wire form (a JSON integer) still deserializes correctly. +func TestSystemDayOfWeek_UnmarshalJSON_IntForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`1`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling int form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_StringForm covers the regression from +// GitHub issue #185: Keyfactor Command v25.5 serializes WeeklyModel.Days as +// day-name strings (e.g. "Monday") rather than integers. +func TestSystemDayOfWeek_UnmarshalJSON_StringForm(t *testing.T) { + var got SystemDayOfWeek + if err := json.Unmarshal([]byte(`"Monday"`), &got); err != nil { + t.Fatalf("unexpected error unmarshaling string form: %v", err) + } + if got != SYSTEMDAYOFWEEK_Monday { + t.Errorf("expected %v, got %v", SYSTEMDAYOFWEEK_Monday, got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_InvalidString verifies a malformed day +// name still produces a clear error rather than silently defaulting. +func TestSystemDayOfWeek_UnmarshalJSON_InvalidString(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`"Funday"`), &got) + if err == nil { + t.Fatalf("expected error for invalid day-name string, got nil (value=%v)", got) + } +} + +// TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt verifies an out-of-range +// integer still produces a clear error rather than silently accepting it. +func TestSystemDayOfWeek_UnmarshalJSON_OutOfRangeInt(t *testing.T) { + var got SystemDayOfWeek + err := json.Unmarshal([]byte(`42`), &got) + if err == nil { + t.Fatalf("expected error for out-of-range int, got nil (value=%v)", got) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayNameStrings is the full round-trip +// regression test for issue #185: a Weekly-shaped schedule payload as +// returned by GET /CertificateAuthority on a v25.5 Command instance must +// deserialize into KeyfactorCommonSchedulingModelsWeeklyModel without error. +func TestWeeklyModel_UnmarshalJSON_DayNameStrings(t *testing.T) { + payload := `{"Days":["Monday","Friday"],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with day-name strings: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } + + wantTime, err := time.Parse(time.RFC3339, "2000-01-01T07:00:00Z") + if err != nil { + t.Fatalf("failed to parse expected time: %v", err) + } + if model.Time == nil || !model.Time.Equal(wantTime) { + t.Errorf("Time: expected %v, got %v", wantTime, model.Time) + } +} + +// TestWeeklyModel_UnmarshalJSON_DayIndexInts verifies the pre-v25.5 integer +// wire form of WeeklyModel.Days still round-trips correctly, guarding +// against a regression in the other direction. +func TestWeeklyModel_UnmarshalJSON_DayIndexInts(t *testing.T) { + payload := `{"Days":[1,5],"Time":"2000-01-01T07:00:00Z"}` + + var model KeyfactorCommonSchedulingModelsWeeklyModel + if err := json.Unmarshal([]byte(payload), &model); err != nil { + t.Fatalf("unexpected error unmarshaling WeeklyModel with int days: %v", err) + } + + wantDays := []SystemDayOfWeek{SYSTEMDAYOFWEEK_Monday, SYSTEMDAYOFWEEK_Friday} + if len(model.Days) != len(wantDays) { + t.Fatalf("expected %d days, got %d (%v)", len(wantDays), len(model.Days), model.Days) + } + for i, want := range wantDays { + if model.Days[i] != want { + t.Errorf("Days[%d]: expected %v, got %v", i, want, model.Days[i]) + } + } +} diff --git a/v25/go.mod b/v25/go.mod index b0ecb0f..3dad71e 100644 --- a/v25/go.mod +++ b/v25/go.mod @@ -1,26 +1,31 @@ module github.com/Keyfactor/keyfactor-go-client-sdk/v25 -go 1.24 +go 1.24.0 -toolchain go1.24.0 - -require github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 +require github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/v25/go.sum b/v25/go.sum index 2d1793f..57b346a 100644 --- a/v25/go.sum +++ b/v25/go.sum @@ -1,31 +1,47 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 h1:otC213b6CYzqeN9b3CRlH1Qj1hTFIN5nqPA8gTlHdLg= -github.com/Keyfactor/keyfactor-auth-client-go v1.3.0/go.mod h1:97vCisBNkdCK0l2TuvOSdjlpvQa4+GHsMut1UTyv1jo= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 h1:nsp5hrG7EtGFOAaAIyRHt7FSLSWJSIDz66GrLaJU4yA= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -38,27 +54,64 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 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/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= -github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=