fix: guard managed setting label inventories

- Treat inherited property names as unavailable settings

- Preserve legacy tile aliases only for declared entries
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 04:01:00 +02:00
1 parent 24c5c49be3
commit 437d916fc6
4 files changed
+61 -7

No files matched your search

@@ -125,4 +125,28 @@ describe("managed record settings forms", () => {
expect(insights).not.toContain("settings.sharedRecord");
}
});
it.each(["toString", "constructor", "__proto__"])(
"renders the unavailable label for prototype property %s",
(id) => {
const coach = render("coach", {
preferences: {
excludeMetrics: [id],
dataClusters: [id],
},
});
const insights = render("insights", {
layout: {
version: 2,
sections: [{ id, visible: true, order: 0 }],
tiles: [{ id, visible: true, order: 0 }],
},
});
expect(coach).not.toContain(`>${id}<`);
expect(insights).toContain("Unavailable insight");
expect(insights).not.toContain(`>${id}<`);
expect(insights).not.toContain("settings.sharedRecord");
},
);
});
@@ -171,18 +171,30 @@ function formNumber(form: FormData, name: string, fallback: number): number {
return Number.isFinite(value) ? value : fallback;
}
function ownStringValue(inventory: object, id: string): string | undefined {
if (!Object.hasOwn(inventory, id)) return undefined;
const value = Reflect.get(inventory, id);
return typeof value === "string" ? value : undefined;
}
function coachMetricLabel(
t: ReturnType<typeof useTranslations>["t"],
metric: (typeof COACH_EXCLUDE_METRICS)[number],
): string {
return t(COACH_EXCLUDE_METRIC_LABEL_KEYS[metric]);
const labelKey = ownStringValue(COACH_EXCLUDE_METRIC_LABEL_KEYS, metric);
return labelKey
? t(labelKey)
: t("settings.sharedRecord.managedSettings.insights.unknownItem");
}
function coachClusterLabel(
t: ReturnType<typeof useTranslations>["t"],
cluster: (typeof COACH_DATA_CLUSTERS)[number],
): string {
return t(COACH_DATA_CLUSTER_LABEL_KEYS[cluster]);
const labelKey = ownStringValue(COACH_DATA_CLUSTER_LABEL_KEYS, cluster);
return labelKey
? t(labelKey)
: t("settings.sharedRecord.managedSettings.insights.unknownItem");
}
function insightItemLabel(
@@ -191,7 +203,7 @@ function insightItemLabel(
id: string,
): string {
if (kind === "section") {
const labelKey = INSIGHTS_SECTION_LABEL_KEYS[id as InsightsSectionId];
const labelKey = ownStringValue(INSIGHTS_SECTION_LABEL_KEYS, id);
return labelKey
? t(labelKey)
: t("settings.sharedRecord.managedSettings.insights.unknownItem");
@@ -199,9 +211,14 @@ function insightItemLabel(
const normalizedId = normalizeInsightsTileId(id);
if (normalizedId === "overview") return t("insights.navOverview");
const tab = SUB_PAGE_TABS[normalizedId as keyof typeof SUB_PAGE_TABS];
return tab
? t(tab.labelKey)
if (!Object.hasOwn(SUB_PAGE_TABS, normalizedId)) {
return t("settings.sharedRecord.managedSettings.insights.unknownItem");
}
const labelKey =
SUB_PAGE_TABS[normalizedId as keyof typeof SUB_PAGE_TABS]?.labelKey;
return typeof labelKey === "string"
? t(labelKey)
: t("settings.sharedRecord.managedSettings.insights.unknownItem");
}
+11
View File
@@ -16,9 +16,20 @@ import {
resolveInsightsLayout,
serializeInsightsLayout,
orderedVisibleSectionIds,
normalizeInsightsTileId,
resolveTileLayout,
} from "@/lib/insights-layout";
describe("normalizeInsightsTileId", () => {
it("only resolves own legacy aliases", () => {
expect(normalizeInsightsTileId("blutdruck")).toBe("blood-pressure");
for (const id of ["toString", "constructor", "__proto__"]) {
expect(normalizeInsightsTileId(id)).toBe(id);
}
});
});
describe("insights-layout v2 — section id universe", () => {
it("carries exactly the default sections in render order", () => {
expect([...INSIGHTS_SECTION_IDS]).toEqual([
+3 -1
View File
@@ -87,7 +87,9 @@ export const ACCEPTED_INSIGHTS_TILE_IDS = [
* resolver's known-id filter drops anything truly unrecognised.
*/
export function normalizeInsightsTileId(id: string): string {
return LEGACY_INSIGHTS_TILE_ID_ALIASES[id] ?? id;
return Object.hasOwn(LEGACY_INSIGHTS_TILE_ID_ALIASES, id)
? LEGACY_INSIGHTS_TILE_ID_ALIASES[id]
: id;
}
/**