diff --git a/frontend-modern/src/utils/__tests__/alertIncidentPresentation.branchcov0717.test.ts b/frontend-modern/src/utils/__tests__/alertIncidentPresentation.branchcov0717.test.ts new file mode 100644 index 0000000000..59179f89e0 --- /dev/null +++ b/frontend-modern/src/utils/__tests__/alertIncidentPresentation.branchcov0717.test.ts @@ -0,0 +1,451 @@ +import { describe, expect, it } from 'vitest'; + +import { + getAlertHistoryStatusPresentation, + getAlertIncidentEventFilterActionButtonClass, + getAlertIncidentEventFilterChipClass, + getAlertIncidentEventFilterContainerClass, + getAlertIncidentEventFilterLabelClass, + getAlertIncidentLevelBadgeClass, + getAlertIncidentNoteSaveButtonClass, + getAlertIncidentNoteTextareaClass, + getAlertIncidentStatusPresentation, + getAlertIncidentTimelineCommandClass, + getAlertIncidentTimelineDetailClass, + getAlertIncidentTimelineEventCardClass, + getAlertIncidentTimelineHeadingClass, + getAlertIncidentTimelineMetaRowClass, + getAlertIncidentTimelineOutputClass, + getAlertResourceIncidentAcknowledgedByLabel, + getAlertIncidentAcknowledgedBadgeClass, + getAlertResourceIncidentActivityChipClass, + getAlertResourceIncidentActivitySummaryClass, + getAlertResourceIncidentCardClass, + getAlertResourceIncidentCountLabel, + getAlertResourceIncidentEmptyState, + getAlertResourceIncidentFilteredEventsEmptyState, + getAlertResourceIncidentLoadFailure, + getAlertResourceIncidentLoadingState, + getAlertResourceIncidentNotePlaceholder, + getAlertResourceIncidentNoteSaveFailure, + getAlertResourceIncidentNoteSavedLabel, + getAlertResourceIncidentPanelTitle, + getAlertResourceIncidentRecentEventsSummary, + getAlertResourceIncidentRefreshLabel, + getAlertResourceIncidentSaveNoteLabel, + getAlertResourceIncidentSummaryRowClass, + getAlertResourceIncidentTimelineFailure, + getAlertResourceIncidentToggleButtonClass, + getAlertResourceIncidentToggleLabel, + getAlertResourceIncidentTruncatedEventsLabel, + getAlertResourceIncidentViewTitle, + normalizeAlertIncidentStatus, +} from '@/utils/alertIncidentPresentation'; + +// Supplemental branch-coverage suite. The pre-existing branchcov suite already +// exercises the happy paths; this file targets the nullish / empty / boundary / +// falsy-guard / switch-default arms that those tests leave open. Every assertion +// pins a concrete return value computed from the source so a regression flips it. + +describe('normalizeAlertIncidentStatus — branch coverage', () => { + it('coerces an undefined status through the ??-coalesce arm onto "unknown"', () => { + // (undefined ?? '') -> '' -> trim/lower -> '' -> '' || 'unknown' -> 'unknown'. + expect(normalizeAlertIncidentStatus(undefined)).toBe('unknown'); + // acknowledged flag is ignored once the normalized status is empty. + expect(normalizeAlertIncidentStatus(undefined, true)).toBe('unknown'); + }); + + it('coerces a null status through the ??-coalesce arm onto "unknown"', () => { + expect(normalizeAlertIncidentStatus(null)).toBe('unknown'); + }); + + it('treats a whitespace-only status as empty (trim -> "" -> `|| "unknown"`) ', () => { + expect(normalizeAlertIncidentStatus(' ')).toBe('unknown'); + }); + + it('reaches "acknowledged" via `normalized === "open" && acknowledged` true arm', () => { + expect(normalizeAlertIncidentStatus('open', true)).toBe('acknowledged'); + }); + + it('honours the &&-short-circuit false arm: open + falsy acknowledged stays "open"', () => { + // acknowledged === false -> `&&` yields false -> falls to the bare `=== 'open'` branch. + expect(normalizeAlertIncidentStatus('open', false)).toBe('open'); + // acknowledged === undefined (omitted) is also falsy. + expect(normalizeAlertIncidentStatus('open')).toBe('open'); + // A truthy-looking-but-falsy 0 also keeps the bare-open branch. + expect(normalizeAlertIncidentStatus('open', 0 as unknown as boolean)).toBe('open'); + }); + + it('maps the "closed" alias onto "resolved" via the `||` second operand', () => { + expect(normalizeAlertIncidentStatus('closed')).toBe('resolved'); + }); + + it('maps an exact "resolved" through the `||` first operand', () => { + expect(normalizeAlertIncidentStatus('resolved')).toBe('resolved'); + }); + + it('returns the trimmed/lowercased token verbatim for an unrecognised status (truthy `||` left)', () => { + // toLowerCase() arm: 'INVESTIGATING' -> 'investigating', which is truthy. + expect(normalizeAlertIncidentStatus('INVESTIGATING')).toBe('investigating'); + }); + + it('normalises cased/whitespace-padded "open" before matching', () => { + // Confirms trim().toLowerCase() runs before the `=== 'open'` comparison. + expect(normalizeAlertIncidentStatus(' OPEN ', true)).toBe('acknowledged'); + expect(normalizeAlertIncidentStatus(' OPEN ', false)).toBe('open'); + }); +}); + +describe('getAlertIncidentStatusPresentation — switch branch coverage', () => { + it('hits the "acknowledged" case directly and via the open+acknowledged normalisation', () => { + const expected = { + label: 'acknowledged', + className: + 'px-2 py-0.5 rounded bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300', + }; + expect(getAlertIncidentStatusPresentation('acknowledged')).toStrictEqual(expected); + // open + acknowledged flag normalises to 'acknowledged' label -> same case. + expect(getAlertIncidentStatusPresentation('open', true)).toStrictEqual(expected); + }); + + it('hits the "open" case', () => { + expect(getAlertIncidentStatusPresentation('open')).toStrictEqual({ + label: 'open', + className: + 'px-2 py-0.5 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300', + }); + }); + + it('routes "resolved" through the default case', () => { + expect(getAlertIncidentStatusPresentation('resolved')).toStrictEqual({ + label: 'resolved', + className: 'px-2 py-0.5 rounded bg-surface-hover text-base-content', + }); + }); + + it('routes a nullish status onto the default case with the "unknown" label', () => { + expect(getAlertIncidentStatusPresentation(undefined)).toStrictEqual({ + label: 'unknown', + className: 'px-2 py-0.5 rounded bg-surface-hover text-base-content', + }); + }); + + it('routes an unrecognised status onto the default case, echoing the normalised label', () => { + expect(getAlertIncidentStatusPresentation('fizzing')).toStrictEqual({ + label: 'fizzing', + className: 'px-2 py-0.5 rounded bg-surface-hover text-base-content', + }); + }); +}); + +describe('getAlertIncidentLevelBadgeClass — branch coverage', () => { + it('returns the critical palette for level === "critical"', () => { + expect(getAlertIncidentLevelBadgeClass('critical')).toBe( + 'px-2 py-0.5 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300', + ); + }); + + it('falls back to the warning palette for the canonical "warning" level', () => { + expect(getAlertIncidentLevelBadgeClass('warning')).toBe( + 'px-2 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300', + ); + }); + + it('falls back to the warning palette for a null level', () => { + expect(getAlertIncidentLevelBadgeClass(null)).toBe( + 'px-2 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300', + ); + }); + + it('falls back to the warning palette for an undefined level', () => { + expect(getAlertIncidentLevelBadgeClass(undefined)).toBe( + 'px-2 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300', + ); + }); + + it('falls back to the warning palette for any non-"critical" string', () => { + expect(getAlertIncidentLevelBadgeClass('info')).toBe( + 'px-2 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300', + ); + }); +}); + +describe('getAlertHistoryStatusPresentation — branch coverage', () => { + it('returns the active presentation for "active"', () => { + expect(getAlertHistoryStatusPresentation('active')).toStrictEqual({ + label: 'active', + className: + 'text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 font-medium', + rowClassName: 'bg-red-50 dark:bg-red-900', + }); + }); + + it('normalises a cased "ACTIVE" to the active branch (toLowerCase arm)', () => { + expect(getAlertHistoryStatusPresentation('ACTIVE')).toStrictEqual( + getAlertHistoryStatusPresentation('active'), + ); + }); + + it('returns the acknowledged presentation with an empty rowClassName', () => { + expect(getAlertHistoryStatusPresentation('acknowledged')).toStrictEqual({ + label: 'acknowledged', + className: + 'text-xs px-2 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300', + rowClassName: '', + }); + }); + + it('returns the resolved presentation for "resolved" (falls through both guards)', () => { + expect(getAlertHistoryStatusPresentation('resolved')).toStrictEqual({ + label: 'resolved', + className: 'text-xs px-2 py-0.5 rounded bg-surface-hover text-base-content', + rowClassName: '', + }); + }); + + it('uses the truthy `||` left operand for an unrecognised non-empty status', () => { + expect(getAlertHistoryStatusPresentation('suppressed')).toStrictEqual({ + label: 'suppressed', + className: 'text-xs px-2 py-0.5 rounded bg-surface-hover text-base-content', + rowClassName: '', + }); + }); + + it('substitutes "resolved" for an empty normalised status via the `|| "resolved"` fallback', () => { + // undefined -> '' -> '' || 'resolved'. + expect(getAlertHistoryStatusPresentation(undefined)).toStrictEqual({ + label: 'resolved', + className: 'text-xs px-2 py-0.5 rounded bg-surface-hover text-base-content', + rowClassName: '', + }); + }); + + it('substitutes "resolved" for a null status via the ??-coalesce arm then || fallback', () => { + expect(getAlertHistoryStatusPresentation(null)).toStrictEqual({ + label: 'resolved', + className: 'text-xs px-2 py-0.5 rounded bg-surface-hover text-base-content', + rowClassName: '', + }); + }); + + it('substitutes "resolved" for a whitespace-only status (trim -> "" -> || fallback)', () => { + expect(getAlertHistoryStatusPresentation(' ')).toStrictEqual({ + label: 'resolved', + className: 'text-xs px-2 py-0.5 rounded bg-surface-hover text-base-content', + rowClassName: '', + }); + }); +}); + +describe('ternary label helpers — both arms', () => { + it('getAlertResourceIncidentCountLabel pluralises on count === 1 boundary', () => { + expect(getAlertResourceIncidentCountLabel(0)).toBe('0 incidents'); + expect(getAlertResourceIncidentCountLabel(1)).toBe('1 incident'); + expect(getAlertResourceIncidentCountLabel(2)).toBe('2 incidents'); + }); + + it('getAlertResourceIncidentRefreshLabel toggles on isLoading', () => { + expect(getAlertResourceIncidentRefreshLabel(true)).toBe('Refreshing...'); + expect(getAlertResourceIncidentRefreshLabel(false)).toBe('Refresh'); + }); + + it('getAlertResourceIncidentToggleLabel toggles on isExpanded and interpolates filteredLabel', () => { + expect(getAlertResourceIncidentToggleLabel(true, '3 of 5')).toBe('Hide events'); + expect(getAlertResourceIncidentToggleLabel(false, '3 of 5')).toBe('Events (3 of 5)'); + }); + + it('getAlertResourceIncidentSaveNoteLabel toggles on isSaving', () => { + expect(getAlertResourceIncidentSaveNoteLabel(true)).toBe('Saving...'); + expect(getAlertResourceIncidentSaveNoteLabel(false)).toBe('Save Note'); + }); +}); + +describe('non-branching label/state helpers — concrete value pins', () => { + it('getAlertResourceIncidentAcknowledgedByLabel interpolates the user', () => { + expect(getAlertResourceIncidentAcknowledgedByLabel('ada')).toBe('Acknowledged by ada'); + }); + + it('getAlertResourceIncidentRecentEventsSummary interpolates the count', () => { + expect(getAlertResourceIncidentRecentEventsSummary(7)).toBe('Showing last 7 events'); + }); + + it('getAlertResourceIncidentLoadingState returns the loading const', () => { + expect(getAlertResourceIncidentLoadingState()).toStrictEqual({ + text: 'Loading incidents...', + }); + }); + + it('getAlertResourceIncidentEmptyState returns the empty const', () => { + expect(getAlertResourceIncidentEmptyState()).toStrictEqual({ + text: 'No incidents recorded for this resource yet.', + }); + }); + + it('getAlertResourceIncidentFilteredEventsEmptyState returns the filtered-empty const', () => { + expect(getAlertResourceIncidentFilteredEventsEmptyState()).toStrictEqual({ + text: 'No events match the selected filters.', + }); + }); + + it('getAlertResourceIncidentPanelTitle returns the panel title const', () => { + expect(getAlertResourceIncidentPanelTitle()).toBe('Resource incidents'); + }); + + it('getAlertResourceIncidentNotePlaceholder returns the placeholder const', () => { + expect(getAlertResourceIncidentNotePlaceholder()).toBe('Add a note for this incident...'); + }); + + it('getAlertResourceIncidentLoadFailure returns the load-failure const', () => { + expect(getAlertResourceIncidentLoadFailure()).toBe('Failed to load resource incidents'); + }); + + it('getAlertResourceIncidentTimelineFailure returns the timeline-failure const', () => { + expect(getAlertResourceIncidentTimelineFailure()).toBe('Failed to load incident timeline'); + }); + + it('getAlertResourceIncidentNoteSavedLabel returns the note-saved const', () => { + expect(getAlertResourceIncidentNoteSavedLabel()).toBe('Incident note saved'); + }); + + it('getAlertResourceIncidentNoteSaveFailure returns the note-save-failure const', () => { + expect(getAlertResourceIncidentNoteSaveFailure()).toBe('Failed to save incident note'); + }); + + it('getAlertResourceIncidentViewTitle returns the view title const', () => { + expect(getAlertResourceIncidentViewTitle()).toBe('View incidents for this resource'); + }); +}); + +describe('event-filter class helpers — variant + selected branch coverage', () => { + it('getAlertIncidentEventFilterContainerClass returns the panel layout for variant "panel"', () => { + expect(getAlertIncidentEventFilterContainerClass('panel')).toBe( + 'flex flex-wrap items-center gap-1.5 rounded border border-border bg-surface-alt/50 p-2', + ); + }); + + it('getAlertIncidentEventFilterContainerClass returns the compact layout for variant "compact"', () => { + expect(getAlertIncidentEventFilterContainerClass('compact')).toBe( + 'flex flex-wrap items-center gap-2 text-[10px] text-muted', + ); + }); + + it('getAlertIncidentEventFilterLabelClass returns the panel label class for variant "panel"', () => { + expect(getAlertIncidentEventFilterLabelClass('panel')).toBe( + 'mr-1 text-xs font-medium text-muted', + ); + }); + + it('getAlertIncidentEventFilterLabelClass returns the compact label class for variant "compact"', () => { + expect(getAlertIncidentEventFilterLabelClass('compact')).toBe( + 'uppercase tracking-wide text-[9px] text-muted', + ); + }); + + it('getAlertIncidentEventFilterActionButtonClass returns the constant action class', () => { + expect(getAlertIncidentEventFilterActionButtonClass()).toBe( + 'px-2 py-0.5 rounded border border-border text-muted hover:bg-surface-hover', + ); + }); + + it('getAlertIncidentEventFilterChipClass returns the selected (blue) chip regardless of variant', () => { + const selected = 'px-2 py-0.5 rounded border text-[10px] transition-colors border-blue-300 bg-blue-100 text-blue-700 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-300'; + // selected short-circuits before the variant check, so both variants are equal. + expect(getAlertIncidentEventFilterChipClass(true, 'compact')).toBe(selected); + expect(getAlertIncidentEventFilterChipClass(true, 'panel')).toBe(selected); + }); + + it('getAlertIncidentEventFilterChipClass returns the unselected panel chip (variant === "panel")', () => { + expect(getAlertIncidentEventFilterChipClass(false, 'panel')).toBe( + 'px-2 py-0.5 rounded border text-[10px] transition-colors font-medium border-border text-muted hover:bg-surface-alt', + ); + }); + + it('getAlertIncidentEventFilterChipClass returns the unselected compact chip (variant !== "panel")', () => { + expect(getAlertIncidentEventFilterChipClass(false, 'compact')).toBe( + 'px-2 py-0.5 rounded border text-[10px] transition-colors border-border text-slate-500', + ); + }); +}); + +describe('timeline + card class helpers — branch coverage', () => { + it('getAlertIncidentTimelineEventCardClass uses bg-surface-alt for variant "alt"', () => { + expect(getAlertIncidentTimelineEventCardClass('alt')).toBe( + 'rounded border border-border bg-surface-alt p-2', + ); + }); + + it('getAlertIncidentTimelineEventCardClass uses bg-surface for variant "surface"', () => { + expect(getAlertIncidentTimelineEventCardClass('surface')).toBe( + 'rounded border border-border bg-surface p-2', + ); + }); + + it('pins the remaining constant class helpers (regression guards)', () => { + expect(getAlertIncidentAcknowledgedBadgeClass()).toBe( + 'px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300', + ); + expect(getAlertIncidentNoteTextareaClass()).toBe( + 'w-full rounded border border-border bg-surface p-2 text-xs text-base-content', + ); + expect(getAlertIncidentNoteSaveButtonClass()).toBe( + 'px-3 py-1.5 text-xs font-medium border rounded-md transition-all bg-surface text-base-content border-border hover:bg-surface-hover disabled:opacity-50 disabled:cursor-not-allowed', + ); + expect(getAlertIncidentTimelineMetaRowClass()).toBe( + 'flex flex-wrap items-center gap-2 text-xs text-muted', + ); + expect(getAlertIncidentTimelineHeadingClass()).toBe('font-medium text-base-content'); + expect(getAlertIncidentTimelineDetailClass()).toBe('mt-1 text-xs text-base-content'); + expect(getAlertIncidentTimelineCommandClass()).toBe('mt-1 font-mono text-xs text-base-content'); + expect(getAlertIncidentTimelineOutputClass()).toBe('mt-1 text-xs text-muted'); + expect(getAlertResourceIncidentCardClass()).toBe('rounded border border-border bg-surface p-3'); + expect(getAlertResourceIncidentSummaryRowClass()).toBe( + 'mt-2 flex flex-wrap items-center justify-between gap-2 text-xs text-muted', + ); + expect(getAlertResourceIncidentActivitySummaryClass()).toBe( + 'flex flex-wrap items-center gap-1.5', + ); + expect(getAlertResourceIncidentActivityChipClass()).toBe( + 'rounded bg-surface-alt px-2 py-0.5 text-[10px] font-medium text-base-content', + ); + expect(getAlertResourceIncidentToggleButtonClass()).toBe( + 'px-2 py-1 text-[10px] border rounded-md border-border text-muted hover:bg-surface-hover', + ); + }); +}); + +describe('getAlertResourceIncidentTruncatedEventsLabel — branch coverage', () => { + it('returns the bare "Showing last N events" when totalCount is undefined (typeof false arm)', () => { + expect(getAlertResourceIncidentTruncatedEventsLabel(5)).toBe('Showing last 5 events'); + }); + + it('returns the bare form when totalCount is a non-number type (typeof false arm)', () => { + expect( + getAlertResourceIncidentTruncatedEventsLabel(5, 'many' as unknown as number), + ).toBe('Showing last 5 events'); + }); + + it('returns "Showing N events" when totalCount is a number <= count (singular at 1)', () => { + // totalCount <= count true arm; totalCount === 1 true arm -> 'event'. + expect(getAlertResourceIncidentTruncatedEventsLabel(5, 1)).toBe('Showing 1 event'); + }); + + it('pluralises within the totalCount <= count branch when totalCount > 1', () => { + // totalCount === 1 false arm -> 'events'. + expect(getAlertResourceIncidentTruncatedEventsLabel(5, 3)).toBe('Showing 3 events'); + }); + + it('honours the <= boundary at equality (count === totalCount)', () => { + expect(getAlertResourceIncidentTruncatedEventsLabel(5, 5)).toBe('Showing 5 events'); + }); + + it('returns "Showing last N events" for totalCount 0 (0 <= count true arm, plural)', () => { + expect(getAlertResourceIncidentTruncatedEventsLabel(3, 0)).toBe('Showing 0 events'); + }); + + it('returns "Showing last count of totalCount events" when totalCount > count', () => { + // totalCount <= count false arm. + expect(getAlertResourceIncidentTruncatedEventsLabel(5, 12)).toBe( + 'Showing last 5 of 12 events', + ); + }); +}); diff --git a/frontend-modern/src/utils/__tests__/alertWebhookPresentation.branchcov0717.test.ts b/frontend-modern/src/utils/__tests__/alertWebhookPresentation.branchcov0717.test.ts new file mode 100644 index 0000000000..afddf0f4d6 --- /dev/null +++ b/frontend-modern/src/utils/__tests__/alertWebhookPresentation.branchcov0717.test.ts @@ -0,0 +1,315 @@ +import { describe, expect, it } from 'vitest'; + +import { + getAlertWebhookCustomFieldInputs, + getAlertWebhookMutationSuccess, + getAlertWebhookNamePlaceholder, + getAlertWebhookSetupInstructionsTitle, + getAlertWebhookSubmitLabel, + getAlertWebhookSummaryLabel, + getAlertWebhookTestLabel, + getAlertWebhookToggleAllLabel, + getAlertWebhookToggleLabel, + getAlertWebhookUrlPlaceholder, + normalizeAlertWebhookCustomFields, +} from '@/utils/alertWebhookPresentation'; + +// Supplemental branch coverage for alertWebhookPresentation. +// Targets the simple getter functions (fn9-fn16, fn21) that were never invoked +// by the existing tests, plus three previously-uncovered branches: +// - b33: the `?? ''` default in getAlertWebhookCustomFieldInputs (preset key +// absent from the normalized existing map). +// - b39: the `&&` right operand / true arm of the pushover app_token -> token +// migration in normalizeAlertWebhookCustomFields. +// - b42: the analogous user_token -> user migration arm. +// Each new function exercised below drives BOTH arms of every ternary, ||, and +// switch it contains so that covering them never leaves a fresh uncovered arm. + +describe('alertWebhookPresentation branch coverage (supplemental 0717)', () => { + describe('getAlertWebhookSetupInstructionsTitle', () => { + it('returns the canonical section title constant', () => { + expect(getAlertWebhookSetupInstructionsTitle()).toBe('Setup Instructions'); + }); + }); + + describe('getAlertWebhookNamePlaceholder', () => { + it('echoes a provided template name (|| left operand)', () => { + expect(getAlertWebhookNamePlaceholder('Discord Incidents')).toBe( + 'Discord Incidents', + ); + }); + + it('falls back to the default placeholder when the name is undefined (|| right operand)', () => { + expect(getAlertWebhookNamePlaceholder(undefined)).toBe('My Webhook'); + }); + + it('falls back to the default placeholder when the name is an empty string (falsy || arm)', () => { + expect(getAlertWebhookNamePlaceholder('')).toBe('My Webhook'); + }); + }); + + describe('getAlertWebhookUrlPlaceholder', () => { + it('echoes a provided url pattern (|| left operand)', () => { + expect( + getAlertWebhookUrlPlaceholder('https://discord.com/api/webhooks/123'), + ).toBe('https://discord.com/api/webhooks/123'); + }); + + it('falls back to the default URL placeholder when undefined (|| right operand)', () => { + expect(getAlertWebhookUrlPlaceholder(undefined)).toBe( + 'https://example.com/webhook', + ); + }); + + it('falls back to the default URL placeholder when the pattern is empty (falsy || arm)', () => { + expect(getAlertWebhookUrlPlaceholder('')).toBe( + 'https://example.com/webhook', + ); + }); + }); + + describe('getAlertWebhookSummaryLabel', () => { + it('interpolates both counts into the " of webhooks enabled" template', () => { + expect(getAlertWebhookSummaryLabel(2, 5)).toBe('2 of 5 webhooks enabled'); + }); + + it('renders the zero-enabled boundary without any special casing', () => { + expect(getAlertWebhookSummaryLabel(0, 3)).toBe('0 of 3 webhooks enabled'); + }); + + it('renders the all-enabled boundary verbatim', () => { + expect(getAlertWebhookSummaryLabel(4, 4)).toBe('4 of 4 webhooks enabled'); + }); + }); + + describe('getAlertWebhookToggleAllLabel', () => { + it('returns "Enable All" when the group is currently disabled (true arm)', () => { + expect(getAlertWebhookToggleAllLabel(true)).toBe('Enable All'); + }); + + it('returns "Disable All" when the group is currently enabled (false arm)', () => { + expect(getAlertWebhookToggleAllLabel(false)).toBe('Disable All'); + }); + }); + + describe('getAlertWebhookToggleLabel', () => { + it('returns "Enabled" for an on webhook (true arm)', () => { + expect(getAlertWebhookToggleLabel(true)).toBe('Enabled'); + }); + + it('returns "Disabled" for an off webhook (false arm)', () => { + expect(getAlertWebhookToggleLabel(false)).toBe('Disabled'); + }); + }); + + describe('getAlertWebhookTestLabel', () => { + it('returns "Test" when not testing (early-return arm, !isTesting truthy)', () => { + expect(getAlertWebhookTestLabel(false)).toBe('Test'); + }); + + it('returns "Test" even when ascii is requested but the button is idle (idle dominates ascii)', () => { + expect(getAlertWebhookTestLabel(false, true)).toBe('Test'); + }); + + it('returns the Unicode ellipsis label while testing with the default ascii flag (false arm)', () => { + // ALERT_WEBHOOK_TESTING_LABEL uses a single U+2026 character, distinct + // from the ASCII three-dot variant asserted below. + expect(getAlertWebhookTestLabel(true)).toBe('Testing…'); + }); + + it('returns the ASCII three-dot label while testing with ascii=true (true arm)', () => { + expect(getAlertWebhookTestLabel(true, true)).toBe('Testing...'); + }); + }); + + describe('getAlertWebhookSubmitLabel', () => { + it('returns "Update Webhook" when editing (true arm)', () => { + expect(getAlertWebhookSubmitLabel(true)).toBe('Update Webhook'); + }); + + it('returns "Add Webhook" when creating (false arm)', () => { + expect(getAlertWebhookSubmitLabel(false)).toBe('Add Webhook'); + }); + }); + + describe('getAlertWebhookMutationSuccess', () => { + it('maps the "add" action to the add-success message', () => { + expect(getAlertWebhookMutationSuccess('add')).toBe( + 'Webhook added successfully', + ); + }); + + it('maps the "update" action to the update-success message', () => { + expect(getAlertWebhookMutationSuccess('update')).toBe( + 'Webhook updated successfully', + ); + }); + + it('maps the "delete" action to the delete-success message', () => { + expect(getAlertWebhookMutationSuccess('delete')).toBe( + 'Webhook deleted successfully', + ); + }); + + it('routes any unrecognised action through the default arm to the delete-success message', () => { + expect( + getAlertWebhookMutationSuccess( + 'nuke' as unknown as 'add' | 'update' | 'delete', + ), + ).toBe('Webhook deleted successfully'); + }); + }); + + describe('normalizeAlertWebhookCustomFields — pushover token/user migration arms', () => { + it('migrates app_token to token when token is missing (b39 true arm)', () => { + const normalized = normalizeAlertWebhookCustomFields('pushover', { + app_token: 'app-key-123', + }); + expect(normalized.token).toBe('app-key-123'); + expect(normalized.app_token).toBeUndefined(); + }); + + it('does NOT overwrite an existing token with app_token (b40 false arm / short-circuit)', () => { + const normalized = normalizeAlertWebhookCustomFields('pushover', { + token: 'existing-token', + app_token: 'legacy-app-token', + }); + expect(normalized.token).toBe('existing-token'); + expect(normalized.app_token).toBeUndefined(); + }); + + it('skips migration when app_token is whitespace-only (right operand falsy)', () => { + const normalized = normalizeAlertWebhookCustomFields('pushover', { + app_token: ' ', + }); + expect(normalized.token).toBeUndefined(); + expect(normalized.app_token).toBeUndefined(); + }); + + it('migrates user_token to user when user is missing (b42 true arm)', () => { + const normalized = normalizeAlertWebhookCustomFields('pushover', { + user_token: 'user-key-456', + }); + expect(normalized.user).toBe('user-key-456'); + expect(normalized.user_token).toBeUndefined(); + }); + + it('does NOT overwrite an existing user with user_token (false arm / short-circuit)', () => { + const normalized = normalizeAlertWebhookCustomFields('pushover', { + user: 'existing-user', + user_token: 'legacy-user-token', + }); + expect(normalized.user).toBe('existing-user'); + expect(normalized.user_token).toBeUndefined(); + }); + + it('skips user migration when user_token is whitespace-only (right operand falsy)', () => { + const normalized = normalizeAlertWebhookCustomFields('pushover', { + user_token: ' ', + }); + expect(normalized.user).toBeUndefined(); + expect(normalized.user_token).toBeUndefined(); + }); + + it('strips both legacy keys and migrates both values in a single pass', () => { + const normalized = normalizeAlertWebhookCustomFields('pushover', { + app_token: 'a-tok', + user_token: 'u-tok', + }); + expect(normalized).toStrictEqual({ token: 'a-tok', user: 'u-tok' }); + }); + + it('returns a shallow copy unchanged for non-pushover services (early return)', () => { + const fields = { token: 'should-stay', custom: 'x' }; + const normalized = normalizeAlertWebhookCustomFields('slack', fields); + expect(normalized).toStrictEqual(fields); + expect(normalized).not.toBe(fields); + }); + + it('treats Pushover with mixed case/whitespace as pushover (trim().toLowerCase())', () => { + const normalized = normalizeAlertWebhookCustomFields(' PushOver ', { + app_token: 'mixed', + }); + expect(normalized.token).toBe('mixed'); + expect(normalized.app_token).toBeUndefined(); + }); + }); + + describe('getAlertWebhookCustomFieldInputs — preset value default arm (b33)', () => { + it('defaults each pushover preset value to empty string when no existing values are supplied (?? right operand)', () => { + const inputs = getAlertWebhookCustomFieldInputs('pushover', {}); + expect(inputs).toHaveLength(2); + expect(inputs).toStrictEqual([ + { + key: 'token', + value: '', + label: 'Application Token', + placeholder: 'Your Pushover application token', + required: true, + }, + { + key: 'user', + value: '', + label: 'User Key', + placeholder: 'Primary user key or group key', + required: true, + }, + ]); + }); + + it('pulls each preset value from the normalized existing map when present (?? left operand)', () => { + const inputs = getAlertWebhookCustomFieldInputs('pushover', { + token: 'tok-1', + user: 'usr-1', + }); + expect(inputs.map((i) => [i.key, i.value])).toStrictEqual([ + ['token', 'tok-1'], + ['user', 'usr-1'], + ]); + }); + + it('appends extra non-preset fields after the pushover presets (extras filter arm)', () => { + const inputs = getAlertWebhookCustomFieldInputs('pushover', { + token: 'tok', + user: 'usr', + priority: '1', + }); + // Presets first, then the filtered extras (priority is not a preset key). + expect(inputs.map((i) => i.key)).toStrictEqual([ + 'token', + 'user', + 'priority', + ]); + const priority = inputs.find((i) => i.key === 'priority'); + expect(priority).toStrictEqual({ key: 'priority', value: '1' }); + }); + + it('migrates pushover app_token/user_token before building preset inputs', () => { + // Confirms the composition: getAlertWebhookCustomFieldInputs runs the + // normalizer first, so app_token feeds the token preset's value. + const inputs = getAlertWebhookCustomFieldInputs('pushover', { + app_token: 'via-app-token', + user_token: 'via-user-token', + }); + expect(inputs.map((i) => [i.key, i.value])).toStrictEqual([ + ['token', 'via-app-token'], + ['user', 'via-user-token'], + ]); + }); + + it('returns bare {key,value} inputs for a service with no presets (presets-absent branch)', () => { + const inputs = getAlertWebhookCustomFieldInputs('discord', { + sound: 'ping', + priority: '5', + }); + expect(inputs).toStrictEqual([ + { key: 'sound', value: 'ping' }, + { key: 'priority', value: '5' }, + ]); + }); + + it('defaults to an empty existing map and yields no inputs for a preset-less service', () => { + expect(getAlertWebhookCustomFieldInputs('slack')).toStrictEqual([]); + }); + }); +}); diff --git a/frontend-modern/src/utils/__tests__/organizationSettingsPresentation.branchcov0717.test.ts b/frontend-modern/src/utils/__tests__/organizationSettingsPresentation.branchcov0717.test.ts new file mode 100644 index 0000000000..4da4746e18 --- /dev/null +++ b/frontend-modern/src/utils/__tests__/organizationSettingsPresentation.branchcov0717.test.ts @@ -0,0 +1,281 @@ +import { describe, expect, it } from 'vitest'; +import type { OrganizationRole, OrganizationShareStatus } from '@/api/orgs'; +import { + getOrganizationAccessInvitationAcceptedMessage, + getOrganizationAccessInvitationDeclinedMessage, + getOrganizationAccessInvitationRevokedMessage, + getOrganizationAccessInvitationSentMessage, + getOrganizationAccessOwnerTransferMemberRequiredMessage, + getOrganizationAccessPendingInvitationsEmptyState, + getOrganizationAccessRoleUpdatedMessage, + getOrganizationAccessYourInvitationsEmptyState, + getOrganizationAddMemberErrorMessage, + getOrganizationDisplayNameUpdateErrorMessage, + getOrganizationIncomingShareAcceptErrorMessage, + getOrganizationIncomingShareDeclineConfirmMessage, + getOrganizationIncomingShareDeclineErrorMessage, + getOrganizationIncomingShareDeclineSuccessMessage, + getOrganizationInvitationActionErrorMessage, + getOrganizationMemberRoleUpdateErrorMessage, + getOrganizationRemoveMemberErrorMessage, + getOrganizationSettingsLoadErrorMessage, + getOrganizationShareCreateErrorMessage, + getOrganizationShareDeleteErrorMessage, + getOrganizationShareStatusDescription, + getOrganizationShareStatusLabel, +} from '@/utils/organizationSettingsPresentation'; + +// Supplemental branch coverage. The primary test exercises the happy path for +// most helpers but leaves the arms below open: the two-`if` precedence in the +// load-error helper, every fully-uncovered invitation helper, the `message ||` +// truthy/falsy arms of several error helpers, and the acceptedAt/acceptedBy +// branch matrix of getOrganizationShareStatusDescription. + +describe('organizationSettingsPresentation branch coverage (supplemental)', () => { + describe('getOrganizationSettingsLoadErrorMessage', () => { + it('honours the 402 check before the 501 check when a message carries both tokens', () => { + // Two sequential `if (message.includes(...))` guards: the 402 arm is checked + // first, so a message containing both substrings must resolve to the license + // message, never the not-enabled message. + expect( + getOrganizationSettingsLoadErrorMessage('gateway returned 402 then 501', 'sharing'), + ).toBe('Organization settings require an Enterprise license.'); + }); + }); + + describe('getOrganizationAccessPendingInvitationsEmptyState', () => { + it('returns the canonical pending-invitations empty state', () => { + expect(getOrganizationAccessPendingInvitationsEmptyState()).toBe( + 'No pending invitations for this organization.', + ); + }); + }); + + describe('getOrganizationAccessYourInvitationsEmptyState', () => { + it('returns the canonical your-invitations empty state', () => { + expect(getOrganizationAccessYourInvitationsEmptyState()).toBe( + 'No invitations are waiting for you.', + ); + }); + }); + + describe('getOrganizationAccessOwnerTransferMemberRequiredMessage', () => { + it('returns the canonical owner-transfer validation message', () => { + expect(getOrganizationAccessOwnerTransferMemberRequiredMessage()).toBe( + 'Ownership can only be transferred to an existing member.', + ); + }); + }); + + describe('getOrganizationAccessInvitationSentMessage', () => { + it('formats the invitation-sent copy for every non-owner role', () => { + // role is typed `Exclude`; exercise each arm. + const roles: Exclude[] = ['admin', 'editor', 'viewer']; + for (const role of roles) { + expect(getOrganizationAccessInvitationSentMessage('bob', role)).toBe( + `Sent bob an invitation for the ${role} role.`, + ); + } + }); + }); + + describe('getOrganizationAccessInvitationAcceptedMessage', () => { + it('formats the invitation-accepted copy', () => { + expect(getOrganizationAccessInvitationAcceptedMessage('carol')).toBe( + 'carol joined the organization.', + ); + }); + }); + + describe('getOrganizationAccessInvitationDeclinedMessage', () => { + it('formats the invitation-declined copy with the org id', () => { + expect(getOrganizationAccessInvitationDeclinedMessage('org-42')).toBe( + 'Declined the invitation for org-42.', + ); + }); + }); + + describe('getOrganizationAccessInvitationRevokedMessage', () => { + it('formats the invitation-revoked copy with the user id', () => { + expect(getOrganizationAccessInvitationRevokedMessage('dave')).toBe( + "Revoked dave's pending invitation.", + ); + }); + }); + + describe('getOrganizationAccessRoleUpdatedMessage', () => { + it('formats the role-updated copy for the owner and editor roles the primary test omits', () => { + expect(getOrganizationAccessRoleUpdatedMessage('alice', 'owner')).toBe( + 'Updated alice to the owner role.', + ); + expect(getOrganizationAccessRoleUpdatedMessage('alice', 'editor')).toBe( + 'Updated alice to the editor role.', + ); + }); + }); + + describe('getOrganizationInvitationActionErrorMessage', () => { + it('returns the provided message on the truthy arm of `message ||`', () => { + expect(getOrganizationInvitationActionErrorMessage('invitation already revoked')).toBe( + 'invitation already revoked', + ); + }); + + it('returns the fallback on the falsy arm when message is undefined', () => { + expect(getOrganizationInvitationActionErrorMessage()).toBe( + 'Unable to update the invitation.', + ); + }); + + it('treats an empty-string message as falsy and falls back', () => { + expect(getOrganizationInvitationActionErrorMessage('')).toBe( + 'Unable to update the invitation.', + ); + }); + }); + + describe('getOrganizationIncomingShareAcceptErrorMessage', () => { + it('returns the provided message on the truthy arm of `message ||`', () => { + expect(getOrganizationIncomingShareAcceptErrorMessage('target rejected')).toBe( + 'target rejected', + ); + }); + + it('treats an empty-string message as falsy and falls back', () => { + expect(getOrganizationIncomingShareAcceptErrorMessage('')).toBe( + 'Unable to accept the incoming share.', + ); + }); + }); + + describe('getOrganizationIncomingShareDeclineErrorMessage', () => { + it('returns the provided message on the truthy arm of `message ||`', () => { + expect(getOrganizationIncomingShareDeclineErrorMessage('still in use')).toBe( + 'still in use', + ); + }); + + it('treats an empty-string message as falsy and falls back', () => { + expect(getOrganizationIncomingShareDeclineErrorMessage('')).toBe( + 'Unable to remove the incoming share.', + ); + }); + }); + + describe('error helpers — empty-string falsy arm of `message || fallback`', () => { + // The primary test only feeds `undefined` for these, hitting the same falsy + // arm; exercising the empty-string variant pins that any falsy message + // (not only undefined) routes to the fallback. + it('getOrganizationDisplayNameUpdateErrorMessage falls back for an empty string', () => { + expect(getOrganizationDisplayNameUpdateErrorMessage('')).toBe( + 'Unable to update the organization name.', + ); + }); + + it('getOrganizationMemberRoleUpdateErrorMessage falls back for an empty string', () => { + expect(getOrganizationMemberRoleUpdateErrorMessage('')).toBe( + 'Unable to update the member role.', + ); + }); + + it('getOrganizationAddMemberErrorMessage falls back for an empty string', () => { + expect(getOrganizationAddMemberErrorMessage('')).toBe('Unable to add the member.'); + }); + + it('getOrganizationRemoveMemberErrorMessage falls back for an empty string', () => { + expect(getOrganizationRemoveMemberErrorMessage('')).toBe('Unable to remove the member.'); + }); + + it('getOrganizationShareCreateErrorMessage falls back for an empty string', () => { + expect(getOrganizationShareCreateErrorMessage('')).toBe('Unable to create the share.'); + }); + + it('getOrganizationShareDeleteErrorMessage falls back for an empty string', () => { + expect(getOrganizationShareDeleteErrorMessage('')).toBe('Unable to remove the share.'); + }); + }); + + describe('getOrganizationShareStatusLabel', () => { + it('routes any non-pending status (including an unrecognised value) to the else arm "Active"', () => { + // Ternary `status === 'pending' ? 'Pending approval' : 'Active'`: a + // deliberately invalid status must take the else arm. + expect( + getOrganizationShareStatusLabel('rejected' as unknown as OrganizationShareStatus), + ).toBe('Active'); + }); + }); + + describe('getOrganizationShareStatusDescription', () => { + it('keeps the pending copy when status is pending even if accepted metadata is supplied', () => { + // status === 'pending' short-circuits before the acceptedAt/acceptedBy logic. + expect( + getOrganizationShareStatusDescription('pending', '2026-04-22T10:30:00Z', 'alice'), + ).toBe('Waiting for a target organization admin to accept.'); + }); + + it('uses formatOrgDate(acceptedAt) and omits the "by ..." segment when acceptedBy is absent', () => { + // acceptedAt truthy arm + acceptedBy falsy arm. Use a value formatOrgDate + // returns verbatim (Invalid Date) so the assertion is deterministic. + expect(getOrganizationShareStatusDescription('accepted', 'not-a-real-date')).toBe( + 'Accepted not-a-real-date.', + ); + }); + + it('uses formatOrgDate(acceptedAt) and appends "by " when acceptedBy is set', () => { + // acceptedAt truthy arm + acceptedBy truthy arm with a real (locale-formatted) + // date: assert structure rather than the locale-dependent date text. + const description = getOrganizationShareStatusDescription( + 'accepted', + '2026-04-22T10:30:00Z', + 'alice', + ); + expect(description).toMatch(/^Accepted .+ by alice\.$/); + expect(description).not.toContain('the target organization'); + }); + + it('falls back to the generic "Accepted by the target organization" when acceptedAt is undefined', () => { + // acceptedAt falsy arm + acceptedBy truthy arm. + expect(getOrganizationShareStatusDescription('accepted', undefined, 'alice')).toBe( + 'Accepted by the target organization by alice.', + ); + }); + + it('omits the "by ..." segment entirely when both acceptedAt and acceptedBy are undefined', () => { + // acceptedAt falsy arm + acceptedBy falsy arm. + expect(getOrganizationShareStatusDescription('accepted')).toBe( + 'Accepted by the target organization.', + ); + }); + + it('treats an empty-string acceptedAt as falsy', () => { + // acceptedAt is `''` (falsy) -> generic accepted copy, no "by" segment. + expect(getOrganizationShareStatusDescription('accepted', '')).toBe( + 'Accepted by the target organization.', + ); + }); + }); + + describe('getOrganizationIncomingShareDeclineConfirmMessage', () => { + it('routes any non-pending status (including an unrecognised value) to the remove arm', () => { + // `status === 'pending' ? ... : ...` else arm via an invalid status. + expect( + getOrganizationIncomingShareDeclineConfirmMessage( + 'High CPU', + 'expired' as unknown as OrganizationShareStatus, + ), + ).toBe('Remove shared access to High CPU?'); + }); + }); + + describe('getOrganizationIncomingShareDeclineSuccessMessage', () => { + it('routes any non-pending status (including an unrecognised value) to the removed arm', () => { + // `status === 'pending' ? ... : ...` else arm via an invalid status. + expect( + getOrganizationIncomingShareDeclineSuccessMessage( + 'High CPU', + 'expired' as unknown as OrganizationShareStatus, + ), + ).toBe('Removed shared access to High CPU.'); + }); + }); +}); diff --git a/frontend-modern/src/utils/__tests__/ssoProviderPresentation.branchcov0717.test.ts b/frontend-modern/src/utils/__tests__/ssoProviderPresentation.branchcov0717.test.ts new file mode 100644 index 0000000000..8d2ac0b843 --- /dev/null +++ b/frontend-modern/src/utils/__tests__/ssoProviderPresentation.branchcov0717.test.ts @@ -0,0 +1,367 @@ +import { describe, expect, it } from 'vitest'; + +import { + getSSOCertificatePresentation, + getSSOCopySuccessMessage, + getSSOConnectionTestErrorMessage, + getSSOConnectionTestFailureMessage, + getSSOConnectionTestSuccessMessage, + getSSOProviderAddButtonLabel, + getSSOProviderCardClass, + getSSOProviderDeleteErrorMessage, + getSSOProviderDeleteSuccessMessage, + getSSOProviderDetailsLoadErrorMessage, + getSSOProviderEmptyStateDescription, + getSSOProviderEmptyStateTitle, + getSSOMetadataFetchErrorMessage, + getSSOMetadataUrlRequiredMessage, + getSSOProviderModalTitle, + getSSOProviderSaveErrorMessage, + getSSOProviderSaveSuccessMessage, + getSSOProviderSummary, + getSSOProvidersLoadingState, + getSSOProvidersLoadErrorMessage, + getSSOProviderToggleErrorMessage, + getSSOProviderToggleSuccessMessage, + getSSOProviderTypeBadgeClass, + getSSOProviderTypeLabel, + getSSOSamlFeatureGateCopy, + getSSOTestResultPresentation, + type SSOProviderSummaryLike, + type SSOProviderType, +} from '@/utils/ssoProviderPresentation'; + +// This supplemental suite targets the branch arms the canonical +// `ssoProviderPresentation.test.ts` leaves open: the `editing === true` arm of +// `getSSOProviderModalTitle`, the falsy `oidcIssuerUrl || ''` arm, and both +// falsy arms of the nested `samlIdpEntityId || samlMetadataUrl || ''` chain in +// `getSSOProviderSummary`. It also pins the full concrete return values and +// `String(error)` coercion behaviour that the sibling suite only spot-checks. + +describe('ssoProviderPresentation branch coverage (supplemental)', () => { + describe('getSSOProviderTypeLabel', () => { + it('returns "OIDC" on the oidc arm of the ternary', () => { + expect(getSSOProviderTypeLabel('oidc')).toBe('OIDC'); + }); + + it('returns "SAML" on the non-oidc arm of the ternary', () => { + expect(getSSOProviderTypeLabel('saml')).toBe('SAML'); + }); + + it('routes a deliberately-invalid type through the else arm onto "SAML"', () => { + // The ternary is `type === 'oidc' ? 'OIDC' : 'SAML'`; anything that is not + // strictly the string 'oidc' falls to the else arm regardless of the + // declared union type. + expect(getSSOProviderTypeLabel('unknown' as unknown as SSOProviderType)).toBe('SAML'); + }); + }); + + describe('getSSOProviderAddButtonLabel', () => { + it('composes "Add OIDC" from the oidc label arm', () => { + expect(getSSOProviderAddButtonLabel('oidc')).toBe('Add OIDC'); + }); + + it('composes "Add SAML" from the saml label arm', () => { + expect(getSSOProviderAddButtonLabel('saml')).toBe('Add SAML'); + }); + }); + + describe('getSSOProviderModalTitle', () => { + it('uses the "Edit" arm when editing is true (oidc label arm)', () => { + // Covers the previously-open `editing ? 'Edit' : 'Add'` true arm, and the + // oidc arm of the nested getSSOProviderTypeLabel call. + expect(getSSOProviderModalTitle(true, 'oidc')).toBe('Edit OIDC Provider'); + }); + + it('uses the "Edit" arm with the saml label', () => { + expect(getSSOProviderModalTitle(true, 'saml')).toBe('Edit SAML Provider'); + }); + + it('uses the "Add" arm with the oidc label', () => { + // The sibling suite only pins (false, 'saml'); this pins the (false, + // 'oidc') combination. + expect(getSSOProviderModalTitle(false, 'oidc')).toBe('Add OIDC Provider'); + }); + }); + + describe('getSSOSamlFeatureGateCopy', () => { + it('returns the full feature-gate copy verbatim', () => { + expect(getSSOSamlFeatureGateCopy()).toStrictEqual({ + title: 'Single Sign-On', + subtitle: 'Single Sign-On', + body: 'OIDC, SAML, and multi-provider SSO are included with Community and higher tiers.', + }); + }); + }); + + describe('getSSOProviderEmptyStateTitle / Description / LoadingState', () => { + it('returns the canonical empty-state copy and loading object', () => { + expect(getSSOProviderEmptyStateTitle()).toBe('No SSO providers configured'); + expect(getSSOProviderEmptyStateDescription()).toBe( + 'Add an OIDC or SAML provider to get started.', + ); + expect(getSSOProvidersLoadingState()).toStrictEqual({ text: 'Loading SSO providers…' }); + }); + }); + + describe('getSSOProvidersLoadErrorMessage / DetailsLoadErrorMessage', () => { + it('returns the canonical load-error strings', () => { + expect(getSSOProvidersLoadErrorMessage()).toBe('Unable to load SSO providers.'); + expect(getSSOProviderDetailsLoadErrorMessage()).toBe( + 'Unable to load SSO provider details.', + ); + }); + }); + + describe('getSSOProviderSaveSuccessMessage', () => { + it('returns the "updated" copy on the isEdit true arm', () => { + expect(getSSOProviderSaveSuccessMessage(true)).toBe('SSO provider has been updated.'); + }); + + it('returns the "created" copy on the isEdit false arm', () => { + expect(getSSOProviderSaveSuccessMessage(false)).toBe('SSO provider has been created.'); + }); + }); + + describe('getSSOProviderSaveErrorMessage', () => { + it('coerces an undefined error to the literal string "undefined" via String()', () => { + // `String(undefined)` => 'undefined' — documents the runtime behaviour of + // the optional `error` parameter when callers omit it. + expect(getSSOProviderSaveErrorMessage()).toBe( + 'Unable to save the SSO provider: undefined', + ); + }); + + it('coerces null to "null"', () => { + expect(getSSOProviderSaveErrorMessage(null)).toBe( + 'Unable to save the SSO provider: null', + ); + }); + + it('coerces a plain object to "[object Object]"', () => { + expect(getSSOProviderSaveErrorMessage({ status: 500 })).toBe( + 'Unable to save the SSO provider: [object Object]', + ); + }); + + it('coerces an Error instance to "Error: "', () => { + expect(getSSOProviderSaveErrorMessage(new Error('boom'))).toBe( + 'Unable to save the SSO provider: Error: boom', + ); + }); + + it('coerces a number to its decimal string form', () => { + expect(getSSOProviderSaveErrorMessage(42)).toBe('Unable to save the SSO provider: 42'); + }); + }); + + describe('getSSOProviderDeleteSuccessMessage / DeleteErrorMessage / ToggleErrorMessage', () => { + it('returns the canonical delete/toggle strings verbatim', () => { + expect(getSSOProviderDeleteSuccessMessage()).toBe('SSO provider has been removed.'); + expect(getSSOProviderDeleteErrorMessage()).toBe('Unable to remove the SSO provider.'); + expect(getSSOProviderToggleErrorMessage()).toBe('Unable to update the SSO provider.'); + }); + }); + + describe('getSSOProviderToggleSuccessMessage', () => { + it('returns the "enabled" copy on the true arm', () => { + expect(getSSOProviderToggleSuccessMessage(true)).toBe('SSO provider has been enabled.'); + }); + + it('returns the "disabled" copy on the false arm', () => { + expect(getSSOProviderToggleSuccessMessage(false)).toBe('SSO provider has been disabled.'); + }); + }); + + describe('getSSOCopySuccessMessage', () => { + it('interpolates the label into the clipboard confirmation', () => { + expect(getSSOCopySuccessMessage('XML')).toBe('XML has been copied to the clipboard.'); + }); + + it('renders an empty label verbatim (leading space kept by the template literal)', () => { + // Edge case: template literal does not trim, so an empty label yields a + // leading space then the fixed copy. + expect(getSSOCopySuccessMessage('')).toBe(' has been copied to the clipboard.'); + }); + }); + + describe('getSSOConnectionTestSuccessMessage / FailureMessage / ErrorMessage', () => { + it('returns the canonical connection-test strings', () => { + expect(getSSOConnectionTestSuccessMessage()).toBe( + 'Connection test completed successfully.', + ); + expect(getSSOConnectionTestFailureMessage('Bad certificate')).toBe( + 'Connection test failed: Bad certificate', + ); + expect(getSSOConnectionTestErrorMessage()).toBe('Unable to run the connection test.'); + }); + + it('renders an empty failure message after the colon', () => { + expect(getSSOConnectionTestFailureMessage('')).toBe('Connection test failed: '); + }); + }); + + describe('getSSOMetadataUrlRequiredMessage / MetadataFetchErrorMessage', () => { + it('returns the canonical metadata-required string', () => { + expect(getSSOMetadataUrlRequiredMessage()).toBe('Enter an IdP metadata URL.'); + }); + + it('coerces an undefined error to "undefined" via String()', () => { + expect(getSSOMetadataFetchErrorMessage()).toBe('Unable to fetch metadata: undefined'); + }); + + it('coerces null to "null"', () => { + expect(getSSOMetadataFetchErrorMessage(null)).toBe('Unable to fetch metadata: null'); + }); + + it('coerces a plain object to "[object Object]"', () => { + expect(getSSOMetadataFetchErrorMessage({ code: 'ECONNRESET' })).toBe( + 'Unable to fetch metadata: [object Object]', + ); + }); + }); + + describe('getSSOProviderSummary', () => { + it('returns the oidcIssuerUrl on the oidc truthy arm', () => { + expect( + getSSOProviderSummary({ + type: 'oidc', + oidcIssuerUrl: 'https://login.example.com/realms/pulse', + }), + ).toBe('https://login.example.com/realms/pulse'); + }); + + it('falls back to "" when an oidc provider has no issuer url (|| falsy arm)', () => { + // Covers the previously-open falsy arm of `provider.oidcIssuerUrl || ''`. + expect(getSSOProviderSummary({ type: 'oidc' })).toBe(''); + }); + + it('falls back to "" when the oidc issuer url is the empty string', () => { + // Same `|| ''` falsy arm, exercised with a present-but-falsy value. + expect(getSSOProviderSummary({ type: 'oidc', oidcIssuerUrl: '' })).toBe(''); + }); + + it('prefers samlIdpEntityId on the saml arm when it is set', () => { + expect( + getSSOProviderSummary({ + type: 'saml', + samlIdpEntityId: 'https://idp.example.com/entity', + samlMetadataUrl: 'https://idp.example.com/metadata', + }), + ).toBe('https://idp.example.com/entity'); + }); + + it('falls back to samlMetadataUrl when samlIdpEntityId is absent (first || falsy arm)', () => { + // Covers the previously-open falsy arm of the first `||` and the truthy + // arm of the second `||`. + expect( + getSSOProviderSummary({ + type: 'saml', + samlMetadataUrl: 'https://idp.example.com/metadata', + }), + ).toBe('https://idp.example.com/metadata'); + }); + + it('falls back to samlMetadataUrl when samlIdpEntityId is the empty string', () => { + // Same first-|| falsy arm, exercised with a present-but-falsy entity id. + expect( + getSSOProviderSummary({ + type: 'saml', + samlIdpEntityId: '', + samlMetadataUrl: 'https://idp.example.com/metadata', + }), + ).toBe('https://idp.example.com/metadata'); + }); + + it('falls back to "" when a saml provider has neither id nor metadata url (final || falsy arm)', () => { + // Covers the previously-open final `|| ''` falsy arm of the nested chain. + expect(getSSOProviderSummary({ type: 'saml' })).toBe(''); + }); + + it('falls back to "" when both saml fields are the empty string', () => { + // Final `|| ''` falsy arm, exercised with present-but-falsy values. + expect( + getSSOProviderSummary({ + type: 'saml', + samlIdpEntityId: '', + samlMetadataUrl: '', + }), + ).toBe(''); + }); + + it('treats a non-oidc/non-saml type as the saml summary path', () => { + // The `if (provider.type === 'oidc')` guard only special-cases oidc; + // anything else (including an invalid type) flows to the saml fallback + // chain. With nothing set, that resolves to "". + expect( + getSSOProviderSummary({ + type: 'unknown' as unknown as SSOProviderType, + samlMetadataUrl: 'https://only.example.com/metadata', + } as unknown as SSOProviderSummaryLike), + ).toBe('https://only.example.com/metadata'); + }); + }); + + describe('getSSOProviderCardClass', () => { + it('returns the full enabled-card class string', () => { + expect(getSSOProviderCardClass(true)).toBe( + 'p-4 rounded-md border bg-surface border-border', + ); + }); + + it('returns the full disabled-card class string', () => { + expect(getSSOProviderCardClass(false)).toBe( + 'p-4 rounded-md border bg-surface-alt border-border opacity-60', + ); + }); + }); + + describe('getSSOProviderTypeBadgeClass', () => { + it('returns the canonical badge class string verbatim', () => { + expect(getSSOProviderTypeBadgeClass()).toBe( + 'px-1.5 py-0.5 text-xs font-medium rounded bg-surface-hover', + ); + }); + }); + + describe('getSSOTestResultPresentation', () => { + it('returns the full success presentation object', () => { + expect(getSSOTestResultPresentation(true)).toStrictEqual({ + panelClass: + 'p-4 rounded-md border bg-green-50 dark:bg-green-900 border-green-200 dark:border-green-800', + iconClass: 'w-5 h-5 text-emerald-500 dark:text-emerald-400 flex-shrink-0 mt-0.5', + titleClass: 'text-sm font-medium text-green-800 dark:text-green-200', + errorClass: 'text-xs text-red-600 dark:text-red-400 mt-1', + }); + }); + + it('returns the full failure presentation object', () => { + expect(getSSOTestResultPresentation(false)).toStrictEqual({ + panelClass: + 'p-4 rounded-md border bg-red-50 dark:bg-red-900 border-red-200 dark:border-red-800', + iconClass: 'w-5 h-5 text-rose-500 dark:text-rose-400 flex-shrink-0 mt-0.5', + titleClass: 'text-sm font-medium text-red-800 dark:text-red-200', + errorClass: 'text-xs text-red-600 dark:text-red-400 mt-1', + }); + }); + }); + + describe('getSSOCertificatePresentation', () => { + it('returns the full expired presentation object', () => { + expect(getSSOCertificatePresentation(true)).toStrictEqual({ + containerClass: + 'text-xs px-2 py-1 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300', + expiredLabelClass: 'ml-1 text-red-600 dark:text-red-400 font-medium', + expiredLabel: '(Expired!)', + }); + }); + + it('returns the full valid presentation object', () => { + expect(getSSOCertificatePresentation(false)).toStrictEqual({ + containerClass: 'text-xs px-2 py-1 rounded bg-surface-hover text-base-content', + expiredLabelClass: 'ml-1 text-red-600 dark:text-red-400 font-medium', + expiredLabel: '(Expired!)', + }); + }); + }); +});