| ... | ... | @@ -2,6 +2,7 @@ |
| 2 | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 3 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; |
| 4 | 4 | import * as NodeFSP from "node:fs/promises"; |
| 5 | import * as NodeOS from "node:os"; |
| 5 | 6 | import { AssetAccessError, AssetPreviewTypeValidationError, ThreadId } from "@t3tools/contracts"; |
| 6 | 7 | import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; |
| 7 | 8 | import { describe, expect, it } from "@effect/vitest"; |
| ... | ... | @@ -35,6 +36,11 @@ vi.mock("node:fs/promises", async (importOriginal) => { |
| 35 | 36 | return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) }; |
| 36 | 37 | }); |
| 37 | 38 | |
| 39 | vi.mock("node:os", async (importOriginal) => { |
| 40 | const actual = await importOriginal<typeof NodeOS>(); |
| 41 | return { ...actual, homedir: vi.fn(actual.homedir) }; |
| 42 | }); |
| 43 | |
| 38 | 44 | const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { |
| 39 | 45 | prefix: "t3-asset-access-test-", |
| 40 | 46 | }); |
| ... | ... | @@ -183,6 +189,55 @@ describe("AssetAccess", () => { |
| 183 | 189 | }).pipe(Effect.provide(testLayer)), |
| 184 | 190 | ); |
| 185 | 191 | |
| 192 | it.effect("resolves home-relative media paths independently of the workspace", () => |
| 193 | Effect.gen(function* () { |
| 194 | const fs = yield* FileSystem.FileSystem; |
| 195 | const path = yield* Path.Path; |
| 196 | const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-home-" }); |
| 197 | const home = path.join(directory, "var", "home", "alice"); |
| 198 | const filePath = path.join(home, "Downloads", "repro.mp4"); |
| 199 | yield* fs.makeDirectory(path.dirname(filePath), { recursive: true }); |
| 200 | yield* fs.writeFileString(filePath, "recording bytes"); |
| 201 | const canonicalFile = yield* fs.realPath(filePath); |
| 202 | const homeSpy = vi.mocked(NodeOS.homedir).mockReturnValue(home); |
| 203 | try { |
| 204 | for (const workspaceRoot of [ |
| 205 | path.join(home, "project"), |
| 206 | path.join(directory, "srv", "project"), |
| 207 | undefined, |
| 208 | ]) { |
| 209 | if (workspaceRoot) yield* fs.makeDirectory(workspaceRoot, { recursive: true }); |
| 210 | for (const requestedPath of ["~/Downloads/repro.mp4", "~\\Downloads/repro.mp4"]) { |
| 211 | const result = yield* issueAssetUrl({ |
| 212 | resource: { |
| 213 | _tag: "media-file", |
| 214 | threadId: ThreadId.make("thread-1"), |
| 215 | path: requestedPath, |
| 216 | }, |
| 217 | ...(workspaceRoot ? { workspaceRoot } : {}), |
| 218 | }); |
| 219 | const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); |
| 220 | const separator = suffix.indexOf("/"); |
| 221 | const asset = yield* resolveAsset( |
| 222 | suffix.slice(0, separator), |
| 223 | suffix.slice(separator + 1), |
| 224 | ); |
| 225 | expect(asset).toMatchObject({ |
| 226 | kind: "file", |
| 227 | path: canonicalFile, |
| 228 | mimeType: "video/mp4", |
| 229 | }); |
| 230 | if (asset?.kind !== "file") throw new Error("Expected a resolved home media file"); |
| 231 | const response = HttpServerResponse.toWeb(yield* assetFileResponse(asset)); |
| 232 | expect(yield* Effect.promise(() => response.text())).toBe("recording bytes"); |
| 233 | } |
| 234 | } |
| 235 | } finally { |
| 236 | homeSpy.mockRestore(); |
| 237 | } |
| 238 | }).pipe(Effect.provide(testLayer)), |
| 239 | ); |
| 240 | |
| 186 | 241 | it.effect.skipIf(!symlinksSupported)( |
| 187 | 242 | "rejects non-previewable files, disguised targets, and directories", |
| 188 | 243 | () => |