From 04a0faf921e3d0856963f13e03bf87b2b63c0d95 Mon Sep 17 00:00:00 2001
From: Dave Liu <7david12liu@gmail.com>
Date: Tue, 14 Jul 2026 01:14:41 -0700
Subject: [PATCH] Redact public shop catalog errors
---
docs/officers/EVENTS_SHOP_MEMBERS.md | 34 ++++++++
src/App.test.jsx | 120 ++++++++++++++++++++++++++-
src/pages/shop/Shop.tsx | 4 +-
3 files changed, 155 insertions(+), 3 deletions(-)
diff --git a/docs/officers/EVENTS_SHOP_MEMBERS.md b/docs/officers/EVENTS_SHOP_MEMBERS.md
index ce30799..d4f0743 100644
--- a/docs/officers/EVENTS_SHOP_MEMBERS.md
+++ b/docs/officers/EVENTS_SHOP_MEMBERS.md
@@ -494,6 +494,40 @@ Officer review steps after the source merge:
No system diagram changes for this source slice because page structure, data movement, permissions, account ownership, and deployment topology are unchanged.
+## Public Shop catalog failure privacy — SOURCE ONLY, NOT LIVE
+
+**Status: NOT AVAILABLE YET**
+
+**Purpose:** give any public Shop visitor one plain next step when the product list cannot load, without showing a database, provider, account, or technical error.
+
+**Approver:** communications lead plus platform/security owner.
+
+**Prerequisites:** issue [#254](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/254) must be merged for source review. Calling the sentence live also requires a protected website publication and an exact revision check on `runmprc.com/shop`. This source change does not deploy Firebase, change database permissions, contact an outside provider, use production data, or prove live behavior.
+
+Officer review steps after the source merge:
+
+1. Keep the public Shop failure sentence marked **NOT AVAILABLE YET**.
+2. Ask the platform owner for the exact #254 issue, pull request, merged commit, and synthetic frontend test result.
+3. Confirm the tests use only a made-up catalog, made-up product, and mocked database result.
+4. Confirm a made-up catalog rejection shows `We could not load the shop right now. Please try again later.`
+5. Confirm the loading sentence stops and the empty-catalog sentence does not appear for that failure.
+6. Confirm no made-up database, provider, account, endpoint, or technical detail appears on the page or in browser console output.
+7. Confirm a hostile rejected value is not inspected.
+8. Confirm a genuinely empty made-up catalog and a successful made-up product still use their existing displays.
+9. Record website publication, `runmprc.com/shop`, Firebase, provider, production-data, and live-behavior evidence as separate results.
+
+**Expected result:** the reviewed source uses one fixed retry-later sentence for a catalog rejection. It does not inspect, display, or log the rejected value. The failure is announced as an alert, while successful and genuinely empty catalogs remain unchanged.
+
+**Stop conditions:** any real member, customer, order, or product data; a request for a database or provider error, account detail, private endpoint, or screenshot containing private values; a production Firebase or provider change; a raw detail on the page or in the console; or a claim that source, tests, merge, or a green workflow proves the sentence is live.
+
+**Success proof:** for source completion, record the exact #254 issue, reviewed pull request, merged commit, intended old-source failures, green synthetic tests, relevant full checks, and independent privacy review. For live availability, separately record the approved website publication, published revision, and a dated `runmprc.com/shop` check that uses no private or production data. Record Firebase deployment, database-permission changes, provider configuration, and production-data actions as **not performed** for this frontend-only change.
+
+**Undo:** before publication, use one reviewed frontend revert or safe roll-forward. After publication, use the same protected website release path and verify the replacement revision on `runmprc.com/shop`. Do not undo by changing a product, order, member account, database record, permission, or provider setting.
+
+**Escalation:** communications lead plus platform/security owner. Add the privacy owner and use the private incident path if any database, provider, account, endpoint, or technical detail appeared. Do not copy the detail into an issue, message, screenshot, email, or AI tool.
+
+No system diagram changes for this source slice because page structure, data movement, permissions, account ownership, and deployment topology are unchanged.
+
## Refund amount and returned-result guards — SOURCE ONLY, NOT LIVE
**Purpose:** make an invalid partial amount stop, and record a refund complete only when Stripe returns a matching final success.
diff --git a/src/App.test.jsx b/src/App.test.jsx
index 4ed337e..6ace4b5 100644
--- a/src/App.test.jsx
+++ b/src/App.test.jsx
@@ -3,6 +3,8 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { resolvePath } from 'react-router-dom';
+import { useServiceLocator } from './services/ServiceLocatorContext';
+import { listActiveProducts } from './services/shop/shopService';
import App from './App';
jest.mock('./services/ServiceLocatorProvider', () => function TestServiceLocatorProvider({
@@ -12,9 +14,17 @@ jest.mock('./services/ServiceLocatorProvider', () => function TestServiceLocator
});
jest.mock('./services/ServiceLocatorContext', () => ({
- useServiceLocator: () => ({ services: null, isReady: false }),
+ useServiceLocator: jest.fn(() => ({ services: null, isReady: false })),
}));
+jest.mock('./services/shop/shopService', () => {
+ const actual = jest.requireActual('./services/shop/shopService');
+ return {
+ ...actual,
+ listActiveProducts: jest.fn(),
+ };
+});
+
jest.mock('./services/hooks/useAuth', () => ({
useAuth: () => ({
user: null,
@@ -28,6 +38,12 @@ jest.mock('./services/hooks/useAuth', () => ({
}),
}));
+beforeEach(() => {
+ useServiceLocator.mockReset();
+ useServiceLocator.mockReturnValue({ services: null, isReady: false });
+ listActiveProducts.mockReset();
+});
+
afterEach(() => {
window.history.replaceState({}, '', '/');
});
@@ -109,3 +125,105 @@ test('renders the Auth action route and removes its query and fragment before us
expect(window.location.search).toBe('');
expect(window.location.hash).toBe('');
});
+
+const SHOP_LOAD_FAILURE = 'We could not load the shop right now. Please try again later.';
+const firestore = { name: 'synthetic-firestore' };
+
+function renderPublicShop() {
+ window.history.pushState({}, '', '/shop');
+ return render(
Loading...
} - {error &&{error}
} + {error &&{error}
} {!loading && !error && products.length === 0 && (No items available right now. Check back soon.
)}