Files
pleno-vue/tests/unit/configuration-secret-key.spec.js
T
Jeppe B 8d9f1e6fde fix(pleno-vue): persist MiniMax API key across refresh + render fix (#281)
Fixes the operator-reported MiniMax configuration bug: the API key appeared to be 'not saved' on every refresh.

The api backend is correct — direct repro against api-v2.truckwash.io (2026-08-10 07:58) showed 200 on POST and `isSet: true` on subsequent GET for both `{variable, value}` and raw-key payload shapes. The bugs were all on the frontend.

## What was actually broken

1. **Read response shape mismatch (root cause).** `ConfigurationXLVask.refreshMiniMaxStatus` parsed the GET response as an object, but the endpoint returns `[{module, variable, type, value, isSecret, isSet}]`. `typeof array === 'string'` is false → `minimaxApiKeyIsSet` was reset to `false` after every reload.
2. **Inline edit-save flow never notified the parent.** `ConfigurationSecretKey` had no event out, so the inline edit-and-save on the api_key field always left `isSet=false` (and the warning visible) until the user fully reloaded the page.
3. **MiniMax 'Enable' toggle was bound to a function reference.** `:value="SessionUser.superUser.modules.minimax.config.enabled.get ? true : false"` evaluates as `function ? true : false` = `true` (every function is truthy), so the switch always rendered as on.
4. **Hardcoded English warning text** in `ConfigurationSecretKey.vue` — i18n-v2 violation.
5. **Missing key registration in `xlvask/Config.vue.keys`.** PR #269 added a switch for `minimax_integration_enabled` on `xlvask.config.keys`, but never registered the key — accessing `.set` on `undefined.set` throws `TypeError` and aborts the Vue render mid-tree. Production build #c353bfa only renders 3 of 4 categories because of this.

## Changes

- `ConfigurationSecretKey.vue` — emits `saved` after a successful `onSave`; stays in edit mode + surfaces error on failure. Warning title/body come from `useI18n` (`configuration.secret_key_not_set` + `common.warning`) with optional prop overrides.
- `ConfigurationXLVask.vue` — `extractConfigEntry` helper unwraps the array response and trusts the explicit `isSet` flag. The MiniMax enable toggle reads `minimaxEnabled` (real boolean) and re-fetches via `onMiniMaxEnabledSwitch` (optimistic rollback on failure). After re-authenticate/remove/inline-save the parent re-fetches status so the UI matches persistence.
- `xlvask/Config.vue.keys` — registers the missing `minimax_integration_enabled` key.
- New i18n key `configuration.secret_key_not_set` + global shared alias; added to da/de/en/no/sv.
- New `tests/unit/configuration-secret-key.spec.js` (4 tests).

## Verification (local)

- `npm run i18n:v2:check` 
- `npm run lint` 
- `npm run format:tests:check` 
- `npm run test:unit:fast`  — 223 files / 1352 tests
- `npm run build` 

Companion api PR: #358 ("test(api): lock MiniMax config redaction + isSet contract") — already merged.
2026-08-10 09:04:35 +02:00

92 lines
3.9 KiB
JavaScript

// @vitest-environment jsdom
import { flushPromises, mount } from "@vue/test-utils";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key, fallback) => (typeof fallback === "string" ? fallback : key),
}),
}));
const parseError = vi.fn();
const getError = vi.fn();
vi.mock("@/components/request/HandleGlobalError.vue", () => ({
parseError: (...args) => parseError(...args),
getError: (...args) => getError(...args),
}));
import ConfigurationSecretKey from "@/components/displays/superuser/configuration/ConfigurationSecretKey.vue";
const findSaveButton = (wrapper) => wrapper.findAll("a.button").find((b) => b.find("i.fa-save").exists());
const findEditButton = (wrapper) => wrapper.findAll("a.button").find((b) => b.find("i.fa-edit").exists());
const mountKey = (props = {}) =>
mount(ConfigurationSecretKey, {
props: {
title: "API key",
description: "desc",
...props,
},
});
describe("ConfigurationSecretKey", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("shows the warning when not set, hidden view when set", () => {
const unset = mountKey({ isSet: false });
expect(unset.find(".message.is-warning").exists()).toBe(true);
const setWrapper = mountKey({ isSet: true });
expect(setWrapper.find(".message.is-warning").exists()).toBe(false);
expect(setWrapper.find('input[value="Hidden"]').exists()).toBe(true);
});
it("emits 'saved' after a successful onSave and exits edit mode", async () => {
const onSave = vi.fn().mockResolvedValue({ ok: true });
const wrapper = mountKey({ onSave, isSet: false });
// The input is disabled until the user enters edit mode — click Edit first,
// then type, then Save. This matches the production UX for ConfigurationXLVask.
await findEditButton(wrapper).trigger("click");
await wrapper.find('input[type="text"]').setValue("super-secret");
await findSaveButton(wrapper).trigger("click");
await flushPromises();
expect(onSave).toHaveBeenCalledWith("super-secret");
expect(wrapper.emitted("saved")).toBeTruthy();
expect(wrapper.emitted("saved").length).toBe(1);
expect(wrapper.emitted("saved")[0][0]).toEqual(expect.objectContaining({ value: "super-secret" }));
// Edit mode exited — the Save button is gone, the Edit button is back.
expect(findSaveButton(wrapper)).toBeUndefined();
expect(findEditButton(wrapper).exists()).toBe(true);
// Whether the "Hidden" view shows after a save depends on the parent updating
// isSet; that's the caller's responsibility and is exercised in the
// ConfigurationXLVask end-to-end coverage.
});
it("stays in edit mode and surfaces the error when onSave rejects", async () => {
const failure = new Error("boom");
const onSave = vi.fn().mockRejectedValue(failure);
getError.mockReturnValue("boom");
const wrapper = mountKey({ onSave, isSet: false });
await findEditButton(wrapper).trigger("click");
await wrapper.find('input[type="text"]').setValue("super-secret");
await findSaveButton(wrapper).trigger("click");
await flushPromises();
expect(onSave).toHaveBeenCalledWith("super-secret");
expect(wrapper.emitted("saved")).toBeFalsy();
expect(parseError).toHaveBeenCalledWith(failure, expect.any(String));
// Edit mode preserved so the user can retry.
expect(findSaveButton(wrapper).exists()).toBe(true);
expect(wrapper.find(".help.is-danger").exists()).toBe(true);
});
it("renders localized warning message instead of hardcoded English", () => {
// useI18n mock returns the fallback for any unknown key, so the warning body
// should reflect whatever the caller passed via the i18n key fallback path.
const wrapper = mountKey({ isSet: false });
const body = wrapper.find(".message.is-warning .message-body").text();
expect(body).toContain("The secret key is not set");
});
});