Admin Feeds — QA Test Plan
Pre-release checklist for the Admin Feed Management module. Treat Critical items as release blockers — feeds drive downstream content ingestion for partners (Vizio, Amazon, Airtel, Tata Play, Google, NBA, etc.), so a malformed payload here can break a partner integration silently.
Totals
| Severity | Count |
|---|---|
| Critical | 10 |
| High | 13 |
| Medium | 13 |
| Low | 6 |
| Total | 42 |
Coverage summary (Critical + High)
5 — payload normalization (C6, C9, H5, H10) and list error path (H7). 5 — happy paths (H1, H2), validation pipeline reaches the resolver but the submit button isn't wired (C1, H12), and Edit prefill works in the common case but rejects records that fail strict-schema parse (H3). 13 — schema-level validations, save-path side effects (toasts, optimistic list update, edit-id guard, site-cookie guard), permission gating, and the not-yet-wired UI flows (submit/cancel, NoData CTA, delete).
Critical — Release blockers
C1 — Submit must run schema before calling the API
Scope: When the dialog Submit button is wired, it must go through feedForm.handleSubmit(onSubmit) (which runs feedFormResolver) — never call saveFeed directly with raw form values.
Tests:
- none yet — the resolver is constructed via
zodResolver(FeedFormSchema)andsuperRefineis exercised indirectly bysrc/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts, but no test asserts the dialog wires Submit throughhandleSubmit.
Steps:
- Open Add Feed.
- Leave every field blank and click Submit.
- Pick a feed type, leave its required per-feed fields blank, click Submit.
Expected: No network call. Each missing field shows its inline error from the schema.
Risk: A direct saveFeed(feedForm.getValues(), …) would bypass validation and persist garbage feed records that downstream pipelines can't parse.
Ref: src/modules/admin/feeds/addOrEdit/index.tsx
C2 — Empty feed is rejected with a clear error
Scope: superRefine bails on empty feed and emits Feed type is required so per-feed validations don't run against a missing selection.
Tests:
- none yet — no schema test for the feeds module.
Steps:
- Open Add Feed.
- Fill name + description, leave Feed dropdown unselected, click Submit.
Expected: "Feed type is required" surfaced on the feed field. No per-feed validation noise.
Risk: Without the early bail, '' slips into the payload as feed, hitting an unknown-provider branch server-side.
Ref: src/modules/admin/feeds/addOrEdit/schema.ts
C3 — Per-feed required fields are enforced
Scope: Each provider's required fields gate submission:
- ESPN / NBA →
league - Vizio →
sourceId,imageUrl(URL),appUrl(URL) - Amazon →
bucketName,contactEmail(must be a valid email) - Airtel / Tata Play →
hlsBaseUri(URL) - Tata Play →
contentType(movie|series)
Tests:
- none yet —
superRefinelogic exists but no schema test covers it.
Steps:
- For each provider above, pick the feed type, leave its required field(s) blank.
- Submit.
- Re-test with malformed values for the URL/email fields (see H8, H9).
Expected: Each missing/invalid field surfaces its specific inline error. Submit is blocked.
Risk: Partner integrations break silently — e.g. a Vizio feed without sourceId may be ingested but never matched to a Vizio catalog row, producing zombie entries.
Ref: src/modules/admin/feeds/addOrEdit/schema.ts
C4 — feed field is read-only in Edit mode
Scope: The form config marks feed as readOnly: (ctx) => ctx?.mode === CommonActionEnum.EDIT. Changing the feed type on an existing record would mismatch every per-feed field server-side.
Tests:
- none yet — no UI/integration test for the dialog asserts the readOnly rule is honored.
Steps:
- Open Edit on any existing feed.
- Try to change the Feed dropdown.
Expected: Dropdown is disabled (or read-only). Cannot be changed by mouse, keyboard, or programmatic input.
Risk: Mutating feed post-creation produces a record whose payload shape no longer matches its provider — partners receive malformed payloads.
Ref: src/config/admin/feedManagementOptions.ts
C5 — Edit refuses to fire without an id
Scope: useFeeds.saveFeed must early-return with an error toast when mode === EDIT and id is null/empty. Otherwise the request hits /admin/v2/feeds/null and the result is undefined behaviour at best, data corruption at worst.
Tests:
- none yet —
useFeeds.test.tsonly coversgetFeeds;saveFeedis unverified.
Steps:
- Force
idto null while in Edit mode (devtools / spec). - Attempt save.
Expected: 'Cannot edit a feed without an id' toast. No PUT request fires.
Risk: PUTting to a malformed URL has caused server-side regressions in the past — best case 404, worst case an accidental update of a different resource.
Ref: src/hooks/admin/useFeeds.ts
C6 — Per-feed payload pruning prevents cross-leak
Scope: feedPayloadNormalizerAdapter includes only fields relevant to the chosen feed. Stale form state from an unrelated provider must never reach the API body.
Tests:
src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts→ "does not leak vizio fields onto other feeds", "does not leak NBA fields onto other feeds", "does not addleaguefor unrelated feeds", "does not add includeAsset for non-MRSS feeds"src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts→ "feeds without per-type branches" (returns only the 5 base keys for ROKU, ROKU_2_0, MiTV, Facebook, Samsung, MRSS Brightcove)
Steps:
- Pick Vizio, fill
sourceId/imageUrl/appUrl. - Switch to Roku, submit.
- Inspect the Network tab → request body.
Expected: Body has only feed, name, description, categories, tags. None of the Vizio fields.
Risk: Cross-leaked fields produce records server-side that can't be edited cleanly by future loads (the form rehydrates with junk).
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
C7 — saveFeed aborts when site cookie is missing
Scope: Both getFeeds and saveFeed early-return when CookieManager.get('site') returns falsy. Without it, the request hits the wrong tenant or fails silently server-side.
Tests:
src/__tests__/hooks/admin/useFeeds.test.tscovers this forgetFeedsonly ("does not call the service when the site cookie is missing").saveFeedis unverified.
Steps:
- Clear the
sitecookie in devtools. - Attempt to create or edit a feed.
Expected: No POST/PUT fires. (Optional: a "site context missing" toast — currently silent for saveFeed; consider adding.)
Risk: Multi-tenant breach if the request lands somewhere unexpected. At minimum, opaque failures for the user.
Ref: src/hooks/admin/useFeeds.ts
C8 — Optimistic list update keys by id, not by index
Scope: On edit, setList must prev.data.map((f) => f.id === id ? { ...f, ...updated } : f). Index-based replacement (the prod handler used to do this) shadows whichever feed happens to be at that index after sort/filter changes.
Tests:
- none yet —
saveFeedis unverified.
Steps:
- Load the list of feeds, change the sort order.
- Edit a feed in the middle of the list.
- Confirm the right feed updates and no unrelated rows visually shift content.
Expected: Only the edited feed's row reflects the new values. List ordering preserved.
Risk: The previous prod handler did feedsCopy[indexToBeReplaced] = result against an unsorted snapshot — this regressed once when the table started sorting client-side. Don't repeat.
Ref: src/hooks/admin/useFeeds.ts
C9 — name and description are trimmed before persistence
Scope: Leading/trailing whitespace on name and description is stripped before send, mirroring the prod handler.
Tests:
src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts→ "trims whitespace on name and description"
Steps:
- Create a feed with name
My Feedand description\tdesc\n. - Submit, then refresh and re-open Edit.
Expected: Persisted name is My Feed, description is desc. No invisible whitespace duplicates ("My Feed" vs "My Feed ").
Risk: Whitespace-only differences masquerade as duplicate feeds and are nearly impossible to debug without DB inspection.
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
C10 — Add Feed action gated by admin.write permission
Scope: The "Add New" button in the section header is rendered only when Permission.has('admin', 'write'). Viewer-role users must not be able to launch the create dialog at all.
Tests:
- none yet — no module render test covers permission gating.
Steps:
- Sign in as a viewer / non-admin role.
- Navigate to Admin → Feed Management.
Expected: No "Add New" button. No way to open the Add dialog (devtools-only access is acceptable as long as the API rejects).
Risk: Privilege escalation surface — if the UI exposes the action, defense-in-depth on the backend becomes the only safety net.
Ref: src/modules/admin/feeds/index.tsx
High — Core flows
H1 — Create — happy path per provider
Scope: Each FeedProviderEnum value can be selected, its required fields filled, and submitting produces the right payload + a new row at the top of the list.
Tests:
src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.tscovers every per-feed branch (payload shape).src/__tests__/services/admin/FeedService.test.ts→create(request shape, V3 invoke, returns created feed).- Gap: no end-to-end / module-level test that exercises form fill → submit → list update for any provider.
Steps:
- For each provider in the dropdown, create a feed with valid required fields.
- After save, confirm the row appears at the top of the list.
Expected: POST /admin/v2/feeds succeeds with the provider-specific body; new row is prepended.
Risk: Provider-specific regressions slip through because the only assertions live in unit tests, not in flows.
Ref: src/hooks/admin/useFeeds.ts, src/services/admin/FeedService.ts
H2 — Edit — happy path
Scope: Opening Edit, modifying a field, saving updates the entity in place via PUT /admin/v2/feeds/:id.
Tests:
src/__tests__/services/admin/FeedService.test.ts→update(URL substitution, PUT method, body shape, returns updated feed).- Gap: no end-to-end test; also blocked on H3 (no prefill yet).
Steps:
- Open Edit on an existing Vizio feed.
- Change
appUrl, save. - Reload the list.
Expected: Row reflects the new appUrl. Server-side record updated.
Risk: Edit silently no-ops, or worse, sends a stale empty payload that wipes server-side fields.
Ref: src/hooks/admin/useFeeds.ts
H3 — Edit — form prefills with existing values
Scope: When the dialog opens with mode === EDIT && id !== null, an effect looks up the feed in list.data, runs it through FeedFormSchema.safeParse (which strips API-only fields like bucket, s3Key, token, id), and calls feedForm.reset(parsed). Defaults stay as FeedFormDefaults for Add mode; reset only fires when the matching feed is found.
Tests:
- none yet — module-level test still pending. Adapter/schema tests cover the inverse direction (form → API) but not API → form.
Steps:
- Click Edit on a feed.
- Click Edit on a feed whose persisted record is missing a conditionally-required field (e.g. a Vizio without
imageUrl).
Expected:
- Form fields populated with that feed's current values.
- Currently: toast surfaces a Zod issue array and the form stays empty. Target: form prefills regardless; submit-time validation gates the save.
Risk: Records that don't already pass strict schema validation become un-editable in the UI — clicking Edit lands the user on what looks like an empty Add form, and saving from there would overwrite the persisted record with defaults.
Follow-up: Pending backend-validation alignment. Once the truly-required fields are confirmed, schema's superRefine and min(1) rules will be loosened to match — at which point the strict-parse path becomes safe and this item flips to covered. Alternatively, swap safeParse to a permissive hydration-only schema (no superRefine, no min(1) on name/description) and keep the strict schema for submit.
Ref: src/modules/admin/feeds/addOrEdit/index.tsx, src/modules/admin/feeds/addOrEdit/schema.ts
H4 — Submit and Cancel buttons are wired in DialogFooter
Scope: The dialog footer must render Submit and Cancel. Submit triggers feedForm.handleSubmit(onSubmit); Cancel calls feedForm.reset() and closes the dialog. Today DialogFooter is empty — the form cannot be submitted from the UI.
Tests:
- none — feature not implemented.
Steps:
- Open Add Feed.
- Look at the footer.
Expected: Two buttons: Cancel (secondary) and Save (primary). Save is disabled while loading === true or while validation is failing.
Risk: The entire create/edit flow is currently dead UI — nothing ships until this lands.
Ref: src/modules/admin/feeds/addOrEdit/index.tsx
H5 — Switching feed type doesn't leak stale per-feed values
Scope: Even if the form retains a value typed under a previous feed selection, the adapter prunes by current feed value before sending.
Tests:
src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts→ all "does not leak …" cases and "feeds without per-type branches".
Steps:
- Pick Amazon, fill
bucketNameandcontactEmail. - Switch to Google.
- Submit.
Expected: Body has Google base + gglMediaSkipTag (if filled). No bucketName, no contactEmail.
Risk: Already mitigated at the adapter layer.
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
H6 — List loads on module mount
Scope: AdminFeedManagementModule calls getFeeds() on mount once.
Tests:
- none yet —
useFeeds.test.tscoversgetFeedsin isolation, not the module wiring.
Steps:
- Open Admin → Feed Management cold.
Expected: A list request fires once. Loading skeleton briefly shown, then either the table or NoData renders.
Risk: Empty page on first load if the effect is silently broken.
Ref: src/modules/admin/feeds/index.tsx
H7 — List service errors surface as a toast
Scope: When FeedService.list rejects, an error toast shows and the previous data is preserved.
Tests:
src/__tests__/hooks/admin/useFeeds.test.ts→ "shows an error toast and keeps the previous data when the request fails"
Steps:
- Simulate a 500 from
/admin/v2/feeds(proxy / devtools). - Open the page.
Expected: Error toast with the message. Existing list (if any) untouched. No infinite spinner.
Risk: Silent failures or stuck spinners.
Ref: src/hooks/admin/useFeeds.ts
H8 — URL fields reject malformed values
Scope: imageUrl, appUrl, hlsBaseUri, appletvUrl, firetvUrl, rokuLink all use optionalUrl() (Zod's z.url() plus the Validations.validateUrl extra check inside requireUrl).
Tests:
- none yet — no schema test.
Steps:
- For each URL field, enter
not-a-url,http://,://example, and a validhttps://example.com.
Expected: Invalid values rejected with "Must be a valid URL". Valid values accepted.
Risk: Malformed URLs silently saved → playback / deep-link breakage on partner devices.
Ref: src/modules/admin/feeds/addOrEdit/schema.ts
H9 — Amazon contactEmail validates as an email
Scope: superRefine runs Validations.validateEmail(val.contactEmail) for Amazon feeds and surfaces "Invalid email address" on failure.
Tests:
- none yet — no schema test.
Steps:
- Pick Amazon, set
contactEmailtonot-an-email,foo@,@bar.com, then a validalerts@example.com.
Expected: First three rejected. Valid one accepted.
Risk: Notification emails to the wrong inbox (or none) → partner alerts go to the void.
Ref: src/modules/admin/feeds/addOrEdit/schema.ts
H10 — Switch fields default to false in the payload
Scope: isSportFeed (Vizio), isDeleteFeed (Airtel/TataPlay), includeAsset (MRSS) default to false when unset, never undefined.
Tests:
src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts→ "adds vizio-specific fields with isSportFeed defaulting to false", "adds shared fields for AIRTEL_FEED with isDeleteFeed defaulting to false", "adds includeAsset, defaulting to false".
Steps:
- Create a Vizio / Airtel / MRSS feed without touching the switch.
- Inspect request body.
Expected: Boolean key present, value false.
Risk: undefined would get JSON-stringified out and the server may treat the field as "unchanged" rather than "off".
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
H11 — "Add First Feed" CTA in NoData opens the create dialog
Scope: When the list is empty, the NoData "Add First Feed" button must open the same Add dialog as the section header button.
Tests:
- none — currently
onClick: console.log(placeholder).
Steps:
- Sign in to a tenant with no feeds.
- Click "Add First Feed".
Expected: Add Feed dialog opens.
Risk: Onboarding dead-end for fresh tenants — they can still use the header button, but the empty-state CTA is broken.
Ref: src/modules/admin/feeds/list/index.tsx
H12 — Form is locked while submit is in flight
Scope: While loading === true, every field is readOnly (driven by f.readOnly?.(formCtx) || loading) and Submit is disabled. Prevents double-submits and mid-request edits.
Tests:
- none yet —
loadingstate plumbed viauseState, but no test asserts the disabled state.
Steps:
- Submit a feed against a slow network (devtools throttle).
- Try to type into a field while the request is pending.
- Click Submit again.
Expected: Fields unresponsive. Submit disabled. Single network request.
Risk: Double-submit creates duplicate rows; mid-request edits send half-finished state.
Ref: src/modules/admin/feeds/addOrEdit/index.tsx
H13 — Success toast after create / edit
Scope: saveFeed calls onMessage('Feed created' | 'Feed updated', { variant: 'success' }) after the API resolves successfully.
Tests:
- none yet —
saveFeedis unverified.
Steps:
- Create, then edit any feed.
Expected: Toast appears in both cases.
Risk: Users repeatedly click Save because they have no feedback that the action landed.
Ref: src/hooks/admin/useFeeds.ts
Medium — Edge cases and UX gaps
M1 — Whitespace-only name / description rejected
Scope: z.string().trim().min(1, …) ensures doesn't satisfy required.
Steps: Enter in name, submit. Expect Name is required.
Risk: Empty-looking records.
Ref: src/modules/admin/feeds/addOrEdit/schema.ts
M2 — Long names / descriptions don't break dialog layout
Scope: 200+ char descriptions should wrap or scroll, not overflow the modal.
Steps: Paste a long string into both fields.
Risk: Visual regression only.
Ref: src/components/ui/field
M3 — Read-only feed field can't be bypassed via paste/keyboard
Scope: When readOnly is true, the dropdown rejects all input vectors.
Steps: In Edit mode, try keyboard (Tab then arrows), paste, and screen reader interaction.
Risk: Silent permission breach (links to C4).
Ref: src/components/Dropdown, src/modules/admin/feeds/addOrEdit/index.tsx
M4 — Closing the dialog mid-submit doesn't strand state
Scope: Dialog onOpenChange(false) while loading === true should not corrupt list state.
Steps: Throttle network. Submit. Close dialog before response.
Expected: Either the request completes and list updates, or it resolves with no UI side-effect.
Risk: Half-rendered rows or stuck loading state.
Ref: src/modules/admin/feeds/addOrEdit/index.tsx
M5 — Reopening Add Feed shows clean defaults
Scope: After Cancel or successful save, opening Add again should render FeedFormDefaults, not stale state. The module currently unmounts the dialog (addOrEditFeed?.mode && Boolean(addOrEditFeed?.mode) ? <Dialog /> : null) so this should be free — verify it stays that way.
Steps: Add Feed → fill fields → Cancel → reopen.
Expected: Empty form.
Risk: Stale state leaks.
Ref: src/modules/admin/feeds/index.tsx
M6 — Tags / Categories serialize as [] not undefined
Scope: feedPayloadNormalizerAdapter normalizes both to [] when undefined.
Tests: src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts → "defaults tags and categories to [] when undefined".
Risk: Server may treat undefined as "do not change" on edit.
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
M7 — Amazon amzMediaSkipTag empty → omitted from payload
Tests: src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts → "omits amzMediaSkipTag when empty".
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
M8 — TataPlay mobilePlayableTag / contentType empty → omitted
Tests: src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts → "omits empty mobilePlayableTag and contentType".
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
M9 — NBA empty UTM / URL extras → omitted
Tests: src/__tests__/adapters/admin/feedPayloadNormalizer.adapter.test.ts → "omits empty NBA extras instead of sending blank strings".
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
M10 — MRSS includeAsset is sent on Edit (closes prod gap)
Scope: Prod's edit handler had no mrss → includeAsset branch; the adapter sends it on both create and edit.
Tests: Adapter test asserts the field is in the payload for MRSS.
Steps: Edit an existing MRSS feed, toggle includeAsset, save, refresh, re-open.
Expected: New value persisted.
Risk: Resurrected prod inconsistency.
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
M11 — NBA league is sent on both Create and Edit (closes prod gap)
Scope: Prod's edit handler only sent league for ESPN. The adapter sends it for both ESPN and NBA on create and edit.
Tests: Adapter test asserts league is added for both ESPN and NBA.
Steps: Edit an NBA feed, change league, save.
Expected: New league value persisted.
Risk: Resurrected prod inconsistency.
Ref: src/adapters/admin/feedPayloadNormalizer.adapter.ts
M12 — Site cookie change between mount and save uses fresh value
Scope: saveFeed reads CookieManager.get('site') at call time, not at mount.
Steps: Open the dialog, switch tenant in another tab, save in the original tab.
Expected: Request goes to the current site cookie, even if it changed since mount.
Risk: Cross-tenant write — high severity if it ever bites; tracked here as Medium because the architecture already reads at call time.
Ref: src/hooks/admin/useFeeds.ts
M13 — List column visibility toggles persist within session
Scope: Visibility component drives setVisibleColumns; default is name/description/feed shown, league hidden.
Steps: Toggle columns, navigate away, navigate back.
Expected: Either preserved (sessionStorage) or reset to defaults — but no broken state.
Risk: UX nit only.
Ref: src/modules/admin/feeds/list/index.tsx
Low — Polish and cleanup
L1 — Field labels and help text match prod copy
Ref: src/config/admin/feedManagementOptions.ts
L2 — Placeholder text appears for every input/select
Ref: src/config/admin/feedManagementOptions.ts
L3 — Add vs Edit dialog shows the right title / subtitle
Scope: FeedAddOrEditConfig.title(mode) and subtitle(mode) swap copy by mode.
Ref: src/config/admin/feedManagementOptions.ts
L4 — No console warnings on dialog mount / unmount
Scope: No act() warnings, no useEffect missing-dependency warnings, no key warnings.
Ref: src/modules/admin/feeds/addOrEdit/index.tsx
L5 — NoData empty state renders with copy that matches the feature
Scope: "No Feeds Available" / "Create a feed to begin pulling in external content from your sources." — confirm wording stays in sync with PM.
Ref: src/modules/admin/feeds/list/index.tsx
L6 — Delete feed flow
Scope: FeedService.delete is a stub; there is no delete UI today. Track here so the next iteration adds it instead of forgetting.
Ref: src/services/admin/FeedService.ts
Unit tests worth adding next
| ID | Title | Where |
|---|---|---|
C2 / C3 / H8 / H9 / M1 |
Schema test: empty feed, per-provider required fields, URL validation, email validation, trim |
src/__tests__/modules/admin/feeds/addOrEdit/schema.test.ts (new) |
C5 / C7 / C8 / H13 |
useFeeds.saveFeed cases: missing id on Edit, missing site cookie, optimistic list keying, success toast, error toast |
src/__tests__/hooks/admin/useFeeds.test.ts (extend) |
C1 / C4 / H4 / H12 |
Dialog integration: Submit goes through handleSubmit, feed field is read-only in Edit, Submit/Cancel render and behave, fields disabled while submitting |
src/__tests__/modules/admin/feeds/addOrEdit/index.test.tsx (new) |
C10 / H6 / H11 |
Module render: Add New hidden without admin.write, getFeeds fires on mount, NoData CTA opens the Add dialog |
src/__tests__/modules/admin/feeds/index.test.tsx (new) |
H3 |
Edit prefill: form resets to the matching feed's values when opened in Edit mode; toast + empty-form fallback when safeParse rejects the persisted record (until schema is loosened) |
src/__tests__/modules/admin/feeds/addOrEdit/index.test.tsx (new) |
L6 |
FeedService.delete happy + error paths |
src/__tests__/services/admin/FeedService.test.ts (extend, currently it.todo) |
Out of scope
- Backend ingestion / partner-side feed parsing — owned by the feed-pipeline service. This checklist stops at the API boundary (
POST /admin/v2/feeds,PUT /admin/v2/feeds/:id,GET /admin/v2/feeds). - Tag and category authoring (the underlying suggestion sources for the comboboxes) — covered by their own modules.
- Permission system itself —
Permission.hassemantics are tested separately; this checklist only verifies the gate is wired. - Token refresh / authentication — verified by Identity tests; this module assumes a valid session.
Retired items (optional)
None yet — this is the first revision.