mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-09 15:15:58 +08:00
impr(ls with schema): verify schema after migration
also improve tests !nuf
This commit is contained in:
parent
38aa79a350
commit
13457cf6c7
2 changed files with 79 additions and 10 deletions
|
@ -64,6 +64,7 @@ describe("local-storage-with-schema.ts", () => {
|
|||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(localStorage.setItem).not.toHaveBeenCalled();
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
|
@ -74,6 +75,7 @@ describe("local-storage-with-schema.ts", () => {
|
|||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith("config");
|
||||
expect(localStorage.setItem).not.toHaveBeenCalled();
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
|
@ -83,6 +85,7 @@ describe("local-storage-with-schema.ts", () => {
|
|||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(localStorage.setItem).not.toHaveBeenCalled();
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
|
@ -97,30 +100,79 @@ describe("local-storage-with-schema.ts", () => {
|
|||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
"config",
|
||||
JSON.stringify(defaultObject)
|
||||
);
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
|
||||
it("should migrate (when function is provided) if schema failed", () => {
|
||||
getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" }));
|
||||
const existingValue = { hi: "hello" };
|
||||
|
||||
getItemMock.mockReturnValue(JSON.stringify(existingValue));
|
||||
|
||||
const migrated = {
|
||||
punctuation: false,
|
||||
mode: "time",
|
||||
fontSize: 1,
|
||||
};
|
||||
|
||||
const migrateFnMock = vi.fn(() => migrated as any);
|
||||
|
||||
const ls = new LocalStorageWithSchema({
|
||||
key: "config",
|
||||
schema: objectSchema,
|
||||
fallback: defaultObject,
|
||||
migrate: () => {
|
||||
return migrated;
|
||||
},
|
||||
migrate: migrateFnMock,
|
||||
});
|
||||
|
||||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(migrateFnMock).toHaveBeenCalledWith(
|
||||
existingValue,
|
||||
expect.any(Array)
|
||||
);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
"config",
|
||||
JSON.stringify(migrated)
|
||||
);
|
||||
expect(res).toEqual(migrated);
|
||||
});
|
||||
|
||||
it("should revert to fallback if migration ran but schema still failed", () => {
|
||||
const existingValue = { hi: "hello" };
|
||||
|
||||
getItemMock.mockReturnValue(JSON.stringify(existingValue));
|
||||
|
||||
const invalidMigrated = {
|
||||
punctuation: 1,
|
||||
mode: "time",
|
||||
fontSize: 1,
|
||||
};
|
||||
|
||||
const migrateFnMock = vi.fn(() => invalidMigrated as any);
|
||||
|
||||
const ls = new LocalStorageWithSchema({
|
||||
key: "config",
|
||||
schema: objectSchema,
|
||||
fallback: defaultObject,
|
||||
migrate: migrateFnMock,
|
||||
});
|
||||
|
||||
const res = ls.get();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("config");
|
||||
expect(migrateFnMock).toHaveBeenCalledWith(
|
||||
existingValue,
|
||||
expect.any(Array)
|
||||
);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
"config",
|
||||
JSON.stringify(defaultObject)
|
||||
);
|
||||
expect(res).toEqual(defaultObject);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ZodIssue } from "zod";
|
||||
import { ZodError, ZodIssue } from "zod";
|
||||
|
||||
export class LocalStorageWithSchema<T> {
|
||||
private key: string;
|
||||
|
@ -29,7 +29,7 @@ export class LocalStorageWithSchema<T> {
|
|||
try {
|
||||
jsonParsed = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
console.log(
|
||||
`Value from localStorage ${this.key} was not a valid JSON, using fallback`,
|
||||
e
|
||||
);
|
||||
|
@ -43,12 +43,25 @@ export class LocalStorageWithSchema<T> {
|
|||
return schemaParsed.data;
|
||||
}
|
||||
|
||||
console.error(
|
||||
console.log(
|
||||
`Value from localStorage ${this.key} failed schema validation, migrating`,
|
||||
schemaParsed.error
|
||||
);
|
||||
const newValue =
|
||||
this.migrate?.(jsonParsed, schemaParsed.error.issues) ?? this.fallback;
|
||||
|
||||
let newValue = this.fallback;
|
||||
if (this.migrate) {
|
||||
const migrated = this.migrate(jsonParsed, schemaParsed.error.issues);
|
||||
const parse = this.schema.safeParse(migrated);
|
||||
if (parse.success) {
|
||||
newValue = migrated;
|
||||
} else {
|
||||
console.error(
|
||||
`Value from localStorage ${this.key} failed schema validation after migration! This is very bad!`,
|
||||
parse.error.issues
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.localStorage.setItem(this.key, JSON.stringify(newValue));
|
||||
return newValue;
|
||||
}
|
||||
|
@ -59,7 +72,11 @@ export class LocalStorageWithSchema<T> {
|
|||
window.localStorage.setItem(this.key, JSON.stringify(parsed));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(`Failed to set ${this.key} in localStorage`, e);
|
||||
console.error(
|
||||
`Failed to set ${this.key} in localStorage`,
|
||||
data,
|
||||
(e as ZodError).issues
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue