Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 165 additions & 25 deletions apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/co
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
Expand All @@ -18,12 +20,20 @@ import * as DesktopEnvironment from "./DesktopEnvironment.ts";

const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const catalogPathFor = (path: Path.Path, stateDir: string) =>
path.join(stateDir, "connection-catalog.json");
const stateDirFor = (path: Path.Path, baseDir: string) => path.join(baseDir, "userdata");
const decodeConnectionCatalog = Schema.decodeEffect(
Schema.fromJsonString(ConnectionCatalogDocument),
);
function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean> | null = null) {
function makeSafeStorageLayer(
available:
| boolean
| Effect.Effect<boolean, ElectronSafeStorage.ElectronSafeStorageAvailabilityError>,
failDecrypt: Ref.Ref<boolean> | null = null,
) {
return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, {
isEncryptionAvailable: Effect.succeed(available),
isEncryptionAvailable: typeof available === "boolean" ? Effect.succeed(available) : available,
encryptString: (value) => Effect.succeed(textEncoder.encode(`encrypted:${value}`)),
decryptString: (value) => {
return Effect.gen(function* () {
Expand All @@ -45,7 +55,9 @@ function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean>

function makeLayer(
baseDir: string,
encryptionAvailable = true,
encryptionAvailable:
| boolean
| Effect.Effect<boolean, ElectronSafeStorage.ElectronSafeStorageAvailabilityError> = true,
failDecrypt: Ref.Ref<boolean> | null = null,
fileSystemLayer: Layer.Layer<FileSystem.FileSystem> = NodeServices.layer,
) {
Expand Down Expand Up @@ -223,39 +235,118 @@ describe("DesktopConnectionCatalogStore", () => {
),
);

it.effect("surfaces malformed catalog documents without deleting them", () =>
it.effect("quarantines malformed catalog documents and treats them as missing", () =>
withStore(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
const catalogPath = `${environment.stateDir}/connection-catalog.json`;
const catalogPath = catalogPathFor(path, environment.stateDir);
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.writeFileString(catalogPath, "{not-json");

const error = yield* store.get.pipe(Effect.flip);
assert.instanceOf(
error,
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDocumentDecodeError,
const corruptDocuments = ["\0".repeat(64), '{"version":1}'] as const;
yield* fileSystem.writeFileString(catalogPath, corruptDocuments[0]);
assert.deepStrictEqual(yield* store.get, Option.none());
yield* fileSystem.writeFileString(catalogPath, corruptDocuments[1]);
assert.deepStrictEqual(yield* store.get, Option.none());

const quarantineNames = (yield* fileSystem.readDirectory(environment.stateDir)).filter(
(name) => name.startsWith("connection-catalog.json.corrupt."),
);
assert.equal(error.catalogPath, catalogPath);
assert.exists(error.cause);
assert.equal(yield* fileSystem.readFileString(catalogPath), "{not-json");
assert.lengthOf(quarantineNames, 2);
assert.notEqual(quarantineNames[0], quarantineNames[1]);
quarantineNames.forEach((name) =>
assert.match(name, /^connection-catalog\.json\.corrupt\.\d+\.[a-f0-9]{32}$/),
);
const quarantinedContents = yield* Effect.all(
quarantineNames.map((name) =>
fileSystem.readFileString(path.join(environment.stateDir, name)),
),
);
assert.deepEqual(quarantinedContents.sort(), [...corruptDocuments].sort());
assert.isFalse(yield* fileSystem.exists(catalogPath));
}),
),
);

it.effect("continues when a malformed catalog cannot be quarantined", () =>
Effect.gen(function* () {
const baseFileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-connection-catalog-test-",
});
const catalogPath = catalogPathFor(path, stateDirFor(path, baseDir));
const quarantineError = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "rename",
pathOrDescriptor: catalogPath,
});
yield* baseFileSystem.makeDirectory(stateDirFor(path, baseDir), { recursive: true });
yield* baseFileSystem.writeFileString(catalogPath, "{not-json");
const fileSystemLayer = Layer.succeed(FileSystem.FileSystem, {
...baseFileSystem,
rename: (oldPath, newPath) =>
String(oldPath) === catalogPath && String(newPath).startsWith(`${catalogPath}.corrupt.`)
? Effect.fail(quarantineError)
: baseFileSystem.rename(oldPath, newPath),
} satisfies FileSystem.FileSystem);
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
Effect.provide(makeLayer(baseDir, true, null, fileSystemLayer)),
);
const logMessages: Array<unknown> = [];
const logger = Logger.make(({ message }) => {
logMessages.push(message);
});

assert.deepStrictEqual(
yield* store.get.pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))),
Option.none(),
);
assert.equal(yield* baseFileSystem.readFileString(catalogPath), "{not-json");
assert.lengthOf(logMessages, 1);
const logMessage = logMessages[0];
if (!Array.isArray(logMessage)) {
return assert.fail("expected structured warning arguments");
}
assert.equal(
logMessage[0],
"Could not quarantine a corrupt desktop connection catalog; continuing without it.",
);
const logDetails = logMessage[1];
if (logDetails === null || typeof logDetails !== "object") {
return assert.fail("expected structured warning metadata");
}
assert.sameMembers(Object.keys(logDetails), [
"catalogPath",
"decodeError",
"quarantineError",
]);
assert.equal("catalogPath" in logDetails ? logDetails.catalogPath : undefined, catalogPath);
const decodeError = "decodeError" in logDetails ? logDetails.decodeError : undefined;
const loggedQuarantineError =
"quarantineError" in logDetails ? logDetails.quarantineError : undefined;
assert.isString(decodeError);
assert.isString(loggedQuarantineError);
assert.notInclude(decodeError, "{not-json");
assert.notInclude(loggedQuarantineError, "{not-json");
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);

it.effect("surfaces catalog filesystem failures instead of treating them as missing", () =>
Effect.gen(function* () {
const baseFileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-connection-catalog-test-",
});
const permissionError = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "readFileString",
pathOrDescriptor: `${baseDir}/userdata/connection-catalog.json`,
pathOrDescriptor: catalogPathFor(path, stateDirFor(path, baseDir)),
});
const fileSystemLayer = Layer.succeed(
FileSystem.FileSystem,
Expand All @@ -272,11 +363,11 @@ describe("DesktopConnectionCatalogStore", () => {
error,
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,
);
assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);
assert.equal(error.catalogPath, catalogPathFor(path, stateDirFor(path, baseDir)));
assert.strictEqual(error.cause, permissionError);
assert.equal(
error.message,
`Failed to read the desktop connection catalog at ${baseDir}/userdata/connection-catalog.json.`,
`Failed to read the desktop connection catalog at ${catalogPathFor(path, stateDirFor(path, baseDir))}.`,
);
assert.notEqual(error.message, permissionError.message);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
Expand All @@ -285,14 +376,15 @@ describe("DesktopConnectionCatalogStore", () => {
it.effect("reports the failed catalog write operation and path", () =>
Effect.gen(function* () {
const baseFileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* baseFileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-connection-catalog-test-",
});
const permissionError = PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "makeDirectory",
pathOrDescriptor: `${baseDir}/userdata`,
pathOrDescriptor: stateDirFor(path, baseDir),
});
const fileSystemLayer = Layer.succeed(
FileSystem.FileSystem,
Expand All @@ -310,11 +402,11 @@ describe("DesktopConnectionCatalogStore", () => {
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreWriteError,
);
assert.equal(error.operation, "create-directory");
assert.equal(error.path, `${baseDir}/userdata`);
assert.equal(error.path, stateDirFor(path, baseDir));
assert.strictEqual(error.cause, permissionError);
assert.equal(
error.message,
`Desktop connection catalog write failed during create-directory at ${baseDir}/userdata.`,
`Desktop connection catalog write failed during create-directory at ${stateDirFor(path, baseDir)}.`,
);
assert.notEqual(error.message, permissionError.message);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
Expand All @@ -325,6 +417,7 @@ describe("DesktopConnectionCatalogStore", () => {
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.writeFileString(environment.savedEnvironmentRegistryPath, "{not-json");
Expand All @@ -335,7 +428,7 @@ describe("DesktopConnectionCatalogStore", () => {
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreMigrationError,
);
assert.equal(error.operation, "read-legacy-registry");
assert.equal(error.catalogPath, `${environment.stateDir}/connection-catalog.json`);
assert.equal(error.catalogPath, catalogPathFor(path, environment.stateDir));
assert.instanceOf(
error.cause,
DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError,
Expand All @@ -345,7 +438,7 @@ describe("DesktopConnectionCatalogStore", () => {
assert.exists(registryError.cause);
assert.equal(
error.message,
`Legacy desktop saved-environment migration failed during read-legacy-registry into ${environment.stateDir}/connection-catalog.json.`,
`Legacy desktop saved-environment migration failed during read-legacy-registry into ${catalogPathFor(path, environment.stateDir)}.`,
);
assert.notEqual(error.message, registryError.message);
}),
Expand All @@ -357,10 +450,12 @@ describe("DesktopConnectionCatalogStore", () => {
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
const catalogPath = `${environment.stateDir}/connection-catalog.json`;
const catalogPath = catalogPathFor(path, environment.stateDir);
yield* fileSystem.makeDirectory(environment.stateDir, { recursive: true });
yield* fileSystem.writeFileString(catalogPath, '{"version":1,"encryptedCatalog":"%%%"}\n');
const document = '{"version":1,"encryptedCatalog":"%%%"}\n';
yield* fileSystem.writeFileString(catalogPath, document);

const error = yield* store.get.pipe(Effect.flip);
assert.instanceOf(
Expand All @@ -375,13 +470,58 @@ describe("DesktopConnectionCatalogStore", () => {
`Failed to decode encryptedCatalog for the desktop connection catalog at ${catalogPath}.`,
);
assert.notInclude(error.message, "%%%");
assert.equal(yield* fileSystem.readFileString(catalogPath), document);
assert.lengthOf(
(yield* fileSystem.readDirectory(environment.stateDir)).filter((name) =>
name.startsWith("connection-catalog.json.corrupt."),
),
0,
);
}),
),
);

it.effect("surfaces secure-storage availability failures without quarantining the catalog", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-connection-catalog-test-",
});
const availabilityError = new ElectronSafeStorage.ElectronSafeStorageAvailabilityError({
cause: new Error("safe storage unavailable"),
});
const layer = makeLayer(baseDir, Effect.fail(availabilityError));
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
Effect.provide(layer),
);
const stateDir = stateDirFor(path, baseDir);
const catalogPath = catalogPathFor(path, stateDir);
const document = '{"version":1,"encryptedCatalog":"ZW5jcnlwdGVkOnt9"}\n';
yield* fileSystem.makeDirectory(stateDir, { recursive: true });
yield* fileSystem.writeFileString(catalogPath, document);

const error = yield* store.get.pipe(Effect.flip);
assert.instanceOf(
error,
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError,
);
assert.equal(error.operation, "check-encryption-availability");
assert.strictEqual(error.cause, availabilityError);
assert.equal(yield* fileSystem.readFileString(catalogPath), document);
assert.lengthOf(
(yield* fileSystem.readDirectory(stateDir)).filter((name) =>
name.startsWith("connection-catalog.json.corrupt."),
),
0,
);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);

it.effect("surfaces a catalog that can no longer be decrypted without deleting it", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-connection-catalog-test-",
});
Expand All @@ -399,14 +539,14 @@ describe("DesktopConnectionCatalogStore", () => {
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError,
);
assert.equal(error.operation, "decrypt-catalog");
assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);
assert.equal(error.catalogPath, catalogPathFor(path, stateDirFor(path, baseDir)));
assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageDecryptError);
const decryptError = error.cause as ElectronSafeStorage.ElectronSafeStorageDecryptError;
assert.instanceOf(decryptError.cause, Error);
assert.equal(decryptError.cause.message, "invalid encrypted catalog");
assert.equal(
error.message,
`Desktop connection catalog protection failed during decrypt-catalog at ${baseDir}/userdata/connection-catalog.json.`,
`Desktop connection catalog protection failed during decrypt-catalog at ${catalogPathFor(path, stateDirFor(path, baseDir))}.`,
);
assert.notEqual(error.message, decryptError.message);
yield* Ref.set(failDecrypt, false);
Expand Down
31 changes: 30 additions & 1 deletion apps/desktop/src/app/DesktopConnectionCatalogStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,38 @@ export const make = Effect.gen(function* () {
return Option.some(encoded);
});

const readRecoverableDocument = readDocument(fileSystem, catalogPath).pipe(
Effect.catchTag("DesktopConnectionCatalogStoreDocumentDecodeError", (decodeError) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statically known tagged failures should be recovered with Effect.catchTags({ ... }), even when only one tag is handled; Effect.catchTag isn't used in this codebase's conventions.

Consider switching to the object form:

-    Effect.catchTag("DesktopConnectionCatalogStoreDocumentDecodeError", (decodeError) =>
-      Effect.gen(function* () {
+    Effect.catchTags({
+      DesktopConnectionCatalogStoreDocumentDecodeError: (decodeError) =>
+        Effect.gen(function* () {

and close it with }), → }, plus the matching brace for the object (the body indentation shifts one level).

Posted via Macroscope — Effect Service Conventions

Effect.gen(function* () {
yield* Effect.gen(function* () {
const suffix = (yield* crypto.randomUUIDv4).replace(/-/g, "");
const quarantinePath = `${catalogPath}.corrupt.${process.pid}.${suffix}`;
yield* fileSystem.rename(catalogPath, quarantinePath);
yield* Effect.logWarning("Quarantined a corrupt desktop connection catalog.", {
catalogPath,
quarantinePath,
decodeError: decodeError.message,
});
}).pipe(
Effect.catch((quarantineError) =>
Effect.logWarning(
"Could not quarantine a corrupt desktop connection catalog; continuing without it.",
{
catalogPath,
decodeError: decodeError.message,
quarantineError: String(quarantineError),
},
),
),
);
return Option.none<EncryptedConnectionCatalogDocument>();
}),
),
);

return DesktopConnectionCatalogStore.of({
get: Effect.gen(function* () {
const document = yield* readDocument(fileSystem, catalogPath);
const document = yield* readRecoverableDocument;
if (Option.isNone(document)) {
return yield* migrateLegacyCatalog;
}
Expand Down
Loading