Skip to content

Commit 9778ca8

Browse files
committed
test(client): re-pin #6714 case B — a custom dataPrefix no longer makes the base underivable
The case-B pin asserted the limitation this change removes: with a custom `crud.dataPrefix`, `_apiBase()` could not find the base, so the scoped client fell back to the `/api/v1` convention. That fallback was never RIGHT on such a deployment -- it is a 404 -- it was only honest, which is why it was pinned rather than fixed. `_dataPrefix()` now recovers the split from a second advertised value rather than a looser parse of the first, so the base IS derivable here and B1 pins the path the server actually mounts. The decline leg the old case carried is kept, on the two shapes that are genuinely still underivable: no `routes.metadata` at all (B2), and a `routes.metadata` that shares no base with `routes.data` and so supplies no equation (B3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
1 parent 78aef4f commit 9778ca8

1 file changed

Lines changed: 51 additions & 5 deletions

File tree

packages/client/src/client.test.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2129,16 +2129,62 @@ describe('ScopedEnvironmentClient', () => {
21292129
expect(String(fetchMock.mock.calls[4][0])).toBe(`${base}/batch`);
21302130
});
21312131

2132-
it('[#6714] a custom dataPrefix makes the base underivable — the convention holds, byte-identical (case B)', async () => {
2132+
it('[#14879] a custom dataPrefix no longer makes the base underivable — `routes.metadata` is the second equation (case B1)', async () => {
21332133
const { client, fetchMock } = createMockClient({ types: [] });
2134-
// routes.data does not end with the conventional `/data`, so the base
2135-
// cannot be derived honestly; the client must NOT guess (contract-first
2136-
// — no lenient re-parsing) and falls back to the convention,
2137-
// byte-identical to the pre-#6714 behavior.
2134+
// WAS pinned the other way. Until #14879 this case asserted the
2135+
// convention `/api/v1/...`, because the only suffix `_apiBase()` knew
2136+
// how to strip was the literal `/data`, so a custom `crud.dataPrefix`
2137+
// made the base undetectable and the client fell back.
2138+
//
2139+
// That fallback was never RIGHT on this deployment — it is a 404; it
2140+
// was merely honest, which is why it was pinned rather than fixed.
2141+
// `_dataPrefix()` now recovers the split without guessing:
2142+
// `routes.metadata` is `{realBase}{metadata.prefix}` over the SAME
2143+
// base, so the two advertised routes share exactly `realBase` plus
2144+
// whatever their prefixes share, and cutting that common run back to
2145+
// its last `/` lands on the boundary. Here that yields `/records`, the
2146+
// base `/backend/api/v9`, and the path the server actually mounts.
2147+
//
2148+
// Contract-first is unchanged: this reads a second ADVERTISED value,
2149+
// it does not loosen the parse of the first.
21382150
(client as any)['discoveryInfo'] = {
21392151
routes: { data: '/backend/api/v9/records', metadata: '/backend/api/v9/meta' },
21402152
};
21412153
await client.environment('proj-123').meta.getTypes();
2154+
expect(String(fetchMock.mock.calls[0][0])).toBe(
2155+
'http://localhost:3000/backend/api/v9/environments/proj-123/meta',
2156+
);
2157+
});
2158+
2159+
it('[#14879] a custom dataPrefix with no second equation still declines to the convention (case B2)', async () => {
2160+
const { client, fetchMock } = createMockClient({ types: [] });
2161+
// The decline leg the case above used to carry, kept alive on the
2162+
// shape that is genuinely still underivable: `routes.data` does not end
2163+
// with the conventional `/data` AND there is no `routes.metadata` to
2164+
// supply the missing equation, so `{realBase}{dataPrefix}` stays one
2165+
// string with two unknowns. The client must NOT guess a split — it
2166+
// falls back to the convention, byte-identical to the pre-#14879
2167+
// behavior.
2168+
(client as any)['discoveryInfo'] = {
2169+
routes: { data: '/backend/api/v9/records' },
2170+
};
2171+
await client.environment('proj-123').meta.getTypes();
2172+
expect(String(fetchMock.mock.calls[0][0])).toBe(
2173+
'http://localhost:3000/api/v1/environments/proj-123/meta',
2174+
);
2175+
});
2176+
2177+
it('[#14879] two advertised routes sharing no base decline rather than mistake the whole path for a prefix (case B3)', async () => {
2178+
const { client, fetchMock } = createMockClient({ types: [] });
2179+
// `routes.metadata` present but NOT substituted from this deployment's
2180+
// base (its endpoints are off, so it still carries the conventional
2181+
// literal). The two routes then share nothing but the leading `/`,
2182+
// which is not a shared `realBase` — deriving from it would hand back
2183+
// the whole of `routes.data` as the prefix. Decline instead.
2184+
(client as any)['discoveryInfo'] = {
2185+
routes: { data: '/backend/api/v9/records', metadata: '/api/v1/meta' },
2186+
};
2187+
await client.environment('proj-123').meta.getTypes();
21422188
expect(String(fetchMock.mock.calls[0][0])).toBe(
21432189
'http://localhost:3000/api/v1/environments/proj-123/meta',
21442190
);

0 commit comments

Comments
 (0)