Skip to content

Commit 2f395e1

Browse files
committed
add mock server for lcal dev and documentation
1 parent e0152db commit 2f395e1

11 files changed

Lines changed: 239 additions & 18 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[
2+
{
3+
"id": "mdb-2947-202410010000",
4+
"feed_id": "mdb-2947",
5+
"hosted_url": "https://files.example.com/mdb-2947/mdb-2947-202410010000.zip",
6+
"note": null,
7+
"downloaded_at": "2024-10-01T00:00:15Z",
8+
"hash": "mockhash2947",
9+
"bounding_box": {
10+
"minimum_latitude": 40.0,
11+
"maximum_latitude": 41.0,
12+
"minimum_longitude": -75.0,
13+
"maximum_longitude": -74.0
14+
},
15+
"validation_report": {
16+
"validated_at": "2024-10-01T00:10:00Z",
17+
"features": ["Transfers", "Shapes"],
18+
"validator_version": "5.0.2-SNAPSHOT",
19+
"total_error": 0,
20+
"total_warning": 10,
21+
"total_info": 0,
22+
"unique_error_count": 0,
23+
"unique_warning_count": 2,
24+
"unique_info_count": 0,
25+
"url_json": "https://files.example.com/mdb-2947/report.json",
26+
"url_html": "https://files.example.com/mdb-2947/report.html"
27+
}
28+
}
29+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"id": "mdb-2947",
3+
"data_type": "gtfs",
4+
"status": "active",
5+
"created_at": "2024-10-01T00:00:00Z",
6+
"external_ids": [
7+
{ "external_id": "2947", "source": "mdb" }
8+
],
9+
"provider": "Example Transit Agency",
10+
"feed_name": "Example City Transit",
11+
"note": "Mock fixture for local dev",
12+
"feed_contact_email": "",
13+
"source_info": {
14+
"producer_url": "https://example.com/gtfs.zip",
15+
"authentication_type": 0,
16+
"authentication_info_url": "",
17+
"api_key_parameter_name": "",
18+
"license_url": ""
19+
},
20+
"redirects": []
21+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"id": "mdb-2947",
3+
"data_type": "gtfs",
4+
"status": "active",
5+
"created_at": "2024-10-01T00:00:00Z",
6+
"external_ids": [
7+
{ "external_id": "2947", "source": "mdb" }
8+
],
9+
"provider": "Example Transit Agency",
10+
"feed_name": "Example City Transit",
11+
"note": "Mock fixture for local dev",
12+
"feed_contact_email": "",
13+
"source_info": {
14+
"producer_url": "https://example.com/gtfs.zip",
15+
"authentication_type": 0,
16+
"authentication_info_url": "",
17+
"api_key_parameter_name": "",
18+
"license_url": ""
19+
},
20+
"redirects": [],
21+
"locations": [
22+
{
23+
"country_code": "US",
24+
"subdivision_name": "Example State",
25+
"municipality": "Example City"
26+
}
27+
],
28+
"latest_dataset": {
29+
"id": "mdb-2947-202410010000",
30+
"hosted_url": "https://files.example.com/mdb-2947/mdb-2947-202410010000.zip",
31+
"bounding_box": {
32+
"minimum_latitude": 40.0,
33+
"maximum_latitude": 41.0,
34+
"minimum_longitude": -75.0,
35+
"maximum_longitude": -74.0
36+
},
37+
"downloaded_at": "2024-10-01T00:00:15Z",
38+
"hash": "mockhash2947",
39+
"validation_report": {
40+
"validated_at": "2024-10-01T00:10:00Z",
41+
"features": ["Transfers", "Shapes"],
42+
"validator_version": "5.0.2-SNAPSHOT",
43+
"total_error": 0,
44+
"total_warning": 10,
45+
"total_info": 0,
46+
"unique_error_count": 0,
47+
"unique_warning_count": 2,
48+
"unique_info_count": 0,
49+
"url_json": "https://files.example.com/mdb-2947/report.json",
50+
"url_html": "https://files.example.com/mdb-2947/report.html"
51+
}
52+
}
53+
}

src/app/providers.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ export function Providers({
1919
children,
2020
remoteConfig,
2121
}: ProvidersProps): React.ReactElement {
22+
// Start MSW in mock mode to intercept API calls client-side
23+
React.useEffect(() => {
24+
if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
25+
// Lazy-load the worker to avoid bundling in prod
26+
import('../mocks/browser')
27+
.then(async ({ worker }) => await worker.start())
28+
.catch((err) => {
29+
console.warn('MSW mock worker failed to start:', err);
30+
});
31+
}
32+
}, []);
33+
2234
return (
2335
<ContextProviders>
2436
<RemoteConfigProvider config={remoteConfig}>

src/app/utils/auth-server.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ async function exchangeCustomTokenForIdToken(
6464
* Caches the token until near expiry to minimize exchanges.
6565
*/
6666
export async function getGcipIdToken(): Promise<string> {
67+
// Dev/mock bypass: allow local runs without Firebase Admin/service accounts
68+
const isMock =
69+
getEnvConfig('NEXT_PUBLIC_API_MOCKING') === 'enabled' ||
70+
getEnvConfig('LOCAL_DEV_NO_ADMIN') === '1';
71+
if (isMock) {
72+
return 'dev-mock-token';
73+
}
6774
// Use cached token if still valid for at least 60 seconds
6875
if (cached != undefined && cached.expiresAt - now() > 60_000) {
6976
return cached.token;
@@ -90,11 +97,6 @@ export async function getGcipIdToken(): Promise<string> {
9097
return idToken;
9198
}
9299

93-
// export interface EndUserIdentity {
94-
// subject?: string;
95-
// email?: string;
96-
// }
97-
98100
/**
99101
* Returns a GCIP ID token suitable for IAP-protected API calls.
100102
* This avoids trusting client tokens and keeps credentials server-side only.

src/lib/firebase-admin.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@ import fs from 'node:fs';
33
import path from 'node:path';
44
import { getEnvConfig, nonEmpty } from '../app/utils/config';
55

6-
/**
7-
* Centralized Firebase Admin initialization.
8-
* Prefers explicit service account credentials via env.
9-
* Fallback to ADC only when GOOGLE_APPLICATION_CREDENTIALS is set.
10-
*/
6+
let adminApp: App | undefined;
117

128
/**
13-
* Server-only Firebase Admin SDK initialization.
14-
* Uses Application Default Credentials (ADC) which works automatically on Cloud Run.
15-
* For local development, you can either:
16-
* 1. Run `gcloud auth application-default login`
17-
* 2. Set GOOGLE_APPLICATION_CREDENTIALS env var to a service account JSON path
9+
* ensureAdminInitialized
10+
* Creates or reuses a singleton Firebase Admin App.
11+
*
12+
* Selection order:
13+
* 1) Reuse an existing Admin app from `getApps()`, preferring the one whose
14+
* `options.projectId` matches `NEXT_PUBLIC_FIREBASE_PROJECT_ID`; falls back
15+
* to the first app if no match.
16+
* 2) If `GOOGLE_SA_JSON` is set (server-only inline JSON), parse and initialize
17+
* with `cert(serviceAccount)`.
18+
* 3) If `GOOGLE_SA_JSON_PATH` is set, read and parse the JSON file and
19+
* initialize with `cert(serviceAccount)`.
20+
* 4) If none of the above are provided, throws an error to avoid implicit ADC
21+
* behavior (metadata server lookups) in serverless environments.
22+
*
23+
* Environment variables accessed:
24+
* - NEXT_PUBLIC_FIREBASE_PROJECT_ID: Used to match existing apps and as fallback
25+
* when the service account JSON lacks `project_id`.
26+
* - GOOGLE_SA_JSON: Server-only inline service account JSON string
27+
* (must include `project_id`, `client_email`, and `private_key`).
28+
* - GOOGLE_SA_JSON_PATH: Path to a service account JSON file containing the
29+
* same required fields.
30+
*
31+
* Notes:
32+
* - Uses `getEnvConfig` and `nonEmpty` to read configuration consistently.
33+
* - Keep credentials server-only; do not expose inline JSON to client code.
1834
*/
19-
20-
let adminApp: App | undefined;
21-
2235
function ensureAdminInitialized(): App {
2336
// Reuse already initialized app
2437
const existingApps = getApps();

src/lib/remote-config.server.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'server-only';
33
import { cache } from 'react';
44
import { getRemoteConfig } from 'firebase-admin/remote-config';
55
import { getFirebaseAdminApp } from './firebase-admin';
6+
import { getEnvConfig } from '../app/utils/config';
67
import {
78
defaultRemoteConfigValues,
89
type RemoteConfigValues,
@@ -51,6 +52,13 @@ function parseConfigValue(
5152
* Returns the template parameters merged with defaults.
5253
*/
5354
async function fetchRemoteConfigFromFirebase(): Promise<RemoteConfigValues> {
55+
// Dev/mock bypass: return defaults without touching Admin SDK
56+
const isMock =
57+
getEnvConfig('NEXT_PUBLIC_API_MOCKING') === 'enabled' ||
58+
getEnvConfig('LOCAL_DEV_NO_ADMIN') === '1';
59+
if (isMock) {
60+
return defaultRemoteConfigValues;
61+
}
5462
const app = getFirebaseAdminApp();
5563
const remoteConfigAdmin = getRemoteConfig(app);
5664

@@ -95,6 +103,13 @@ async function fetchRemoteConfigFromFirebase(): Promise<RemoteConfigValues> {
95103
*/
96104
export const getRemoteConfigValues = cache(
97105
async (): Promise<RemoteConfigValues> => {
106+
// Dev/mock bypass: use defaults immediately
107+
const isMock =
108+
getEnvConfig('NEXT_PUBLIC_API_MOCKING') === 'enabled' ||
109+
getEnvConfig('LOCAL_DEV_NO_ADMIN') === '1';
110+
if (isMock) {
111+
return defaultRemoteConfigValues;
112+
}
98113
const now = Date.now();
99114
const cacheAge = (now - cacheTimestamp) / 1000;
100115

src/mocks/browser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { setupWorker } from 'msw/browser';
2+
import { handlers } from './handlers';
3+
4+
// MSW browser worker to intercept client-side fetch/XHR
5+
export const worker = setupWorker(...handlers);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"id": "mdb-2947",
3+
"data_type": "gtfs",
4+
"provider": "Example Transit Agency",
5+
"feed_name": "Example City Transit",
6+
"locations": [
7+
{
8+
"country_code": "US",
9+
"subdivision_name": "Example State",
10+
"municipality": "Example City",
11+
"country": "United States"
12+
}
13+
]
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"id": "test-516",
3+
"data_type": "gtfs",
4+
"provider": "Metropolitan Transit Authority (MTA)",
5+
"feed_name": "NYC Subway",
6+
"locations": [
7+
{
8+
"country_code": "US",
9+
"subdivision_name": "New York",
10+
"municipality": "New York City",
11+
"country": "United States"
12+
}
13+
]
14+
}

0 commit comments

Comments
 (0)