Skip to content

Move BMC User Account Unit tests to go mock server#841

Merged
afritzler merged 1 commit intomainfrom
accountmgm
Apr 27, 2026
Merged

Move BMC User Account Unit tests to go mock server#841
afritzler merged 1 commit intomainfrom
accountmgm

Conversation

@nagadeesh-nagaraja
Copy link
Copy Markdown
Contributor

@nagadeesh-nagaraja nagadeesh-nagaraja commented Apr 27, 2026

Fixes #840

Summary by CodeRabbit

  • New Features

    • Mock Redfish server now supports configurable Basic Authentication and an "unavailable" mode (returns 503).
  • Chores

    • Tests and test framework updated to use per-server authentication controls and server-level credential APIs.
  • Data

    • Mock account data updated (usernames, passwords, account types) and account collection membership adjusted.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Account/auth test logic was moved from in-memory helpers into the Go mock HTTP server. The mock server gained configurable Basic Auth, unavailable mode, collection POST/DELETE handling with hooks, an embedded account store with auth APIs, and mock account data was updated/expanded for tests.

Changes

Cohort / File(s) Summary
Mock Account Data
bmc/mock/server/data/AccountService/Accounts/1/index.json, bmc/mock/server/data/AccountService/Accounts/2/index.json, bmc/mock/server/data/AccountService/Accounts/index.json
Updated account payloads (concrete usernames/passwords, removed some contact fields), added a second account, and updated collection member count.
Mock Server Refactor
bmc/mock/server/server.go
Introduced Options pattern to NewMockServer, added WithAuth, SetUnavailable, authentication store and APIs (Authenticate, GetAccountNames, ResetAccounts), action handler dispatch (including ManagerAccount.ChangePassword), collection POST/DELETE with onCreate/onDelete hooks, and persisted collection override handling.
Local BMC Client Cleanup
bmc/redfish_local.go
Removed local account-management methods and replaced local credential checks with reliance on the mock HTTP server’s Basic Auth handling; client construction simplified.
Controller Test & Logic Updates
internal/controller/biossettings_controller_test.go, internal/controller/bmc_controller_test.go, internal/controller/bmcuser_controller_test.go, internal/controller/suite_test.go
Tests updated to use Basic Auth and per-server availability controls (SetUnavailable); test setup now creates mock servers with WithAuth(); BMC user tests use mock server APIs (GetAccountNames, ResetAccounts).
Credential Probe Change
internal/controller/bmcuser_controller.go
bmcConnectionTest now verifies credentials by calling GetAccountService() and treats 401/403 as invalid credentials rather than connection failure.

Sequence Diagram(s)

sequenceDiagram
  participant Controller
  participant MockServer
  participant AccountStore
  Controller->>MockServer: BasicAuth request (e.g., GetAccountService / change password)
  MockServer->>AccountStore: Authenticate / lookup account / apply change
  AccountStore-->>MockServer: success / failure
  MockServer-->>Controller: HTTP 200 / 401/403 / 503
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

enhancement, area/metal-automation

Suggested reviewers

  • afritzler
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: moving BMC user account unit tests from in-process mocks to a Go mock server.
Description check ✅ Passed The description is minimal but sufficient, providing the issue reference (#840) that links to full context, though it doesn't explicitly detail the changes in the PR description itself.
Linked Issues check ✅ Passed The PR fulfills the objectives: account management unit test logic is removed from mockup.go and redfish_local.go and relocated into the Go mock server (bmc/mock/server/server.go).
Out of Scope Changes check ✅ Passed All changes are in scope: test data updates (account JSON files), mock server enhancements, test file updates, and removal of deprecated account management methods align with the refactoring objective.

✏️ 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 accountmgm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/controller/bmcuser_controller_test.go (1)

295-339: ⚠️ Potential issue | 🟡 Minor

Move account reset to AfterEach to ensure consistent test isolation.

ResetAccounts() is only called in this spec, while the other specs in this Describe block (e.g., "should create a BMC user and secret", "should just create additional BMC users", "should rotate password ...") also create accounts on the shared mock server. Because the mock is process-global and Ginkgo can be run with --randomize-all, accounts created by earlier specs persist into later ones, which can mask regressions or cause flakes. Per the ResetAccounts() docstring ("Call this in AfterEach"), move the reset to an AfterEach block to ensure every spec in the suite starts with a clean account state.

(Note: GetAccountNames() is thread-safe—it calls loadResource() which holds RLock and returns a copy, so there is no concurrent map access risk.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/bmcuser_controller_test.go` around lines 295 - 339, The
test calls mockServers[0].ResetAccounts() only inside the "should delete BMC
user and secret on User deletion" It block, which leaves state from other specs;
remove that call from the spec and add an AfterEach in the Describe block that
invokes mockServers[0].ResetAccounts() (or iterates all mockServers and calls
ResetAccounts() on each) so every spec starts with a clean mock server state;
ensure the AfterEach references the existing mockServers slice and
ResetAccounts() helper.
🧹 Nitpick comments (2)
internal/controller/suite_test.go (1)

192-196: LGTM!

server.WithAuth() is consistently applied to both mock-server construction paths, and BMCReconciler now carries BMCOptions matching the other reconcilers — required for the new BasicAuth probe in bmcConnectionTest to be exercised.

Optional: the identical bmc.Options{...} literal is repeated across seven reconciler setups. Extracting it to a shared var defaultBMCOptions = bmc.Options{...} would reduce duplication and make future option changes single-touch.

Also applies to: 354-354, 366-366

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/suite_test.go` around lines 192 - 196, Multiple
reconciler setups repeat the same bmc.Options literal; extract that repeated
BMCOptions block into a single shared variable (e.g., defaultBMCOptions of type
bmc.Options) and replace each inline bmc.Options{...} (used when constructing
BMCReconciler instances and test setups exercising bmcConnectionTest) with that
variable, ensuring server.WithAuth() usage remains unchanged and BMCReconciler's
BMCOptions field is assigned from defaultBMCOptions.
bmc/mock/server/server.go (1)

733-754: handleChangePassword updates only the auth store, not the resource override.

s.accounts[username] = req.NewPassword at Line 751 keeps Basic Auth working, but the account resource's Password field in s.overrides is unchanged. A subsequent GET /redfish/v1/AccountService/Accounts/<id> will still return the original/seed password, which can confuse future tests that assert on the resource body. Also, an empty NewPassword or missing UserName is silently treated as success (returns 204) — consider returning 400 to match the strict ChangePassword contract.

♻️ Optional fix
 	if username, ok := account["UserName"].(string); ok && username != "" && req.NewPassword != "" {
 		s.accounts[username] = req.NewPassword
+		account["Password"] = req.NewPassword
+		s.overrides[filePath] = account
+	} else {
+		http.Error(w, "missing UserName or NewPassword", http.StatusBadRequest)
+		return
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bmc/mock/server/server.go` around lines 733 - 754, The handleChangePassword
handler currently updates only the auth store (s.accounts) but not the account
resource override, and it treats missing/empty NewPassword or UserName as
success; update the function so after loading the account (using
loadResourceLocked and the resolved filePath) you extract the username (from
account["UserName"]) and validate req.NewPassword is non-empty and username is
present, returning http.StatusBadRequest on invalid input, then set
s.accounts[username] = req.NewPassword and also update the account resource in
s.overrides (e.g., find/modify s.overrides[filePath] or the map entry returned
by loadResourceLocked to set its "Password" field to req.NewPassword) while
holding the existing mutex to keep state consistent, and finally return 204 only
on success.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bmc/mock/server/server.go`:
- Around line 423-451: The POST handler currently in handleCollectionPost
computes new IDs using len(base.Members)+1 which can collide after non-trailing
deletes and the empty-collection branch omits setting Id/@odata.id, storing the
new resource, and calling onCreate hooks; fix by computing newID as (max
existing numeric ID)+1 by parsing each Member.OdataID (use strconv.Atoi on the
last path segment) to find the highest ID, then set newID = strconv.Itoa(max+1);
unify the two branches so you always inject update["Id"] and
update["@odata.id"], set s.overrides[newMemberPath] = update, set the Location
header when appropriate, and dispatch matching s.onCreate hooks (the code around
s.overrides[urlPath], s.overrides[newMemberPath], update injection, and the for
loop over s.onCreate); add "strconv" to the imports.

---

Outside diff comments:
In `@internal/controller/bmcuser_controller_test.go`:
- Around line 295-339: The test calls mockServers[0].ResetAccounts() only inside
the "should delete BMC user and secret on User deletion" It block, which leaves
state from other specs; remove that call from the spec and add an AfterEach in
the Describe block that invokes mockServers[0].ResetAccounts() (or iterates all
mockServers and calls ResetAccounts() on each) so every spec starts with a clean
mock server state; ensure the AfterEach references the existing mockServers
slice and ResetAccounts() helper.

---

Nitpick comments:
In `@bmc/mock/server/server.go`:
- Around line 733-754: The handleChangePassword handler currently updates only
the auth store (s.accounts) but not the account resource override, and it treats
missing/empty NewPassword or UserName as success; update the function so after
loading the account (using loadResourceLocked and the resolved filePath) you
extract the username (from account["UserName"]) and validate req.NewPassword is
non-empty and username is present, returning http.StatusBadRequest on invalid
input, then set s.accounts[username] = req.NewPassword and also update the
account resource in s.overrides (e.g., find/modify s.overrides[filePath] or the
map entry returned by loadResourceLocked to set its "Password" field to
req.NewPassword) while holding the existing mutex to keep state consistent, and
finally return 204 only on success.

In `@internal/controller/suite_test.go`:
- Around line 192-196: Multiple reconciler setups repeat the same bmc.Options
literal; extract that repeated BMCOptions block into a single shared variable
(e.g., defaultBMCOptions of type bmc.Options) and replace each inline
bmc.Options{...} (used when constructing BMCReconciler instances and test setups
exercising bmcConnectionTest) with that variable, ensuring server.WithAuth()
usage remains unchanged and BMCReconciler's BMCOptions field is assigned from
defaultBMCOptions.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8595b610-1ffb-47bc-8cc0-0ff9e5fbd68d

📥 Commits

Reviewing files that changed from the base of the PR and between 33a0eb0 and d2b4ab6.

📒 Files selected for processing (10)
  • bmc/mock/server/data/AccountService/Accounts/1/index.json
  • bmc/mock/server/data/AccountService/Accounts/2/index.json
  • bmc/mock/server/data/AccountService/Accounts/index.json
  • bmc/mock/server/server.go
  • bmc/redfish_local.go
  • internal/controller/biossettings_controller_test.go
  • internal/controller/bmc_controller_test.go
  • internal/controller/bmcuser_controller.go
  • internal/controller/bmcuser_controller_test.go
  • internal/controller/suite_test.go

Comment thread bmc/mock/server/server.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bmc/mock/server/data/AccountService/Accounts/2/index.json (1)

1-28: ⚠️ Potential issue | 🟡 Minor

Note: this fixture is also the template for newly-created accounts.

In bmc/mock/server/server.go, the /AccountService/Accounts onCreate hook reads data/AccountService/Accounts/2/index.json and seeds missing fields on every newly POSTed account (with Actions/@odata.id/PasswordExpiration rewritten per-account). Future edits here will silently affect every new account created in tests, not just account 2. Worth a brief comment in this JSON or alongside the hook so the coupling is discoverable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bmc/mock/server/data/AccountService/Accounts/2/index.json` around lines 1 -
28, Add a short, explicit comment that this JSON
(data/AccountService/Accounts/2/index.json) is used as the template for
newly-created accounts and that edits will affect every new account; place the
comment at the top of that JSON and also adjacent to the onCreate hook in
server.go (the onCreate handler that seeds missing fields and rewrites
Actions/@odata.id/PasswordExpiration). The comment should list which fields are
rewritten per-account (Actions, `@odata.id`, PasswordExpiration) and advise
maintainers to update the template and hook together when changing defaults.
🧹 Nitpick comments (4)
bmc/mock/server/server.go (3)

1241-1247: Mock-only auth — plain string compare is fine, just noting.

Authenticate uses == rather than a constant-time compare. That's appropriate for a mock; if anyone repurposes this server outside of unit tests, switch to crypto/subtle.ConstantTimeCompare to avoid leaking credential length/contents through timing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bmc/mock/server/server.go` around lines 1241 - 1247, The Authenticate method
on MockServer currently compares passwords with plain == which can leak via
timing; update MockServer.Authenticate to use crypto/subtle.ConstantTimeCompare
for comparing the stored password and the provided password (convert both to
[]byte and ensure lengths are handled) while keeping the same RLock/RUnlock and
accounts lookup logic so behavior stays identical for mocks but is safe if
reused outside tests.

737-769: handleChangePassword ignores OldPassword — document or enforce.

The Redfish ManagerAccount.ChangePassword action contract (and gofish's Account.ChangePassword(new, old)) sends both NewPassword and OldPassword. The handler only validates NewPassword, so a test with a wrong/missing old password still succeeds, which can mask real-world failure modes during credential rotation. Either drop a comment that this is intentional for the mock, or also parse and (optionally) check OldPassword against s.accounts[username] for parity with real BMCs.

🔧 Optional: validate the old password
 	var req struct {
+		OldPassword string `json:"OldPassword"`
 		NewPassword string `json:"NewPassword"`
 	}
 	if err := json.Unmarshal(body, &req); err != nil {
 		http.Error(w, "invalid body", http.StatusBadRequest)
 		return
 	}
 	if req.NewPassword == "" {
 		http.Error(w, "NewPassword must not be empty", http.StatusBadRequest)
 		return
 	}
@@
 	username, ok := account["UserName"].(string)
 	if !ok || username == "" {
 		http.Error(w, "account has no UserName", http.StatusBadRequest)
 		return
 	}
+	if stored, ok := s.accounts[username]; ok && req.OldPassword != "" && stored != req.OldPassword {
+		http.Error(w, "Forbidden", http.StatusForbidden)
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bmc/mock/server/server.go` around lines 737 - 769, handleChangePassword
currently ignores OldPassword; update it to unmarshal and require OldPassword
and compare it to the stored password before changing: extend the req struct in
handleChangePassword to include OldPassword, after resolving username fetch the
current password from s.accounts[username] (or account["Password"]) and if
OldPassword is missing or does not match return http.StatusBadRequest (or
http.StatusUnauthorized) with a clear message, only then update
s.accounts[username], account["Password"], and s.overrides[filePath];
alternatively, if you prefer the mock to accept any old password, add a short
comment in handleChangePassword noting that OldPassword is intentionally ignored
for the mock rather than enforcing parity with real BMCs.

205-238: Template seeding can leak account 2's password into newly created accounts.

The onCreate hook copies any field missing from the POST body out of data/AccountService/Accounts/2/index.json (including Password: "bar"). If a future test POSTs an account without a Password, the new entry inherits "bar" and s.accounts[username] = "bar" — silently making the account log-in-able with the template's password. The controller path under test does always provide a password, so this is currently latent, but a small explicit guard would prevent surprises:

🔧 Optional: skip Password from template seeding
 					for k, v := range tmpl {
+						if k == "Password" {
+							continue
+						}
 						if _, exists := data[k]; !exists {
 							data[k] = deepCopyAny(v)
 						}
 					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bmc/mock/server/server.go` around lines 205 - 238, The template-seeding
currently copies all fields (including "Password") from the template into the
new account and then writes s.accounts[username]=password, which can leak the
template password; modify the onCreate handler (the map entry for
"/AccountService/Accounts" in MockServer) to skip copying the "Password" key
when iterating tmpl -> data (use the same deepCopyAny loop but continue if k ==
"Password"), and also guard the authentication store update so
s.accounts[username] is only set when the resulting password is non-empty and
originated from the request (i.e., check password != "" before assigning) to
avoid inheriting the template password.
internal/controller/bmcuser_controller_test.go (1)

295-339: Consider moving ResetAccounts() to BeforeEach/AfterEach for test isolation.

ResetAccounts() is invoked only in this single spec, while other specs (should create a BMC user and secret, should just create additional BMC users, should rotate password if rotationPeriod is set) rely on the controller's finalizer-driven DeleteAccount calls to clean up mock state. EnsureCleanState does not list BMCUserList, so if a spec finishes before the BMC user finalizer completes (or the rotation test leaks rotated secrets, as the inline comment already acknowledges), mock-server account state can leak between specs and skew assertions like GetAccountNames/Members@odata.count. Centralizing mockServers[0].ResetAccounts() in AfterEach would make this deterministic.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@bmc/mock/server/data/AccountService/Accounts/2/index.json`:
- Around line 1-28: Add a short, explicit comment that this JSON
(data/AccountService/Accounts/2/index.json) is used as the template for
newly-created accounts and that edits will affect every new account; place the
comment at the top of that JSON and also adjacent to the onCreate hook in
server.go (the onCreate handler that seeds missing fields and rewrites
Actions/@odata.id/PasswordExpiration). The comment should list which fields are
rewritten per-account (Actions, `@odata.id`, PasswordExpiration) and advise
maintainers to update the template and hook together when changing defaults.

---

Nitpick comments:
In `@bmc/mock/server/server.go`:
- Around line 1241-1247: The Authenticate method on MockServer currently
compares passwords with plain == which can leak via timing; update
MockServer.Authenticate to use crypto/subtle.ConstantTimeCompare for comparing
the stored password and the provided password (convert both to []byte and ensure
lengths are handled) while keeping the same RLock/RUnlock and accounts lookup
logic so behavior stays identical for mocks but is safe if reused outside tests.
- Around line 737-769: handleChangePassword currently ignores OldPassword;
update it to unmarshal and require OldPassword and compare it to the stored
password before changing: extend the req struct in handleChangePassword to
include OldPassword, after resolving username fetch the current password from
s.accounts[username] (or account["Password"]) and if OldPassword is missing or
does not match return http.StatusBadRequest (or http.StatusUnauthorized) with a
clear message, only then update s.accounts[username], account["Password"], and
s.overrides[filePath]; alternatively, if you prefer the mock to accept any old
password, add a short comment in handleChangePassword noting that OldPassword is
intentionally ignored for the mock rather than enforcing parity with real BMCs.
- Around line 205-238: The template-seeding currently copies all fields
(including "Password") from the template into the new account and then writes
s.accounts[username]=password, which can leak the template password; modify the
onCreate handler (the map entry for "/AccountService/Accounts" in MockServer) to
skip copying the "Password" key when iterating tmpl -> data (use the same
deepCopyAny loop but continue if k == "Password"), and also guard the
authentication store update so s.accounts[username] is only set when the
resulting password is non-empty and originated from the request (i.e., check
password != "" before assigning) to avoid inheriting the template password.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f36298f0-1a66-48e2-b9b7-a4da2b9a8b96

📥 Commits

Reviewing files that changed from the base of the PR and between d2b4ab6 and a8d9fbd.

📒 Files selected for processing (10)
  • bmc/mock/server/data/AccountService/Accounts/1/index.json
  • bmc/mock/server/data/AccountService/Accounts/2/index.json
  • bmc/mock/server/data/AccountService/Accounts/index.json
  • bmc/mock/server/server.go
  • bmc/redfish_local.go
  • internal/controller/biossettings_controller_test.go
  • internal/controller/bmc_controller_test.go
  • internal/controller/bmcuser_controller.go
  • internal/controller/bmcuser_controller_test.go
  • internal/controller/suite_test.go
✅ Files skipped from review due to trivial changes (2)
  • bmc/mock/server/data/AccountService/Accounts/1/index.json
  • bmc/mock/server/data/AccountService/Accounts/index.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/controller/biossettings_controller_test.go

@afritzler afritzler added the enhancement New feature or request label Apr 27, 2026
@afritzler afritzler merged commit 9743d21 into main Apr 27, 2026
18 checks passed
@afritzler afritzler deleted the accountmgm branch April 27, 2026 15:09
@github-project-automation github-project-automation Bot moved this to Done in Roadmap Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Move Account management unit tests to go mock server

3 participants