| ... | @@ -2,6 +2,7 @@ | ... | @@ -2,6 +2,7 @@ |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; | 3 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; |
| 4 | import * as NodeFSP from "node:fs/promises"; | 4 | import * as NodeFSP from "node:fs/promises"; |
| | 5 | import * as NodeOS from "node:os"; |
| 5 | import { AssetAccessError, AssetPreviewTypeValidationError, ThreadId } from "@t3tools/contracts"; | 6 | import { AssetAccessError, AssetPreviewTypeValidationError, ThreadId } from "@t3tools/contracts"; |
| 6 | import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; | 7 | import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; |
| 7 | import { describe, expect, it } from "@effect/vitest"; | 8 | import { describe, expect, it } from "@effect/vitest"; |
| ... | @@ -35,6 +36,11 @@ vi.mock("node:fs/promises", async (importOriginal) => { | ... | @@ -35,6 +36,11 @@ vi.mock("node:fs/promises", async (importOriginal) => { |
| 35 | return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) }; | 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 | const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | 44 | const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { |
| 39 | prefix: "t3-asset-access-test-", | 45 | prefix: "t3-asset-access-test-", |
| 40 | }); | 46 | }); |
| ... | @@ -183,6 +189,55 @@ describe("AssetAccess", () => { | ... | @@ -183,6 +189,55 @@ describe("AssetAccess", () => { |
| 183 | }).pipe(Effect.provide(testLayer)), | 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 | it.effect.skipIf(!symlinksSupported)( | 241 | it.effect.skipIf(!symlinksSupported)( |
| 187 | "rejects non-previewable files, disguised targets, and directories", | 242 | "rejects non-previewable files, disguised targets, and directories", |
| 188 | () => | 243 | () => |