Skip to content

Commit 6f6af5e

Browse files
authored
fix(presets): move migration to Config.apply (@fehmer) (monkeytypegame#6814)
1 parent b63b073 commit 6f6af5e

File tree

6 files changed

+106
-47
lines changed

6 files changed

+106
-47
lines changed

frontend/__tests__/root/config.spec.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,6 @@ describe("Config", () => {
685685
it("setAds", () => {
686686
expect(Config.setAds("on")).toBe(true);
687687
expect(Config.setAds("sellout")).toBe(true);
688-
expect(Config.setAds("invalid" as any)).toBe(false);
689688
});
690689
it("setRepeatQuotes", () => {
691690
expect(Config.setRepeatQuotes("off")).toBe(true);
@@ -1098,9 +1097,9 @@ describe("Config", () => {
10981097
});
10991098

11001099
describe("apply", () => {
1101-
it("should fill missing values with defaults", () => {
1100+
it("should fill missing values with defaults", async () => {
11021101
//GIVEN
1103-
Config.apply({
1102+
await Config.apply({
11041103
numbers: true,
11051104
punctuation: true,
11061105
});
@@ -1124,18 +1123,29 @@ describe("Config", () => {
11241123
},
11251124
{
11261125
display: "mode incompatible with funbox",
1127-
value: { mode: "quote", funbox: ["58008"] as any },
1126+
value: { mode: "quote", funbox: ["58008"] },
11281127
expected: { funbox: [] },
11291128
},
11301129
{
1131-
display: "invalid customLayoutfluid",
1132-
value: { funbox: ["58008", "gibberish"] as any },
1130+
display: "invalid combination of funboxes",
1131+
value: { funbox: ["58008", "gibberish"] },
11331132
expected: { funbox: [] },
11341133
},
1134+
{
1135+
display: "sanitizes config, remove extra keys",
1136+
value: { mode: "zen", unknownKey: true, unknownArray: [1, 2] } as any,
1137+
expected: { mode: "zen" },
1138+
},
1139+
{
1140+
display: "applies config migration",
1141+
value: { mode: "zen", swapEscAndTab: true } as any,
1142+
expected: { mode: "zen", quickRestart: "esc" },
1143+
},
11351144
];
11361145

1137-
it.each(testCases)("$display", ({ value, expected }) => {
1138-
Config.apply(value);
1146+
it.each(testCases)("$display", async ({ value, expected }) => {
1147+
await Config.apply(value);
1148+
11391149
const config = getConfig();
11401150
const applied = Object.fromEntries(
11411151
Object.entries(config).filter(([key]) =>
@@ -1159,8 +1169,8 @@ describe("Config", () => {
11591169
},
11601170
];
11611171

1162-
it.each(testCases)("$display", ({ value, expected }) => {
1163-
Config.apply(value);
1172+
it.each(testCases)("$display", async ({ value, expected }) => {
1173+
await Config.apply(value);
11641174
const config = getConfig();
11651175
const applied = Object.fromEntries(
11661176
Object.entries(config).filter(([key]) =>
@@ -1171,29 +1181,31 @@ describe("Config", () => {
11711181
});
11721182
});
11731183

1174-
it("should apply a partial config but keep the rest unchanged", () => {
1184+
it("should apply a partial config but keep the rest unchanged", async () => {
11751185
replaceConfig({
11761186
numbers: true,
11771187
});
1178-
Config.apply({
1188+
await Config.apply({
11791189
punctuation: true,
11801190
});
11811191
const config = getConfig();
11821192
expect(config.numbers).toBe(true);
11831193
});
11841194

1185-
it("should reset all values to default if fullReset is true", () => {
1195+
it("should reset all values to default if fullReset is true", async () => {
11861196
replaceConfig({
11871197
numbers: true,
1198+
theme: "serika",
11881199
});
1189-
Config.apply(
1200+
await Config.apply(
11901201
{
11911202
punctuation: true,
11921203
},
11931204
true
11941205
);
11951206
const config = getConfig();
11961207
expect(config.numbers).toBe(false);
1208+
expect(config.theme).toEqual("serika_dark");
11971209
});
11981210
});
11991211
});

frontend/__tests__/utils/misc.spec.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,14 @@ describe("misc.ts", () => {
231231
});
232232
});
233233
describe("sanitize function", () => {
234-
const schema = z.object({
235-
name: z.string(),
236-
age: z.number().positive(),
237-
tags: z.array(z.string()),
238-
});
234+
const schema = z
235+
.object({
236+
name: z.string(),
237+
age: z.number().positive(),
238+
tags: z.array(z.string()),
239+
})
240+
.partial()
241+
.strip();
239242

240243
it("should return the same object if it is valid", () => {
241244
const obj = { name: "Alice", age: 30, tags: ["developer", "coder"] };
@@ -266,20 +269,42 @@ describe("misc.ts", () => {
266269

267270
it("should remove entire property if all array elements are invalid", () => {
268271
const obj = { name: "Alice", age: 30, tags: [123, 456] as any };
269-
expect(sanitize(schema, obj)).toEqual({
272+
const sanitized = sanitize(schema, obj);
273+
expect(sanitized).toEqual({
270274
name: "Alice",
271275
age: 30,
272-
tags: undefined,
273276
});
277+
expect(sanitized).not.toHaveProperty("tags");
274278
});
275279

276280
it("should remove object properties if they are invalid", () => {
277281
const obj = { name: 123 as any, age: 30, tags: ["developer", "coder"] };
278-
expect(sanitize(schema, obj)).toEqual({
282+
const sanitized = sanitize(schema, obj);
283+
expect(sanitized).toEqual({
279284
age: 30,
280285
tags: ["developer", "coder"],
281-
name: undefined,
282286
});
287+
expect(sanitized).not.toHaveProperty("name");
288+
});
289+
290+
it("should strip extra keys", () => {
291+
const obj = {
292+
name: "bob",
293+
age: 30,
294+
tags: ["developer", "coder"],
295+
powerLevel: 9001,
296+
} as any;
297+
const stripped = sanitize(schema.strip(), obj);
298+
expect(stripped).not.toHaveProperty("powerLevel");
299+
});
300+
it("should strip extra keys on error", () => {
301+
const obj = {
302+
name: "bob",
303+
age: 30,
304+
powerLevel: 9001,
305+
} as any;
306+
const stripped = sanitize(schema.strip(), obj);
307+
expect(stripped).not.toHaveProperty("powerLevel");
283308
});
284309
});
285310
});

frontend/src/ts/config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import { Config, FunboxName } from "@monkeytype/schemas/configs";
2020
import { Mode } from "@monkeytype/schemas/shared";
2121
import { Language } from "@monkeytype/schemas/languages";
2222
import { LocalStorageWithSchema } from "./utils/local-storage-with-schema";
23-
import { migrateConfig } from "./utils/config";
23+
import {
24+
migrateConfig,
25+
replaceLegacyValues,
26+
sanitizeConfig,
27+
} from "./utils/config";
2428
import { getDefaultConfig } from "./constants/default-config";
2529
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
2630
import { ZodSchema } from "zod";
@@ -821,6 +825,9 @@ export async function apply(
821825
): Promise<void> {
822826
if (configToApply === undefined || configToApply === null) return;
823827

828+
//remove additional keys, migrate old values if needed
829+
configToApply = sanitizeConfig(replaceLegacyValues(configToApply));
830+
824831
ConfigEvent.dispatch("fullConfigChange");
825832

826833
const defaultConfig = getDefaultConfig();

frontend/src/ts/controllers/preset-controller.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as UpdateConfig from "../config";
33
import * as DB from "../db";
44
import * as Notifications from "../elements/notifications";
55
import * as TestLogic from "../test/test-logic";
6-
import { migrateConfig, replaceLegacyValues } from "../utils/config";
76
import * as TagController from "./tag-controller";
87
import { SnapshotPreset } from "../constants/default-snapshot";
98

@@ -17,7 +16,7 @@ export async function apply(_id: string): Promise<void> {
1716
}
1817

1918
await UpdateConfig.apply(
20-
migrateConfig(replaceLegacyValues(presetToApply.config)),
19+
presetToApply.config,
2120
!isPartialPreset(presetToApply)
2221
);
2322

frontend/src/ts/utils/config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ function mergeWithDefaultConfig(config: PartialConfig): Config {
3030
/**
3131
* remove all values from the config which are not valid
3232
*/
33-
function sanitizeConfig(
33+
export function sanitizeConfig(
3434
config: ConfigSchemas.PartialConfig
3535
): ConfigSchemas.PartialConfig {
36-
return sanitize(ConfigSchemas.PartialConfigSchema, config);
36+
//make sure to use strip()
37+
return sanitize(ConfigSchemas.PartialConfigSchema.strip(), config);
3738
}
3839

3940
export function replaceLegacyValues(

frontend/src/ts/utils/misc.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -723,8 +723,9 @@ export function sanitize<T extends z.ZodTypeAny>(
723723
const validate = schema.safeParse(obj);
724724

725725
if (validate.success) {
726+
//use the parsed data, not the obj. keys might been removed
726727
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
727-
return obj;
728+
return validate.data;
728729
}
729730

730731
const errors: Map<string, number[] | undefined> = new Map();
@@ -738,26 +739,40 @@ export function sanitize<T extends z.ZodTypeAny>(
738739
}
739740

740741
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
741-
return Object.fromEntries(
742-
Object.entries(obj).map(([key, value]) => {
743-
if (!errors.has(key)) {
744-
return [key, value];
745-
}
742+
const cleanedObject = Object.fromEntries(
743+
Object.entries(obj)
744+
.map(([key, value]) => {
745+
if (!errors.has(key)) {
746+
return [key, value];
747+
}
746748

747-
const error = errors.get(key);
748-
749-
if (
750-
Array.isArray(value) &&
751-
error !== undefined && //error is not on the array itself
752-
error.length < value.length //not all items in the array are invalid
753-
) {
754-
//some items of the array are invalid
755-
return [key, value.filter((_element, index) => !error.includes(index))];
756-
} else {
757-
return [key, undefined];
758-
}
759-
})
749+
const error = errors.get(key);
750+
751+
if (
752+
Array.isArray(value) &&
753+
error !== undefined && //error is not on the array itself
754+
error.length < value.length //not all items in the array are invalid
755+
) {
756+
//some items of the array are invalid
757+
return [
758+
key,
759+
value.filter((_element, index) => !error.includes(index)),
760+
];
761+
} else {
762+
return [key, undefined];
763+
}
764+
})
765+
.filter((it) => it[1] !== undefined)
760766
) as z.infer<T>;
767+
768+
const cleanValidate = schema.safeParse(cleanedObject);
769+
if (cleanValidate.success) {
770+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
771+
return cleanValidate.data;
772+
}
773+
throw new Error(
774+
"unable to sanitize: " + cleanValidate.error.errors.join(",")
775+
);
761776
}
762777

763778
export function triggerResize(): void {

0 commit comments

Comments
 (0)