refactor(recovery): canonicalize item resource ids

This commit is contained in:
rcourtman 2026-03-26 20:55:07 +00:00
parent f15a186a04
commit 5ef3fb59bf
16 changed files with 234 additions and 34 deletions

View file

@ -728,6 +728,11 @@ operator-facing filter query for lifecycle-adjacent drill-down links. Any
legacy `provider` alias support must remain compatibility-only input behind
the shared API/router layer rather than becoming the route shape lifecycle
surfaces copy back out to operators.
That same lifecycle-adjacent recovery drill-down boundary must also stay on
canonical `itemResourceId` filter and payload vocabulary. When lifecycle
surfaces deep-link into shared recovery handlers or consume recovery payloads,
they should treat legacy `subjectResourceId` only as an API-layer compatibility
alias rather than reviving it as the route or runtime model they expose.
The updater/runtime surfaces must preserve the one-shot `updated_from`
continuity handoff and the non-TLS continuity path for supported self-hosted

View file

@ -200,6 +200,7 @@ Own canonical runtime payload shapes between backend and frontend.
13. Keep recovery payload filters canonical across `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets`: when `internal/api/recovery_handlers.go` adds a governed recovery filter or display field such as provider-neutral `itemType`, the same normalized transport must land across all four endpoints and the contract tests must pin both outbound payload shape and accepted query aliases in the same slice
14. Keep recovery platform-query vocabulary canonical across that same `/api/recovery/*` surface: operator-facing transport must emit `platform` as the canonical query field, accepted legacy `provider` aliases must remain compatibility-only input, and `internal/api/contract_test.go` must pin that fallback behavior in the same slice as any handler change
15. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model
16. Keep recovery linked-resource vocabulary canonical across that same `/api/recovery/*` surface: points and rollups must expose `itemResourceId` as the canonical linked-resource field, accepted legacy `subjectResourceId` aliases must remain compatibility-only input or secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change
## Current State
@ -1388,6 +1389,12 @@ operator-facing query field across `/api/recovery/rollups`, `/api/recovery/point
mapping that boundary onto internal provider fields, but accepted legacy
`provider` aliases must be compatibility-only input and must not replace the
canonical transport query shape.
That same recovery API boundary must also treat `itemResourceId` as the
canonical linked-resource filter and payload field across those same
`/api/recovery/*` endpoints. Accepted legacy `subjectResourceId` aliases may
remain as compatibility-only input or secondary payload fields during the v6
transition, but the shared transport contract and frontend decode path must
normalize them back onto canonical `itemResourceId`.
That same outbound recovery transport now also treats `platform` and
`platforms` as the canonical response fields for point and rollup payloads.
Compatibility `provider` and `providers` fields may remain during the v6

View file

@ -200,6 +200,10 @@ That same rule applies to recovery detail helpers. Provider-specific helper
names like `isPbsProvider` should become platform-specific helpers like
`isPbsPlatform` once the runtime recovery model is already canonically
platform-first.
The same canonical boundary applies to linked-resource identifiers. Recovery
API payloads, query filters, and normalized frontend runtime models should use
`itemResourceId` as the canonical field while accepting or emitting
`subjectResourceId` only as a compatibility alias during the transition.
That same presenter boundary should also own canonical item-type derivation.
Recovery surfaces must resolve rollup and point item types through one shared
item-type helper instead of repeating `display.itemType` / `subjectType` /

View file

@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest';
import {
normalizeRecoveryPointsResponse,
normalizeRecoveryRollupsResponse,
} from '@/utils/recoveryPlatformModel';
describe('recovery transport', () => {
it('normalizes legacy subject resource ids onto canonical item resource ids', () => {
expect(
normalizeRecoveryPointsResponse({
data: [
{
id: 'point-1',
provider: 'truenas',
kind: 'snapshot',
mode: 'snapshot',
outcome: 'success',
subjectResourceId: 'res-1',
},
],
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
}),
).toEqual({
data: [
{
id: 'point-1',
platform: 'truenas',
kind: 'snapshot',
mode: 'snapshot',
outcome: 'success',
itemResourceId: 'res-1',
},
],
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
});
expect(
normalizeRecoveryRollupsResponse({
data: [
{
rollupId: 'rollup-1',
lastOutcome: 'success',
providers: ['truenas'],
subjectResourceId: 'res-1',
},
],
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
}),
).toEqual({
data: [
{
rollupId: 'rollup-1',
lastOutcome: 'success',
platforms: ['truenas'],
itemResourceId: 'res-1',
},
],
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
});
});
});

View file

@ -126,7 +126,7 @@ const Recovery: Component = () => {
const label = getRecoveryRollupItemLabel(rollup, resourceIndex);
const haystack = [
rollup.rollupId,
rollup.subjectResourceId || '',
rollup.itemResourceId || '',
label,
rollupItemType,
rollup.subjectRef?.namespace || '',

View file

@ -175,7 +175,7 @@ export const RecoveryPointDetails: Component<RecoveryPointDetailsProps> = (props
if (p.verified != null) pairs.push({ k: 'Verified', v: p.verified ? 'Verified' : 'Not Verified' });
if (p.encrypted != null) pairs.push({ k: 'Encrypted', v: p.encrypted ? 'Encrypted' : 'Not Encrypted' });
if (p.subjectResourceId) pairs.push({ k: 'Item Resource', v: p.subjectResourceId });
if (p.itemResourceId) pairs.push({ k: 'Item Resource', v: p.itemResourceId });
if (p.repositoryResourceId) pairs.push({ k: 'Target Resource', v: p.repositoryResourceId });
if (p.subjectRef) pairs.push({ k: 'Item Ref', v: labelForRef(p.subjectRef) });
if (p.repositoryRef) pairs.push({ k: 'Target Ref', v: labelForRef(p.repositoryRef) });

View file

@ -23,7 +23,7 @@ vi.mock('@solidjs/router', async () => {
const rollupsPayload = [
{
rollupId: 'res:vm-123',
subjectResourceId: 'vm-123',
itemResourceId: 'vm-123',
display: { subjectType: 'proxmox-vm', itemType: 'vm' },
lastAttemptAt: '2026-02-14T10:00:00.000Z',
lastSuccessAt: '2026-02-14T10:00:00.000Z',
@ -260,7 +260,7 @@ describe('Recovery', () => {
it('renders canonical rollup and history item labels when linked resources are unavailable', async () => {
rollupsPayload.push({
rollupId: 'res:vm-404',
subjectResourceId: 'vm-404',
itemResourceId: 'vm-404',
display: { itemLabel: 'Archive VM' },
lastAttemptAt: '2026-02-12T08:00:00.000Z',
lastSuccessAt: '2026-02-12T08:00:00.000Z',

View file

@ -23,6 +23,7 @@ export type RecoveryPointsQuery = {
mode?: string | null;
outcome?: string | null;
itemType?: string | null;
itemResourceId?: string | null;
subjectResourceId?: string | null;
// Normalized filters (server-side)
@ -58,7 +59,8 @@ const normalizeQuery = (query: RecoveryPointsQuery | undefined): RecoveryPointsQ
mode: norm(q.mode) || null,
outcome: norm(q.outcome) || null,
itemType: norm(q.itemType) || null,
subjectResourceId: norm(q.subjectResourceId) || null,
itemResourceId: norm(q.itemResourceId) || norm(q.subjectResourceId) || null,
subjectResourceId: null,
q: norm(q.q) || null,
cluster: norm(q.cluster) || null,
@ -98,7 +100,7 @@ const buildURL = (query: RecoveryPointsQuery | undefined): string => {
if (q.mode) params.set('mode', q.mode);
if (q.outcome) params.set('outcome', q.outcome);
if (q.itemType) params.set('itemType', q.itemType);
if (q.subjectResourceId) params.set('subjectResourceId', q.subjectResourceId);
if (q.itemResourceId) params.set('itemResourceId', q.itemResourceId);
if (q.q) params.set('q', q.q);
if (q.cluster) params.set('cluster', q.cluster);

View file

@ -15,6 +15,7 @@ export type RecoveryRollupsQuery = {
mode?: string | null;
outcome?: string | null;
itemType?: string | null;
itemResourceId?: string | null;
subjectResourceId?: string | null;
q?: string | null;
cluster?: string | null;
@ -36,7 +37,8 @@ const normalizeQuery = (query: RecoveryRollupsQuery | undefined): RecoveryRollup
mode: norm(q.mode) || null,
outcome: norm(q.outcome) || null,
itemType: norm(q.itemType) || null,
subjectResourceId: norm(q.subjectResourceId) || null,
itemResourceId: norm(q.itemResourceId) || norm(q.subjectResourceId) || null,
subjectResourceId: null,
q: norm(q.q) || null,
cluster: norm(q.cluster) || null,
node: norm(q.node) || null,
@ -72,7 +74,7 @@ const buildURL = (page: number, limit: number, query: RecoveryRollupsQuery | und
if (q.mode) params.set('mode', q.mode);
if (q.outcome) params.set('outcome', q.outcome);
if (q.itemType) params.set('itemType', q.itemType);
if (q.subjectResourceId) params.set('subjectResourceId', q.subjectResourceId);
if (q.itemResourceId) params.set('itemResourceId', q.itemResourceId);
if (q.q) params.set('q', q.q);
if (q.cluster) params.set('cluster', q.cluster);
if (q.node) params.set('node', q.node);

View file

@ -58,7 +58,7 @@ export interface RecoveryPoint {
encrypted?: boolean | null;
immutable?: boolean | null;
subjectResourceId?: string;
itemResourceId?: string;
repositoryResourceId?: string;
subjectRef?: RecoveryExternalRef | null;
repositoryRef?: RecoveryExternalRef | null;
@ -70,6 +70,7 @@ export interface RecoveryPoint {
export interface RecoveryPointTransport extends RecoveryPoint {
display?: RecoveryPointDisplayTransport | null;
provider?: RecoveryPlatform;
subjectResourceId?: string;
}
export interface RecoveryResponseMeta {
@ -91,7 +92,7 @@ export interface RecoveryPointsTransportResponse {
export interface ProtectionRollup {
rollupId: string;
subjectResourceId?: string;
itemResourceId?: string;
subjectRef?: RecoveryExternalRef | null;
display?: RecoveryPointDisplay | null;
@ -105,6 +106,7 @@ export interface ProtectionRollup {
export interface ProtectionRollupTransport extends ProtectionRollup {
display?: RecoveryPointDisplayTransport | null;
providers?: RecoveryPlatform[];
subjectResourceId?: string;
}
export interface RecoveryRollupsResponse {

View file

@ -48,6 +48,7 @@ describe('recoveryPlatformModel', () => {
kind: 'backup',
mode: 'remote',
outcome: 'success',
subjectResourceId: 'vm-123',
display: {
subjectLabel: 'Archive VM',
subjectType: 'proxmox-vm',
@ -59,6 +60,7 @@ describe('recoveryPlatformModel', () => {
kind: 'backup',
mode: 'remote',
outcome: 'success',
itemResourceId: 'vm-123',
display: {
itemLabel: 'Archive VM',
itemType: 'proxmox-vm',
@ -69,6 +71,7 @@ describe('recoveryPlatformModel', () => {
normalizeRecoveryRollup({
rollupId: 'rollup-1',
lastOutcome: 'success',
subjectResourceId: 'vm-123',
providers: ['proxmox-pbs', 'kubernetes'],
display: {
subjectLabel: 'Legacy Dataset',
@ -78,6 +81,7 @@ describe('recoveryPlatformModel', () => {
).toEqual({
rollupId: 'rollup-1',
lastOutcome: 'success',
itemResourceId: 'vm-123',
platforms: ['proxmox-pbs', 'kubernetes'],
display: {
itemLabel: 'Legacy Dataset',
@ -96,19 +100,21 @@ describe('recoveryPlatformModel', () => {
kind: 'snapshot',
mode: 'snapshot',
outcome: 'success',
subjectResourceId: 'res-1',
},
],
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
}),
).toEqual({
data: [
{
id: 'point-1',
platform: 'truenas',
kind: 'snapshot',
mode: 'snapshot',
outcome: 'success',
},
{
id: 'point-1',
platform: 'truenas',
kind: 'snapshot',
mode: 'snapshot',
outcome: 'success',
itemResourceId: 'res-1',
},
],
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
});
@ -119,6 +125,7 @@ describe('recoveryPlatformModel', () => {
{
rollupId: 'rollup-1',
lastOutcome: 'warning',
subjectResourceId: 'res-1',
providers: ['truenas'],
},
],
@ -126,11 +133,12 @@ describe('recoveryPlatformModel', () => {
}),
).toEqual({
data: [
{
rollupId: 'rollup-1',
lastOutcome: 'warning',
platforms: ['truenas'],
},
{
rollupId: 'rollup-1',
lastOutcome: 'warning',
itemResourceId: 'res-1',
platforms: ['truenas'],
},
],
meta: { page: 1, limit: 100, total: 1, totalPages: 1 },
});

View file

@ -21,11 +21,11 @@ describe('recoveryRecordPresentation', () => {
const rollup = {
rollupId: 'rollup-1',
subjectResourceId: 'res-1',
itemResourceId: 'res-1',
} as ProtectionRollup;
const point = {
id: 'point-1',
subjectResourceId: 'res-1',
itemResourceId: 'res-1',
} as RecoveryPoint;
expect(getRecoveryRollupItemLabel(rollup, resources)).toBe('db-01');
@ -69,12 +69,12 @@ describe('recoveryRecordPresentation', () => {
const rollup = {
rollupId: 'res:res-3',
subjectResourceId: 'res-3',
itemResourceId: 'res-3',
display: { itemLabel: 'Archive VM' },
} as ProtectionRollup;
const linkedPoint = {
id: 'point-3',
subjectResourceId: 'res-2',
itemResourceId: 'res-2',
display: { itemLabel: 'billing-api' },
} as RecoveryPoint;

View file

@ -25,6 +25,11 @@ interface RecoveryRollupPlatformsLike {
providers?: string[] | null;
}
interface RecoveryItemResourceLike {
itemResourceId?: string | null;
subjectResourceId?: string | null;
}
const normalizeRecoveryDisplay = (
display: RecoveryPointDisplay | RecoveryPointDisplayTransport | null | undefined,
): RecoveryPointDisplay | null | undefined => {
@ -48,6 +53,10 @@ const normalizeRecoveryDisplay = (
};
};
const getRecoveryItemResourceId = (
value: RecoveryItemResourceLike | null | undefined,
): string => toTrimmedString(value?.itemResourceId) || toTrimmedString(value?.subjectResourceId);
export const getRecoveryPointPlatform = (
point: RecoveryPointPlatformLike | null | undefined,
): string => toTrimmedString(point?.platform) || toTrimmedString(point?.provider);
@ -73,12 +82,19 @@ const normalizeRecoveryMeta = (meta: RecoveryResponseMeta | null | undefined): R
export const normalizeRecoveryPoint = (
point: RecoveryPointTransport | RecoveryPoint,
): RecoveryPoint => {
const { provider: _provider, display, ...rest } = point as RecoveryPointTransport;
const {
provider: _provider,
subjectResourceId: _subjectResourceId,
display,
...rest
} = point as RecoveryPointTransport;
const platform = getRecoveryPointPlatform(point);
const itemResourceId = getRecoveryItemResourceId(point);
const normalizedDisplay = normalizeRecoveryDisplay(display);
return {
...(rest as RecoveryPoint),
...(platform ? { platform } : {}),
...(itemResourceId ? { itemResourceId } : {}),
...(display !== undefined ? { display: normalizedDisplay } : {}),
};
};
@ -86,12 +102,19 @@ export const normalizeRecoveryPoint = (
export const normalizeRecoveryRollup = (
rollup: ProtectionRollupTransport | ProtectionRollup,
): ProtectionRollup => {
const { providers: _providers, display, ...rest } = rollup as ProtectionRollupTransport;
const {
providers: _providers,
subjectResourceId: _subjectResourceId,
display,
...rest
} = rollup as ProtectionRollupTransport;
const platforms = getRecoveryRollupPlatforms(rollup);
const itemResourceId = getRecoveryItemResourceId(rollup);
const normalizedDisplay = normalizeRecoveryDisplay(display);
return {
...(rest as ProtectionRollup),
...(platforms.length > 0 ? { platforms } : {}),
...(itemResourceId ? { itemResourceId } : {}),
...(display !== undefined ? { display: normalizedDisplay } : {}),
};
};

View file

@ -24,7 +24,7 @@ export function getRecoveryRollupItemLabel(
rollup: ProtectionRollup,
resourcesById: Map<string, Resource>,
): string {
const itemResourceId = (rollup.subjectResourceId || '').trim();
const itemResourceId = (rollup.itemResourceId || '').trim();
const displayLabel = String(rollup.display?.itemLabel || '').trim();
const linkedResourceLabel = getRecoveryLinkedResourceLabel(itemResourceId, resourcesById);
if (linkedResourceLabel) return linkedResourceLabel;
@ -47,7 +47,7 @@ export function getRecoveryPointItemLabel(
point: RecoveryPoint,
resourcesById: Map<string, Resource>,
): string {
const itemResourceId = (point.subjectResourceId || '').trim();
const itemResourceId = (point.itemResourceId || '').trim();
const displayLabel = String(point.display?.itemLabel || '').trim();
const linkedResourceLabel = getRecoveryLinkedResourceLabel(itemResourceId, resourcesById);
if (linkedResourceLabel) return linkedResourceLabel;

View file

@ -63,6 +63,7 @@ type recoveryPointPayload struct {
Encrypted *bool `json:"encrypted,omitempty"`
Immutable *bool `json:"immutable,omitempty"`
ItemResourceID string `json:"itemResourceId,omitempty"`
SubjectResourceID string `json:"subjectResourceId,omitempty"`
RepositoryResourceID string `json:"repositoryResourceId,omitempty"`
SubjectRef *recovery.ExternalRef `json:"subjectRef,omitempty"`
@ -73,6 +74,7 @@ type recoveryPointPayload struct {
type recoveryRollupPayload struct {
RollupID string `json:"rollupId"`
ItemResourceID string `json:"itemResourceId,omitempty"`
SubjectResourceID string `json:"subjectResourceId,omitempty"`
SubjectRef *recovery.ExternalRef `json:"subjectRef,omitempty"`
Display *recovery.RecoveryPointDisplay `json:"display,omitempty"`
@ -97,6 +99,7 @@ func buildRecoveryPointPayload(point recovery.RecoveryPoint) recoveryPointPayloa
Verified: point.Verified,
Encrypted: point.Encrypted,
Immutable: point.Immutable,
ItemResourceID: point.SubjectResourceID,
SubjectResourceID: point.SubjectResourceID,
RepositoryResourceID: point.RepositoryResourceID,
SubjectRef: point.SubjectRef,
@ -109,6 +112,7 @@ func buildRecoveryPointPayload(point recovery.RecoveryPoint) recoveryPointPayloa
func buildRecoveryRollupPayload(rollup recovery.ProtectionRollup) recoveryRollupPayload {
return recoveryRollupPayload{
RollupID: rollup.RollupID,
ItemResourceID: rollup.SubjectResourceID,
SubjectResourceID: rollup.SubjectResourceID,
SubjectRef: rollup.SubjectRef,
Display: rollup.Display,
@ -160,6 +164,13 @@ func parseRecoveryPlatformQuery(qs url.Values) recovery.Provider {
)))
}
func parseRecoveryItemResourceIDQuery(qs url.Values) string {
return strings.TrimSpace(firstNonEmpty(
qs.Get("itemResourceId"),
qs.Get("subjectResourceId"),
))
}
func (h *RecoveryHandlers) HandleListPoints(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@ -190,7 +201,7 @@ func (h *RecoveryHandlers) HandleListPoints(w http.ResponseWriter, r *http.Reque
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
RollupID: strings.TrimSpace(qs.Get("rollupId")),
From: from,
To: to,
@ -307,7 +318,7 @@ func (h *RecoveryHandlers) HandleListSeries(w http.ResponseWriter, r *http.Reque
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
RollupID: strings.TrimSpace(qs.Get("rollupId")),
From: from,
To: to,
@ -373,7 +384,7 @@ func (h *RecoveryHandlers) HandleListFacets(w http.ResponseWriter, r *http.Reque
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
RollupID: strings.TrimSpace(qs.Get("rollupId")),
From: from,
To: to,
@ -635,7 +646,7 @@ func (h *RecoveryHandlers) HandleListRollups(w http.ResponseWriter, r *http.Requ
Mode: recovery.Mode(strings.TrimSpace(qs.Get("mode"))),
Outcome: recovery.Outcome(strings.TrimSpace(qs.Get("outcome"))),
ItemType: recovery.NormalizeRecoveryItemType(firstNonEmpty(qs.Get("itemType"), qs.Get("type"))),
SubjectResourceID: strings.TrimSpace(qs.Get("subjectResourceId")),
SubjectResourceID: parseRecoveryItemResourceIDQuery(qs),
RollupID: strings.TrimSpace(qs.Get("rollupId")),
From: from,
To: to,

View file

@ -52,6 +52,47 @@ func TestParseRecoveryPlatformQuery(t *testing.T) {
}
}
func TestParseRecoveryItemResourceIDQuery(t *testing.T) {
t.Parallel()
tests := []struct {
name string
qs url.Values
want string
}{
{
name: "prefers canonical item resource query",
qs: url.Values{
"itemResourceId": []string{" vm-123 "},
"subjectResourceId": []string{"legacy-vm"},
},
want: "vm-123",
},
{
name: "falls back to legacy subject resource query",
qs: url.Values{
"subjectResourceId": []string{" vm-404 "},
},
want: "vm-404",
},
{
name: "returns empty when neither is present",
qs: url.Values{},
want: "",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := parseRecoveryItemResourceIDQuery(tc.qs); got != tc.want {
t.Fatalf("parseRecoveryItemResourceIDQuery() = %q, want %q", got, tc.want)
}
})
}
}
func TestHandleListPointsAcceptsCanonicalPlatformQuery(t *testing.T) {
prevMock := mock.IsMockEnabled()
mock.SetEnabled(true)
@ -90,6 +131,24 @@ func TestHandleListPointsAcceptsCanonicalPlatformQuery(t *testing.T) {
}
}
func TestBuildRecoveryPointPayloadExposesCanonicalItemResourceIDField(t *testing.T) {
payload := buildRecoveryPointPayload(recovery.RecoveryPoint{
ID: "point-1",
Provider: recovery.Provider("truenas"),
Kind: recovery.Kind("snapshot"),
Mode: recovery.Mode("snapshot"),
Outcome: recovery.Outcome("success"),
SubjectResourceID: "vm-123",
})
if payload.ItemResourceID != "vm-123" {
t.Fatalf("payload.ItemResourceID = %q, want %q", payload.ItemResourceID, "vm-123")
}
if payload.SubjectResourceID != "vm-123" {
t.Fatalf("payload.SubjectResourceID = %q, want %q", payload.SubjectResourceID, "vm-123")
}
}
func TestHandleListRollupsExposeCanonicalPlatformsPayload(t *testing.T) {
prevMock := mock.IsMockEnabled()
mock.SetEnabled(true)
@ -127,3 +186,18 @@ func TestHandleListRollupsExposeCanonicalPlatformsPayload(t *testing.T) {
}
}
}
func TestBuildRecoveryRollupPayloadExposesCanonicalItemResourceIDField(t *testing.T) {
payload := buildRecoveryRollupPayload(recovery.ProtectionRollup{
RollupID: "res:vm-123",
SubjectResourceID: "vm-123",
LastOutcome: recovery.Outcome("success"),
})
if payload.ItemResourceID != "vm-123" {
t.Fatalf("payload.ItemResourceID = %q, want %q", payload.ItemResourceID, "vm-123")
}
if payload.SubjectResourceID != "vm-123" {
t.Fatalf("payload.SubjectResourceID = %q, want %q", payload.SubjectResourceID, "vm-123")
}
}