@open-kingdom/shared-backend-integration-test-doubles 0.0.2-17 → 0.0.2-18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +75 -5
  2. package/dist/gcs/gcs-error-mode.d.ts +161 -0
  3. package/dist/gcs/gcs-error-mode.d.ts.map +1 -0
  4. package/dist/gcs/gcs-error-mode.js +256 -0
  5. package/dist/gcs/gcs-error-mode.js.map +1 -0
  6. package/dist/gcs/gcs-error-simulation.interceptor.d.ts +21 -0
  7. package/dist/gcs/gcs-error-simulation.interceptor.d.ts.map +1 -0
  8. package/dist/gcs/gcs-error-simulation.interceptor.js +40 -0
  9. package/dist/gcs/gcs-error-simulation.interceptor.js.map +1 -0
  10. package/dist/gcs/gcs-twin.d.ts +1 -0
  11. package/dist/gcs/gcs-twin.d.ts.map +1 -1
  12. package/dist/gcs/gcs-twin.js +19 -1
  13. package/dist/gcs/gcs-twin.js.map +1 -1
  14. package/dist/gcs/index.d.ts +3 -0
  15. package/dist/gcs/index.d.ts.map +1 -1
  16. package/dist/gcs/index.js +2 -0
  17. package/dist/gcs/index.js.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/interceptor/http-helper.d.ts +37 -0
  23. package/dist/interceptor/http-helper.d.ts.map +1 -0
  24. package/dist/interceptor/http-helper.js +109 -0
  25. package/dist/interceptor/http-helper.js.map +1 -0
  26. package/dist/interceptor/index.d.ts +4 -0
  27. package/dist/interceptor/index.d.ts.map +1 -0
  28. package/dist/interceptor/index.js +5 -0
  29. package/dist/interceptor/index.js.map +1 -0
  30. package/dist/interceptor/node-interceptor.d.ts +40 -0
  31. package/dist/interceptor/node-interceptor.d.ts.map +1 -0
  32. package/dist/interceptor/node-interceptor.js +188 -0
  33. package/dist/interceptor/node-interceptor.js.map +1 -0
  34. package/dist/interceptor/routing-table.config.d.ts +3 -0
  35. package/dist/interceptor/routing-table.config.d.ts.map +1 -0
  36. package/dist/interceptor/routing-table.config.js +25 -0
  37. package/dist/interceptor/routing-table.config.js.map +1 -0
  38. package/dist/interceptor/routing-table.d.ts +15 -0
  39. package/dist/interceptor/routing-table.d.ts.map +1 -0
  40. package/dist/interceptor/routing-table.js +44 -0
  41. package/dist/interceptor/routing-table.js.map +1 -0
  42. package/dist/shared/constants.d.ts +4 -0
  43. package/dist/shared/constants.d.ts.map +1 -1
  44. package/dist/shared/constants.js +3 -1
  45. package/dist/shared/constants.js.map +1 -1
  46. package/dist/youtube/index.d.ts +15 -0
  47. package/dist/youtube/index.d.ts.map +1 -0
  48. package/dist/youtube/index.js +11 -0
  49. package/dist/youtube/index.js.map +1 -0
  50. package/dist/youtube/msw-handlers.d.ts +40 -0
  51. package/dist/youtube/msw-handlers.d.ts.map +1 -0
  52. package/dist/youtube/msw-handlers.js +45 -0
  53. package/dist/youtube/msw-handlers.js.map +1 -0
  54. package/dist/youtube/player-shim.d.ts +3 -0
  55. package/dist/youtube/player-shim.d.ts.map +1 -0
  56. package/dist/youtube/player-shim.js +296 -0
  57. package/dist/youtube/player-shim.js.map +1 -0
  58. package/dist/youtube/search-fixtures.d.ts +29 -0
  59. package/dist/youtube/search-fixtures.d.ts.map +1 -0
  60. package/dist/youtube/search-fixtures.js +392 -0
  61. package/dist/youtube/search-fixtures.js.map +1 -0
  62. package/dist/youtube/search-response.d.ts +51 -0
  63. package/dist/youtube/search-response.d.ts.map +1 -0
  64. package/dist/youtube/search-response.js +53 -0
  65. package/dist/youtube/search-response.js.map +1 -0
  66. package/dist/youtube/youtube-error-mode.d.ts +36 -0
  67. package/dist/youtube/youtube-error-mode.d.ts.map +1 -0
  68. package/dist/youtube/youtube-error-mode.js +87 -0
  69. package/dist/youtube/youtube-error-mode.js.map +1 -0
  70. package/dist/youtube/youtube-twin.config.d.ts +13 -0
  71. package/dist/youtube/youtube-twin.config.d.ts.map +1 -0
  72. package/dist/youtube/youtube-twin.config.js +32 -0
  73. package/dist/youtube/youtube-twin.config.js.map +1 -0
  74. package/dist/youtube/youtube-twin.d.ts +14 -0
  75. package/dist/youtube/youtube-twin.d.ts.map +1 -0
  76. package/dist/youtube/youtube-twin.js +162 -0
  77. package/dist/youtube/youtube-twin.js.map +1 -0
  78. package/package.json +15 -2
  79. package/test-assets/youtube/sample.mp4 +0 -0
  80. package/test-assets/youtube/thumbnails/thumbnail-01.jpg +0 -0
  81. package/test-assets/youtube/thumbnails/thumbnail-02.jpg +0 -0
  82. package/test-assets/youtube/thumbnails/thumbnail-03.jpg +0 -0
  83. package/test-assets/youtube/thumbnails/thumbnail-04.jpg +0 -0
  84. package/test-assets/youtube/thumbnails/thumbnail-05.jpg +0 -0
  85. package/test-assets/youtube/thumbnails/thumbnail-06.jpg +0 -0
  86. package/test-assets/youtube/thumbnails/thumbnail-07.jpg +0 -0
  87. package/test-assets/youtube/thumbnails/thumbnail-08.jpg +0 -0
  88. package/test-assets/youtube/thumbnails/thumbnail-09.jpg +0 -0
  89. package/test-assets/youtube/thumbnails/thumbnail-10.jpg +0 -0
  90. package/test-assets/youtube/thumbnails/thumbnail-11.jpg +0 -0
  91. package/test-assets/youtube/thumbnails/thumbnail-12.jpg +0 -0
  92. package/test-assets/youtube/thumbnails/thumbnail-13.jpg +0 -0
  93. package/test-assets/youtube/thumbnails/thumbnail-14.jpg +0 -0
  94. package/test-assets/youtube/thumbnails/thumbnail-15.jpg +0 -0
  95. package/test-assets/youtube/thumbnails/thumbnail-16.jpg +0 -0
  96. package/test-assets/youtube/thumbnails/thumbnail-17.jpg +0 -0
  97. package/test-assets/youtube/thumbnails/thumbnail-18.jpg +0 -0
  98. package/test-assets/youtube/thumbnails/thumbnail-19.jpg +0 -0
  99. package/test-assets/youtube/thumbnails/thumbnail-20.jpg +0 -0
package/README.md CHANGED
@@ -37,6 +37,30 @@ const client = new Storage({
37
37
  await twin.stop();
38
38
  ```
39
39
 
40
+ ### Using YouTube Twin (Server-Side Interception)
41
+
42
+ ```typescript
43
+ import { YoutubeTwin, NodeInterceptor, RoutingTable, defaultRoutingEntries } from '@open-kingdom/shared-backend-integration-test-doubles';
44
+
45
+ // Start the twin (default port 9016)
46
+ const twin = new YoutubeTwin();
47
+ await twin.start();
48
+
49
+ // Install the global fetch interceptor
50
+ const routingTable = new RoutingTable(defaultRoutingEntries);
51
+ const interceptor = new NodeInterceptor(routingTable);
52
+ interceptor.install();
53
+
54
+ // Call the production YouTube API endpoint (internally routed to twin)
55
+ const res = await fetch('https://www.googleapis.com/youtube/v3/search?q=yoga&key=valid-key');
56
+ const data = await res.json();
57
+ console.log(data.kind); // "youtube#searchListResponse"
58
+
59
+ // Clean up
60
+ interceptor.uninstall();
61
+ await twin.stop();
62
+ ```
63
+
40
64
  ### NestJS Integration
41
65
 
42
66
  Use the `TwinService` to manage the twin lifecycle within a NestJS application:
@@ -93,6 +117,52 @@ interface BucketConfig {
93
117
  }
94
118
  ```
95
119
 
120
+ ### YouTubeTwin & Network Interceptor
121
+
122
+ #### YouTubeTwin Constructor
123
+
124
+ ```typescript
125
+ constructor(overrides?: Partial<YoutubeTwinConfig>)
126
+ ```
127
+
128
+ - `overrides.port`: Port to run the twin on (default: 9016, or from `YOUTUBE_TWIN_PORT` env var; must be within 9010-9020)
129
+ - `overrides.externalUrl`: External URL for the twin (default: `http://localhost:{port}`)
130
+
131
+ #### YouTubeTwin Methods
132
+
133
+ - `start()`: Start the Express server
134
+ - `stop()`: Stop the Express server
135
+ - `reset()`: Reset search fixtures to defaults and clear any active error mode
136
+ - `getEmulatorHost()`: Get the twin URL (e.g., `http://localhost:9016`)
137
+ - `isHealthy()`: Check if the twin is running and responding
138
+
139
+ #### NodeInterceptor & RoutingTable
140
+
141
+ - `new NodeInterceptor(routingTable)`: Instantiate interceptor
142
+ - `install()`: Patches `globalThis.fetch` to intercept matching domains
143
+ - `uninstall()`: Restores `globalThis.fetch` to its original reference
144
+ - `isActive()`: Check if interceptor is currently active
145
+
146
+ #### Browser-Side MSW Interception
147
+
148
+ Use `getYoutubeMswHandlerConfigs(twinBaseUrl)` to export mock route definitions for browser-side testing frameworks like MSW:
149
+
150
+ ```typescript
151
+ import { http, HttpResponse } from 'msw';
152
+ import { getYoutubeMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';
153
+
154
+ const configs = getYoutubeMswHandlerConfigs('http://localhost:9016');
155
+ const handlers = configs.map((config) =>
156
+ http.get(config.url, async () => {
157
+ const res = await fetch(config.proxyTo);
158
+ const body = await res.text();
159
+ return new HttpResponse(body, {
160
+ headers: { 'Content-Type': config.contentType },
161
+ });
162
+ })
163
+ );
164
+ ```
165
+
96
166
  ### Exports
97
167
 
98
168
  ```typescript
@@ -127,7 +197,7 @@ The library reserves ports 9010-9020 for test doubles:
127
197
  | GCS | 9013 |
128
198
  | Gmail | 9014 (future) |
129
199
  | Google Auth | 9015 (future) |
130
- | YouTube | 9016 (future) |
200
+ | YouTube | 9016 |
131
201
  | Google Calendar | 9017 (future) |
132
202
  | Spotify | 9018 (future) |
133
203
 
@@ -145,14 +215,14 @@ Port validation ensures the value is within the reserved range (9010-9020).
145
215
 
146
216
  ## Scripts
147
217
 
148
- ### Start Twin Manually
218
+ ### Start Twins Manually
149
219
 
150
220
  ```bash
151
- # Start on default port (9013)
221
+ # Start GCS twin manually on default port (9013)
152
222
  npx tsx scripts/start-twin.ts
153
223
 
154
- # Start on specific port
155
- npx tsx scripts/start-twin.ts 9013
224
+ # Start YouTube twin manually on default port (9016)
225
+ npx nx run integration-test-doubles:start:youtube-twin
156
226
  ```
157
227
 
158
228
  The script outputs the twin URL and sets `GCS_EMULATOR_URL`. Keeps running until Ctrl+C.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * GCS error mode simulation — pure state management and request matching.
3
+ *
4
+ * The GcsErrorModeManager knows nothing about HTTP, NestJS, or networking.
5
+ * It holds the currently active error mode, matches incoming requests against
6
+ * the mode's criteria, and returns the appropriate GCS-shaped error response.
7
+ *
8
+ * Design decisions:
9
+ * - Single-mode constraint: only one error mode can be active at a time.
10
+ * setMode() replaces any existing mode.
11
+ * - Intermittent-failure uses a deterministic counter (every Nth request fails),
12
+ * not random percentage, for reproducible test results.
13
+ * - reset() and clearMode() clear the mode and counter, ensuring no leakage
14
+ * between test runs.
15
+ */
16
+ /**
17
+ * The four supported GCS error simulation modes.
18
+ */
19
+ export type GcsErrorModeType = 'bucket-not-found' | 'quota-exceeded' | 'permission-denied' | 'intermittent-failure';
20
+ /**
21
+ * Configuration for bucket-not-found mode.
22
+ * Matches requests targeting a specific bucket name.
23
+ */
24
+ export interface BucketNotFoundConfig {
25
+ type: 'bucket-not-found';
26
+ /** The bucket name that will trigger the 404 response. */
27
+ bucketName: string;
28
+ }
29
+ /**
30
+ * Configuration for quota-exceeded mode.
31
+ * Matches upload/create operations (POST to upload paths).
32
+ */
33
+ export interface QuotaExceededConfig {
34
+ type: 'quota-exceeded';
35
+ }
36
+ /**
37
+ * Configuration for permission-denied mode.
38
+ * Matches all GCS data operations.
39
+ */
40
+ export interface PermissionDeniedConfig {
41
+ type: 'permission-denied';
42
+ }
43
+ /**
44
+ * Configuration for intermittent-failure mode.
45
+ * Every Nth request fails with 500, deterministic counter.
46
+ */
47
+ export interface IntermittentFailureConfig {
48
+ type: 'intermittent-failure';
49
+ /** Fail every Nth request. Defaults to 2 (every other request). */
50
+ every?: number;
51
+ }
52
+ /**
53
+ * Discriminated union of all error mode configurations.
54
+ * The `type` field discriminates which kind of mode is active.
55
+ */
56
+ export type GcsErrorModeConfig = BucketNotFoundConfig | QuotaExceededConfig | PermissionDeniedConfig | IntermittentFailureConfig;
57
+ /**
58
+ * A GCS-shaped error response that the NestJS interceptor will use
59
+ * to short-circuit matching requests.
60
+ */
61
+ export interface GcsErrorSimulationResult {
62
+ /** HTTP status code to respond with. */
63
+ status: number;
64
+ /** GCS-shaped JSON error body. */
65
+ body: {
66
+ error: {
67
+ code: number;
68
+ message: string;
69
+ errors: Array<{
70
+ domain: string;
71
+ reason: string;
72
+ message?: string;
73
+ }>;
74
+ };
75
+ };
76
+ }
77
+ /**
78
+ * Extract the bucket name from a GCS API path.
79
+ *
80
+ * Supports both path-style and virtual-hosted-style paths:
81
+ * - `/storage/v1/b/my-bucket/o` → "my-bucket"
82
+ * - `/storage/v1/b/my-bucket` → "my-bucket"
83
+ * - `/upload/storage/v1/b/my-bucket/o` → "my-bucket"
84
+ * - `/download/storage/v1/b/my-bucket/o` → "my-bucket"
85
+ * - `/b/my-bucket/o` → "my-bucket"
86
+ *
87
+ * Returns undefined if the path doesn't match any known GCS pattern.
88
+ */
89
+ export declare function extractBucketName(path: string): string | undefined;
90
+ /**
91
+ * Determine if a request is an upload/create operation.
92
+ *
93
+ * Upload operations are POST requests to paths containing "/upload/" or
94
+ * POST requests to bucket or object creation endpoints.
95
+ */
96
+ export declare function isUploadOperation(method: string, path: string): boolean;
97
+ /**
98
+ * Determine if a request is a GCS data operation.
99
+ *
100
+ * GCS data operations include CRUD on buckets and objects.
101
+ * They exclude twin control endpoints (/api/twin/...) and health checks.
102
+ */
103
+ export declare function isGcsDataOperation(method: string, path: string): boolean;
104
+ /**
105
+ * Pure state manager for GCS error simulation modes.
106
+ *
107
+ * Tracks the active error mode and matches incoming requests against it.
108
+ * Returns null for pass-through when no mode is active.
109
+ *
110
+ * Thread-safety: this class is NOT thread-safe. In NestJS, requests are
111
+ * handled concurrently, so the counter for intermittent-failure mode must
112
+ * be managed with atomic operations or a mutex in the interceptor layer.
113
+ * For now, we use a simple counter since NestJS interceptors run on the
114
+ * event loop and the matching logic is synchronous.
115
+ */
116
+ export declare class GcsErrorModeManager {
117
+ private activeMode;
118
+ private requestCounter;
119
+ /**
120
+ * Activate an error mode. Replaces any existing mode (single-mode constraint).
121
+ * Resets the request counter for intermittent-failure mode.
122
+ */
123
+ setMode(config: GcsErrorModeConfig): void;
124
+ /**
125
+ * Deactivate the current error mode. Returns to pass-through.
126
+ * Resets the request counter.
127
+ */
128
+ clearMode(): void;
129
+ /**
130
+ * Get the currently active error mode configuration.
131
+ * Returns null when no mode is active (pass-through).
132
+ */
133
+ getMode(): GcsErrorModeConfig | null;
134
+ /**
135
+ * Match an incoming request against the active error mode.
136
+ *
137
+ * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
138
+ * @param path - Request path (e.g., /storage/v1/b/my-bucket/o)
139
+ * @returns GcsErrorSimulationResult if the request matches, null for pass-through
140
+ */
141
+ matchRequest(method: string, path: string): GcsErrorSimulationResult | null;
142
+ /**
143
+ * Simulate a sample error for the currently active mode.
144
+ * This produces the same GcsErrorSimulationResult that the interceptor would
145
+ * return for a matching request, without mutating the request counter.
146
+ *
147
+ * Used by the GET /api/twin/gcs/error-mode endpoint so the frontend can
148
+ * display the actual error response the backend will produce.
149
+ */
150
+ simulateSampleError(): GcsErrorSimulationResult | null;
151
+ /**
152
+ * Reset all state: clear the active mode and reset the request counter.
153
+ * Called by GcsTwin.reset() and GcsTwin.stop() to prevent leakage between tests.
154
+ */
155
+ reset(): void;
156
+ private matchBucketNotFound;
157
+ private matchQuotaExceeded;
158
+ private matchPermissionDenied;
159
+ private matchIntermittentFailure;
160
+ }
161
+ //# sourceMappingURL=gcs-error-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs-error-mode.d.ts","sourceRoot":"","sources":["../../src/gcs/gcs-error-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,kBAAkB,GAClB,gBAAgB,GAChB,mBAAmB,GACnB,sBAAsB,CAAC;AAE3B;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC;IACzB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,mBAAmB,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAC1B,oBAAoB,GACpB,mBAAmB,GACnB,sBAAsB,GACtB,yBAAyB,CAAC;AAE9B;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,IAAI,EAAE;QACJ,KAAK,EAAE;YACL,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,EAAE,MAAM,CAAC;YAChB,MAAM,EAAE,KAAK,CAAC;gBACZ,MAAM,EAAE,MAAM,CAAC;gBACf,MAAM,EAAE,MAAM,CAAC;gBACf,OAAO,CAAC,EAAE,MAAM,CAAC;aAClB,CAAC,CAAC;SACJ,CAAC;KACH,CAAC;CACH;AAmCD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsClE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAuBvE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CA8BxE;AAMD;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,cAAc,CAAK;IAE3B;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAKzC;;;OAGG;IACH,SAAS,IAAI,IAAI;IAKjB;;;OAGG;IACH,OAAO,IAAI,kBAAkB,GAAG,IAAI;IAIpC;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,wBAAwB,GAAG,IAAI;IAe3E;;;;;;;OAOG;IACH,mBAAmB,IAAI,wBAAwB,GAAG,IAAI;IAgBtD;;;OAGG;IACH,KAAK,IAAI,IAAI;IASb,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,wBAAwB;CAkBjC"}
@@ -0,0 +1,256 @@
1
+ /**
2
+ * GCS error mode simulation — pure state management and request matching.
3
+ *
4
+ * The GcsErrorModeManager knows nothing about HTTP, NestJS, or networking.
5
+ * It holds the currently active error mode, matches incoming requests against
6
+ * the mode's criteria, and returns the appropriate GCS-shaped error response.
7
+ *
8
+ * Design decisions:
9
+ * - Single-mode constraint: only one error mode can be active at a time.
10
+ * setMode() replaces any existing mode.
11
+ * - Intermittent-failure uses a deterministic counter (every Nth request fails),
12
+ * not random percentage, for reproducible test results.
13
+ * - reset() and clearMode() clear the mode and counter, ensuring no leakage
14
+ * between test runs.
15
+ */ // ---------------------------------------------------------------------------
16
+ // Type definitions
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * The four supported GCS error simulation modes.
20
+ */ // ---------------------------------------------------------------------------
21
+ // GCS error response factory
22
+ // ---------------------------------------------------------------------------
23
+ /** Creates a standard GCS error response shape. */ function gcsError(status, message, reason, detailMessage) {
24
+ return {
25
+ status,
26
+ body: {
27
+ error: {
28
+ code: status,
29
+ message,
30
+ errors: [
31
+ {
32
+ domain: 'global',
33
+ reason,
34
+ message: detailMessage != null ? detailMessage : message
35
+ }
36
+ ]
37
+ }
38
+ }
39
+ };
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Request-matching helpers
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * Extract the bucket name from a GCS API path.
46
+ *
47
+ * Supports both path-style and virtual-hosted-style paths:
48
+ * - `/storage/v1/b/my-bucket/o` → "my-bucket"
49
+ * - `/storage/v1/b/my-bucket` → "my-bucket"
50
+ * - `/upload/storage/v1/b/my-bucket/o` → "my-bucket"
51
+ * - `/download/storage/v1/b/my-bucket/o` → "my-bucket"
52
+ * - `/b/my-bucket/o` → "my-bucket"
53
+ *
54
+ * Returns undefined if the path doesn't match any known GCS pattern.
55
+ */ export function extractBucketName(path) {
56
+ // Strip leading slash for consistent processing
57
+ const normalized = path.startsWith('/') ? path.slice(1) : path;
58
+ // Match /api/gcs/files?bucket=<name> (query-string style)
59
+ const apiGcsBucketMatch = normalized.match(/api\/gcs\/files[?&]bucket=([^&]+)/);
60
+ if (apiGcsBucketMatch) {
61
+ return decodeURIComponent(apiGcsBucketMatch[1]);
62
+ }
63
+ // Match /api/gcs/files/<bucket>/<name>/... or /api/gcs/files/<bucket>/<name>
64
+ const apiGcsPathMatch = normalized.match(/api\/gcs\/files\/([^/]+)/);
65
+ if (apiGcsPathMatch) {
66
+ return decodeURIComponent(apiGcsPathMatch[1]);
67
+ }
68
+ // Match /api/gcs/upload (uses query param or default bucket — derive from body is not possible here)
69
+ // Uploads via /api/gcs/upload use the bucket in the request body, not URL. Return undefined for now.
70
+ // Match patterns with optional prefixes (upload/, download/) followed by
71
+ // the GCS API path structure.
72
+ const patterns = [
73
+ // Full v1 API paths: storage/v1/b/{bucket}/... or upload/storage/v1/b/{bucket}/...
74
+ /(?:upload\/|download\/)?storage\/v1\/b\/([^/]+)/,
75
+ // Short form: b/{bucket}/...
76
+ /b\/([^/]+)/
77
+ ];
78
+ for (const pattern of patterns){
79
+ const match = normalized.match(pattern);
80
+ if (match) {
81
+ return match[1];
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Determine if a request is an upload/create operation.
88
+ *
89
+ * Upload operations are POST requests to paths containing "/upload/" or
90
+ * POST requests to bucket or object creation endpoints.
91
+ */ export function isUploadOperation(method, path) {
92
+ if (method.toUpperCase() !== 'POST') return false;
93
+ const normalized = path.toLowerCase();
94
+ // /api/gcs/upload — the NestJS GCS upload endpoint
95
+ if (normalized.includes('/api/gcs/upload')) return true;
96
+ // /api/gcs/files/<bucket>/<name>/delete — treated as a write operation for permission-denied
97
+ // (not an upload, but this function is specifically for quota-exceeded which targets uploads)
98
+ // Direct upload path prefix (GCS native)
99
+ if (normalized.includes('/upload/storage/')) return true;
100
+ // Object insert via bucket's objects collection
101
+ // POST /storage/v1/b/{bucket}/o
102
+ if (normalized.includes('/b/') && normalized.includes('/o')) return true;
103
+ // Bucket creation
104
+ // POST /storage/v1/b
105
+ if (normalized.match(/\/storage\/v1\/b\/?$/)) return true;
106
+ return false;
107
+ }
108
+ /**
109
+ * Determine if a request is a GCS data operation.
110
+ *
111
+ * GCS data operations include CRUD on buckets and objects.
112
+ * They exclude twin control endpoints (/api/twin/...) and health checks.
113
+ */ export function isGcsDataOperation(method, path) {
114
+ const normalized = path.toLowerCase();
115
+ // Exclude twin control endpoints
116
+ if (normalized.startsWith('/api/twin')) return false;
117
+ // Exclude error-mode control endpoints (control plane, not data plane)
118
+ if (normalized.startsWith('/api/gcs/error-mode')) return false;
119
+ // Exclude health checks and non-GCS paths
120
+ if (normalized === '/' || normalized === '/health' || normalized === '/healthz') {
121
+ return false;
122
+ }
123
+ // NestJS GCS controller routes: /api/gcs/*
124
+ if (normalized.startsWith('/api/gcs')) return true;
125
+ // Include known GCS API paths
126
+ if (normalized.includes('/storage/v1/')) return true;
127
+ if (normalized.includes('/upload/storage/')) return true;
128
+ if (normalized.includes('/download/storage/')) return true;
129
+ // Include paths that match GCS bucket/object patterns
130
+ if (normalized.match(/\/b\/[^/]+/)) return true;
131
+ return false;
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // GcsErrorModeManager
135
+ // ---------------------------------------------------------------------------
136
+ /**
137
+ * Pure state manager for GCS error simulation modes.
138
+ *
139
+ * Tracks the active error mode and matches incoming requests against it.
140
+ * Returns null for pass-through when no mode is active.
141
+ *
142
+ * Thread-safety: this class is NOT thread-safe. In NestJS, requests are
143
+ * handled concurrently, so the counter for intermittent-failure mode must
144
+ * be managed with atomic operations or a mutex in the interceptor layer.
145
+ * For now, we use a simple counter since NestJS interceptors run on the
146
+ * event loop and the matching logic is synchronous.
147
+ */ export class GcsErrorModeManager {
148
+ /**
149
+ * Activate an error mode. Replaces any existing mode (single-mode constraint).
150
+ * Resets the request counter for intermittent-failure mode.
151
+ */ setMode(config) {
152
+ this.activeMode = config;
153
+ this.requestCounter = 0;
154
+ }
155
+ /**
156
+ * Deactivate the current error mode. Returns to pass-through.
157
+ * Resets the request counter.
158
+ */ clearMode() {
159
+ this.activeMode = null;
160
+ this.requestCounter = 0;
161
+ }
162
+ /**
163
+ * Get the currently active error mode configuration.
164
+ * Returns null when no mode is active (pass-through).
165
+ */ getMode() {
166
+ return this.activeMode;
167
+ }
168
+ /**
169
+ * Match an incoming request against the active error mode.
170
+ *
171
+ * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
172
+ * @param path - Request path (e.g., /storage/v1/b/my-bucket/o)
173
+ * @returns GcsErrorSimulationResult if the request matches, null for pass-through
174
+ */ matchRequest(method, path) {
175
+ if (!this.activeMode) return null;
176
+ switch(this.activeMode.type){
177
+ case 'bucket-not-found':
178
+ return this.matchBucketNotFound(method, path, this.activeMode);
179
+ case 'quota-exceeded':
180
+ return this.matchQuotaExceeded(method, path, this.activeMode);
181
+ case 'permission-denied':
182
+ return this.matchPermissionDenied(method, path, this.activeMode);
183
+ case 'intermittent-failure':
184
+ return this.matchIntermittentFailure(method, path, this.activeMode);
185
+ }
186
+ }
187
+ /**
188
+ * Simulate a sample error for the currently active mode.
189
+ * This produces the same GcsErrorSimulationResult that the interceptor would
190
+ * return for a matching request, without mutating the request counter.
191
+ *
192
+ * Used by the GET /api/twin/gcs/error-mode endpoint so the frontend can
193
+ * display the actual error response the backend will produce.
194
+ */ simulateSampleError() {
195
+ if (!this.activeMode) return null;
196
+ switch(this.activeMode.type){
197
+ case 'bucket-not-found':
198
+ // Representative: a GET to the configured bucket
199
+ return gcsError(404, 'Not Found', 'notFound');
200
+ case 'quota-exceeded':
201
+ return gcsError(403, 'Quota exceeded', 'quotaExceeded');
202
+ case 'permission-denied':
203
+ return gcsError(403, 'Forbidden', 'forbidden');
204
+ case 'intermittent-failure':
205
+ return gcsError(500, 'Internal Server Error', 'internalError');
206
+ }
207
+ }
208
+ /**
209
+ * Reset all state: clear the active mode and reset the request counter.
210
+ * Called by GcsTwin.reset() and GcsTwin.stop() to prevent leakage between tests.
211
+ */ reset() {
212
+ this.activeMode = null;
213
+ this.requestCounter = 0;
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // Private matching methods
217
+ // ---------------------------------------------------------------------------
218
+ matchBucketNotFound(_method, path, config) {
219
+ const bucket = extractBucketName(path);
220
+ if (bucket === config.bucketName) {
221
+ return gcsError(404, 'Not Found', 'notFound');
222
+ }
223
+ return null;
224
+ }
225
+ matchQuotaExceeded(method, path, _config) {
226
+ if (isUploadOperation(method, path)) {
227
+ return gcsError(403, 'Quota exceeded', 'quotaExceeded');
228
+ }
229
+ return null;
230
+ }
231
+ matchPermissionDenied(method, path, _config) {
232
+ if (isGcsDataOperation(method, path)) {
233
+ return gcsError(403, 'Forbidden', 'forbidden');
234
+ }
235
+ return null;
236
+ }
237
+ matchIntermittentFailure(method, path, config) {
238
+ // Only apply to GCS data operations
239
+ if (!isGcsDataOperation(method, path)) {
240
+ return null;
241
+ }
242
+ this.requestCounter++;
243
+ var _config_every;
244
+ const every = (_config_every = config.every) != null ? _config_every : 2;
245
+ if (this.requestCounter % every === 0) {
246
+ return gcsError(500, 'Internal Server Error', 'internalError');
247
+ }
248
+ return null;
249
+ }
250
+ constructor(){
251
+ this.activeMode = null;
252
+ this.requestCounter = 0;
253
+ }
254
+ }
255
+
256
+ //# sourceMappingURL=gcs-error-mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/gcs/gcs-error-mode.ts"],"sourcesContent":["/**\n * GCS error mode simulation — pure state management and request matching.\n *\n * The GcsErrorModeManager knows nothing about HTTP, NestJS, or networking.\n * It holds the currently active error mode, matches incoming requests against\n * the mode's criteria, and returns the appropriate GCS-shaped error response.\n *\n * Design decisions:\n * - Single-mode constraint: only one error mode can be active at a time.\n * setMode() replaces any existing mode.\n * - Intermittent-failure uses a deterministic counter (every Nth request fails),\n * not random percentage, for reproducible test results.\n * - reset() and clearMode() clear the mode and counter, ensuring no leakage\n * between test runs.\n */\n\n// ---------------------------------------------------------------------------\n// Type definitions\n// ---------------------------------------------------------------------------\n\n/**\n * The four supported GCS error simulation modes.\n */\nexport type GcsErrorModeType =\n | 'bucket-not-found'\n | 'quota-exceeded'\n | 'permission-denied'\n | 'intermittent-failure';\n\n/**\n * Configuration for bucket-not-found mode.\n * Matches requests targeting a specific bucket name.\n */\nexport interface BucketNotFoundConfig {\n type: 'bucket-not-found';\n /** The bucket name that will trigger the 404 response. */\n bucketName: string;\n}\n\n/**\n * Configuration for quota-exceeded mode.\n * Matches upload/create operations (POST to upload paths).\n */\nexport interface QuotaExceededConfig {\n type: 'quota-exceeded';\n}\n\n/**\n * Configuration for permission-denied mode.\n * Matches all GCS data operations.\n */\nexport interface PermissionDeniedConfig {\n type: 'permission-denied';\n}\n\n/**\n * Configuration for intermittent-failure mode.\n * Every Nth request fails with 500, deterministic counter.\n */\nexport interface IntermittentFailureConfig {\n type: 'intermittent-failure';\n /** Fail every Nth request. Defaults to 2 (every other request). */\n every?: number;\n}\n\n/**\n * Discriminated union of all error mode configurations.\n * The `type` field discriminates which kind of mode is active.\n */\nexport type GcsErrorModeConfig =\n | BucketNotFoundConfig\n | QuotaExceededConfig\n | PermissionDeniedConfig\n | IntermittentFailureConfig;\n\n/**\n * A GCS-shaped error response that the NestJS interceptor will use\n * to short-circuit matching requests.\n */\nexport interface GcsErrorSimulationResult {\n /** HTTP status code to respond with. */\n status: number;\n /** GCS-shaped JSON error body. */\n body: {\n error: {\n code: number;\n message: string;\n errors: Array<{\n domain: string;\n reason: string;\n message?: string;\n }>;\n };\n };\n}\n\n// ---------------------------------------------------------------------------\n// GCS error response factory\n// ---------------------------------------------------------------------------\n\n/** Creates a standard GCS error response shape. */\nfunction gcsError(\n status: number,\n message: string,\n reason: string,\n detailMessage?: string\n): GcsErrorSimulationResult {\n return {\n status,\n body: {\n error: {\n code: status,\n message,\n errors: [\n {\n domain: 'global',\n reason,\n message: detailMessage ?? message,\n },\n ],\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Request-matching helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the bucket name from a GCS API path.\n *\n * Supports both path-style and virtual-hosted-style paths:\n * - `/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/storage/v1/b/my-bucket` → \"my-bucket\"\n * - `/upload/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/download/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/b/my-bucket/o` → \"my-bucket\"\n *\n * Returns undefined if the path doesn't match any known GCS pattern.\n */\nexport function extractBucketName(path: string): string | undefined {\n // Strip leading slash for consistent processing\n const normalized = path.startsWith('/') ? path.slice(1) : path;\n\n // Match /api/gcs/files?bucket=<name> (query-string style)\n const apiGcsBucketMatch = normalized.match(\n /api\\/gcs\\/files[?&]bucket=([^&]+)/\n );\n if (apiGcsBucketMatch) {\n return decodeURIComponent(apiGcsBucketMatch[1]);\n }\n\n // Match /api/gcs/files/<bucket>/<name>/... or /api/gcs/files/<bucket>/<name>\n const apiGcsPathMatch = normalized.match(/api\\/gcs\\/files\\/([^/]+)/);\n if (apiGcsPathMatch) {\n return decodeURIComponent(apiGcsPathMatch[1]);\n }\n\n // Match /api/gcs/upload (uses query param or default bucket — derive from body is not possible here)\n // Uploads via /api/gcs/upload use the bucket in the request body, not URL. Return undefined for now.\n\n // Match patterns with optional prefixes (upload/, download/) followed by\n // the GCS API path structure.\n const patterns = [\n // Full v1 API paths: storage/v1/b/{bucket}/... or upload/storage/v1/b/{bucket}/...\n /(?:upload\\/|download\\/)?storage\\/v1\\/b\\/([^/]+)/,\n // Short form: b/{bucket}/...\n /b\\/([^/]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = normalized.match(pattern);\n if (match) {\n return match[1];\n }\n }\n\n return undefined;\n}\n\n/**\n * Determine if a request is an upload/create operation.\n *\n * Upload operations are POST requests to paths containing \"/upload/\" or\n * POST requests to bucket or object creation endpoints.\n */\nexport function isUploadOperation(method: string, path: string): boolean {\n if (method.toUpperCase() !== 'POST') return false;\n\n const normalized = path.toLowerCase();\n\n // /api/gcs/upload — the NestJS GCS upload endpoint\n if (normalized.includes('/api/gcs/upload')) return true;\n\n // /api/gcs/files/<bucket>/<name>/delete — treated as a write operation for permission-denied\n // (not an upload, but this function is specifically for quota-exceeded which targets uploads)\n\n // Direct upload path prefix (GCS native)\n if (normalized.includes('/upload/storage/')) return true;\n\n // Object insert via bucket's objects collection\n // POST /storage/v1/b/{bucket}/o\n if (normalized.includes('/b/') && normalized.includes('/o')) return true;\n\n // Bucket creation\n // POST /storage/v1/b\n if (normalized.match(/\\/storage\\/v1\\/b\\/?$/)) return true;\n\n return false;\n}\n\n/**\n * Determine if a request is a GCS data operation.\n *\n * GCS data operations include CRUD on buckets and objects.\n * They exclude twin control endpoints (/api/twin/...) and health checks.\n */\nexport function isGcsDataOperation(method: string, path: string): boolean {\n const normalized = path.toLowerCase();\n\n // Exclude twin control endpoints\n if (normalized.startsWith('/api/twin')) return false;\n\n // Exclude error-mode control endpoints (control plane, not data plane)\n if (normalized.startsWith('/api/gcs/error-mode')) return false;\n\n // Exclude health checks and non-GCS paths\n if (\n normalized === '/' ||\n normalized === '/health' ||\n normalized === '/healthz'\n ) {\n return false;\n }\n\n // NestJS GCS controller routes: /api/gcs/*\n if (normalized.startsWith('/api/gcs')) return true;\n\n // Include known GCS API paths\n if (normalized.includes('/storage/v1/')) return true;\n if (normalized.includes('/upload/storage/')) return true;\n if (normalized.includes('/download/storage/')) return true;\n\n // Include paths that match GCS bucket/object patterns\n if (normalized.match(/\\/b\\/[^/]+/)) return true;\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// GcsErrorModeManager\n// ---------------------------------------------------------------------------\n\n/**\n * Pure state manager for GCS error simulation modes.\n *\n * Tracks the active error mode and matches incoming requests against it.\n * Returns null for pass-through when no mode is active.\n *\n * Thread-safety: this class is NOT thread-safe. In NestJS, requests are\n * handled concurrently, so the counter for intermittent-failure mode must\n * be managed with atomic operations or a mutex in the interceptor layer.\n * For now, we use a simple counter since NestJS interceptors run on the\n * event loop and the matching logic is synchronous.\n */\nexport class GcsErrorModeManager {\n private activeMode: GcsErrorModeConfig | null = null;\n private requestCounter = 0;\n\n /**\n * Activate an error mode. Replaces any existing mode (single-mode constraint).\n * Resets the request counter for intermittent-failure mode.\n */\n setMode(config: GcsErrorModeConfig): void {\n this.activeMode = config;\n this.requestCounter = 0;\n }\n\n /**\n * Deactivate the current error mode. Returns to pass-through.\n * Resets the request counter.\n */\n clearMode(): void {\n this.activeMode = null;\n this.requestCounter = 0;\n }\n\n /**\n * Get the currently active error mode configuration.\n * Returns null when no mode is active (pass-through).\n */\n getMode(): GcsErrorModeConfig | null {\n return this.activeMode;\n }\n\n /**\n * Match an incoming request against the active error mode.\n *\n * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)\n * @param path - Request path (e.g., /storage/v1/b/my-bucket/o)\n * @returns GcsErrorSimulationResult if the request matches, null for pass-through\n */\n matchRequest(method: string, path: string): GcsErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'bucket-not-found':\n return this.matchBucketNotFound(method, path, this.activeMode);\n case 'quota-exceeded':\n return this.matchQuotaExceeded(method, path, this.activeMode);\n case 'permission-denied':\n return this.matchPermissionDenied(method, path, this.activeMode);\n case 'intermittent-failure':\n return this.matchIntermittentFailure(method, path, this.activeMode);\n }\n }\n\n /**\n * Simulate a sample error for the currently active mode.\n * This produces the same GcsErrorSimulationResult that the interceptor would\n * return for a matching request, without mutating the request counter.\n *\n * Used by the GET /api/twin/gcs/error-mode endpoint so the frontend can\n * display the actual error response the backend will produce.\n */\n simulateSampleError(): GcsErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'bucket-not-found':\n // Representative: a GET to the configured bucket\n return gcsError(404, 'Not Found', 'notFound');\n case 'quota-exceeded':\n return gcsError(403, 'Quota exceeded', 'quotaExceeded');\n case 'permission-denied':\n return gcsError(403, 'Forbidden', 'forbidden');\n case 'intermittent-failure':\n return gcsError(500, 'Internal Server Error', 'internalError');\n }\n }\n\n /**\n * Reset all state: clear the active mode and reset the request counter.\n * Called by GcsTwin.reset() and GcsTwin.stop() to prevent leakage between tests.\n */\n reset(): void {\n this.activeMode = null;\n this.requestCounter = 0;\n }\n\n // ---------------------------------------------------------------------------\n // Private matching methods\n // ---------------------------------------------------------------------------\n\n private matchBucketNotFound(\n _method: string,\n path: string,\n config: BucketNotFoundConfig\n ): GcsErrorSimulationResult | null {\n const bucket = extractBucketName(path);\n if (bucket === config.bucketName) {\n return gcsError(404, 'Not Found', 'notFound');\n }\n return null;\n }\n\n private matchQuotaExceeded(\n method: string,\n path: string,\n _config: QuotaExceededConfig\n ): GcsErrorSimulationResult | null {\n if (isUploadOperation(method, path)) {\n return gcsError(403, 'Quota exceeded', 'quotaExceeded');\n }\n return null;\n }\n\n private matchPermissionDenied(\n method: string,\n path: string,\n _config: PermissionDeniedConfig\n ): GcsErrorSimulationResult | null {\n if (isGcsDataOperation(method, path)) {\n return gcsError(403, 'Forbidden', 'forbidden');\n }\n return null;\n }\n\n private matchIntermittentFailure(\n method: string,\n path: string,\n config: IntermittentFailureConfig\n ): GcsErrorSimulationResult | null {\n // Only apply to GCS data operations\n if (!isGcsDataOperation(method, path)) {\n return null;\n }\n\n this.requestCounter++;\n const every = config.every ?? 2;\n\n if (this.requestCounter % every === 0) {\n return gcsError(500, 'Internal Server Error', 'internalError');\n }\n return null;\n }\n}\n"],"names":["gcsError","status","message","reason","detailMessage","body","error","code","errors","domain","extractBucketName","path","normalized","startsWith","slice","apiGcsBucketMatch","match","decodeURIComponent","apiGcsPathMatch","patterns","pattern","undefined","isUploadOperation","method","toUpperCase","toLowerCase","includes","isGcsDataOperation","GcsErrorModeManager","setMode","config","activeMode","requestCounter","clearMode","getMode","matchRequest","type","matchBucketNotFound","matchQuotaExceeded","matchPermissionDenied","matchIntermittentFailure","simulateSampleError","reset","_method","bucket","bucketName","_config","every"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
+ import { Observable } from 'rxjs';
3
+ import { GcsErrorModeManager } from './gcs-error-mode.js';
4
+ /**
5
+ * NestJS interceptor that checks the active GCS error mode and short-circuits
6
+ * matching requests with GCS-shaped error responses.
7
+ *
8
+ * Registered as APP_INTERCEPTOR (global) but exits immediately for non-GCS routes.
9
+ * The route-prefix check is a cheap string comparison before any mode lookup.
10
+ *
11
+ * When an error mode matches, the interceptor throws an HttpException with the
12
+ * GCS error status code (403, 404, 500) so that the frontend receives a proper
13
+ * HTTP error response — not a 200 with an error body.
14
+ */
15
+ export declare class GcsErrorSimulationInterceptor implements NestInterceptor {
16
+ private readonly errorModeManager;
17
+ private readonly logger;
18
+ constructor(errorModeManager: GcsErrorModeManager);
19
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
20
+ }
21
+ //# sourceMappingURL=gcs-error-simulation.interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs-error-simulation.interceptor.d.ts","sourceRoot":"","sources":["../../src/gcs/gcs-error-simulation.interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,eAAe,EACf,gBAAgB,EAChB,WAAW,EAGZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EAAE,mBAAmB,EAAsB,MAAM,qBAAqB,CAAC;AAE9E;;;;;;;;;;GAUG;AACH,qBACa,6BAA8B,YAAW,eAAe;IAGvD,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAF7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkD;gBAE5C,gBAAgB,EAAE,mBAAmB;IAElE,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC;CAgC7E"}
@@ -0,0 +1,40 @@
1
+ import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
2
+ import { _ as _ts_metadata } from "@swc/helpers/_/_ts_metadata";
3
+ import { Injectable, Logger, HttpException } from '@nestjs/common';
4
+ import { GcsErrorModeManager, isGcsDataOperation } from './gcs-error-mode.js';
5
+ export class GcsErrorSimulationInterceptor {
6
+ intercept(context, next) {
7
+ const request = context.switchToHttp().getRequest();
8
+ const method = request.method;
9
+ const path = request.url;
10
+ // Fast exit: only intercept GCS data operation routes
11
+ if (!isGcsDataOperation(method, path)) {
12
+ return next.handle();
13
+ }
14
+ const result = this.errorModeManager.matchRequest(method, path);
15
+ if (result) {
16
+ this.logger.warn(`Error mode intercepted ${method} ${path} → ${result.status} (${result.body.error.errors[0].reason})`);
17
+ // Throw with the real HTTP status code so the client gets a proper error response
18
+ throw new HttpException(result.body, result.status);
19
+ }
20
+ // Debug-level log when a mode is active but this request passed through
21
+ const activeMode = this.errorModeManager.getMode();
22
+ if (activeMode) {
23
+ this.logger.debug(`Error mode '${activeMode.type}' active but ${method} ${path} passed through`);
24
+ }
25
+ return next.handle();
26
+ }
27
+ constructor(errorModeManager){
28
+ this.errorModeManager = errorModeManager;
29
+ this.logger = new Logger(GcsErrorSimulationInterceptor.name);
30
+ }
31
+ }
32
+ GcsErrorSimulationInterceptor = _ts_decorate([
33
+ Injectable(),
34
+ _ts_metadata("design:type", Function),
35
+ _ts_metadata("design:paramtypes", [
36
+ typeof GcsErrorModeManager === "undefined" ? Object : GcsErrorModeManager
37
+ ])
38
+ ], GcsErrorSimulationInterceptor);
39
+
40
+ //# sourceMappingURL=gcs-error-simulation.interceptor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/gcs/gcs-error-simulation.interceptor.ts"],"sourcesContent":["import {\n Injectable,\n NestInterceptor,\n ExecutionContext,\n CallHandler,\n Logger,\n HttpException,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { GcsErrorModeManager, isGcsDataOperation } from './gcs-error-mode.js';\n\n/**\n * NestJS interceptor that checks the active GCS error mode and short-circuits\n * matching requests with GCS-shaped error responses.\n *\n * Registered as APP_INTERCEPTOR (global) but exits immediately for non-GCS routes.\n * The route-prefix check is a cheap string comparison before any mode lookup.\n *\n * When an error mode matches, the interceptor throws an HttpException with the\n * GCS error status code (403, 404, 500) so that the frontend receives a proper\n * HTTP error response — not a 200 with an error body.\n */\n@Injectable()\nexport class GcsErrorSimulationInterceptor implements NestInterceptor {\n private readonly logger = new Logger(GcsErrorSimulationInterceptor.name);\n\n constructor(private readonly errorModeManager: GcsErrorModeManager) {}\n\n intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {\n const request = context\n .switchToHttp()\n .getRequest<{ method: string; url: string }>();\n const method: string = request.method;\n const path: string = request.url;\n\n // Fast exit: only intercept GCS data operation routes\n if (!isGcsDataOperation(method, path)) {\n return next.handle();\n }\n\n const result = this.errorModeManager.matchRequest(method, path);\n\n if (result) {\n this.logger.warn(\n `Error mode intercepted ${method} ${path} → ${result.status} (${result.body.error.errors[0].reason})`\n );\n // Throw with the real HTTP status code so the client gets a proper error response\n throw new HttpException(result.body, result.status);\n }\n\n // Debug-level log when a mode is active but this request passed through\n const activeMode = this.errorModeManager.getMode();\n if (activeMode) {\n this.logger.debug(\n `Error mode '${activeMode.type}' active but ${method} ${path} passed through`\n );\n }\n\n return next.handle();\n }\n}\n"],"names":["Injectable","Logger","HttpException","GcsErrorModeManager","isGcsDataOperation","GcsErrorSimulationInterceptor","intercept","context","next","request","switchToHttp","getRequest","method","path","url","handle","result","errorModeManager","matchRequest","logger","warn","status","body","error","errors","reason","activeMode","getMode","debug","type","constructor","name"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":";;AAAA,SACEA,UAAU,EAIVC,MAAM,EACNC,aAAa,QACR,iBAAiB;AAExB,SAASC,mBAAmB,EAAEC,kBAAkB,QAAQ,sBAAsB;AAc9E,OAAO,MAAMC;IAKXC,UAAUC,OAAyB,EAAEC,IAAiB,EAAuB;QAC3E,MAAMC,UAAUF,QACbG,YAAY,GACZC,UAAU;QACb,MAAMC,SAAiBH,QAAQG,MAAM;QACrC,MAAMC,OAAeJ,QAAQK,GAAG;QAEhC,sDAAsD;QACtD,IAAI,CAACV,mBAAmBQ,QAAQC,OAAO;YACrC,OAAOL,KAAKO,MAAM;QACpB;QAEA,MAAMC,SAAS,IAAI,CAACC,gBAAgB,CAACC,YAAY,CAACN,QAAQC;QAE1D,IAAIG,QAAQ;YACV,IAAI,CAACG,MAAM,CAACC,IAAI,CACd,CAAC,uBAAuB,EAAER,OAAO,CAAC,EAAEC,KAAK,GAAG,EAAEG,OAAOK,MAAM,CAAC,EAAE,EAAEL,OAAOM,IAAI,CAACC,KAAK,CAACC,MAAM,CAAC,EAAE,CAACC,MAAM,CAAC,CAAC,CAAC;YAEvG,kFAAkF;YAClF,MAAM,IAAIvB,cAAcc,OAAOM,IAAI,EAAEN,OAAOK,MAAM;QACpD;QAEA,wEAAwE;QACxE,MAAMK,aAAa,IAAI,CAACT,gBAAgB,CAACU,OAAO;QAChD,IAAID,YAAY;YACd,IAAI,CAACP,MAAM,CAACS,KAAK,CACf,CAAC,YAAY,EAAEF,WAAWG,IAAI,CAAC,aAAa,EAAEjB,OAAO,CAAC,EAAEC,KAAK,eAAe,CAAC;QAEjF;QAEA,OAAOL,KAAKO,MAAM;IACpB;IAjCAe,YAAY,AAAiBb,gBAAqC,CAAE;aAAvCA,mBAAAA;aAFZE,SAAS,IAAIlB,OAAOI,8BAA8B0B,IAAI;IAEF;AAkCvE;AArCa1B;IADZL;;;eAIgD,+CAAA;;GAHpCK"}
@@ -11,6 +11,7 @@ export declare class GcsTwin {
11
11
  private readonly docker;
12
12
  private container;
13
13
  private readonly containerName;
14
+ private dockerUnavailable;
14
15
  /**
15
16
  * Create a new GCS twin.
16
17
  *