C1 (Critical): getStudioSession was stamped with the same TECH_LOG_STUDIO_SESSION auth profile as every other Studio operation, and that profile requires the CSRF header it is getStudioSession's own job to issue -- an unconditional cycle that recursed without bound in HTTP mode. Fixed with a credential-free TECH_LOG_STUDIO_BOOTSTRAP auth profile for getStudioSession alone, a synchronous re-entrancy guard in createCsrfTokenProvider as defense in depth, and a throwing stub in place of the prior `let x!: T` assertion. Added a composition-level regression test that wires the real executor, CSRF provider, and credential-attach function together and proves getStudioSession dispatches exactly once while its token reaches both a JSON operation and the multipart upload. Also: invalidate the cached CSRF token on a 401/403 from the upload path (I2), a throwing useStudioAssetGateway() accessor so Task 11 cannot silently compile a null-gateway UI (I3), and the M1-M5 minors from the review (guard a malformed success body, cover the untested error fallbacks, align aborted uploads with the JSON path's non-retryable CANCELLED mapping, derive the credential header name from one source instead of two, and correct the adapter review doc's operation count). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
66 lines
2.2 KiB
TypeScript
66 lines
2.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import assert from "node:assert/strict";
|
|
import { renderHook } from "@testing-library/react";
|
|
import type { ReactNode } from "react";
|
|
import { test } from "vitest";
|
|
|
|
import type { StudioAssetGateway } from "../../../src/features/tech-log/application/ports/studio-asset-gateway.ts";
|
|
import {
|
|
StudioContext,
|
|
useStudioAssetGateway,
|
|
type StudioContextValue,
|
|
} from "../../../src/features/tech-log/presentation/studio/use-studio.ts";
|
|
|
|
/**
|
|
* I3 (Task 7 fix round 1). `StudioContextValue.assetGateway` stays nullable
|
|
* so the many test harnesses that render `StudioProvider` without a
|
|
* `createAssetGateway` prop keep compiling — but that nullability would let
|
|
* Asset UI (Task 11) write `if (!assetGateway) return null` and ship a
|
|
* confusingly empty screen with the type checker satisfied. This pins the
|
|
* throwing accessor Asset UI must use instead.
|
|
*/
|
|
function contextValue(
|
|
assetGateway: StudioAssetGateway | null,
|
|
): StudioContextValue {
|
|
return {
|
|
gateway: {} as never,
|
|
assetGateway,
|
|
resolvePublishedLabel: () => undefined,
|
|
now: () => new Date("2026-08-14T01:00:00.000Z"),
|
|
editor: null,
|
|
requestAnnouncement: "",
|
|
setRequestAnnouncement: () => {},
|
|
navigateInternal: () => {},
|
|
beginEditor: () => {},
|
|
updateEditorDraft: () => {},
|
|
setEditorStatus: () => {},
|
|
clearEditor: () => {},
|
|
};
|
|
}
|
|
|
|
function wrapperFor(assetGateway: StudioAssetGateway | null) {
|
|
return function Wrapper({ children }: { children: ReactNode }) {
|
|
return (
|
|
<StudioContext.Provider value={contextValue(assetGateway)}>
|
|
{children}
|
|
</StudioContext.Provider>
|
|
);
|
|
};
|
|
}
|
|
|
|
test("returns the asset gateway when the provider supplied one", () => {
|
|
const assetGateway = {} as StudioAssetGateway;
|
|
const { result } = renderHook(() => useStudioAssetGateway(), {
|
|
wrapper: wrapperFor(assetGateway),
|
|
});
|
|
assert.equal(result.current, assetGateway);
|
|
});
|
|
|
|
test("throws a clear error instead of returning null when no asset gateway was supplied", () => {
|
|
assert.throws(
|
|
() => renderHook(() => useStudioAssetGateway(), { wrapper: wrapperFor(null) }),
|
|
/useStudioAssetGateway must be used within a StudioProvider that was given a createAssetGateway prop/,
|
|
);
|
|
});
|