Skip to content

test(live): apikey CRUD, user lifecycle, template versions, cleanup policies, cluster CRUD#100

Merged
omattsson merged 2 commits into
mainfrom
test/live-coverage-expansion
May 28, 2026
Merged

test(live): apikey CRUD, user lifecycle, template versions, cleanup policies, cluster CRUD#100
omattsson merged 2 commits into
mainfrom
test/live-coverage-expansion

Conversation

@omattsson

@omattsson omattsson commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

Adds five new endpoint-group live tests covering the highest-blast-radius surfaces still missing from cli/test/live/. All wire-shape focused (no real workloads), all clean up after themselves, all pass under both rancher-desktop and the CI api-only flow.

What's covered

File What it locks down
apikey_live_test.go Create → list → revoke against the calling user. Asserts the raw_key (sk_-prefixed, returned once) contract that the CI bootstrap implicitly depends on but never asserted.
user_live_test.go Register a throwaway user → list (admin path) → disable → enable → reset-password → delete. Never touches the admin caller.
template_versions_live_test.go Publishes the same template twice to materialise two version snapshots, then exercises list → get → diff with shape asserts on left / right / chart_diffs.
cleanup_policy_live_test.go Admin CRUD + a dry-run execution. The condition idle_days:9999 deliberately matches nothing so the run can't mutate a real instance.
cluster_lifecycle_live_test.go Stub-cluster create → get → update → delete. IsDefault stays false to avoid disrupting requireCluster() for other tests. Covers the registry_* / image_pull_secret_name fields whose drift triggered #95.

Bonus findings (left as commented follow-ups, not blockers)

Validating these five tests against rancher-desktop surfaced three stackctl/backend contract gaps — exactly what this layer is for:

  1. UpdateTemplateRequest.Name is omitempty, but the backend rejects PUT with "name is required" when omitted. Either drop the omitempty or relax the server-side requirement.
  2. CreateTemplateRequest has no version field, so the template-level Version is unsettable from the CLI — the version snapshot's version round-trips empty as a result.
  3. Backend rejects kubeconfig_data unless KUBECONFIG_ENCRYPTION_KEY is set; kubeconfig_path works without that prerequisite (used in the cluster lifecycle test).

Each is called out inline with a Note: comment explaining the workaround.

Verification

Full live suite against rancher-desktop k8s-stack-manager:

21 passed, 2 skipped (heavy-gated), 0 failed

Once PR #99 (the live-tests CI workflow) merges, these tests will also run on every PR via the same job.

Test plan

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Added live integration tests covering: API key lifecycle (create/list/delete), cleanup policy CRUD with dry-run execution and result validation, cluster create/update/delete flow, template versioning with publish/list/get/diff checks, and user register/list/disable/enable/password-reset/delete scenarios. All tests include best-effort cleanup to reduce flakiness.

Review Change Stack

…ions, cleanup policies, cluster CRUD

Adds five new endpoint-group live tests covering the highest-blast-radius
surfaces still missing from cli/test/live/. All tests are wire-shape
focused (no real workloads created), follow the existing helpers/cleanup
conventions in this package, and run cleanly under the CI api-only flow
introduced in #99.

New files:
  apikey_live_test.go
      - Create → list → revoke cycle against the calling user (whoami).
      - Locks the raw_key contract: sk_-prefixed, returned once, never
        in list. Was implicitly relied on by the CI bootstrap but
        never asserted.

  user_live_test.go
      - Register a throwaway user, list (admin-only path), disable,
        enable, reset-password, delete. Never operates on admin —
        locking out the caller would break the rest of the suite.

  template_versions_live_test.go
      - Publishes the same template twice (description-only change in
        between) to materialise two version snapshots, then exercises
        list → get → diff with shape assertions on left/right/chart_diffs.

  cleanup_policy_live_test.go
      - Full admin CRUD plus a dry-run execution. The condition
        "idle_days:9999" deliberately matches nothing so the run never
        mutates a real instance.

  cluster_lifecycle_live_test.go
      - Stub-cluster create → get → update → delete. IsDefault stays
        false so the test never disrupts requireCluster() for other
        tests. Exercises registry_* + image_pull_secret_name fields
        (the registry_password drift in PR #95 is the canonical
        example of why this surface needs a live test).

Bonus findings surfaced during local validation against rancher-desktop
(left as commented follow-ups, not blockers for this PR):

  - stackctl's UpdateTemplateRequest.Name is `omitempty` but the
    backend rejects PUT with "name is required" when omitted. Either
    drop the omitempty or relax the backend.
  - stackctl's CreateTemplateRequest has no `version` field, so the
    template-level Version is unsettable through the CLI — the version
    snapshot's `version` round-trips empty as a result.
  - Backend rejects kubeconfig_data unless KUBECONFIG_ENCRYPTION_KEY
    is configured (the CI compose env doesn't set it); kubeconfig_path
    works without that prerequisite.

Verified locally against rancher-desktop k8s-stack-manager: full live
suite passes (21 passed, 2 skipped by design, 0 failed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a664880-a0e2-4c17-a99e-15a5f56b9021

📥 Commits

Reviewing files that changed from the base of the PR and between d5ccbcf and 3171695.

📒 Files selected for processing (2)
  • cli/test/live/apikey_live_test.go
  • cli/test/live/user_live_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/test/live/apikey_live_test.go

📝 Walkthrough

Walkthrough

Adds five new live-only Go tests (build tag: live) that perform end-to-end create/read/update/delete and workflow checks for API keys, users, clusters, cleanup policies, and template versioning against a live backend.

Changes

Live Integration Tests for Core API Resources

Layer / File(s) Summary
API key and user lifecycle tests
cli/test/live/apikey_live_test.go, cli/test/live/user_live_test.go
API key test creates a key with expiry, asserts returned ID/Prefix/RawKey/ExpiresAt, lists keys, revokes and verifies removal. User test registers a throwaway user, confirms admin listing visibility, toggles Disabled via DisableUser/EnableUser, calls ResetUserPassword, and deletes the user; both use t.Cleanup safety handlers.
Cluster lifecycle test
cli/test/live/cluster_lifecycle_live_test.go
Creates a cluster with registry and kubeconfig inputs, GETs to verify core fields and non-default status, updates description while re-sending registry secret, deletes cluster, and asserts GET fails after deletion.
Cleanup policy CRUD and dry-run test
cli/test/live/cleanup_policy_live_test.go
Creates a dry-run cleanup policy with a non-matching idle_days condition, validates echoed fields, lists to confirm presence, updates Enabled, executes dry-run and validates per-entry InstanceID and Status values, then deletes and confirms removal.
Template versioning list/get/diff test
cli/test/live/template_versions_live_test.go
Creates a temporary template and publishes twice with a description-only change, lists versions (asserts ≥2 rows with IDs/timestamps), fetches newest version to validate snapshot/chart decoding, and diffs oldest vs newest asserting populated left/right snapshot names and chart_diffs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • omattsson/stackctl#88: Implements the API-key lifecycle and CLI/client methods (CreateAPIKey, ListAPIKeys, DeleteAPIKey) exercised by the API key live test.
  • omattsson/stackctl#85: Adds cleanup-policy client/CLI surfaces that the cleanup-policy live test validates (create/list/update/dry-run/delete).
  • omattsson/stackctl#87: Implements user-management CLI/client endpoints used by the user lifecycle live test (register, list-users, disable/enable, reset-password, delete).
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title comprehensively and specifically summarizes the five new live integration tests added across different API endpoints and workflows.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/live-coverage-expansion

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/test/live/apikey_live_test.go`:
- Around line 40-41: The test uses assert.Truef for the length check on
created.RawKey but then slices created.RawKey[:3], which can panic if the length
check fails; replace assert.Truef(t, len(created.RawKey) > len("sk_"), ...) with
require.Truef(t, len(created.RawKey) > len("sk_"), ...) so the test aborts on
failure and prevents the subsequent slice from panicking, and ensure the
testify/require import is available in the test file.

In `@cli/test/live/cleanup_policy_live_test.go`:
- Around line 22-106: Refactor TestLiveCleanupPolicy_CRUDAndDryRun into a
table-driven set of subtests: define a slice of test cases (e.g., []struct{name
string; doDryRun bool; ...}) and iterate with for _, tt := range cases { tt :=
tt; t.Run(tt.name, func(t *testing.T){ t.Parallel(); c := newLiveClient(t);
login(t,c); cluster := requireCluster(t,c); ... perform the same
create/list/update/run/delete flow but use tt fields to vary behavior (e.g.,
DryRun flag) and keep the existing cleanup via t.Cleanup calling
c.DeleteCleanupPolicy(created.ID) }); } so each subtest runs in parallel and
uses the tt := tt capture pattern; preserve all assertions and reference
functions CreateCleanupPolicy, ListCleanupPolicies, UpdateCleanupPolicy,
RunCleanupPolicy, DeleteCleanupPolicy and
types.Create/UpdateCleanupPolicyRequest to build requests.

In `@cli/test/live/user_live_test.go`:
- Around line 57-65: The loop that checks Disabled after calling
c.DisableUser(created.ID) may skip the assertion if the user isn't present;
change to find the user first from the slice returned by c.ListUsers() (same
find-first pattern used earlier), then require.NotNilf(t, user, "expected user
%s in list after disable") to fail the test if missing, and only then
assert.True(t, user.Disabled, "user must be marked disabled after DisableUser");
reference the variables/functions: c.DisableUser, c.ListUsers, created.ID, and
the local slice variable (after) when locating the user.
- Around line 67-75: The test should first locate the updated user in the
ListUsers result before asserting re-enabled state: after calling
c.EnableUser(created.ID) and ListUsers (variable after2), loop to find the user
with ID == created.ID, then use require.NotNilf to fail the test if not found,
and only then assert that foundUser.Disabled is false; update the block that
currently iterates over after2 to follow the find-first pattern (use EnableUser,
after2, created.ID, and Disabled to locate and assert).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cdff74fd-6ca7-4961-b044-ba745658ac4e

📥 Commits

Reviewing files that changed from the base of the PR and between 72e8a2a and d5ccbcf.

📒 Files selected for processing (5)
  • cli/test/live/apikey_live_test.go
  • cli/test/live/cleanup_policy_live_test.go
  • cli/test/live/cluster_lifecycle_live_test.go
  • cli/test/live/template_versions_live_test.go
  • cli/test/live/user_live_test.go

Comment thread cli/test/live/apikey_live_test.go Outdated
Comment on lines +22 to +106
func TestLiveCleanupPolicy_CRUDAndDryRun(t *testing.T) {
c := newLiveClient(t)
login(t, c)

cluster := requireCluster(t, c)
prefix := liveResourcePrefix()

// 1. Create — use the "stop" action and an idle_days condition that
// will never match in CI (no stack has been idle for 9999 days), so
// run --dry-run is guaranteed to return an empty result set.
created, err := c.CreateCleanupPolicy(&types.CreateCleanupPolicyRequest{
Name: prefix + "-cleanup",
ClusterID: cluster.ID,
Action: "stop",
Condition: "idle_days:9999",
Schedule: "0 3 * * *",
Enabled: false,
DryRun: true,
})
require.NoError(t, err, "create cleanup policy")
require.NotEmpty(t, created.ID, "created policy must have an ID")
assert.Equal(t, cluster.ID, created.ClusterID, "policy must echo cluster_id")
assert.Equal(t, "stop", created.Action, "policy must echo action")
assert.Equal(t, "idle_days:9999", created.Condition, "policy must echo condition")
assert.False(t, created.Enabled, "fresh policy must echo enabled=false")
assert.True(t, created.DryRun, "fresh policy must echo dry_run=true")

// Always best-effort delete so a failed assertion doesn't leave the
// policy in the cluster's schedule.
t.Cleanup(func() {
_ = c.DeleteCleanupPolicy(created.ID)
})

// 2. List — newly-created policy must be visible.
policies, err := c.ListCleanupPolicies()
require.NoError(t, err, "list cleanup policies")
var found *types.CleanupPolicy
for i := range policies {
if policies[i].ID == created.ID {
found = &policies[i]
break
}
}
require.NotNilf(t, found, "newly-created cleanup policy %s must appear in list", created.ID)
assert.Equal(t, created.Name, found.Name, "list entry must echo name")

// 3. Update — flip the enabled flag. UpdateCleanupPolicy is a full PUT
// so we must re-send every field (the type comment in types.go calls
// this out explicitly).
updated, err := c.UpdateCleanupPolicy(created.ID, &types.UpdateCleanupPolicyRequest{
Name: created.Name,
ClusterID: created.ClusterID,
Action: created.Action,
Condition: created.Condition,
Schedule: created.Schedule,
Enabled: true,
DryRun: created.DryRun,
})
require.NoError(t, err, "update cleanup policy")
assert.True(t, updated.Enabled, "enabled flag must round-trip through PUT")

// 4. Run with dry_run=true — wire-shape assertion only. Backend will
// return an empty slice when nothing matches; that's fine. What
// matters is that the response decodes into []CleanupResult without
// dropping fields.
results, err := c.RunCleanupPolicy(created.ID, true)
require.NoError(t, err, "run cleanup policy (dry-run)")
require.NotNil(t, results, "results slice must be non-nil (may be empty)")
for i, r := range results {
// On a real match each entry must populate the action +
// status fields. Status MUST be one of the documented values.
assert.NotEmptyf(t, r.InstanceID, "results[%d].instance_id must be set", i)
assert.Containsf(t, []string{"success", "error", "dry_run"}, r.Status,
"results[%d].status %q must be one of the documented values", i, r.Status)
}

// 5. Delete (explicit — cleanup is the safety net).
require.NoError(t, c.DeleteCleanupPolicy(created.ID), "delete cleanup policy")
after, err := c.ListCleanupPolicies()
require.NoError(t, err, "list cleanup policies after delete")
for _, p := range after {
assert.NotEqualf(t, created.ID, p.ID,
"deleted policy %s must not appear in list", created.ID)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
f="cli/test/live/cleanup_policy_live_test.go"
echo "Verifying required test patterns in $f"
rg -n 'func Test|t\.Parallel\(|\[\]struct|t\.Run\(|tt := tt' "$f" || true

Repository: omattsson/stackctl

Length of output: 200


Refactor TestLiveCleanupPolicy_CRUDAndDryRun to table-driven subtests with t.Parallel()

cli/test/live/cleanup_policy_live_test.go currently contains only the single test function and no t.Parallel(), t.Run(...), table-driven []struct{...}, or tt := tt pattern.

Suggested refactor skeleton
 func TestLiveCleanupPolicy_CRUDAndDryRun(t *testing.T) {
-	c := newLiveClient(t)
-	login(t, c)
+	t.Parallel()
+
+	tests := []struct {
+		name string
+	}{
+		{name: "crud-and-dry-run"},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			c := newLiveClient(t)
+			login(t, c)
 
-	cluster := requireCluster(t, c)
-	prefix := liveResourcePrefix()
+			cluster := requireCluster(t, c)
+			prefix := liveResourcePrefix()
 
-	// ... keep current test body unchanged ...
+			// ... keep current test body unchanged ...
+		})
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/live/cleanup_policy_live_test.go` around lines 22 - 106, Refactor
TestLiveCleanupPolicy_CRUDAndDryRun into a table-driven set of subtests: define
a slice of test cases (e.g., []struct{name string; doDryRun bool; ...}) and
iterate with for _, tt := range cases { tt := tt; t.Run(tt.name, func(t
*testing.T){ t.Parallel(); c := newLiveClient(t); login(t,c); cluster :=
requireCluster(t,c); ... perform the same create/list/update/run/delete flow but
use tt fields to vary behavior (e.g., DryRun flag) and keep the existing cleanup
via t.Cleanup calling c.DeleteCleanupPolicy(created.ID) }); } so each subtest
runs in parallel and uses the tt := tt capture pattern; preserve all assertions
and reference functions CreateCleanupPolicy, ListCleanupPolicies,
UpdateCleanupPolicy, RunCleanupPolicy, DeleteCleanupPolicy and
types.Create/UpdateCleanupPolicyRequest to build requests.

Comment thread cli/test/live/user_live_test.go Outdated
Comment thread cli/test/live/user_live_test.go
…okups

Two correctness fixes from CodeRabbit on PR #100:

  - apikey_live_test.go: promote the raw_key length check from
    assert.Truef to require.Truef. A failing length check followed
    by created.RawKey[:3] would panic instead of failing cleanly.

  - user_live_test.go: after DisableUser/EnableUser, look up the
    created user in the list response first (find-first pattern,
    matched by the other live tests) and require.NotNil before
    asserting on the flag. The previous range-and-skip would silently
    pass if the user was missing from the response.

The companion table-driven + t.Parallel() refactor suggestion for
cleanup_policy_live_test.go is deliberately skipped — same reason as
on PR #99: every other *_live_test.go file in this package uses
ad-hoc subtests and runs serially against a shared backend by design.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@omattsson omattsson merged commit f69ad60 into main May 28, 2026
8 checks passed
@omattsson omattsson deleted the test/live-coverage-expansion branch May 28, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant