From 4a55960904728e140dd0c3943bc1849667972fac Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 15 Jun 2026 12:42:28 +0200 Subject: [PATCH 01/12] test(schemas): add tests for schemas (@fehmer) --- packages/schemas/__tests__/ape-keys.spec.ts | 156 ++++++++++++++++++ packages/schemas/__tests__/challenges.spec.ts | 91 ++++++++++ packages/schemas/__tests__/setup.ts | 39 +++++ packages/schemas/__tests__/tsconfig.json | 1 + packages/schemas/__tests__/vitest.d.ts | 12 ++ packages/schemas/vitest.config.ts | 1 + 6 files changed, 300 insertions(+) create mode 100644 packages/schemas/__tests__/ape-keys.spec.ts create mode 100644 packages/schemas/__tests__/challenges.spec.ts create mode 100644 packages/schemas/__tests__/setup.ts create mode 100644 packages/schemas/__tests__/vitest.d.ts diff --git a/packages/schemas/__tests__/ape-keys.spec.ts b/packages/schemas/__tests__/ape-keys.spec.ts new file mode 100644 index 000000000000..69fd2aa64916 --- /dev/null +++ b/packages/schemas/__tests__/ape-keys.spec.ts @@ -0,0 +1,156 @@ +import { it, expect, describe } from "vitest"; +import { + ApeKeyNameSchema, + ApeKeyUserDefinedSchema, + ApeKeySchema, + ApeKeysSchema, +} from "../src/ape-keys"; + +describe("ape-keys schemas", () => { + describe("ApeKeyNameSchema", () => { + it.each([ + { + description: "valid slug within max length", + input: "my-ape-key", + }, + { + description: "exceeds max length", + input: "this-name-is-way-too-long-for-schema", + expectedError: "String must contain at most 20 character", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ApeKeyNameSchema).toReject(input, expectedError); + } else { + expect(ApeKeyNameSchema).toValidate(input); + } + }); + }); + + describe("ApeKeyUserDefinedSchema", () => { + it.each([ + { + description: "minimal valid user-defined ape key", + input: { + name: "test-key", + enabled: true, + }, + }, + { + description: "missing name", + input: { enabled: false }, + expectedError: "Required", + }, + { + description: "missing enabled", + input: { name: "test-key" }, + expectedError: "Required", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ApeKeyUserDefinedSchema).toReject(input, expectedError); + } else { + expect(ApeKeyUserDefinedSchema).toValidate(input); + } + }); + }); + + describe("ApeKeySchema", () => { + it.each([ + { + description: "minimal valid ape key", + input: { + name: "test-key", + enabled: true, + createdOn: 0, + modifiedOn: 0, + lastUsedOn: 0, + }, + }, + { + description: "lastUsedOn is -1", + input: { + name: "test-key", + enabled: false, + createdOn: 1234567890, + modifiedOn: 1234567890, + lastUsedOn: -1, + }, + }, + { + description: "missing createdOn", + input: { + name: "test-key", + enabled: true, + modifiedOn: 0, + lastUsedOn: 0, + }, + expectedError: "Required", + }, + { + description: "createdOn negative", + input: { + name: "test-key", + enabled: true, + createdOn: -1, + modifiedOn: 0, + lastUsedOn: 0, + }, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "lastUsedOn negative and not -1", + input: { + name: "test-key", + enabled: true, + createdOn: 0, + modifiedOn: 0, + lastUsedOn: -2, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ApeKeySchema).toReject(input, expectedError); + } else { + expect(ApeKeySchema).toValidate(input); + } + }); + }); + + describe("ApeKeysSchema", () => { + it.each([ + { + description: "valid record of ape keys", + input: { + key1: { + name: "test-key", + enabled: true, + createdOn: 0, + modifiedOn: 0, + lastUsedOn: 0, + }, + }, + }, + { + description: "invalid value in record", + input: { + key1: { + name: "test-key", + enabled: true, + createdOn: -1, + modifiedOn: 0, + lastUsedOn: 0, + }, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ApeKeysSchema).toReject(input, expectedError); + } else { + expect(ApeKeysSchema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/challenges.spec.ts b/packages/schemas/__tests__/challenges.spec.ts new file mode 100644 index 000000000000..9230bb0e5703 --- /dev/null +++ b/packages/schemas/__tests__/challenges.spec.ts @@ -0,0 +1,91 @@ +import { it, expect, describe } from "vitest"; +import { ChallengeSchema } from "../src/challenges"; + +describe("challenges schema", () => { + describe("ChallengeSchema", () => { + it.each([ + { + description: "minimal valid challenge", + input: { + name: "test-challenge", + display: "Test Challenge", + type: "other", + parameters: [], + }, + }, + { + description: "full challenge with requirements", + input: { + name: "speed-run", + display: "Speed Run", + autoRole: true, + type: "customTime", + message: "Complete quickly", + parameters: [60, true, null, "easy", ["58008"]], + requirements: { + wpm: { min: 100 }, + acc: { exact: 0.95 }, + afk: { max: 5 }, + time: { min: 60 }, + funbox: { exact: ["58008"] }, + raw: { exact: 120 }, + con: { exact: 10 }, + config: { punctuation: true }, + }, + }, + }, + { + description: "exact wpm challenge", + input: { + name: "exact-wpm", + display: "Exact WPM", + type: "accuracy", + parameters: [], + requirements: { wpm: { exact: 50 } }, + }, + }, + { + description: "missing name", + input: { display: "Test", type: "other", parameters: [] }, + expectedError: "Required", + }, + { + description: "missing display", + input: { name: "test", type: "other", parameters: [] }, + expectedError: "Required", + }, + { + description: "missing type", + input: { name: "test", display: "Test", parameters: [] }, + expectedError: "Required", + }, + { + description: "invalid type enum", + input: { + name: "test", + display: "Test", + type: "invalid", + parameters: [], + }, + expectedError: "Invalid enum value", + }, + { + description: "unrecognized key", + input: { + name: "test", + display: "Test", + type: "other", + parameters: [], + extra: true, + }, + expectedError: "Unrecognized key", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ChallengeSchema).toReject(input, expectedError); + } else { + expect(ChallengeSchema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/setup.ts b/packages/schemas/__tests__/setup.ts new file mode 100644 index 000000000000..58f780cdee68 --- /dev/null +++ b/packages/schemas/__tests__/setup.ts @@ -0,0 +1,39 @@ +import { expect } from "vitest"; +import { z } from "zod"; + +expect.extend({ + toValidate(schema: z.ZodType, input: unknown) { + const result = schema.safeParse(input); + if (result.success) { + return { pass: true, message: () => "" }; + } + return { + pass: false, + message: () => + `expected input to be valid, got errors: ${JSON.stringify(result.error.issues.map((i) => i.message))}`, + }; + }, + + toReject(schema: z.ZodType, input: unknown, errorMessage?: string) { + const result = schema.safeParse(input); + if (!result.success) { + const errors = result.error.issues.map((i) => i.message); + if (errorMessage !== undefined) { + const match = errors.some((e) => e.includes(errorMessage)); + if (match) { + return { pass: true, message: () => "" }; + } + return { + pass: false, + message: () => + `expected "${errorMessage}" in errors: ${JSON.stringify(errors)}`, + }; + } + return { pass: true, message: () => "" }; + } + return { + pass: false, + message: () => `expected input to be invalid, but it passed validation`, + }; + }, +}); diff --git a/packages/schemas/__tests__/tsconfig.json b/packages/schemas/__tests__/tsconfig.json index bc5ae47e535d..8f192081b529 100644 --- a/packages/schemas/__tests__/tsconfig.json +++ b/packages/schemas/__tests__/tsconfig.json @@ -3,5 +3,6 @@ "compilerOptions": { "noEmit": true }, + "files": ["vitest.d.ts"], "include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"] } diff --git a/packages/schemas/__tests__/vitest.d.ts b/packages/schemas/__tests__/vitest.d.ts new file mode 100644 index 000000000000..78004f5389bd --- /dev/null +++ b/packages/schemas/__tests__/vitest.d.ts @@ -0,0 +1,12 @@ +// oxlint-disable typescript/consistent-type-definitions +import type { Assertion, AsymmetricMatchersContaining } from "vitest"; + +interface SchemaMachers { + toValidate(input: unknown): void; + toReject(input: unknown, errorMessage?: string): void; +} + +declare module "vitest" { + // oxlint-disable-next-line typescript/no-empty-object-type + interface Assertion extends SchemaMachers {} +} diff --git a/packages/schemas/vitest.config.ts b/packages/schemas/vitest.config.ts index 6e75403ba84c..d2b88fdf1de5 100644 --- a/packages/schemas/vitest.config.ts +++ b/packages/schemas/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + setupFiles: ["./__tests__/setup.ts"], coverage: { include: ["**/*.ts"], }, From 2deb01fa674e36ba4aff61a8d680b50f0b41c00b Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 15 Jun 2026 12:54:39 +0200 Subject: [PATCH 02/12] test(schemas): add rejection cases to IdSchema and NullableStringSchema --- packages/schemas/__tests__/util.spec.ts | 334 +++++++++++++++++++++--- 1 file changed, 299 insertions(+), 35 deletions(-) diff --git a/packages/schemas/__tests__/util.spec.ts b/packages/schemas/__tests__/util.spec.ts index 8f29eb0496b9..faa5065750a5 100644 --- a/packages/schemas/__tests__/util.spec.ts +++ b/packages/schemas/__tests__/util.spec.ts @@ -1,56 +1,320 @@ import { describe, it, expect } from "vitest"; -import { nameWithSeparators, slug } from "../src/util"; +import { + StringNumberSchema, + token, + slug, + nameWithSeparators, + IdSchema, + TagSchema, + NullableStringSchema, + PercentageSchema, + WpmSchema, + CustomTextModeSchema, + CustomTextLimitModeSchema, + PageNumberSchema, +} from "../src/util"; + +describe("util schemas", () => { + describe("StringNumberSchema", () => { + it.each([ + { + description: "valid numeric string", + input: "123", + }, + { + description: "valid number", + input: 123, + }, + { + description: "string with letters", + input: "abc", + expectedError: + "Needs to be a number or a number represented as a string", + }, + { + description: "string with mixed content", + input: "123abc", + expectedError: + "Needs to be a number or a number represented as a string", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(StringNumberSchema).toReject(input, expectedError); + } else { + expect(StringNumberSchema).toValidate(input); + } + }); + }); + + describe("token", () => { + it.each([ + { + description: "valid alphanumeric with underscore", + input: "abc_123", + }, + { + description: "contains hyphen", + input: "abc-123", + expectedError: "Invalid", + }, + { + description: "contains space", + input: "abc 123", + expectedError: "Invalid", + }, + ] as const)("$description", ({ input, expectedError }) => { + const schema = token(); + if (expectedError) { + expect(schema).toReject(input, expectedError); + } else { + expect(schema).toValidate(input); + } + }); + }); + + describe("slug", () => { + it.each([ + { + description: "valid slug with dots and hyphens", + input: "abc-123_test.def", + }, + { + description: "starts with dot", + input: ".invalid", + expectedError: "Cannot start with a dot", + }, + { + description: "contains comma", + input: "abc,def", + expectedError: + "Only letters, numbers, underscores, dots and hyphens allowed", + }, + ] as const)("$description", ({ input, expectedError }) => { + const schema = slug(); + if (expectedError) { + expect(schema).toReject(input, expectedError); + } else { + expect(schema).toValidate(input); + } + }); + }); -describe("Schema Validation Tests", () => { describe("nameWithSeparators", () => { - const schema = nameWithSeparators(); + it.each([ + { + description: "valid name with separators", + input: "abc_def-123", + }, + { + description: "starts with separator", + input: "_invalid", + expectedError: "Separators cannot be at the start or end", + }, + { + description: "consecutive separators", + input: "inv__alid", + expectedError: "Separators cannot be at the start or end", + }, + { + description: "contains dot", + input: "invalid.name", + expectedError: "Only letters, numbers, underscores and hyphens allowed", + }, + ] as const)("$description", ({ input, expectedError }) => { + const schema = nameWithSeparators(); + if (expectedError) { + expect(schema).toReject(input, expectedError); + } else { + expect(schema).toValidate(input); + } + }); + }); - it("accepts valid names", () => { - expect(schema.safeParse("valid_name").success).toBe(true); - expect(schema.safeParse("valid-name").success).toBe(true); - expect(schema.safeParse("valid123").success).toBe(true); - expect(schema.safeParse("Valid_Name-Check").success).toBe(true); + describe("IdSchema", () => { + it.each([ + { + description: "valid id", + input: "abc_123", + }, + { + description: "contains hyphen", + input: "abc-123", + expectedError: "Invalid", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(IdSchema).toReject(input, expectedError); + } else { + expect(IdSchema).toValidate(input); + } }); + }); - it("rejects leading/trailing separators", () => { - expect(schema.safeParse("_invalid").success).toBe(false); - expect(schema.safeParse("invalid-").success).toBe(false); + describe("TagSchema", () => { + it.each([ + { + description: "valid tag within max length", + input: "abc_123", + }, + { + description: "exceeds max length", + input: "a".repeat(51), + expectedError: "String must contain at most 50 character", + }, + { + description: "contains invalid character", + input: "abc-123", + expectedError: "Invalid", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TagSchema).toReject(input, expectedError); + } else { + expect(TagSchema).toValidate(input); + } }); + }); - it("rejects consecutive separators", () => { - expect(schema.safeParse("inv__alid").success).toBe(false); - expect(schema.safeParse("inv--alid").success).toBe(false); - expect(schema.safeParse("inv-_alid").success).toBe(false); + describe("NullableStringSchema", () => { + it.each([ + { + description: "valid string", + input: "hello", + }, + { + description: "null transforms to undefined", + input: null, + }, + { + description: "undefined is accepted", + input: undefined, + }, + { + description: "boolean is rejected", + input: true, + expectedError: "Expected string", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(NullableStringSchema).toReject(input, expectedError); + } else { + expect(NullableStringSchema).toValidate(input); + } }); + }); - it("rejects dots", () => { - expect(schema.safeParse("invalid.dot").success).toBe(false); - expect(schema.safeParse(".invalid").success).toBe(false); + describe("PercentageSchema", () => { + it.each([ + { + description: "valid percentage", + input: 50, + }, + { + description: "exceeds max", + input: 101, + expectedError: "Number must be less than or equal to 100", + }, + { + description: "negative value", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PercentageSchema).toReject(input, expectedError); + } else { + expect(PercentageSchema).toValidate(input); + } }); }); - describe("slug", () => { - const schema = slug(); + describe("WpmSchema", () => { + it.each([ + { + description: "valid wpm", + input: 100, + }, + { + description: "exceeds max", + input: 421, + expectedError: "Number must be less than or equal to 420", + }, + { + description: "negative value", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(WpmSchema).toReject(input, expectedError); + } else { + expect(WpmSchema).toValidate(input); + } + }); + }); - it("accepts valid slugs", () => { - expect(schema.safeParse("valid-slug.123_test").success).toBe(true); - expect(schema.safeParse("valid.dots").success).toBe(true); - expect(schema.safeParse("_leading_underscore_is_fine").success).toBe( - true, - ); - expect(schema.safeParse("-leading_hyphen_is_fine").success).toBe(true); - expect(schema.safeParse("trailing_is_fine_in_slug_").success).toBe(true); + describe("CustomTextModeSchema", () => { + it.each([ + { + description: "valid mode", + input: "repeat", + }, + { + description: "invalid mode", + input: "invalid", + expectedError: "Invalid", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomTextModeSchema).toReject(input, expectedError); + } else { + expect(CustomTextModeSchema).toValidate(input); + } }); + }); - it("rejects leading dots", () => { - expect(schema.safeParse(".invalid").success).toBe(false); + describe("CustomTextLimitModeSchema", () => { + it.each([ + { + description: "valid limit mode", + input: "word", + }, + { + description: "invalid limit mode", + input: "invalid", + expectedError: "Invalid", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomTextLimitModeSchema).toReject(input, expectedError); + } else { + expect(CustomTextLimitModeSchema).toValidate(input); + } }); + }); - it("rejects invalid characters", () => { - expect(schema.safeParse("invalid,comma").success).toBe(false); - expect(schema.safeParse(",invalid").success).toBe(false); - expect(schema.safeParse("invalid space").success).toBe(false); - expect(schema.safeParse("invalid#hash").success).toBe(false); + describe("PageNumberSchema", () => { + it.each([ + { + description: "valid page number", + input: 5, + }, + { + description: "negative value", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "non-integer", + input: 1.5, + expectedError: "Expected integer, received float", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PageNumberSchema).toReject(input, expectedError); + } else { + expect(PageNumberSchema).toValidate(input); + } }); }); }); From 537d669aa57a73ab30ac58073e5aff8a157f27b7 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 15 Jun 2026 14:56:29 +0200 Subject: [PATCH 03/12] configs enums --- packages/schemas/__tests__/config.spec.ts | 83 -- packages/schemas/__tests__/configs.spec.ts | 896 +++++++++++++++++++++ 2 files changed, 896 insertions(+), 83 deletions(-) delete mode 100644 packages/schemas/__tests__/config.spec.ts create mode 100644 packages/schemas/__tests__/configs.spec.ts diff --git a/packages/schemas/__tests__/config.spec.ts b/packages/schemas/__tests__/config.spec.ts deleted file mode 100644 index 50db32000f94..000000000000 --- a/packages/schemas/__tests__/config.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { CustomBackgroundSchema } from "@monkeytype/schemas/configs"; - -describe("config schema", () => { - describe("CustomBackgroundSchema", () => { - it.for([ - { - name: "http", - input: `http://example.com/path/image.png`, - }, - { - name: "https", - input: `https://example.com/path/image.png`, - }, - { - name: "png", - input: `https://example.com/path/image.png`, - }, - { - name: "gif", - input: `https://example.com/path/image.gif?width=5`, - }, - { - name: "jpeg", - input: `https://example.com/path/image.jpeg`, - }, - { - name: "jpg", - input: `https://example.com/path/image.jpg`, - }, - { - name: "tiff", - input: `https://example.com/path/image.tiff`, - expectedError: "Unsupported image format", - }, - { - name: "non-url", - input: `test`, - expectedError: "Needs to be an URI", - }, - { - name: "single quotes", - input: `https://example.com/404.jpg?q=alert('1')`, - expectedError: "May not contain quotes", - }, - { - name: "double quotes", - input: `https://example.com/404.jpg?q=alert("1")`, - expectedError: "May not contain quotes", - }, - { - name: "back tick", - input: `https://example.com/404.jpg?q=alert(\`1\`)`, - expectedError: "May not contain quotes", - }, - { - name: "javascript url", - input: `javascript:alert('asdf');//https://example.com/img.jpg`, - expectedError: "Unsupported protocol", - }, - { - name: "data url", - input: `data:image/gif;base64,data`, - expectedError: "Unsupported protocol", - }, - { - name: "long url", - input: `https://example.com/path/image.jpeg?q=${new Array(2048) - .fill("x") - .join()}`, - expectedError: "URL is too long", - }, - ])(`$name`, ({ input, expectedError }) => { - const parsed = CustomBackgroundSchema.safeParse(input); - if (expectedError !== undefined) { - expect(parsed.success).toEqual(false); - expect(parsed.error?.issues[0]?.message).toEqual(expectedError); - } else { - expect(parsed.success).toEqual(true); - } - }); - }); -}); diff --git a/packages/schemas/__tests__/configs.spec.ts b/packages/schemas/__tests__/configs.spec.ts new file mode 100644 index 000000000000..3435271f6ed9 --- /dev/null +++ b/packages/schemas/__tests__/configs.spec.ts @@ -0,0 +1,896 @@ +import { it, expect, describe } from "vitest"; +import { + SmoothCaretSchema, + QuickRestartSchema, + QuoteLengthSchema, + CaretStyleSchema, + ConfidenceModeSchema, + IndicateTyposSchema, + CompositionDisplaySchema, + TimerStyleSchema, + LiveSpeedAccBurstStyleSchema, + RandomThemeSchema, + TimerColorSchema, + TimerOpacitySchema, + StopOnErrorSchema, + KeymapModeSchema, + KeymapStyleSchema, + KeymapLegendStyleSchema, + KeymapShowTopRowSchema, + SingleListCommandLineSchema, + PlaySoundOnErrorSchema, + PlaySoundOnClickSchema, + PaceCaretSchema, + MinimumWordsPerMinuteSchema, + HighlightModeSchema, + TypedEffectSchema, + TapeModeSchema, + TypingSpeedUnitSchema, + AdsSchema, + MinimumAccuracySchema, + RepeatQuotesSchema, + OppositeShiftModeSchema, + CustomBackgroundSchema, + CustomBackgroundSizeSchema, + MonkeyPowerLevelSchema, + MinimumBurstSchema, + ShowAverageSchema, + FunboxNameSchema, + PlayTimeWarningSchema, + ConfigGroupNameSchema, +} from "../src/configs"; + +describe("configs schemas", () => { + describe("SmoothCaretSchema", () => { + it.each([ + { + description: "valid value 'off'", + input: "off", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'slow' | 'medium' | 'fast', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(SmoothCaretSchema).toReject(input, expectedError); + } else { + expect(SmoothCaretSchema).toValidate(input); + } + }); + }); + + describe("QuickRestartSchema", () => { + it.each([ + { + description: "valid value 'esc'", + input: "esc", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'esc' | 'tab' | 'enter', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuickRestartSchema).toReject(input, expectedError); + } else { + expect(QuickRestartSchema).toValidate(input); + } + }); + }); + + describe("QuoteLengthSchema", () => { + it.each([ + { + description: "valid value -3", + input: -3, + }, + { + description: "invalid value", + input: 4, + expectedError: "Invalid input", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteLengthSchema).toReject(input, expectedError); + } else { + expect(QuoteLengthSchema).toValidate(input); + } + }); + }); + + describe("CaretStyleSchema", () => { + it.each([ + { + description: "valid value 'monkey'", + input: "monkey", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'default' | 'block' | 'outline' | 'underline' | 'carrot' | 'banana' | 'monkey', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CaretStyleSchema).toReject(input, expectedError); + } else { + expect(CaretStyleSchema).toValidate(input); + } + }); + }); + + describe("ConfidenceModeSchema", () => { + it.each([ + { + description: "valid value 'max'", + input: "max", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'on' | 'max', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ConfidenceModeSchema).toReject(input, expectedError); + } else { + expect(ConfidenceModeSchema).toValidate(input); + } + }); + }); + + describe("IndicateTyposSchema", () => { + it.each([ + { + description: "valid value 'both'", + input: "both", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'below' | 'replace' | 'both', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(IndicateTyposSchema).toReject(input, expectedError); + } else { + expect(IndicateTyposSchema).toValidate(input); + } + }); + }); + + describe("CompositionDisplaySchema", () => { + it.each([ + { + description: "valid value 'replace'", + input: "replace", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'below' | 'replace', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CompositionDisplaySchema).toReject(input, expectedError); + } else { + expect(CompositionDisplaySchema).toValidate(input); + } + }); + }); + + describe("TimerStyleSchema", () => { + it.each([ + { + description: "valid value 'flash_mini'", + input: "flash_mini", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'bar' | 'text' | 'mini' | 'flash_text' | 'flash_mini', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TimerStyleSchema).toReject(input, expectedError); + } else { + expect(TimerStyleSchema).toValidate(input); + } + }); + }); + + describe("LiveSpeedAccBurstStyleSchema", () => { + it.each([ + { + description: "valid value 'mini'", + input: "mini", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'text' | 'mini', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(LiveSpeedAccBurstStyleSchema).toReject(input, expectedError); + } else { + expect(LiveSpeedAccBurstStyleSchema).toValidate(input); + } + }); + }); + + describe("RandomThemeSchema", () => { + it.each([ + { + description: "valid value 'auto'", + input: "auto", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'on' | 'fav' | 'light' | 'dark' | 'custom' | 'auto', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(RandomThemeSchema).toReject(input, expectedError); + } else { + expect(RandomThemeSchema).toValidate(input); + } + }); + }); + + describe("TimerColorSchema", () => { + it.each([ + { + description: "valid value 'main'", + input: "main", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'black' | 'sub' | 'text' | 'main', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TimerColorSchema).toReject(input, expectedError); + } else { + expect(TimerColorSchema).toValidate(input); + } + }); + }); + + describe("TimerOpacitySchema", () => { + it.each([ + { + description: "valid value '0.75'", + input: "0.75", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected '0.25' | '0.5' | '0.75' | '1', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TimerOpacitySchema).toReject(input, expectedError); + } else { + expect(TimerOpacitySchema).toValidate(input); + } + }); + }); + + describe("StopOnErrorSchema", () => { + it.each([ + { + description: "valid value 'letter'", + input: "letter", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'word' | 'letter', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(StopOnErrorSchema).toReject(input, expectedError); + } else { + expect(StopOnErrorSchema).toValidate(input); + } + }); + }); + + describe("KeymapModeSchema", () => { + it.each([ + { + description: "valid value 'react'", + input: "react", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'static' | 'react' | 'next', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(KeymapModeSchema).toReject(input, expectedError); + } else { + expect(KeymapModeSchema).toValidate(input); + } + }); + }); + + describe("KeymapStyleSchema", () => { + it.each([ + { + description: "valid value 'steno_matrix'", + input: "steno_matrix", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'staggered' | 'alice' | 'matrix' | 'split' | 'split_matrix' | 'steno' | 'steno_matrix', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(KeymapStyleSchema).toReject(input, expectedError); + } else { + expect(KeymapStyleSchema).toValidate(input); + } + }); + }); + + describe("KeymapLegendStyleSchema", () => { + it.each([ + { + description: "valid value 'dynamic'", + input: "dynamic", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'lowercase' | 'uppercase' | 'blank' | 'dynamic', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(KeymapLegendStyleSchema).toReject(input, expectedError); + } else { + expect(KeymapLegendStyleSchema).toValidate(input); + } + }); + }); + + describe("KeymapShowTopRowSchema", () => { + it.each([ + { + description: "valid value 'layout'", + input: "layout", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'always' | 'layout' | 'never', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(KeymapShowTopRowSchema).toReject(input, expectedError); + } else { + expect(KeymapShowTopRowSchema).toValidate(input); + } + }); + }); + + describe("SingleListCommandLineSchema", () => { + it.each([ + { + description: "valid value 'on'", + input: "on", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'manual' | 'on', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(SingleListCommandLineSchema).toReject(input, expectedError); + } else { + expect(SingleListCommandLineSchema).toValidate(input); + } + }); + }); + + describe("PlaySoundOnErrorSchema", () => { + it.each([ + { + description: "valid value '3'", + input: "3", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | '1' | '2' | '3' | '4', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PlaySoundOnErrorSchema).toReject(input, expectedError); + } else { + expect(PlaySoundOnErrorSchema).toValidate(input); + } + }); + }); + + describe("PlaySoundOnClickSchema", () => { + it.each([ + { + description: "valid value '13'", + input: "13", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | '13' | '14' | '15' | '16' | '17' | '18' | '19' | '20' | '21' | '22' | '23' | '24' | '25' | '26', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PlaySoundOnClickSchema).toReject(input, expectedError); + } else { + expect(PlaySoundOnClickSchema).toValidate(input); + } + }); + }); + + describe("PaceCaretSchema", () => { + it.each([ + { + description: "valid value 'daily'", + input: "daily", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'average' | 'pb' | 'tagPb' | 'last' | 'custom' | 'daily', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PaceCaretSchema).toReject(input, expectedError); + } else { + expect(PaceCaretSchema).toValidate(input); + } + }); + }); + + describe("MinimumWordsPerMinuteSchema", () => { + it.each([ + { + description: "valid value 'custom'", + input: "custom", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'custom', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MinimumWordsPerMinuteSchema).toReject(input, expectedError); + } else { + expect(MinimumWordsPerMinuteSchema).toValidate(input); + } + }); + }); + + describe("HighlightModeSchema", () => { + it.each([ + { + description: "valid value 'next_three_words'", + input: "next_three_words", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'letter' | 'word' | 'next_word' | 'next_two_words' | 'next_three_words', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(HighlightModeSchema).toReject(input, expectedError); + } else { + expect(HighlightModeSchema).toValidate(input); + } + }); + }); + + describe("TypedEffectSchema", () => { + it.each([ + { + description: "valid value 'dots'", + input: "dots", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'keep' | 'hide' | 'fade' | 'dots', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TypedEffectSchema).toReject(input, expectedError); + } else { + expect(TypedEffectSchema).toValidate(input); + } + }); + }); + + describe("TapeModeSchema", () => { + it.each([ + { + description: "valid value 'word'", + input: "word", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'letter' | 'word', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TapeModeSchema).toReject(input, expectedError); + } else { + expect(TapeModeSchema).toValidate(input); + } + }); + }); + + describe("TypingSpeedUnitSchema", () => { + it.each([ + { + description: "valid value 'wph'", + input: "wph", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'wpm' | 'cpm' | 'wps' | 'cps' | 'wph', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TypingSpeedUnitSchema).toReject(input, expectedError); + } else { + expect(TypingSpeedUnitSchema).toValidate(input); + } + }); + }); + + describe("AdsSchema", () => { + it.each([ + { + description: "valid value 'sellout'", + input: "sellout", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'result' | 'on' | 'sellout', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(AdsSchema).toReject(input, expectedError); + } else { + expect(AdsSchema).toValidate(input); + } + }); + }); + + describe("MinimumAccuracySchema", () => { + it.each([ + { + description: "valid value 'custom'", + input: "custom", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'custom', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MinimumAccuracySchema).toReject(input, expectedError); + } else { + expect(MinimumAccuracySchema).toValidate(input); + } + }); + }); + + describe("RepeatQuotesSchema", () => { + it.each([ + { + description: "valid value 'typing'", + input: "typing", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'typing', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(RepeatQuotesSchema).toReject(input, expectedError); + } else { + expect(RepeatQuotesSchema).toValidate(input); + } + }); + }); + + describe("OppositeShiftModeSchema", () => { + it.each([ + { + description: "valid value 'keymap'", + input: "keymap", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'on' | 'keymap', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(OppositeShiftModeSchema).toReject(input, expectedError); + } else { + expect(OppositeShiftModeSchema).toValidate(input); + } + }); + }); + + describe("CustomBackgroundSizeSchema", () => { + it.each([ + { + description: "valid value 'cover'", + input: "cover", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'cover' | 'contain' | 'max', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomBackgroundSizeSchema).toReject(input, expectedError); + } else { + expect(CustomBackgroundSizeSchema).toValidate(input); + } + }); + }); + + describe("MonkeyPowerLevelSchema", () => { + it.each([ + { + description: "valid value '3'", + input: "3", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | '1' | '2' | '3' | '4', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MonkeyPowerLevelSchema).toReject(input, expectedError); + } else { + expect(MonkeyPowerLevelSchema).toValidate(input); + } + }); + }); + + describe("MinimumBurstSchema", () => { + it.each([ + { + description: "valid value 'fixed'", + input: "fixed", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'fixed' | 'flex', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MinimumBurstSchema).toReject(input, expectedError); + } else { + expect(MinimumBurstSchema).toValidate(input); + } + }); + }); + + describe("ShowAverageSchema", () => { + it.each([ + { + description: "valid value 'speed'", + input: "speed", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | 'speed' | 'acc' | 'both', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ShowAverageSchema).toReject(input, expectedError); + } else { + expect(ShowAverageSchema).toValidate(input); + } + }); + }); + + describe("FunboxNameSchema", () => { + it.each([ + { + description: "valid value 'mirror'", + input: "mirror", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected '58008' | 'mirror' | 'upside_down' | 'nausea' | 'round_round_baby' | 'simon_says' | 'tts' | 'choo_choo' | 'arrows' | 'rAnDoMcAsE' | 'sPoNgEcAsE' | 'capitals' | 'layout_mirror' | 'layoutfluid' | 'earthquake' | 'space_balls' | 'gibberish' | 'ascii' | 'specials' | 'plus_zero' | 'plus_one' | 'plus_two' | 'plus_three' | 'read_ahead_easy' | 'read_ahead' | 'read_ahead_hard' | 'memory' | 'nospace' | 'poetry' | 'wikipedia' | 'weakspot' | 'pseudolang' | 'IPv4' | 'IPv6' | 'binary' | 'hexadecimal' | 'zipf' | 'morse' | 'crt' | 'backwards' | 'ddoouubblleedd' | 'instant_messaging' | 'underscore_spaces' | 'ALL_CAPS' | 'polyglot' | 'asl' | 'rot13' | 'no_quit', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(FunboxNameSchema).toReject(input, expectedError); + } else { + expect(FunboxNameSchema).toValidate(input); + } + }); + }); + + describe("PlayTimeWarningSchema", () => { + it.each([ + { + description: "valid value '5'", + input: "5", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'off' | '1' | '3' | '5' | '10', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PlayTimeWarningSchema).toReject(input, expectedError); + } else { + expect(PlayTimeWarningSchema).toValidate(input); + } + }); + }); + + describe("ConfigGroupNameSchema", () => { + it.each([ + { + description: "valid value 'appearance'", + input: "appearance", + }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'hidden' | 'ads', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ConfigGroupNameSchema).toReject(input, expectedError); + } else { + expect(ConfigGroupNameSchema).toValidate(input); + } + }); + }); + + describe("CustomBackgroundSchema", () => { + it.each([ + { + description: "http", + input: `http://example.com/path/image.png`, + }, + { + description: "https", + input: `https://example.com/path/image.png`, + }, + { + description: "png", + input: `https://example.com/path/image.png`, + }, + { + description: "gif", + input: `https://example.com/path/image.gif?width=5`, + }, + { + description: "jpeg", + input: `https://example.com/path/image.jpeg`, + }, + { + description: "jpg", + input: `https://example.com/path/image.jpg`, + }, + { + description: "tiff", + input: `https://example.com/path/image.tiff`, + expectedError: "Unsupported image format", + }, + { + description: "non-url", + input: `test`, + expectedError: "Needs to be an URI", + }, + { + description: "single quotes", + input: `https://example.com/404.jpg?q=alert('1')`, + expectedError: "May not contain quotes", + }, + { + description: "double quotes", + input: `https://example.com/404.jpg?q=alert("1")`, + expectedError: "May not contain quotes", + }, + { + description: "back tick", + input: `https://example.com/404.jpg?q=alert(\`1\`)`, + expectedError: "May not contain quotes", + }, + { + description: "javascript url", + input: `javascript:alert('asdf');//https://example.com/img.jpg`, + expectedError: "Unsupported protocol", + }, + { + description: "data url", + input: `data:image/gif;base64,data`, + expectedError: "Unsupported protocol", + }, + { + description: "long url", + input: `https://example.com/path/image.jpeg?q=${new Array(2048) + .fill("x") + .join()}`, + expectedError: "URL is too long", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomBackgroundSchema).toReject(input, expectedError); + } else { + expect(CustomBackgroundSchema).toValidate(input); + } + }); + }); +}); From 83b7948ecf835ebcc6e06eaececcb877f2583b72 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 12:07:52 +0200 Subject: [PATCH 04/12] configuration schema --- .../schemas/__tests__/configuration.spec.ts | 459 ++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 packages/schemas/__tests__/configuration.spec.ts diff --git a/packages/schemas/__tests__/configuration.spec.ts b/packages/schemas/__tests__/configuration.spec.ts new file mode 100644 index 000000000000..87dee5119081 --- /dev/null +++ b/packages/schemas/__tests__/configuration.spec.ts @@ -0,0 +1,459 @@ +import { it, expect, describe } from "vitest"; +import { + ValidModeRuleSchema, + RewardBracketSchema, + ConfigurationSchema, +} from "../src/configuration"; + +describe("configuration schemas", () => { + describe("ValidModeRuleSchema", () => { + it.each([ + { + description: "valid mode rule", + input: { + language: "english", + mode: "time", + mode2: "30", + }, + }, + { + description: "missing mode", + input: { + language: "english", + mode2: "30", + }, + expectedError: "Required", + }, + { + description: "extra field", + input: { + language: "english", + mode: "time", + mode2: "30", + extra: true, + }, + expectedError: "Unrecognized key(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ValidModeRuleSchema).toReject(input, expectedError); + } else { + expect(ValidModeRuleSchema).toValidate(input); + } + }); + }); + + describe("RewardBracketSchema", () => { + it.each([ + { + description: "valid reward bracket", + input: { + minRank: 1, + maxRank: 10, + minReward: 100, + maxReward: 500, + }, + }, + { + description: "zero values are valid", + input: { + minRank: 0, + maxRank: 0, + minReward: 0, + maxReward: 0, + }, + }, + { + description: "negative minRank", + input: { + minRank: -1, + maxRank: 10, + minReward: 100, + maxReward: 500, + }, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "non-integer value", + input: { + minRank: 1.5, + maxRank: 10, + minReward: 100, + maxReward: 500, + }, + expectedError: "Expected integer, received float", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(RewardBracketSchema).toReject(input, expectedError); + } else { + expect(RewardBracketSchema).toValidate(input); + } + }); + }); + + describe("ConfigurationSchema", () => { + it.each([ + { + description: "valid full configuration", + input: { + maintenance: false, + dev: { + responseSlowdownMs: 0, + }, + quotes: { + reporting: { + enabled: true, + maxReports: 5, + contentReportLimit: 3, + }, + submissionsEnabled: true, + maxFavorites: 100, + }, + results: { + savingEnabled: true, + objectHashCheckEnabled: true, + filterPresets: { + enabled: true, + maxPresetsPerUser: 10, + }, + limits: { + regularUser: 50, + premiumUser: 200, + }, + maxBatchSize: 100, + }, + users: { + signUp: true, + lastHashesCheck: { + enabled: true, + maxHashes: 5, + }, + autoBan: { + enabled: true, + maxCount: 3, + maxHours: 24, + }, + profiles: { + enabled: true, + }, + discordIntegration: { + enabled: true, + }, + xp: { + enabled: true, + funboxBonus: 1.5, + gainMultiplier: 1, + maxDailyBonus: 1000, + minDailyBonus: 100, + streak: { + enabled: true, + maxStreakDays: 30, + maxStreakMultiplier: 2, + }, + }, + inbox: { + enabled: true, + maxMail: 50, + }, + premium: { + enabled: true, + }, + }, + admin: { + endpointsEnabled: true, + }, + apeKeys: { + endpointsEnabled: true, + acceptKeys: true, + maxKeysPerUser: 5, + apeKeyBytes: 32, + apeKeySaltRounds: 10, + }, + rateLimiting: { + badAuthentication: { + enabled: true, + penalty: 60, + flaggedStatusCodes: [401, 403], + }, + }, + dailyLeaderboards: { + enabled: true, + leaderboardExpirationTimeInDays: 1, + maxResults: 500, + validModeRules: [ + { language: "english", mode: "time", mode2: "30" }, + ], + scheduleRewardsModeRules: [ + { language: "english", mode: "time", mode2: "(15|60)" }, + ], + topResultsToAnnounce: 3, + xpRewardBrackets: [ + { + minRank: 1, + maxRank: 10, + minReward: 100, + maxReward: 500, + }, + ], + }, + leaderboards: { + minTimeTyping: 10, + weeklyXp: { + enabled: true, + expirationTimeInDays: 7, + xpRewardBrackets: [ + { + minRank: 1, + maxRank: 5, + minReward: 200, + maxReward: 1000, + }, + ], + }, + }, + connections: { + enabled: true, + maxPerUser: 3, + }, + }, + }, + { + description: "maintenance as true", + input: { + maintenance: true, + dev: { responseSlowdownMs: 0 }, + quotes: { + reporting: { enabled: false, maxReports: 0, contentReportLimit: 0 }, + submissionsEnabled: false, + maxFavorites: 0, + }, + results: { + savingEnabled: false, + objectHashCheckEnabled: false, + filterPresets: { enabled: false, maxPresetsPerUser: 0 }, + limits: { regularUser: 0, premiumUser: 0 }, + maxBatchSize: 0, + }, + users: { + signUp: false, + lastHashesCheck: { enabled: false, maxHashes: 0 }, + autoBan: { enabled: false, maxCount: 0, maxHours: 0 }, + profiles: { enabled: false }, + discordIntegration: { enabled: false }, + xp: { + enabled: false, + funboxBonus: 0, + gainMultiplier: 0, + maxDailyBonus: 0, + minDailyBonus: 0, + streak: { + enabled: false, + maxStreakDays: 0, + maxStreakMultiplier: 0, + }, + }, + inbox: { enabled: false, maxMail: 0 }, + premium: { enabled: false }, + }, + admin: { endpointsEnabled: false }, + apeKeys: { + endpointsEnabled: false, + acceptKeys: false, + maxKeysPerUser: 0, + apeKeyBytes: 0, + apeKeySaltRounds: 0, + }, + rateLimiting: { + badAuthentication: { + enabled: false, + penalty: 0, + flaggedStatusCodes: [], + }, + }, + dailyLeaderboards: { + enabled: false, + leaderboardExpirationTimeInDays: 0, + maxResults: 0, + validModeRules: [], + scheduleRewardsModeRules: [], + topResultsToAnnounce: 1, + xpRewardBrackets: [], + }, + leaderboards: { + minTimeTyping: 0, + weeklyXp: { + enabled: false, + expirationTimeInDays: 0, + xpRewardBrackets: [], + }, + }, + connections: { enabled: false, maxPerUser: 0 }, + }, + }, + { + description: "missing required top-level field", + input: { + maintenance: false, + dev: { responseSlowdownMs: 0 }, + // quotes missing + } as any, + expectedError: "Required", + }, + { + description: "topResultsToAnnounce cannot be zero", + input: { + maintenance: false, + dev: { responseSlowdownMs: 0 }, + quotes: { + reporting: { enabled: false, maxReports: 0, contentReportLimit: 0 }, + submissionsEnabled: false, + maxFavorites: 0, + }, + results: { + savingEnabled: false, + objectHashCheckEnabled: false, + filterPresets: { enabled: false, maxPresetsPerUser: 0 }, + limits: { regularUser: 0, premiumUser: 0 }, + maxBatchSize: 0, + }, + users: { + signUp: false, + lastHashesCheck: { enabled: false, maxHashes: 0 }, + autoBan: { enabled: false, maxCount: 0, maxHours: 0 }, + profiles: { enabled: false }, + discordIntegration: { enabled: false }, + xp: { + enabled: false, + funboxBonus: 0, + gainMultiplier: 0, + maxDailyBonus: 0, + minDailyBonus: 0, + streak: { + enabled: false, + maxStreakDays: 0, + maxStreakMultiplier: 0, + }, + }, + inbox: { enabled: false, maxMail: 0 }, + premium: { enabled: false }, + }, + admin: { endpointsEnabled: false }, + apeKeys: { + endpointsEnabled: false, + acceptKeys: false, + maxKeysPerUser: 0, + apeKeyBytes: 0, + apeKeySaltRounds: 0, + }, + rateLimiting: { + badAuthentication: { + enabled: false, + penalty: 0, + flaggedStatusCodes: [], + }, + }, + dailyLeaderboards: { + enabled: false, + leaderboardExpirationTimeInDays: 0, + maxResults: 0, + validModeRules: [], + scheduleRewardsModeRules: [], + topResultsToAnnounce: 0, + xpRewardBrackets: [], + }, + leaderboards: { + minTimeTyping: 0, + weeklyXp: { + enabled: false, + expirationTimeInDays: 0, + xpRewardBrackets: [], + }, + }, + connections: { enabled: false, maxPerUser: 0 }, + }, + expectedError: "Number must be greater than 0", + }, + { + description: "minTimeTyping cannot be negative", + input: { + maintenance: false, + dev: { responseSlowdownMs: 0 }, + quotes: { + reporting: { enabled: false, maxReports: 0, contentReportLimit: 0 }, + submissionsEnabled: false, + maxFavorites: 0, + }, + results: { + savingEnabled: false, + objectHashCheckEnabled: false, + filterPresets: { enabled: false, maxPresetsPerUser: 0 }, + limits: { regularUser: 0, premiumUser: 0 }, + maxBatchSize: 0, + }, + users: { + signUp: false, + lastHashesCheck: { enabled: false, maxHashes: 0 }, + autoBan: { enabled: false, maxCount: 0, maxHours: 0 }, + profiles: { enabled: false }, + discordIntegration: { enabled: false }, + xp: { + enabled: false, + funboxBonus: 0, + gainMultiplier: 0, + maxDailyBonus: 0, + minDailyBonus: 0, + streak: { + enabled: false, + maxStreakDays: 0, + maxStreakMultiplier: 0, + }, + }, + inbox: { enabled: false, maxMail: 0 }, + premium: { enabled: false }, + }, + admin: { endpointsEnabled: false }, + apeKeys: { + endpointsEnabled: false, + acceptKeys: false, + maxKeysPerUser: 0, + apeKeyBytes: 0, + apeKeySaltRounds: 0, + }, + rateLimiting: { + badAuthentication: { + enabled: false, + penalty: 0, + flaggedStatusCodes: [], + }, + }, + dailyLeaderboards: { + enabled: false, + leaderboardExpirationTimeInDays: 0, + maxResults: 0, + validModeRules: [], + scheduleRewardsModeRules: [], + topResultsToAnnounce: 1, + xpRewardBrackets: [], + }, + leaderboards: { + minTimeTyping: -1, + weeklyXp: { + enabled: false, + expirationTimeInDays: 0, + xpRewardBrackets: [], + }, + }, + connections: { enabled: false, maxPerUser: 0 }, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ConfigurationSchema).toReject(input, expectedError); + } else { + expect(ConfigurationSchema).toValidate(input); + } + }); + }); +}); From 5588c54fae4e0eed33f513464eceb74225789ed3 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 12:11:09 +0200 Subject: [PATCH 05/12] connections schema --- .../schemas/__tests__/connections.spec.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 packages/schemas/__tests__/connections.spec.ts diff --git a/packages/schemas/__tests__/connections.spec.ts b/packages/schemas/__tests__/connections.spec.ts new file mode 100644 index 000000000000..d9b8f7ecaeb5 --- /dev/null +++ b/packages/schemas/__tests__/connections.spec.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from "vitest"; +import { + ConnectionStatusSchema, + ConnectionTypeSchema, + ConnectionSchema, +} from "../src/connections"; + +describe("ConnectionStatusSchema", () => { + it.each([ + { description: "valid status: pending", input: "pending" }, + { description: "valid status: accepted", input: "accepted" }, + { description: "valid status: blocked", input: "blocked" }, + { + description: "invalid status", + input: "unknown", + expectedError: "Invalid enum value", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ConnectionStatusSchema).toReject(input, expectedError); + } else { + expect(ConnectionStatusSchema).toValidate(input); + } + }); +}); + +describe("ConnectionTypeSchema", () => { + it.each([ + { description: "valid type: incoming", input: "incoming" }, + { description: "valid type: outgoing", input: "outgoing" }, + { + description: "invalid type", + input: "unknown", + expectedError: "Invalid enum value", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ConnectionTypeSchema).toReject(input, expectedError); + } else { + expect(ConnectionTypeSchema).toValidate(input); + } + }); +}); + +describe("ConnectionSchema", () => { + it.each([ + { + description: "valid connection", + input: { + _id: "abc_123", + initiatorUid: "user_1", + initiatorName: "Alice", + receiverUid: "user_2", + receiverName: "Bob", + lastModified: 1700000000, + status: "pending", + }, + }, + { + description: "invalid status", + input: { + _id: "abc_123", + initiatorUid: "user_1", + initiatorName: "Alice", + receiverUid: "user_2", + receiverName: "Bob", + lastModified: 1700000000, + status: "unknown", + }, + expectedError: "Invalid enum value", + }, + { + description: "negative lastModified", + input: { + _id: "abc_123", + initiatorUid: "user_1", + initiatorName: "Alice", + receiverUid: "user_2", + receiverName: "Bob", + lastModified: -1, + status: "pending", + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ConnectionSchema).toReject(input, expectedError); + } else { + expect(ConnectionSchema).toValidate(input); + } + }); +}); From e92fb9d051b81c74fb8146af11d1868be3627d6a Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 14:16:22 +0200 Subject: [PATCH 06/12] psas, public --- packages/schemas/__tests__/psas.spec.ts | 56 +++++++++++++++++++ packages/schemas/__tests__/public.spec.ts | 66 +++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 packages/schemas/__tests__/psas.spec.ts create mode 100644 packages/schemas/__tests__/public.spec.ts diff --git a/packages/schemas/__tests__/psas.spec.ts b/packages/schemas/__tests__/psas.spec.ts new file mode 100644 index 000000000000..560fb4bfe421 --- /dev/null +++ b/packages/schemas/__tests__/psas.spec.ts @@ -0,0 +1,56 @@ +import { it, expect, describe } from "vitest"; +import { PSASchema } from "../src/psas"; + +describe("psas schemas", () => { + describe("PSASchema", () => { + it.each([ + { + description: "minimal valid PSA", + input: { + _id: "abc123", + message: "Important announcement", + }, + }, + { + description: "valid PSA with all fields", + input: { + _id: "psa_001", + message: "Server maintenance", + date: 1700000000, + level: 2, + sticky: true, + }, + }, + { + description: "invalid _id with special characters", + input: { + _id: "abc@123", + message: "Test", + }, + expectedError: "Invalid", + }, + { + description: "missing message", + input: { + _id: "abc123", + }, + expectedError: "Required", + }, + { + description: "date is negative", + input: { + _id: "abc123", + message: "Test", + date: -1, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PSASchema).toReject(input, expectedError); + } else { + expect(PSASchema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/public.spec.ts b/packages/schemas/__tests__/public.spec.ts new file mode 100644 index 000000000000..e179dd9a3e97 --- /dev/null +++ b/packages/schemas/__tests__/public.spec.ts @@ -0,0 +1,66 @@ +import { it, expect, describe } from "vitest"; +import { SpeedHistogramSchema, TypingStatsSchema } from "../src/public"; + +describe("public schemas", () => { + describe("SpeedHistogramSchema", () => { + it.each([ + { + description: "valid record with numeric string keys and int values", + input: { + "10": 5, + "20": 3, + }, + }, + { + description: "non-integer value fails", + input: { + "10": 1.5, + }, + expectedError: "Expected integer, received float", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(SpeedHistogramSchema).toReject(input, expectedError); + } else { + expect(SpeedHistogramSchema).toValidate(input); + } + }); + }); + + describe("TypingStatsSchema", () => { + it.each([ + { + description: "valid typing stats", + input: { + timeTyping: 100, + testsCompleted: 10, + testsStarted: 12, + }, + }, + { + description: "negative timeTyping fails", + input: { + timeTyping: -1, + testsCompleted: 0, + testsStarted: 0, + }, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "non-integer testsCompleted fails", + input: { + timeTyping: 0, + testsCompleted: 1.5, + testsStarted: 0, + }, + expectedError: "Expected integer, received float", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TypingStatsSchema).toReject(input, expectedError); + } else { + expect(TypingStatsSchema).toValidate(input); + } + }); + }); +}); From 1525c71e9f6b191498ded177f4de85a5e78d8fb0 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 14:52:56 +0200 Subject: [PATCH 07/12] quotes --- packages/schemas/__tests__/quotes.spec.ts | 687 ++++++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100644 packages/schemas/__tests__/quotes.spec.ts diff --git a/packages/schemas/__tests__/quotes.spec.ts b/packages/schemas/__tests__/quotes.spec.ts new file mode 100644 index 000000000000..6b472b402bbe --- /dev/null +++ b/packages/schemas/__tests__/quotes.spec.ts @@ -0,0 +1,687 @@ +import { it, expect, describe } from "vitest"; +import { + QuoteIdSchema, + ApproveQuoteSchema, + QuoteSchema, + QuoteRatingSchema, + QuoteReportReasonSchema, + QuoteDataQuoteSchema, + QuoteDataSchema, +} from "../src/quotes"; + +describe("quotes schemas", () => { + describe("QuoteIdSchema", () => { + it.each([ + { + description: "valid numeric quote id", + input: 123, + }, + { + description: "valid numeric string quote id", + input: "456", + }, + { + description: "valid zero quote id", + input: 0, + }, + { + description: "valid numeric string zero quote id", + input: "0", + }, + { + description: "negative number", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "non-numeric string", + input: "abc", + expectedError: "Invalid", + }, + { + description: "float number", + input: 1.5, + expectedError: "Expected integer, received float", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteIdSchema).toReject(input, expectedError); + } else { + expect(QuoteIdSchema).toValidate(input); + } + }); + }); + + describe("ApproveQuoteSchema", () => { + it.each([ + { + description: "valid approve quote", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + length: 18, + approvedBy: "John Doe", + }, + }, + { + description: "missing id", + input: { + text: "Test quote text", + source: "Test source", + length: 18, + approvedBy: "John Doe", + }, + expectedError: "Invalid input", + }, + { + description: "missing text", + input: { + id: 123, + source: "Test source", + length: 18, + approvedBy: "John Doe", + }, + expectedError: "Required", + }, + { + description: "missing source", + input: { + id: 123, + text: "Test quote text", + length: 18, + approvedBy: "John Doe", + }, + expectedError: "Required", + }, + { + description: "missing length", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + approvedBy: "John Doe", + }, + expectedError: "Required", + }, + { + description: "missing approvedBy", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + length: 18, + }, + expectedError: "Required", + }, + { + description: "length is zero", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + length: 0, + approvedBy: "John Doe", + }, + expectedError: "Number must be greater than 0", + }, + { + description: "length is negative", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + length: -1, + approvedBy: "John Doe", + }, + expectedError: "Number must be greater than 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ApproveQuoteSchema).toReject(input, expectedError); + } else { + expect(ApproveQuoteSchema).toValidate(input); + } + }); + }); + + describe("QuoteSchema", () => { + it.each([ + { + description: "valid quote", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + source: "Test source", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: 1625097993000, + approved: true, + }, + }, + { + description: "approved is false", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + source: "Test source", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: 1625097993000, + approved: false, + }, + }, + { + description: "missing _id", + input: { + text: "Test quote text", + source: "Test source", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: 1625097993000, + approved: true, + }, + expectedError: "Required", + }, + { + description: "missing text", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + source: "Test source", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: 1625097993000, + approved: true, + }, + expectedError: "Required", + }, + { + description: "missing source", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: 1625097993000, + approved: true, + }, + expectedError: "Required", + }, + { + description: "missing language", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + source: "Test source", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: 1625097993000, + approved: true, + }, + expectedError: "Required", + }, + { + description: "missing submittedBy", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + source: "Test source", + language: "english", + timestamp: 1625097993000, + approved: true, + }, + expectedError: "Required", + }, + { + description: "missing timestamp", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + source: "Test source", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + approved: true, + }, + expectedError: "Required", + }, + { + description: "missing approved", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + source: "Test source", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: 1625097993000, + }, + expectedError: "Required", + }, + { + description: "timestamp is negative", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + text: "Test quote text", + source: "Test source", + language: "english", + submittedBy: "60d5f3d9e4b0a71f8d9f1234", + timestamp: -1, + approved: true, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteSchema).toReject(input, expectedError); + } else { + expect(QuoteSchema).toValidate(input); + } + }); + }); + + describe("QuoteRatingSchema", () => { + it.each([ + { + description: "valid quote rating", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + quoteId: 123, + average: 4.5, + ratings: 10, + totalRating: 45, + }, + }, + { + description: "average is zero", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + quoteId: 123, + average: 0, + ratings: 0, + totalRating: 0, + }, + }, + { + description: "missing _id", + input: { + language: "english", + quoteId: 123, + average: 4.5, + ratings: 10, + totalRating: 45, + }, + expectedError: "Required", + }, + { + description: "missing language", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + quoteId: 123, + average: 4.5, + ratings: 10, + totalRating: 45, + }, + expectedError: "Required", + }, + { + description: "missing quoteId", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + average: 4.5, + ratings: 10, + totalRating: 45, + }, + expectedError: "Invalid input", + }, + { + description: "missing average", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + quoteId: 123, + ratings: 10, + totalRating: 45, + }, + expectedError: "Required", + }, + { + description: "missing ratings", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + quoteId: 123, + average: 4.5, + totalRating: 45, + }, + expectedError: "Required", + }, + { + description: "missing totalRating", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + quoteId: 123, + average: 4.5, + ratings: 10, + }, + expectedError: "Required", + }, + { + description: "average is negative", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + quoteId: 123, + average: -0.5, + ratings: 10, + totalRating: 45, + }, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "ratings is negative", + input: { + _id: "60d5f3d9e4b0a71f8d9f1234", + language: "english", + quoteId: 123, + average: 4.5, + ratings: -1, + totalRating: 45, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteRatingSchema).toReject(input, expectedError); + } else { + expect(QuoteRatingSchema).toValidate(input); + } + }); + }); + + describe("QuoteReportReasonSchema", () => { + it.each([ + { + description: "valid reason - grammatical error", + input: "Grammatical error", + }, + { + description: "valid reason - duplicate quote", + input: "Duplicate quote", + }, + { + description: "valid reason - inappropriate content", + input: "Inappropriate content", + }, + { + description: "valid reason - low quality content", + input: "Low quality content", + }, + { + description: "valid reason - incorrect source", + input: "Incorrect source", + }, + { + description: "invalid reason", + input: "Invalid reason", + expectedError: + "Invalid enum value. Expected 'Grammatical error' | 'Duplicate quote' | 'Inappropriate content' | 'Low quality content' | 'Incorrect source', received 'Invalid reason'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteReportReasonSchema).toReject(input, expectedError); + } else { + expect(QuoteReportReasonSchema).toValidate(input); + } + }); + }); + + describe("QuoteDataQuoteSchema", () => { + it.each([ + { + description: "valid quote data quote with britishText", + input: { + id: 123, + text: "Test quote text", + britishText: "British spelling", + source: "Test source", + length: 18, + approvedBy: "John Doe", + }, + }, + { + description: + "valid quote data quote without britishText and approvedBy", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + length: 18, + }, + }, + { + description: "missing id", + input: { + text: "Test quote text", + source: "Test source", + length: 18, + }, + expectedError: "Required", + }, + { + description: "missing text", + input: { + id: 123, + source: "Test source", + length: 18, + }, + expectedError: "Required", + }, + { + description: "missing source", + input: { + id: 123, + text: "Test quote text", + length: 18, + }, + expectedError: "Required", + }, + { + description: "missing length", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + }, + expectedError: "Required", + }, + { + description: "extra property not allowed", + input: { + id: 123, + text: "Test quote text", + source: "Test source", + length: 18, + extraProp: "value", + }, + expectedError: "Unrecognized key(s) in object: 'extraProp'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteDataQuoteSchema).toReject(input, expectedError); + } else { + expect(QuoteDataQuoteSchema).toValidate(input); + } + }); + }); + + describe("QuoteDataSchema", () => { + it.each([ + { + description: "valid quote data", + input: { + language: "english", + groups: [ + [0, 10], + [10, 20], + [20, 30], + [30, 40], + ], + quotes: [ + { + id: 1, + text: "Quote 1", + source: "Source 1", + length: 8, + }, + { + id: 2, + text: "Quote 2", + britishText: "British quote 2", + source: "Source 2", + length: 9, + approvedBy: "John Doe", + }, + ], + }, + }, + { + description: "missing language", + input: { + groups: [ + [0, 10], + [10, 20], + [20, 30], + [30, 40], + ], + quotes: [ + { + id: 1, + text: "Quote 1", + source: "Source 1", + length: 8, + }, + ], + }, + expectedError: "Required", + }, + { + description: "missing groups", + input: { + language: "english", + quotes: [ + { + id: 1, + text: "Quote 1", + source: "Source 1", + length: 8, + }, + ], + }, + expectedError: "Required", + }, + { + description: "missing quotes", + input: { + language: "english", + groups: [ + [0, 10], + [10, 20], + [20, 30], + [30, 40], + ], + }, + expectedError: "Required", + }, + { + description: "groups length is not 4", + input: { + language: "english", + groups: [ + [0, 10], + [10, 20], + ], + quotes: [ + { + id: 1, + text: "Quote 1", + source: "Source 1", + length: 8, + }, + ], + }, + expectedError: "Array must contain exactly 4 element(s)", + }, + { + description: "groups item is not tuple of length 2", + input: { + language: "english", + groups: [ + [0, 10, 20], + [10, 20], + [20, 30], + [30, 40], + ], + quotes: [ + { + id: 1, + text: "Quote 1", + source: "Source 1", + length: 8, + }, + ], + }, + expectedError: "Array must contain at most 2 element(s)", + }, + { + description: "quotes item has extra property not allowed", + input: { + language: "english", + groups: [ + [0, 10], + [10, 20], + [20, 30], + [30, 40], + ], + quotes: [ + { + id: 1, + text: "Quote 1", + source: "Source 1", + length: 8, + extraProp: "value", + }, + ], + }, + expectedError: "Unrecognized key(s) in object: 'extraProp'", + }, + { + description: "extra property not allowed at root level", + input: { + language: "english", + groups: [ + [0, 10], + [10, 20], + [20, 30], + [30, 40], + ], + quotes: [ + { + id: 1, + text: "Quote 1", + source: "Source 1", + length: 8, + }, + ], + }, + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteDataSchema).toReject(input, expectedError); + } else { + expect(QuoteDataSchema).toValidate(input); + } + }); + }); +}); From 4cbec0e375f1869adffbc761e17230d5301d55e4 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 14:59:40 +0200 Subject: [PATCH 08/12] results --- packages/schemas/__tests__/results.spec.ts | 291 +++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 packages/schemas/__tests__/results.spec.ts diff --git a/packages/schemas/__tests__/results.spec.ts b/packages/schemas/__tests__/results.spec.ts new file mode 100644 index 000000000000..7a885fab2313 --- /dev/null +++ b/packages/schemas/__tests__/results.spec.ts @@ -0,0 +1,291 @@ +import { describe, expect, it } from "vitest"; +import { + CharStatsSchema, + ChartDataSchema, + CompletedEventCustomTextSchema, + CompletedEventSchema, + CustomTextSettingsSchema, + IncompleteTestSchema, + KeyStatsSchema, + OldChartDataSchema, + PostResultResponseSchema, + ResultMinifiedSchema, + ResultSchema, + XpBreakdownSchema, +} from "../src/results"; + +describe("results schemas", () => { + describe("IncompleteTestSchema", () => { + it.each([ + { + description: "valid incomplete test", + input: { + acc: 100, + seconds: 30, + }, + }, + { + description: "acc at minimum", + input: { + acc: 50, + seconds: 0, + }, + }, + ] as const)("$description", ({ input }) => { + expect(IncompleteTestSchema).toValidate(input); + }); + }); + + describe("OldChartDataSchema", () => { + it.each([ + { + description: "valid chart data", + input: { + wpm: [100, 110, 120], + raw: [105, 115, 125], + err: [0, 1, 2], + }, + }, + ] as const)("$description", ({ input }) => { + expect(OldChartDataSchema).toValidate(input); + }); + }); + + describe("ChartDataSchema", () => { + it.each([ + { + description: "valid chart data", + input: { + wpm: [100, 110, 120], + burst: [95, 105, 115], + err: [0, 1, 2], + }, + }, + ] as const)("$description", ({ input }) => { + expect(ChartDataSchema).toValidate(input); + }); + }); + + describe("KeyStatsSchema", () => { + it.each([ + { + description: "valid key stats", + input: { + average: 50, + sd: 10, + }, + }, + { + description: "zero values", + input: { + average: 0, + sd: 0, + }, + }, + ] as const)("$description", ({ input }) => { + expect(KeyStatsSchema).toValidate(input); + }); + }); + + describe("CompletedEventCustomTextSchema", () => { + it.each([ + { + description: "valid custom text settings", + input: { + textLen: 100, + mode: "repeat", + pipeDelimiter: false, + limit: { + mode: "word", + value: 100, + }, + }, + }, + ] as const)("$description", ({ input }) => { + expect(CompletedEventCustomTextSchema).toValidate(input); + }); + }); + + describe("CustomTextSettingsSchema", () => { + it.each([ + { + description: "valid custom text settings", + input: { + text: ["hello", "world"], + mode: "repeat", + pipeDelimiter: false, + limit: { + mode: "word", + value: 100, + }, + }, + }, + ] as const)("$description", ({ input }) => { + expect(CustomTextSettingsSchema).toValidate(input); + }); + }); + + describe("CharStatsSchema", () => { + it.each([ + { + description: "valid char stats", + input: [100, 5, 2, 3], + }, + ] as const)("$description", ({ input }) => { + expect(CharStatsSchema).toValidate(input); + }); + }); + + describe("ResultSchema", () => { + it.each([ + { + description: "valid result", + input: { + wpm: 100, + rawWpm: 110, + charStats: [100, 5, 2, 3], + acc: 98, + mode: "time", + mode2: "15", + timestamp: 1000000000, + testDuration: 30, + consistency: 95, + keyConsistency: 90, + chartData: { wpm: [100], burst: [95], err: [0] }, + uid: "abc123", + _id: "def456", + name: "Test Result", + }, + }, + ] as const)("$description", ({ input }) => { + expect(ResultSchema).toValidate(input); + }); + }); + + describe("ResultMinifiedSchema", () => { + it.each([ + { + description: "valid minified result", + input: { + wpm: 100, + rawWpm: 110, + charStats: [100, 5, 2, 3], + acc: 98, + mode: "time", + mode2: "15", + timestamp: 1000000000, + testDuration: 30, + consistency: 95, + keyConsistency: 90, + uid: "abc123", + _id: "def456", + }, + }, + ] as const)("$description", ({ input }) => { + expect(ResultMinifiedSchema).toValidate(input); + }); + }); + + describe("CompletedEventSchema", () => { + it.each([ + { + description: "valid completed event", + input: { + wpm: 100, + rawWpm: 110, + charStats: [100, 5, 2, 3], + acc: 98, + mode: "time", + mode2: "15", + timestamp: 1000000000, + testDuration: 30, + restartCount: 1, + incompleteTestSeconds: 5, + afkDuration: 0, + tags: ["abc123"], + bailedOut: false, + blindMode: false, + lazyMode: false, + funbox: ["ascii"], + language: "english", + difficulty: "normal", + numbers: false, + punctuation: false, + consistency: 95, + keyConsistency: 90, + uid: "uid123", + chartData: { wpm: [100], burst: [95], err: [0] }, + charTotal: 150, + challenge: "abc_123", + hash: "abc123", + keyDuration: [100, 120, 90], + keySpacing: [50, 60, 45], + keyOverlap: 10, + lastKeyToEnd: 200, + startToFirstKey: 500, + wpmConsistency: 95, + stopOnLetter: false, + incompleteTests: [{ acc: 100, seconds: 30 }], + }, + }, + ] as const)("$description", ({ input }) => { + expect(CompletedEventSchema).toValidate(input); + }); + }); + + describe("XpBreakdownSchema", () => { + it.each([ + { + description: "valid xp breakdown", + input: { + base: 10, + fullAccuracy: 5, + quote: 2, + corrected: 3, + punctuation: 1, + numbers: 0, + funbox: 0, + streak: 0, + incomplete: 0, + daily: 0, + accPenalty: 0, + configMultiplier: 1, + }, + }, + ] as const)("$description", ({ input }) => { + expect(XpBreakdownSchema).toValidate(input); + }); + }); + + describe("PostResultResponseSchema", () => { + it.each([ + { + description: "valid post result response", + input: { + insertedId: "abc123", + isPb: true, + tagPbs: [], + xp: 15, + dailyXpBonus: false, + xpBreakdown: { + base: 10, + fullAccuracy: 5, + quote: 2, + corrected: 3, + punctuation: 1, + numbers: 0, + funbox: 0, + streak: 0, + incomplete: 0, + daily: 0, + accPenalty: 0, + configMultiplier: 1, + }, + streak: 5, + }, + }, + ] as const)("$description", ({ input }) => { + expect(PostResultResponseSchema).toValidate(input); + }); + }); +}); From 45e0e718a00644646b0dc3fb272f38dac7fc0b06 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 15:17:41 +0200 Subject: [PATCH 09/12] all --- packages/schemas/__tests__/configs.spec.ts | 664 ++++++++++++++++++ packages/schemas/__tests__/fonts.spec.ts | 32 + packages/schemas/__tests__/languages.spec.ts | 55 ++ packages/schemas/__tests__/layouts.spec.ts | 23 + .../schemas/__tests__/leaderboards.spec.ts | 23 + packages/schemas/__tests__/presets.spec.ts | 21 + packages/schemas/__tests__/shared.spec.ts | 287 ++++++++ packages/schemas/__tests__/themes.spec.ts | 23 + packages/schemas/__tests__/users.spec.ts | 12 + packages/schemas/__tests__/util.spec.ts | 200 ++---- 10 files changed, 1213 insertions(+), 127 deletions(-) create mode 100644 packages/schemas/__tests__/fonts.spec.ts create mode 100644 packages/schemas/__tests__/languages.spec.ts create mode 100644 packages/schemas/__tests__/layouts.spec.ts create mode 100644 packages/schemas/__tests__/leaderboards.spec.ts create mode 100644 packages/schemas/__tests__/presets.spec.ts create mode 100644 packages/schemas/__tests__/shared.spec.ts create mode 100644 packages/schemas/__tests__/themes.spec.ts create mode 100644 packages/schemas/__tests__/users.spec.ts diff --git a/packages/schemas/__tests__/configs.spec.ts b/packages/schemas/__tests__/configs.spec.ts index 3435271f6ed9..fcefaec3d3a6 100644 --- a/packages/schemas/__tests__/configs.spec.ts +++ b/packages/schemas/__tests__/configs.spec.ts @@ -38,6 +38,32 @@ import { FunboxNameSchema, PlayTimeWarningSchema, ConfigGroupNameSchema, + QuoteLengthConfigSchema, + KeymapSizeSchema, + SoundVolumeSchema, + AccountChartSchema, + TapeMarginSchema, + CustomBackgroundFilterSchema, + CustomLayoutFluidSchema, + CustomPolyglotSchema, + ShowPbSchema, + ColorHexValueSchema, + CustomThemeColorsSchema, + FunboxSchema, + PaceCaretCustomSpeedSchema, + MinWpmCustomSpeedSchema, + MinimumAccuracyCustomSchema, + MinimumBurstCustomSpeedSchema, + TimeConfigSchema, + WordCountSchema, + KeymapLayoutSchema, + LayoutSchema, + FontSizeSchema, + MaxLineWidthSchema, + ConfigSchema, + ConfigKeySchema, + PartialConfigSchema, + FavThemesSchema, } from "../src/configs"; describe("configs schemas", () => { @@ -893,4 +919,642 @@ describe("configs schemas", () => { } }); }); + + describe("QuoteLengthConfigSchema", () => { + it.each([ + { + description: "valid array with one value", + input: [-3], + }, + { + description: "valid array with multiple values", + input: [0, 1, 2], + }, + { + description: "invalid value", + input: [4], + expectedError: "Invalid input", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteLengthConfigSchema).toReject(input, expectedError); + } else { + expect(QuoteLengthConfigSchema).toValidate(input); + } + }); + }); + + describe("KeymapSizeSchema", () => { + it.each([ + { description: "valid value 1.0", input: 1.0 }, + { description: "valid min value 0.5", input: 0.5 }, + { description: "valid max value 3.5", input: 3.5 }, + { + description: "invalid min below range", + input: 0.4, + expectedError: "Number must be greater than or equal to 0.5", + }, + { + description: "invalid max above range", + input: 3.6, + expectedError: "Number must be less than or equal to 3.5", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(KeymapSizeSchema).toReject(input, expectedError); + } else { + expect(KeymapSizeSchema).toValidate(input); + } + }); + }); + + describe("SoundVolumeSchema", () => { + it.each([ + { description: "valid 0", input: 0 }, + { description: "valid 0.5", input: 0.5 }, + { description: "valid 1", input: 1 }, + { + description: "invalid below range", + input: -0.1, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "invalid above range", + input: 1.1, + expectedError: "Number must be less than or equal to 1", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(SoundVolumeSchema).toReject(input, expectedError); + } else { + expect(SoundVolumeSchema).toValidate(input); + } + }); + }); + + describe("AccountChartSchema", () => { + it.each([ + { description: "valid all on", input: ["on", "on", "on", "on"] }, + { description: "valid mixed values", input: ["off", "on", "off", "on"] }, + { + description: "invalid value", + input: ["on", "yes", "no", "off"], + expectedError: "Invalid enum value", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(AccountChartSchema).toReject(input, expectedError); + } else { + expect(AccountChartSchema).toValidate(input); + } + }); + }); + + describe("TapeMarginSchema", () => { + it.each([ + { description: "valid min 10", input: 10 }, + { description: "valid middle 50", input: 50 }, + { description: "valid max 90", input: 90 }, + { + description: "invalid below range", + input: 5, + expectedError: "Number must be greater than or equal to 10", + }, + { + description: "invalid above range", + input: 95, + expectedError: "Number must be less than or equal to 90", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TapeMarginSchema).toReject(input, expectedError); + } else { + expect(TapeMarginSchema).toValidate(input); + } + }); + }); + + describe("CustomBackgroundFilterSchema", () => { + it.each([ + { description: "valid tuple [0, 0, 0, 0]", input: [0, 0, 0, 0] }, + { + description: "valid tuple [100, 50, 25, 75]", + input: [100, 50, 25, 75], + }, + { + description: "invalid - too few items", + input: [0, 0, 0], + expectedError: "Array must contain at least 4 element(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomBackgroundFilterSchema).toReject(input, expectedError); + } else { + expect(CustomBackgroundFilterSchema).toValidate(input); + } + }); + }); + + describe("CustomLayoutFluidSchema", () => { + it.each([ + { description: "valid array with 2 items", input: ["dvorak", "colemak"] }, + { + description: "valid array with 15 items", + input: [ + "dvorak", + "colemak", + "qwerty", + "azerty", + "qwertz", + "dvorak", + "colemak", + "qwerty", + "azerty", + "qwertz", + "dvorak", + "colemak", + "qwerty", + "azerty", + "qwertz", + ], + }, + { + description: "invalid array with 1 item", + input: ["qwerty"], + expectedError: "Array must contain at least 2 element(s)", + }, + { + description: "invalid array with 16 items", + input: [ + "qwerty", + "azerty", + "qwertz", + "dvorak", + "colemak", + "qwerty", + "azerty", + "qwertz", + "dvorak", + "colemak", + "qwerty", + "azerty", + "qwertz", + "dvorak", + "colemak", + "qwerty", + ], + expectedError: "Array must contain at most 15 element(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomLayoutFluidSchema).toReject(input, expectedError); + } else { + expect(CustomLayoutFluidSchema).toValidate(input); + } + }); + }); + + describe("CustomPolyglotSchema", () => { + it.each([ + { + description: "valid array with 2 languages", + input: ["english", "spanish"], + }, + { + description: "invalid array with 1 language", + input: ["english"], + expectedError: "Array must contain at least 2 element(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomPolyglotSchema).toReject(input, expectedError); + } else { + expect(CustomPolyglotSchema).toValidate(input); + } + }); + }); + + describe("ShowPbSchema", () => { + it.each([ + { description: "valid true", input: true }, + { description: "valid false", input: false }, + ] as const)("$description", ({ input }) => { + expect(ShowPbSchema).toValidate(input); + }); + }); + + describe("ColorHexValueSchema", () => { + it.each([ + { description: "valid short #fff", input: "#fff" }, + { description: "valid long #ffffff", input: "#ffffff" }, + { description: "valid uppercase #ABC123", input: "#ABC123" }, + { + description: "invalid - missing #", + input: "ffffff", + expectedError: "Invalid", + }, + { + description: "invalid - wrong format", + input: "#gggggg", + expectedError: "Invalid", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ColorHexValueSchema).toReject(input, expectedError); + } else { + expect(ColorHexValueSchema).toValidate(input); + } + }); + }); + + describe("CustomThemeColorsSchema", () => { + it.each([ + { + description: "valid tuple of 10 colors", + input: [ + "#ffffff", + "#000000", + "#ff0000", + "#00ff00", + "#0000ff", + "#ffff00", + "#ff00ff", + "#00ffff", + "#123456", + "#abcdef", + ], + }, + { + description: "invalid - too few items", + input: ["#ffffff"], + expectedError: "Array must contain at least 10 element(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomThemeColorsSchema).toReject(input, expectedError); + } else { + expect(CustomThemeColorsSchema).toValidate(input); + } + }); + }); + + describe("FavThemesSchema", () => { + it.each([ + { description: "valid empty array", input: [] }, + { description: "valid single theme", input: ["dracula"] }, + { + description: "valid multiple themes", + input: ["dracula", "rose_pine", "monokai"], + }, + ] as const)("$description", ({ input }) => { + expect(FavThemesSchema).toValidate(input); + }); + }); + + describe("FunboxSchema", () => { + it.each([ + { description: "valid empty array", input: [] }, + { description: "valid single funbox", input: ["mirror"] }, + { + description: "valid multiple funboxes", + input: ["mirror", "upside_down"], + }, + { description: "valid 15 funboxes", input: Array(15).fill("mirror") }, + { + description: "invalid 16 funboxes", + input: Array(16).fill("mirror"), + expectedError: "Array must contain at most 15 element(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(FunboxSchema).toReject(input, expectedError); + } else { + expect(FunboxSchema).toValidate(input); + } + }); + }); + + describe("PaceCaretCustomSpeedSchema", () => { + it.each([ + { description: "valid 0", input: 0 }, + { description: "valid positive number", input: 100 }, + { + description: "invalid negative", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PaceCaretCustomSpeedSchema).toReject(input, expectedError); + } else { + expect(PaceCaretCustomSpeedSchema).toValidate(input); + } + }); + }); + + describe("MinWpmCustomSpeedSchema", () => { + it.each([ + { description: "valid 0", input: 0 }, + { description: "valid positive number", input: 80 }, + { + description: "invalid negative", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MinWpmCustomSpeedSchema).toReject(input, expectedError); + } else { + expect(MinWpmCustomSpeedSchema).toValidate(input); + } + }); + }); + + describe("MinimumAccuracyCustomSchema", () => { + it.each([ + { description: "valid 0", input: 0 }, + { description: "valid 50", input: 50 }, + { description: "valid max 100", input: 100 }, + { + description: "invalid negative", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "invalid above max", + input: 150, + expectedError: "Number must be less than or equal to 100", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MinimumAccuracyCustomSchema).toReject(input, expectedError); + } else { + expect(MinimumAccuracyCustomSchema).toValidate(input); + } + }); + }); + + describe("MinimumBurstCustomSpeedSchema", () => { + it.each([ + { description: "valid 0", input: 0 }, + { description: "valid positive number", input: 100 }, + { + description: "invalid negative", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MinimumBurstCustomSpeedSchema).toReject(input, expectedError); + } else { + expect(MinimumBurstCustomSpeedSchema).toValidate(input); + } + }); + }); + + describe("TimeConfigSchema", () => { + it.each([ + { description: "valid 0", input: 0 }, + { description: "valid positive number", input: 30 }, + { + description: "invalid negative", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "invalid decimal", + input: 30.5, + expectedError: "Expected integer, received float", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(TimeConfigSchema).toReject(input, expectedError); + } else { + expect(TimeConfigSchema).toValidate(input); + } + }); + }); + + describe("WordCountSchema", () => { + it.each([ + { description: "valid 0", input: 0 }, + { description: "valid positive number", input: 100 }, + { + description: "invalid negative", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + { + description: "invalid decimal", + input: 10.5, + expectedError: "Expected integer, received float", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(WordCountSchema).toReject(input, expectedError); + } else { + expect(WordCountSchema).toValidate(input); + } + }); + }); + + describe("KeymapLayoutSchema", () => { + it.each([ + { description: "valid overrideSync", input: "overrideSync" }, + { description: "valid layout name", input: "qwerty" }, + ] as const)("$description", ({ input }) => { + expect(KeymapLayoutSchema).toValidate(input); + }); + }); + + describe("LayoutSchema", () => { + it.each([ + { description: "valid default", input: "default" }, + { description: "valid layout name", input: "qwerty" }, + ] as const)("$description", ({ input }) => { + expect(LayoutSchema).toValidate(input); + }); + }); + + describe("FontSizeSchema", () => { + it.each([ + { description: "valid positive number", input: 12 }, + { description: "valid small positive", input: 0.5 }, + { + description: "invalid zero", + input: 0, + expectedError: "Number must be greater than 0", + }, + { + description: "invalid negative", + input: -1, + expectedError: "Number must be greater than 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(FontSizeSchema).toReject(input, expectedError); + } else { + expect(FontSizeSchema).toValidate(input); + } + }); + }); + + describe("MaxLineWidthSchema", () => { + it.each([ + { description: "valid min 20", input: 20 }, + { description: "valid middle value", input: 500 }, + { description: "valid max 1000", input: 1000 }, + { description: "valid zero (no limit)", input: 0 }, + { + description: "invalid below min", + input: 19, + expectedError: "Number must be greater than or equal to 20", + }, + { + description: "invalid above max", + input: 1001, + expectedError: "Number must be less than or equal to 1000", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(MaxLineWidthSchema).toReject(input, expectedError); + } else { + expect(MaxLineWidthSchema).toValidate(input); + } + }); + }); + + describe("ConfigSchema", () => { + it.each([ + { + description: "valid minimal config", + input: { + punctuation: false, + numbers: false, + words: 10, + time: 15, + mode: "time", + quoteLength: [], + language: "english", + burstHeatmap: false, + difficulty: "normal", + quickRestart: "off", + repeatQuotes: "off", + resultSaving: true, + blindMode: false, + alwaysShowWordsHistory: false, + singleListCommandLine: "manual", + minWpm: "off", + minWpmCustomSpeed: 0, + minAcc: "off", + minAccCustom: 0, + minBurst: "off", + minBurstCustomSpeed: 0, + britishEnglish: false, + funbox: [], + customLayoutfluid: ["qwerty", "colemak"], + customPolyglot: ["english", "spanish"], + freedomMode: false, + strictSpace: false, + oppositeShiftMode: "off", + stopOnError: "off", + confidenceMode: "off", + quickEnd: false, + indicateTypos: "off", + compositionDisplay: "off", + hideExtraLetters: false, + lazyMode: false, + layout: "default", + codeUnindentOnBackspace: false, + soundVolume: 1, + playSoundOnClick: "off", + playSoundOnError: "off", + playTimeWarning: "off", + smoothCaret: "off", + caretStyle: "default", + paceCaret: "off", + paceCaretCustomSpeed: 0, + paceCaretStyle: "default", + repeatedPace: false, + timerStyle: "bar", + liveSpeedStyle: "off", + liveAccStyle: "off", + liveBurstStyle: "off", + timerColor: "black", + timerOpacity: "1", + highlightMode: "off", + typedEffect: "keep", + tapeMode: "off", + tapeMargin: 50, + smoothLineScroll: false, + showAllLines: false, + alwaysShowDecimalPlaces: false, + typingSpeedUnit: "wpm", + startGraphsAtZero: true, + maxLineWidth: 500, + fontSize: 16, + fontFamily: "terranova", + keymapMode: "off", + keymapLayout: "overrideSync", + keymapStyle: "staggered", + keymapLegendStyle: "lowercase", + keymapShowTopRow: "always", + keymapSize: 1, + flipTestColors: false, + colorfulMode: false, + customBackground: "", + customBackgroundSize: "cover", + customBackgroundFilter: [0, 0, 0, 0], + autoSwitchTheme: false, + themeLight: "dracula", + themeDark: "rose_pine", + randomTheme: "off", + favThemes: [], + theme: "dark", + customTheme: false, + customThemeColors: Array(10).fill("#ffffff"), + showKeyTips: false, + showOutOfFocusWarning: true, + capsLockWarning: false, + showAverage: "off", + showPb: false, + accountChart: ["off", "off", "off", "off"], + monkey: false, + monkeyPowerLevel: "off", + ads: "off", + }, + }, + ] as const)("$description", ({ input }) => { + expect(ConfigSchema).toValidate(input); + }); + }); + + describe("ConfigKeySchema", () => { + it.each([ + { description: "valid key punctuation", input: "punctuation" }, + { description: "valid key time", input: "time" }, + { + description: "invalid key", + input: "invalid_key", + expectedError: "Invalid enum value", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ConfigKeySchema).toReject(input, expectedError); + } else { + expect(ConfigKeySchema).toValidate(input); + } + }); + }); + + describe("PartialConfigSchema", () => { + it.each([ + { + description: "valid partial config", + input: { punctuation: true }, + }, + ] as const)("$description", ({ input }) => { + expect(PartialConfigSchema).toValidate(input); + }); + }); }); diff --git a/packages/schemas/__tests__/fonts.spec.ts b/packages/schemas/__tests__/fonts.spec.ts new file mode 100644 index 000000000000..ca1f1045ea02 --- /dev/null +++ b/packages/schemas/__tests__/fonts.spec.ts @@ -0,0 +1,32 @@ +import { it, expect, describe } from "vitest"; +import { FontNameSchema } from "../src/fonts"; + +describe("fonts schemas", () => { + describe("FontNameSchema", () => { + it.each([ + { description: "valid known font Roboto_Mono", input: "Roboto_Mono" }, + { description: "valid known font Inter_Tight", input: "Inter_Tight" }, + { + description: "valid custom font with underscore", + input: "Custom_Font", + }, + { description: "valid custom font with hyphen", input: "Custom-Font" }, + { + description: "invalid font with space", + input: "Custom Font", + expectedError: "Invalid", + }, + { + description: "invalid font exceeds max length 50", + input: "a".repeat(51), + expectedError: "String must contain at most 50 character", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(FontNameSchema).toReject(input, expectedError); + } else { + expect(FontNameSchema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/languages.spec.ts b/packages/schemas/__tests__/languages.spec.ts new file mode 100644 index 000000000000..f6049867cc74 --- /dev/null +++ b/packages/schemas/__tests__/languages.spec.ts @@ -0,0 +1,55 @@ +import { it, expect, describe } from "vitest"; +import { LanguageSchema, LanguageObjectSchema } from "../src/languages"; + +describe("languages schemas", () => { + describe("LanguageSchema", () => { + it.each([ + { description: "valid language english", input: "english" }, + { description: "valid language spanish", input: "spanish" }, + { + description: "invalid language", + input: "invalid_language", + expectedError: "Must be a supported language", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(LanguageSchema).toReject(input, expectedError); + } else { + expect(LanguageSchema).toValidate(input); + } + }); + }); + + describe("LanguageObjectSchema", () => { + it.each([ + { + description: "valid language object", + input: { + name: "english", + words: ["hello", "world"], + }, + }, + { + description: "invalid - missing name", + input: { words: ["hello", "world"] }, + expectedError: "Required", + }, + { + description: "invalid - missing words", + input: { name: "english" }, + expectedError: "Required", + }, + { + description: "invalid - extra property", + input: { name: "english", words: ["hello"], extra: true }, + expectedError: "Unrecognized key", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(LanguageObjectSchema).toReject(input, expectedError); + } else { + expect(LanguageObjectSchema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/layouts.spec.ts b/packages/schemas/__tests__/layouts.spec.ts new file mode 100644 index 000000000000..af9f3917aff9 --- /dev/null +++ b/packages/schemas/__tests__/layouts.spec.ts @@ -0,0 +1,23 @@ +import { it, expect, describe } from "vitest"; +import { LayoutNameSchema } from "../src/layouts"; + +describe("layouts schemas", () => { + describe("LayoutNameSchema", () => { + it.each([ + { description: "valid layout qwerty", input: "qwerty" }, + { description: "valid layout dvorak", input: "dvorak" }, + { description: "valid layout colemak_dh", input: "colemak_dh" }, + { + description: "invalid layout", + input: "invalid_layout", + expectedError: "Must be a supported layout", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(LayoutNameSchema).toReject(input, expectedError); + } else { + expect(LayoutNameSchema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/leaderboards.spec.ts b/packages/schemas/__tests__/leaderboards.spec.ts new file mode 100644 index 000000000000..7815a048b9b6 --- /dev/null +++ b/packages/schemas/__tests__/leaderboards.spec.ts @@ -0,0 +1,23 @@ +import { it, expect, describe } from "vitest"; +import { LeaderboardEntrySchema } from "../src/leaderboards"; + +describe("leaderboards schemas", () => { + describe("LeaderboardEntrySchema", () => { + it.each([ + { + description: "valid leaderboard entry", + input: { + wpm: 100, + acc: 95, + timestamp: 1234567890, + raw: 105, + uid: "user123", + name: "Test User", + rank: 1, + }, + }, + ] as const)("$description", ({ input }) => { + expect(LeaderboardEntrySchema).toValidate(input); + }); + }); +}); diff --git a/packages/schemas/__tests__/presets.spec.ts b/packages/schemas/__tests__/presets.spec.ts new file mode 100644 index 000000000000..d8c3f6a89372 --- /dev/null +++ b/packages/schemas/__tests__/presets.spec.ts @@ -0,0 +1,21 @@ +import { it, expect, describe } from "vitest"; +import { PresetNameSchema, PresetTypeSchema } from "../src/presets"; + +describe("presets schemas", () => { + describe("PresetNameSchema", () => { + it.each([ + { description: "valid preset name", input: "my_preset" }, + ] as const)("$description", ({ input }) => { + expect(PresetNameSchema).toValidate(input); + }); + }); + + describe("PresetTypeSchema", () => { + it.each([ + { description: "valid type full", input: "full" }, + { description: "valid type partial", input: "partial" }, + ] as const)("$description", ({ input }) => { + expect(PresetTypeSchema).toValidate(input); + }); + }); +}); diff --git a/packages/schemas/__tests__/shared.spec.ts b/packages/schemas/__tests__/shared.spec.ts new file mode 100644 index 000000000000..3645826342c5 --- /dev/null +++ b/packages/schemas/__tests__/shared.spec.ts @@ -0,0 +1,287 @@ +import { it, expect, describe } from "vitest"; +import { + DifficultySchema, + PersonalBestSchema, + PersonalBestsSchema, + DefaultWordsModeSchema, + DefaultTimeModeSchema, + QuoteLengthSchema, + ModeSchema, + Mode2Schema, +} from "../src/shared"; + +describe("shared schemas", () => { + describe("DifficultySchema", () => { + it.each([ + { description: "valid normal", input: "normal" }, + { description: "valid expert", input: "expert" }, + { description: "valid master", input: "master" }, + { + description: "invalid difficulty", + input: "invalid", + expectedError: "Invalid enum value", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(DifficultySchema).toReject(input, expectedError); + } else { + expect(DifficultySchema).toValidate(input); + } + }); + }); + + describe("PersonalBestSchema", () => { + it.each([ + { + description: "valid personal best", + input: { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + }, + { + description: "acc exceeds 100", + input: { + acc: 101, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + expectedError: "Number must be less than or equal to 100", + }, + { + description: "wpm is negative", + input: { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: -1, + wpm: 100, + timestamp: 1234567890, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PersonalBestSchema).toReject(input, expectedError); + } else { + expect(PersonalBestSchema).toValidate(input); + } + }); + }); + + describe("PersonalBestsSchema", () => { + it.each([ + { + description: "valid personal bests record", + input: { + time: { + "10": [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + words: { + "10": [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + quote: { + "1": [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + custom: { + custom: [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + zen: { + zen: [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + }, + }, + { + description: "invalid personal best in record", + input: { + time: { + "10": [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: -1, + timestamp: 1234567890, + }, + ], + }, + words: {}, + quote: {}, + custom: { custom: [] }, + zen: { zen: [] }, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PersonalBestsSchema).toReject(input, expectedError); + } else { + expect(PersonalBestsSchema).toValidate(input); + } + }); + }); + + describe("DefaultWordsModeSchema", () => { + it.each([ + { description: "valid 10", input: "10" }, + { description: "valid 25", input: "25" }, + { description: "valid 50", input: "50" }, + { description: "valid 100", input: "100" }, + { + description: "invalid mode", + input: "30", + expectedError: "Invalid input", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(DefaultWordsModeSchema).toReject(input, expectedError); + } else { + expect(DefaultWordsModeSchema).toValidate(input); + } + }); + }); + + describe("DefaultTimeModeSchema", () => { + it.each([ + { description: "valid 15", input: "15" }, + { description: "valid 30", input: "30" }, + { description: "valid 60", input: "60" }, + { description: "valid 120", input: "120" }, + { + description: "invalid mode", + input: "45", + expectedError: "Invalid input", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(DefaultTimeModeSchema).toReject(input, expectedError); + } else { + expect(DefaultTimeModeSchema).toValidate(input); + } + }); + }); + + describe("QuoteLengthSchema", () => { + it.each([ + { description: "valid short", input: "short" }, + { description: "valid medium", input: "medium" }, + { description: "valid long", input: "long" }, + { description: "valid thicc", input: "thicc" }, + { + description: "invalid length", + input: "tiny", + expectedError: "Invalid input", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(QuoteLengthSchema).toReject(input, expectedError); + } else { + expect(QuoteLengthSchema).toValidate(input); + } + }); + }); + + describe("ModeSchema", () => { + it.each([ + { description: "valid mode time", input: "time" }, + { description: "valid mode words", input: "words" }, + { description: "valid mode quote", input: "quote" }, + { description: "valid mode custom", input: "custom" }, + { description: "valid mode zen", input: "zen" }, + { + description: "invalid mode", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ModeSchema).toReject(input, expectedError); + } else { + expect(ModeSchema).toValidate(input); + } + }); + }); + + describe("Mode2Schema", () => { + it.each([ + { description: "valid number string", input: "10" }, + { description: "valid zen", input: "zen" }, + { description: "valid custom", input: "custom" }, + { + description: "invalid value", + input: "invalid", + expectedError: + "Needs to be a number or a number represented as a string", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(Mode2Schema).toReject(input, expectedError); + } else { + expect(Mode2Schema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/themes.spec.ts b/packages/schemas/__tests__/themes.spec.ts new file mode 100644 index 000000000000..dca4753a9cae --- /dev/null +++ b/packages/schemas/__tests__/themes.spec.ts @@ -0,0 +1,23 @@ +import { it, expect, describe } from "vitest"; +import { ThemeNameSchema } from "../src/themes"; + +describe("themes schemas", () => { + describe("ThemeNameSchema", () => { + it.each([ + { description: "valid theme dracula", input: "dracula" }, + { description: "valid theme rose_pine", input: "rose_pine" }, + { description: "valid theme future_funk", input: "future_funk" }, + { + description: "invalid theme", + input: "invalid_theme", + expectedError: "Must be a known theme", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ThemeNameSchema).toReject(input, expectedError); + } else { + expect(ThemeNameSchema).toValidate(input); + } + }); + }); +}); diff --git a/packages/schemas/__tests__/users.spec.ts b/packages/schemas/__tests__/users.spec.ts new file mode 100644 index 000000000000..66b8062a27c1 --- /dev/null +++ b/packages/schemas/__tests__/users.spec.ts @@ -0,0 +1,12 @@ +import { it, expect, describe } from "vitest"; +import { ResultFilterPresetNameSchema } from "../src/users"; + +describe("users schemas", () => { + describe("ResultFilterPresetNameSchema", () => { + it.each([ + { description: "valid preset name", input: "my_preset" }, + ] as const)("$description", ({ input }) => { + expect(ResultFilterPresetNameSchema).toValidate(input); + }); + }); +}); diff --git a/packages/schemas/__tests__/util.spec.ts b/packages/schemas/__tests__/util.spec.ts index faa5065750a5..a91cab831f66 100644 --- a/packages/schemas/__tests__/util.spec.ts +++ b/packages/schemas/__tests__/util.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { it, expect, describe } from "vitest"; import { StringNumberSchema, token, @@ -17,23 +17,12 @@ import { describe("util schemas", () => { describe("StringNumberSchema", () => { it.each([ + { description: "valid number string", input: "10" }, + { description: "valid large number string", input: "123456" }, + { description: "valid number input", input: 10 }, { - description: "valid numeric string", - input: "123", - }, - { - description: "valid number", - input: 123, - }, - { - description: "string with letters", - input: "abc", - expectedError: - "Needs to be a number or a number represented as a string", - }, - { - description: "string with mixed content", - input: "123abc", + description: "invalid string with letters", + input: "abc123", expectedError: "Needs to be a number or a number represented as a string", }, @@ -46,99 +35,87 @@ describe("util schemas", () => { }); }); - describe("token", () => { + describe("token function", () => { + const TokenSchema = token(); it.each([ + { description: "valid token", input: "my_token_123" }, { - description: "valid alphanumeric with underscore", - input: "abc_123", - }, - { - description: "contains hyphen", - input: "abc-123", - expectedError: "Invalid", - }, - { - description: "contains space", - input: "abc 123", + description: "invalid token with hyphen", + input: "my-token", expectedError: "Invalid", }, ] as const)("$description", ({ input, expectedError }) => { - const schema = token(); if (expectedError) { - expect(schema).toReject(input, expectedError); + expect(TokenSchema).toReject(input, expectedError); } else { - expect(schema).toValidate(input); + expect(TokenSchema).toValidate(input); } }); }); - describe("slug", () => { + describe("slug function", () => { + const SlugSchema = slug(); it.each([ + { description: "valid slug", input: "my-slug" }, { - description: "valid slug with dots and hyphens", - input: "abc-123_test.def", + description: "valid slug with dots and underscores", + input: "my.slug_name", }, { - description: "starts with dot", - input: ".invalid", + description: "invalid slug starts with dot", + input: ".hidden-slug", expectedError: "Cannot start with a dot", }, { - description: "contains comma", - input: "abc,def", + description: "invalid slug with special char", + input: "my@slug", expectedError: "Only letters, numbers, underscores, dots and hyphens allowed", }, ] as const)("$description", ({ input, expectedError }) => { - const schema = slug(); if (expectedError) { - expect(schema).toReject(input, expectedError); + expect(SlugSchema).toReject(input, expectedError); } else { - expect(schema).toValidate(input); + expect(SlugSchema).toValidate(input); } }); }); - describe("nameWithSeparators", () => { + describe("nameWithSeparators function", () => { + const NameWithSeparatorsSchema = nameWithSeparators(); it.each([ + { description: "valid name", input: "my-name" }, + { description: "valid name with underscores", input: "my_name" }, { - description: "valid name with separators", - input: "abc_def-123", - }, - { - description: "starts with separator", - input: "_invalid", + description: "invalid name starts with separator", + input: "-my-name", expectedError: "Separators cannot be at the start or end", }, { - description: "consecutive separators", - input: "inv__alid", + description: "invalid name with double separator", + input: "my--name", expectedError: "Separators cannot be at the start or end", }, { - description: "contains dot", - input: "invalid.name", + description: "invalid name with special char", + input: "my@name", expectedError: "Only letters, numbers, underscores and hyphens allowed", }, ] as const)("$description", ({ input, expectedError }) => { - const schema = nameWithSeparators(); if (expectedError) { - expect(schema).toReject(input, expectedError); + expect(NameWithSeparatorsSchema).toReject(input, expectedError); } else { - expect(schema).toValidate(input); + expect(NameWithSeparatorsSchema).toValidate(input); } }); }); describe("IdSchema", () => { it.each([ + { description: "valid id", input: "test_id_123" }, { - description: "valid id", - input: "abc_123", - }, - { - description: "contains hyphen", - input: "abc-123", + description: "invalid id with hyphen", + input: "test-id", expectedError: "Invalid", }, ] as const)("$description", ({ input, expectedError }) => { @@ -152,19 +129,12 @@ describe("util schemas", () => { describe("TagSchema", () => { it.each([ + { description: "valid tag under max length", input: "testtag" }, + { description: "tag at max length (50 chars)", input: "a".repeat(50) }, { - description: "valid tag within max length", - input: "abc_123", - }, - { - description: "exceeds max length", + description: "tag exceeds max length", input: "a".repeat(51), - expectedError: "String must contain at most 50 character", - }, - { - description: "contains invalid character", - input: "abc-123", - expectedError: "Invalid", + expectedError: "String must contain at most 50 character(s)", }, ] as const)("$description", ({ input, expectedError }) => { if (expectedError) { @@ -177,45 +147,26 @@ describe("util schemas", () => { describe("NullableStringSchema", () => { it.each([ - { - description: "valid string", - input: "hello", - }, - { - description: "null transforms to undefined", - input: null, - }, - { - description: "undefined is accepted", - input: undefined, - }, - { - description: "boolean is rejected", - input: true, - expectedError: "Expected string", - }, - ] as const)("$description", ({ input, expectedError }) => { - if (expectedError) { - expect(NullableStringSchema).toReject(input, expectedError); - } else { - expect(NullableStringSchema).toValidate(input); - } + { description: "valid string", input: "test" }, + { description: "valid null", input: null }, + { description: "valid undefined", input: undefined }, + ] as const)("$description", ({ input }) => { + expect(NullableStringSchema).toValidate(input); }); }); describe("PercentageSchema", () => { it.each([ + { description: "valid percentage", input: 50 }, + { description: "valid 0%", input: 0 }, + { description: "valid 100%", input: 100 }, { - description: "valid percentage", - input: 50, - }, - { - description: "exceeds max", - input: 101, + description: "percentage exceeds 100", + input: 150, expectedError: "Number must be less than or equal to 100", }, { - description: "negative value", + description: "negative percentage", input: -1, expectedError: "Number must be greater than or equal to 0", }, @@ -230,17 +181,16 @@ describe("util schemas", () => { describe("WpmSchema", () => { it.each([ + { description: "valid wpm", input: 100 }, + { description: "valid 0 wpm", input: 0 }, + { description: "valid max wpm (420)", input: 420 }, { - description: "valid wpm", - input: 100, - }, - { - description: "exceeds max", - input: 421, + description: "wpm exceeds max", + input: 500, expectedError: "Number must be less than or equal to 420", }, { - description: "negative value", + description: "negative wpm", input: -1, expectedError: "Number must be greater than or equal to 0", }, @@ -255,14 +205,13 @@ describe("util schemas", () => { describe("CustomTextModeSchema", () => { it.each([ - { - description: "valid mode", - input: "repeat", - }, + { description: "valid repeat", input: "repeat" }, + { description: "valid random", input: "random" }, + { description: "valid shuffle", input: "shuffle" }, { description: "invalid mode", input: "invalid", - expectedError: "Invalid", + expectedError: "Invalid enum value", }, ] as const)("$description", ({ input, expectedError }) => { if (expectedError) { @@ -275,14 +224,13 @@ describe("util schemas", () => { describe("CustomTextLimitModeSchema", () => { it.each([ + { description: "valid word", input: "word" }, + { description: "valid time", input: "time" }, + { description: "valid section", input: "section" }, { - description: "valid limit mode", - input: "word", - }, - { - description: "invalid limit mode", + description: "invalid mode", input: "invalid", - expectedError: "Invalid", + expectedError: "Invalid enum value", }, ] as const)("$description", ({ input, expectedError }) => { if (expectedError) { @@ -295,17 +243,15 @@ describe("util schemas", () => { describe("PageNumberSchema", () => { it.each([ + { description: "valid page number", input: 0 }, + { description: "valid positive page", input: 5 }, { - description: "valid page number", - input: 5, - }, - { - description: "negative value", + description: "invalid negative page", input: -1, expectedError: "Number must be greater than or equal to 0", }, { - description: "non-integer", + description: "invalid non-integer", input: 1.5, expectedError: "Expected integer, received float", }, From 296dd1a60b4ed197c3ed440d957cb374cae1f4e3 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 15:37:15 +0200 Subject: [PATCH 10/12] simplify --- .../schemas/__tests__/configuration.spec.ts | 539 +++++++----------- .../schemas/__tests__/leaderboards.spec.ts | 33 +- packages/schemas/__tests__/presets.spec.ts | 27 +- packages/schemas/__tests__/results.spec.ts | 387 ++++++++----- packages/schemas/__tests__/users.spec.ts | 13 +- 5 files changed, 502 insertions(+), 497 deletions(-) diff --git a/packages/schemas/__tests__/configuration.spec.ts b/packages/schemas/__tests__/configuration.spec.ts index 87dee5119081..9c41c0810742 100644 --- a/packages/schemas/__tests__/configuration.spec.ts +++ b/packages/schemas/__tests__/configuration.spec.ts @@ -5,6 +5,198 @@ import { ConfigurationSchema, } from "../src/configuration"; +// Constants for large configuration objects +const minimalConfiguration = { + maintenance: false, + dev: { responseSlowdownMs: 0 }, +}; + +const disabledConfiguration = { + ...minimalConfiguration, + quotes: { + reporting: { enabled: false, maxReports: 0, contentReportLimit: 0 }, + submissionsEnabled: false, + maxFavorites: 0, + }, + results: { + savingEnabled: false, + objectHashCheckEnabled: false, + filterPresets: { enabled: false, maxPresetsPerUser: 0 }, + limits: { regularUser: 0, premiumUser: 0 }, + maxBatchSize: 0, + }, + users: { + signUp: false, + lastHashesCheck: { enabled: false, maxHashes: 0 }, + autoBan: { enabled: false, maxCount: 0, maxHours: 0 }, + profiles: { enabled: false }, + discordIntegration: { enabled: false }, + xp: { + enabled: false, + funboxBonus: 0, + gainMultiplier: 0, + maxDailyBonus: 0, + minDailyBonus: 0, + streak: { + enabled: false, + maxStreakDays: 0, + maxStreakMultiplier: 0, + }, + }, + inbox: { enabled: false, maxMail: 0 }, + premium: { enabled: false }, + }, + admin: { endpointsEnabled: false }, + apeKeys: { + endpointsEnabled: false, + acceptKeys: false, + maxKeysPerUser: 0, + apeKeyBytes: 0, + apeKeySaltRounds: 0, + }, + rateLimiting: { + badAuthentication: { + enabled: false, + penalty: 0, + flaggedStatusCodes: [], + }, + }, + dailyLeaderboards: { + enabled: false, + leaderboardExpirationTimeInDays: 0, + maxResults: 0, + validModeRules: [], + scheduleRewardsModeRules: [], + topResultsToAnnounce: 0, + xpRewardBrackets: [], + }, + leaderboards: { + minTimeTyping: 0, + weeklyXp: { + enabled: false, + expirationTimeInDays: 0, + xpRewardBrackets: [], + }, + }, + connections: { enabled: false, maxPerUser: 0 }, +}; + +const fullConfiguration = { + maintenance: false, + dev: { responseSlowdownMs: 0 }, + quotes: { + reporting: { + enabled: true, + maxReports: 5, + contentReportLimit: 3, + }, + submissionsEnabled: true, + maxFavorites: 100, + }, + results: { + savingEnabled: true, + objectHashCheckEnabled: true, + filterPresets: { + enabled: true, + maxPresetsPerUser: 10, + }, + limits: { + regularUser: 50, + premiumUser: 200, + }, + maxBatchSize: 100, + }, + users: { + signUp: true, + lastHashesCheck: { + enabled: true, + maxHashes: 5, + }, + autoBan: { + enabled: true, + maxCount: 3, + maxHours: 24, + }, + profiles: { + enabled: true, + }, + discordIntegration: { + enabled: true, + }, + xp: { + enabled: true, + funboxBonus: 1.5, + gainMultiplier: 1, + maxDailyBonus: 1000, + minDailyBonus: 100, + streak: { + enabled: true, + maxStreakDays: 30, + maxStreakMultiplier: 2, + }, + }, + inbox: { + enabled: true, + maxMail: 50, + }, + premium: { + enabled: true, + }, + }, + admin: { endpointsEnabled: true }, + apeKeys: { + endpointsEnabled: true, + acceptKeys: true, + maxKeysPerUser: 5, + apeKeyBytes: 32, + apeKeySaltRounds: 10, + }, + rateLimiting: { + badAuthentication: { + enabled: true, + penalty: 60, + flaggedStatusCodes: [401, 403], + }, + }, + dailyLeaderboards: { + enabled: true, + leaderboardExpirationTimeInDays: 1, + maxResults: 500, + validModeRules: [{ language: "english", mode: "time", mode2: "30" }], + scheduleRewardsModeRules: [ + { language: "english", mode: "time", mode2: "(15|60)" }, + ], + topResultsToAnnounce: 3, + xpRewardBrackets: [ + { + minRank: 1, + maxRank: 10, + minReward: 100, + maxReward: 500, + }, + ], + }, + leaderboards: { + minTimeTyping: 10, + weeklyXp: { + enabled: true, + expirationTimeInDays: 7, + xpRewardBrackets: [ + { + minRank: 1, + maxRank: 5, + minReward: 200, + maxReward: 1000, + }, + ], + }, + }, + connections: { + enabled: true, + maxPerUser: 3, + }, +}; + describe("configuration schemas", () => { describe("ValidModeRuleSchema", () => { it.each([ @@ -65,22 +257,12 @@ describe("configuration schemas", () => { }, { description: "negative minRank", - input: { - minRank: -1, - maxRank: 10, - minReward: 100, - maxReward: 500, - }, + input: { ...disabledConfiguration, minRank: -1 }, expectedError: "Number must be greater than or equal to 0", }, { description: "non-integer value", - input: { - minRank: 1.5, - maxRank: 10, - minReward: 100, - maxReward: 500, - }, + input: { ...disabledConfiguration, minRank: 1.5 }, expectedError: "Expected integer, received float", }, ] as const)("$description", ({ input, expectedError }) => { @@ -96,355 +278,36 @@ describe("configuration schemas", () => { it.each([ { description: "valid full configuration", - input: { - maintenance: false, - dev: { - responseSlowdownMs: 0, - }, - quotes: { - reporting: { - enabled: true, - maxReports: 5, - contentReportLimit: 3, - }, - submissionsEnabled: true, - maxFavorites: 100, - }, - results: { - savingEnabled: true, - objectHashCheckEnabled: true, - filterPresets: { - enabled: true, - maxPresetsPerUser: 10, - }, - limits: { - regularUser: 50, - premiumUser: 200, - }, - maxBatchSize: 100, - }, - users: { - signUp: true, - lastHashesCheck: { - enabled: true, - maxHashes: 5, - }, - autoBan: { - enabled: true, - maxCount: 3, - maxHours: 24, - }, - profiles: { - enabled: true, - }, - discordIntegration: { - enabled: true, - }, - xp: { - enabled: true, - funboxBonus: 1.5, - gainMultiplier: 1, - maxDailyBonus: 1000, - minDailyBonus: 100, - streak: { - enabled: true, - maxStreakDays: 30, - maxStreakMultiplier: 2, - }, - }, - inbox: { - enabled: true, - maxMail: 50, - }, - premium: { - enabled: true, - }, - }, - admin: { - endpointsEnabled: true, - }, - apeKeys: { - endpointsEnabled: true, - acceptKeys: true, - maxKeysPerUser: 5, - apeKeyBytes: 32, - apeKeySaltRounds: 10, - }, - rateLimiting: { - badAuthentication: { - enabled: true, - penalty: 60, - flaggedStatusCodes: [401, 403], - }, - }, - dailyLeaderboards: { - enabled: true, - leaderboardExpirationTimeInDays: 1, - maxResults: 500, - validModeRules: [ - { language: "english", mode: "time", mode2: "30" }, - ], - scheduleRewardsModeRules: [ - { language: "english", mode: "time", mode2: "(15|60)" }, - ], - topResultsToAnnounce: 3, - xpRewardBrackets: [ - { - minRank: 1, - maxRank: 10, - minReward: 100, - maxReward: 500, - }, - ], - }, - leaderboards: { - minTimeTyping: 10, - weeklyXp: { - enabled: true, - expirationTimeInDays: 7, - xpRewardBrackets: [ - { - minRank: 1, - maxRank: 5, - minReward: 200, - maxReward: 1000, - }, - ], - }, - }, - connections: { - enabled: true, - maxPerUser: 3, - }, - }, + input: fullConfiguration, }, { description: "maintenance as true", - input: { - maintenance: true, - dev: { responseSlowdownMs: 0 }, - quotes: { - reporting: { enabled: false, maxReports: 0, contentReportLimit: 0 }, - submissionsEnabled: false, - maxFavorites: 0, - }, - results: { - savingEnabled: false, - objectHashCheckEnabled: false, - filterPresets: { enabled: false, maxPresetsPerUser: 0 }, - limits: { regularUser: 0, premiumUser: 0 }, - maxBatchSize: 0, - }, - users: { - signUp: false, - lastHashesCheck: { enabled: false, maxHashes: 0 }, - autoBan: { enabled: false, maxCount: 0, maxHours: 0 }, - profiles: { enabled: false }, - discordIntegration: { enabled: false }, - xp: { - enabled: false, - funboxBonus: 0, - gainMultiplier: 0, - maxDailyBonus: 0, - minDailyBonus: 0, - streak: { - enabled: false, - maxStreakDays: 0, - maxStreakMultiplier: 0, - }, - }, - inbox: { enabled: false, maxMail: 0 }, - premium: { enabled: false }, - }, - admin: { endpointsEnabled: false }, - apeKeys: { - endpointsEnabled: false, - acceptKeys: false, - maxKeysPerUser: 0, - apeKeyBytes: 0, - apeKeySaltRounds: 0, - }, - rateLimiting: { - badAuthentication: { - enabled: false, - penalty: 0, - flaggedStatusCodes: [], - }, - }, - dailyLeaderboards: { - enabled: false, - leaderboardExpirationTimeInDays: 0, - maxResults: 0, - validModeRules: [], - scheduleRewardsModeRules: [], - topResultsToAnnounce: 1, - xpRewardBrackets: [], - }, - leaderboards: { - minTimeTyping: 0, - weeklyXp: { - enabled: false, - expirationTimeInDays: 0, - xpRewardBrackets: [], - }, - }, - connections: { enabled: false, maxPerUser: 0 }, - }, + input: { ...fullConfiguration, maintenance: true }, }, { description: "missing required top-level field", - input: { - maintenance: false, - dev: { responseSlowdownMs: 0 }, - // quotes missing - } as any, + input: { ...minimalConfiguration, quotes: undefined } as any, expectedError: "Required", }, { description: "topResultsToAnnounce cannot be zero", input: { - maintenance: false, - dev: { responseSlowdownMs: 0 }, - quotes: { - reporting: { enabled: false, maxReports: 0, contentReportLimit: 0 }, - submissionsEnabled: false, - maxFavorites: 0, - }, - results: { - savingEnabled: false, - objectHashCheckEnabled: false, - filterPresets: { enabled: false, maxPresetsPerUser: 0 }, - limits: { regularUser: 0, premiumUser: 0 }, - maxBatchSize: 0, - }, - users: { - signUp: false, - lastHashesCheck: { enabled: false, maxHashes: 0 }, - autoBan: { enabled: false, maxCount: 0, maxHours: 0 }, - profiles: { enabled: false }, - discordIntegration: { enabled: false }, - xp: { - enabled: false, - funboxBonus: 0, - gainMultiplier: 0, - maxDailyBonus: 0, - minDailyBonus: 0, - streak: { - enabled: false, - maxStreakDays: 0, - maxStreakMultiplier: 0, - }, - }, - inbox: { enabled: false, maxMail: 0 }, - premium: { enabled: false }, - }, - admin: { endpointsEnabled: false }, - apeKeys: { - endpointsEnabled: false, - acceptKeys: false, - maxKeysPerUser: 0, - apeKeyBytes: 0, - apeKeySaltRounds: 0, - }, - rateLimiting: { - badAuthentication: { - enabled: false, - penalty: 0, - flaggedStatusCodes: [], - }, - }, + ...fullConfiguration, dailyLeaderboards: { - enabled: false, - leaderboardExpirationTimeInDays: 0, - maxResults: 0, - validModeRules: [], - scheduleRewardsModeRules: [], + ...fullConfiguration.dailyLeaderboards, topResultsToAnnounce: 0, - xpRewardBrackets: [], - }, - leaderboards: { - minTimeTyping: 0, - weeklyXp: { - enabled: false, - expirationTimeInDays: 0, - xpRewardBrackets: [], - }, }, - connections: { enabled: false, maxPerUser: 0 }, }, expectedError: "Number must be greater than 0", }, { description: "minTimeTyping cannot be negative", input: { - maintenance: false, - dev: { responseSlowdownMs: 0 }, - quotes: { - reporting: { enabled: false, maxReports: 0, contentReportLimit: 0 }, - submissionsEnabled: false, - maxFavorites: 0, - }, - results: { - savingEnabled: false, - objectHashCheckEnabled: false, - filterPresets: { enabled: false, maxPresetsPerUser: 0 }, - limits: { regularUser: 0, premiumUser: 0 }, - maxBatchSize: 0, - }, - users: { - signUp: false, - lastHashesCheck: { enabled: false, maxHashes: 0 }, - autoBan: { enabled: false, maxCount: 0, maxHours: 0 }, - profiles: { enabled: false }, - discordIntegration: { enabled: false }, - xp: { - enabled: false, - funboxBonus: 0, - gainMultiplier: 0, - maxDailyBonus: 0, - minDailyBonus: 0, - streak: { - enabled: false, - maxStreakDays: 0, - maxStreakMultiplier: 0, - }, - }, - inbox: { enabled: false, maxMail: 0 }, - premium: { enabled: false }, - }, - admin: { endpointsEnabled: false }, - apeKeys: { - endpointsEnabled: false, - acceptKeys: false, - maxKeysPerUser: 0, - apeKeyBytes: 0, - apeKeySaltRounds: 0, - }, - rateLimiting: { - badAuthentication: { - enabled: false, - penalty: 0, - flaggedStatusCodes: [], - }, - }, - dailyLeaderboards: { - enabled: false, - leaderboardExpirationTimeInDays: 0, - maxResults: 0, - validModeRules: [], - scheduleRewardsModeRules: [], - topResultsToAnnounce: 1, - xpRewardBrackets: [], - }, + ...fullConfiguration, leaderboards: { + ...fullConfiguration.leaderboards, minTimeTyping: -1, - weeklyXp: { - enabled: false, - expirationTimeInDays: 0, - xpRewardBrackets: [], - }, }, - connections: { enabled: false, maxPerUser: 0 }, }, expectedError: "Number must be greater than or equal to 0", }, diff --git a/packages/schemas/__tests__/leaderboards.spec.ts b/packages/schemas/__tests__/leaderboards.spec.ts index 7815a048b9b6..690ffd1af050 100644 --- a/packages/schemas/__tests__/leaderboards.spec.ts +++ b/packages/schemas/__tests__/leaderboards.spec.ts @@ -1,23 +1,34 @@ import { it, expect, describe } from "vitest"; import { LeaderboardEntrySchema } from "../src/leaderboards"; +const validLeaderboardEntry = { + wpm: 100, + acc: 95, + timestamp: 1234567890, + raw: 105, + uid: "user123", + name: "Test User", + rank: 1, +}; + describe("leaderboards schemas", () => { describe("LeaderboardEntrySchema", () => { it.each([ { description: "valid leaderboard entry", - input: { - wpm: 100, - acc: 95, - timestamp: 1234567890, - raw: 105, - uid: "user123", - name: "Test User", - rank: 1, - }, + input: validLeaderboardEntry, + }, + { + description: "invalid - negative wpm", + input: { ...validLeaderboardEntry, wpm: -1 }, + expectedError: "Number must be greater than or equal to 0", }, - ] as const)("$description", ({ input }) => { - expect(LeaderboardEntrySchema).toValidate(input); + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(LeaderboardEntrySchema).toReject(input, expectedError); + } else { + expect(LeaderboardEntrySchema).toValidate(input); + } }); }); }); diff --git a/packages/schemas/__tests__/presets.spec.ts b/packages/schemas/__tests__/presets.spec.ts index d8c3f6a89372..1f8beb3a883b 100644 --- a/packages/schemas/__tests__/presets.spec.ts +++ b/packages/schemas/__tests__/presets.spec.ts @@ -5,8 +5,17 @@ describe("presets schemas", () => { describe("PresetNameSchema", () => { it.each([ { description: "valid preset name", input: "my_preset" }, - ] as const)("$description", ({ input }) => { - expect(PresetNameSchema).toValidate(input); + { + description: "invalid preset name too long", + input: "a".repeat(17), + expectedError: "String must contain at most 16 character(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PresetNameSchema).toReject(input, expectedError); + } else { + expect(PresetNameSchema).toValidate(input); + } }); }); @@ -14,8 +23,18 @@ describe("presets schemas", () => { it.each([ { description: "valid type full", input: "full" }, { description: "valid type partial", input: "partial" }, - ] as const)("$description", ({ input }) => { - expect(PresetTypeSchema).toValidate(input); + { + description: "invalid type", + input: "invalid", + expectedError: + "Invalid enum value. Expected 'full' | 'partial', received 'invalid'", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PresetTypeSchema).toReject(input, expectedError); + } else { + expect(PresetTypeSchema).toValidate(input); + } }); }); }); diff --git a/packages/schemas/__tests__/results.spec.ts b/packages/schemas/__tests__/results.spec.ts index 7a885fab2313..79e96cd566fc 100644 --- a/packages/schemas/__tests__/results.spec.ts +++ b/packages/schemas/__tests__/results.spec.ts @@ -31,38 +31,67 @@ describe("results schemas", () => { seconds: 0, }, }, - ] as const)("$description", ({ input }) => { - expect(IncompleteTestSchema).toValidate(input); + { + description: "invalid - acc exceeds 100", + input: { acc: 101, seconds: 30 }, + expectedError: "Number must be less than or equal to 100", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(IncompleteTestSchema).toReject(input, expectedError); + } else { + expect(IncompleteTestSchema).toValidate(input); + } }); }); describe("OldChartDataSchema", () => { + const validChart = { + wpm: [100, 110, 120], + raw: [105, 115, 125], + err: [0, 1, 2], + }; it.each([ { description: "valid chart data", - input: { - wpm: [100, 110, 120], - raw: [105, 115, 125], - err: [0, 1, 2], - }, + input: validChart, }, - ] as const)("$description", ({ input }) => { - expect(OldChartDataSchema).toValidate(input); + { + description: "invalid - negative value in wpm array", + input: { ...validChart, wpm: [-1, 110, 120] }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(OldChartDataSchema).toReject(input, expectedError); + } else { + expect(OldChartDataSchema).toValidate(input); + } }); }); describe("ChartDataSchema", () => { + const validChart = { + wpm: [100, 110, 120], + burst: [95, 105, 115], + err: [0, 1, 2], + }; it.each([ { description: "valid chart data", - input: { - wpm: [100, 110, 120], - burst: [95, 105, 115], - err: [0, 1, 2], - }, + input: validChart, }, - ] as const)("$description", ({ input }) => { - expect(ChartDataSchema).toValidate(input); + { + description: "invalid - negative value in burst array", + input: { ...validChart, burst: [95, -105, 115] }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ChartDataSchema).toReject(input, expectedError); + } else { + expect(ChartDataSchema).toValidate(input); + } }); }); @@ -82,8 +111,17 @@ describe("results schemas", () => { sd: 0, }, }, - ] as const)("$description", ({ input }) => { - expect(KeyStatsSchema).toValidate(input); + { + description: "invalid - negative average", + input: { average: -50, sd: 10 }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(KeyStatsSchema).toReject(input, expectedError); + } else { + expect(KeyStatsSchema).toValidate(input); + } }); }); @@ -101,27 +139,52 @@ describe("results schemas", () => { }, }, }, - ] as const)("$description", ({ input }) => { - expect(CompletedEventCustomTextSchema).toValidate(input); + { + description: "invalid - negative textLen", + input: { + ...({} as any), + textLen: -100, + mode: "repeat", + pipeDelimiter: false, + limit: { mode: "word", value: 100 }, + }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CompletedEventCustomTextSchema).toReject(input, expectedError); + } else { + expect(CompletedEventCustomTextSchema).toValidate(input); + } }); }); describe("CustomTextSettingsSchema", () => { + const validSettings = { + text: ["hello", "world"], + mode: "repeat", + pipeDelimiter: false, + limit: { + mode: "word", + value: 100, + }, + }; it.each([ { description: "valid custom text settings", - input: { - text: ["hello", "world"], - mode: "repeat", - pipeDelimiter: false, - limit: { - mode: "word", - value: 100, - }, - }, + input: validSettings, }, - ] as const)("$description", ({ input }) => { - expect(CustomTextSettingsSchema).toValidate(input); + { + description: "invalid - empty text array", + input: { ...validSettings, text: [] }, + expectedError: "Array must contain at least 1 element(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CustomTextSettingsSchema).toReject(input, expectedError); + } else { + expect(CustomTextSettingsSchema).toValidate(input); + } }); }); @@ -137,120 +200,150 @@ describe("results schemas", () => { }); describe("ResultSchema", () => { + const validResult = { + wpm: 100, + rawWpm: 110, + charStats: [100, 5, 2, 3], + acc: 98, + mode: "time", + mode2: "15", + timestamp: 1000000000, + testDuration: 30, + consistency: 95, + keyConsistency: 90, + chartData: { wpm: [100], burst: [95], err: [0] }, + uid: "abc123", + _id: "def456", + name: "Test Result", + }; it.each([ { description: "valid result", - input: { - wpm: 100, - rawWpm: 110, - charStats: [100, 5, 2, 3], - acc: 98, - mode: "time", - mode2: "15", - timestamp: 1000000000, - testDuration: 30, - consistency: 95, - keyConsistency: 90, - chartData: { wpm: [100], burst: [95], err: [0] }, - uid: "abc123", - _id: "def456", - name: "Test Result", - }, + input: validResult, }, - ] as const)("$description", ({ input }) => { - expect(ResultSchema).toValidate(input); + { + description: "invalid - wpm exceeds max", + input: { ...validResult, wpm: 501 }, + expectedError: "Number must be less than or equal to 420", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ResultSchema).toReject(input, expectedError); + } else { + expect(ResultSchema).toValidate(input); + } }); }); describe("ResultMinifiedSchema", () => { + const validResult = { + wpm: 100, + rawWpm: 110, + charStats: [100, 5, 2, 3], + acc: 98, + mode: "time", + mode2: "15", + timestamp: 1000000000, + testDuration: 30, + consistency: 95, + keyConsistency: 90, + uid: "abc123", + _id: "def456", + }; it.each([ { description: "valid minified result", - input: { - wpm: 100, - rawWpm: 110, - charStats: [100, 5, 2, 3], - acc: 98, - mode: "time", - mode2: "15", - timestamp: 1000000000, - testDuration: 30, - consistency: 95, - keyConsistency: 90, - uid: "abc123", - _id: "def456", - }, + input: validResult, }, - ] as const)("$description", ({ input }) => { - expect(ResultMinifiedSchema).toValidate(input); + { + description: "invalid - acc below minimum 50", + input: { ...validResult, acc: 49 }, + expectedError: "Number must be greater than or equal to 50", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ResultMinifiedSchema).toReject(input, expectedError); + } else { + expect(ResultMinifiedSchema).toValidate(input); + } }); }); describe("CompletedEventSchema", () => { + const validEvent = { + wpm: 100, + rawWpm: 110, + charStats: [100, 5, 2, 3], + acc: 98, + mode: "time", + mode2: "15", + timestamp: 1000000000, + testDuration: 30, + restartCount: 1, + incompleteTestSeconds: 5, + afkDuration: 0, + tags: ["abc123"], + bailedOut: false, + blindMode: false, + lazyMode: false, + funbox: ["ascii"], + language: "english", + difficulty: "normal", + numbers: false, + punctuation: false, + consistency: 95, + keyConsistency: 90, + uid: "uid123", + chartData: { wpm: [100], burst: [95], err: [0] }, + charTotal: 150, + hash: "abc123", + keyDuration: [100, 120, 90], + keySpacing: [50, 60, 45], + keyOverlap: 10, + lastKeyToEnd: 200, + startToFirstKey: 500, + wpmConsistency: 95, + stopOnLetter: false, + incompleteTests: [{ acc: 100, seconds: 30 }], + }; it.each([ { description: "valid completed event", - input: { - wpm: 100, - rawWpm: 110, - charStats: [100, 5, 2, 3], - acc: 98, - mode: "time", - mode2: "15", - timestamp: 1000000000, - testDuration: 30, - restartCount: 1, - incompleteTestSeconds: 5, - afkDuration: 0, - tags: ["abc123"], - bailedOut: false, - blindMode: false, - lazyMode: false, - funbox: ["ascii"], - language: "english", - difficulty: "normal", - numbers: false, - punctuation: false, - consistency: 95, - keyConsistency: 90, - uid: "uid123", - chartData: { wpm: [100], burst: [95], err: [0] }, - charTotal: 150, - challenge: "abc_123", - hash: "abc123", - keyDuration: [100, 120, 90], - keySpacing: [50, 60, 45], - keyOverlap: 10, - lastKeyToEnd: 200, - startToFirstKey: 500, - wpmConsistency: 95, - stopOnLetter: false, - incompleteTests: [{ acc: 100, seconds: 30 }], - }, + input: validEvent, }, - ] as const)("$description", ({ input }) => { - expect(CompletedEventSchema).toValidate(input); + { + description: "invalid - wpm exceeds max", + input: { ...validEvent, wpm: 501 }, + expectedError: "Number must be less than or equal to 420", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(CompletedEventSchema).toReject(input, expectedError); + } else { + expect(CompletedEventSchema).toValidate(input); + } }); }); describe("XpBreakdownSchema", () => { + const validBreakdown = { + base: 10, + fullAccuracy: 5, + quote: 2, + corrected: 3, + punctuation: 1, + numbers: 0, + funbox: 0, + streak: 0, + incomplete: 0, + daily: 0, + accPenalty: 0, + configMultiplier: 1, + }; it.each([ { description: "valid xp breakdown", - input: { - base: 10, - fullAccuracy: 5, - quote: 2, - corrected: 3, - punctuation: 1, - numbers: 0, - funbox: 0, - streak: 0, - incomplete: 0, - daily: 0, - accPenalty: 0, - configMultiplier: 1, - }, + input: validBreakdown, }, ] as const)("$description", ({ input }) => { expect(XpBreakdownSchema).toValidate(input); @@ -258,34 +351,44 @@ describe("results schemas", () => { }); describe("PostResultResponseSchema", () => { + const validResponse = { + insertedId: "abc123", + isPb: true, + tagPbs: [], + xp: 15, + dailyXpBonus: false, + xpBreakdown: { + base: 10, + fullAccuracy: 5, + quote: 2, + corrected: 3, + punctuation: 1, + numbers: 0, + funbox: 0, + streak: 0, + incomplete: 0, + daily: 0, + accPenalty: 0, + configMultiplier: 1, + }, + streak: 5, + }; it.each([ { description: "valid post result response", - input: { - insertedId: "abc123", - isPb: true, - tagPbs: [], - xp: 15, - dailyXpBonus: false, - xpBreakdown: { - base: 10, - fullAccuracy: 5, - quote: 2, - corrected: 3, - punctuation: 1, - numbers: 0, - funbox: 0, - streak: 0, - incomplete: 0, - daily: 0, - accPenalty: 0, - configMultiplier: 1, - }, - streak: 5, - }, + input: validResponse, }, - ] as const)("$description", ({ input }) => { - expect(PostResultResponseSchema).toValidate(input); + { + description: "invalid - xp is negative", + input: { ...validResponse, xp: -1 }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(PostResultResponseSchema).toReject(input, expectedError); + } else { + expect(PostResultResponseSchema).toValidate(input); + } }); }); }); diff --git a/packages/schemas/__tests__/users.spec.ts b/packages/schemas/__tests__/users.spec.ts index 66b8062a27c1..b125b15feeae 100644 --- a/packages/schemas/__tests__/users.spec.ts +++ b/packages/schemas/__tests__/users.spec.ts @@ -5,8 +5,17 @@ describe("users schemas", () => { describe("ResultFilterPresetNameSchema", () => { it.each([ { description: "valid preset name", input: "my_preset" }, - ] as const)("$description", ({ input }) => { - expect(ResultFilterPresetNameSchema).toValidate(input); + { + description: "invalid preset name too long", + input: "a".repeat(17), + expectedError: "String must contain at most 16 character(s)", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(ResultFilterPresetNameSchema).toReject(input, expectedError); + } else { + expect(ResultFilterPresetNameSchema).toValidate(input); + } }); }); }); From fabd7bd4c5866feec24ccc70cb21c13ede33eebc Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 16:10:33 +0200 Subject: [PATCH 11/12] missing tests --- .../schemas/__tests__/leaderboards.spec.ts | 142 ++- packages/schemas/__tests__/presets.spec.ts | 73 +- packages/schemas/__tests__/users.spec.ts | 911 +++++++++++++++++- 3 files changed, 1114 insertions(+), 12 deletions(-) diff --git a/packages/schemas/__tests__/leaderboards.spec.ts b/packages/schemas/__tests__/leaderboards.spec.ts index 690ffd1af050..e40061644a6c 100644 --- a/packages/schemas/__tests__/leaderboards.spec.ts +++ b/packages/schemas/__tests__/leaderboards.spec.ts @@ -1,34 +1,168 @@ import { it, expect, describe } from "vitest"; -import { LeaderboardEntrySchema } from "../src/leaderboards"; +import { + LeaderboardEntrySchema, + RedisDailyLeaderboardEntrySchema, + RedisXpLeaderboardEntrySchema, + RedisXpLeaderboardScoreSchema, + XpLeaderboardEntrySchema, +} from "../src/leaderboards"; const validLeaderboardEntry = { wpm: 100, acc: 95, timestamp: 1234567890, raw: 105, + consistency: 90, uid: "user123", name: "Test User", rank: 1, }; +const validRedisDailyLeaderboardEntry = { + wpm: 100, + acc: 95, + timestamp: 1234567890, + raw: 105, + uid: "user123", + name: "Test User", +}; + +const validRedisXpLeaderboardEntry = { + uid: "user123", + name: "Test User", + lastActivityTimestamp: 1234567890, + timeTypedSeconds: 3600, +}; + +const validRedisXpLeaderboardScore = 100; + +const validXpLeaderboardEntry = { + uid: "user123", + name: "Test User", + lastActivityTimestamp: 1234567890, + timeTypedSeconds: 3600, + totalXp: 1000, + rank: 1, +}; + describe("leaderboards schemas", () => { describe("LeaderboardEntrySchema", () => { it.each([ + { description: "valid leaderboard entry", input: validLeaderboardEntry }, { - description: "valid leaderboard entry", - input: validLeaderboardEntry, + description: "with optional fields", + input: { + ...validLeaderboardEntry, + discordId: "discord123", + badgeId: 1, + }, }, { description: "invalid - negative wpm", input: { ...validLeaderboardEntry, wpm: -1 }, expectedError: "Number must be greater than or equal to 0", }, + { + description: "invalid - acc exceeds 100", + input: { ...validLeaderboardEntry, acc: 101 }, + expectedError: "Number must be less than or equal to 100", + }, ] as const)("$description", ({ input, expectedError }) => { - if (expectedError) { + if (expectedError !== undefined) { expect(LeaderboardEntrySchema).toReject(input, expectedError); } else { expect(LeaderboardEntrySchema).toValidate(input); } }); }); + + describe("RedisDailyLeaderboardEntrySchema", () => { + it.each([ + { + description: "valid redis daily leaderboard entry", + input: validRedisDailyLeaderboardEntry, + }, + { + description: "invalid - missing uid", + input: { ...validRedisDailyLeaderboardEntry, uid: undefined }, + expectedError: "Required", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(RedisDailyLeaderboardEntrySchema).toReject(input, expectedError); + } else { + expect(RedisDailyLeaderboardEntrySchema).toValidate(input); + } + }); + }); + + describe("RedisXpLeaderboardEntrySchema", () => { + it.each([ + { + description: "valid redis xp leaderboard entry", + input: validRedisXpLeaderboardEntry, + expectedError: undefined, + }, + { + description: "with discordId and discordAvatar", + input: { + ...validRedisXpLeaderboardEntry, + discordId: "discord123", + discordAvatar: "avatar.png", + }, + expectedError: undefined, + }, + { + description: "with null discordId (transformed to undefined)", + input: { + ...validRedisXpLeaderboardEntry, + discordId: null as unknown as string | undefined, + }, + expectedError: undefined, + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(RedisXpLeaderboardEntrySchema).toReject(input, expectedError); + } else { + expect(RedisXpLeaderboardEntrySchema).toValidate(input); + } + }); + }); + + describe("RedisXpLeaderboardScoreSchema", () => { + it.each([ + { description: "valid score", input: validRedisXpLeaderboardScore }, + { + description: "invalid - negative score", + input: -1, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(RedisXpLeaderboardScoreSchema).toReject(input, expectedError); + } else { + expect(RedisXpLeaderboardScoreSchema).toValidate(input); + } + }); + }); + + describe("XpLeaderboardEntrySchema", () => { + it.each([ + { + description: "valid xp leaderboard entry", + input: validXpLeaderboardEntry, + }, + { + description: "invalid - negative totalXp", + input: { ...validXpLeaderboardEntry, totalXp: -1 }, + expectedError: "Number must be greater than or equal to 0", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(XpLeaderboardEntrySchema).toReject(input, expectedError); + } else { + expect(XpLeaderboardEntrySchema).toValidate(input); + } + }); + }); }); diff --git a/packages/schemas/__tests__/presets.spec.ts b/packages/schemas/__tests__/presets.spec.ts index 1f8beb3a883b..2f9655915777 100644 --- a/packages/schemas/__tests__/presets.spec.ts +++ b/packages/schemas/__tests__/presets.spec.ts @@ -1,5 +1,10 @@ import { it, expect, describe } from "vitest"; -import { PresetNameSchema, PresetTypeSchema } from "../src/presets"; +import { + PresetNameSchema, + PresetTypeSchema, + PresetSchema, + EditPresetRequestSchema, +} from "../src/presets"; describe("presets schemas", () => { describe("PresetNameSchema", () => { @@ -11,7 +16,7 @@ describe("presets schemas", () => { expectedError: "String must contain at most 16 character(s)", }, ] as const)("$description", ({ input, expectedError }) => { - if (expectedError) { + if (expectedError !== undefined) { expect(PresetNameSchema).toReject(input, expectedError); } else { expect(PresetNameSchema).toValidate(input); @@ -30,11 +35,73 @@ describe("presets schemas", () => { "Invalid enum value. Expected 'full' | 'partial', received 'invalid'", }, ] as const)("$description", ({ input, expectedError }) => { - if (expectedError) { + if (expectedError !== undefined) { expect(PresetTypeSchema).toReject(input, expectedError); } else { expect(PresetTypeSchema).toValidate(input); } }); }); + + describe("PresetSchema", () => { + const validPresetMinimal = { + _id: "preset123", + name: "my_preset", + config: {}, + }; + + const validPresetWithConfig = { + _id: "preset123", + name: "my_preset", + config: { punctuation: true }, + }; + + const validPresetWithSettingGroups = { + _id: "preset123", + name: "my_preset", + settingGroups: ["test", "behavior"], + config: {}, + }; + + it.each([ + { description: "valid preset minimal", input: validPresetMinimal }, + { description: "valid preset with config", input: validPresetWithConfig }, + { + description: "valid preset with settingGroups", + input: validPresetWithSettingGroups, + }, + { + description: "invalid - missing name", + input: { _id: "preset123" }, + expectedError: "Required", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(PresetSchema).toReject(input, expectedError); + } else { + expect(PresetSchema).toValidate(input); + } + }); + }); + + describe("EditPresetRequestSchema", () => { + it.each([ + { + description: "valid edit preset request with all required fields", + input: { _id: "preset123", name: "updated_preset" }, + expectedError: undefined, + }, + { + description: "valid edit preset request with config update", + input: { _id: "preset123", name: "updated_preset", config: {} }, + expectedError: undefined, + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(EditPresetRequestSchema).toReject(input, expectedError); + } else { + expect(EditPresetRequestSchema).toValidate(input); + } + }); + }); }); diff --git a/packages/schemas/__tests__/users.spec.ts b/packages/schemas/__tests__/users.spec.ts index b125b15feeae..02a9b502fb12 100644 --- a/packages/schemas/__tests__/users.spec.ts +++ b/packages/schemas/__tests__/users.spec.ts @@ -1,21 +1,922 @@ import { it, expect, describe } from "vitest"; -import { ResultFilterPresetNameSchema } from "../src/users"; +import { + ResultFilterPresetNameSchema, + ResultFiltersSchema, + StreakHourOffsetSchema, + UserStreakSchema, + TagNameSchema, + UserTagSchema, + TwitterProfileSchema, + GithubProfileSchema, + WebsiteSchema, + UserProfileDetailsSchema, + CustomThemeNameSchema, + CustomThemeSchema, + PremiumInfoSchema, + UserQuoteRatingsSchema, + UserLbMemorySchema, + RankAndCountSchema, + AllTimeLbsSchema, + BadgeSchema, + UserInventorySchema, + QuoteModSchema, + TestActivitySchema, + CountByYearAndDaySchema, + FavoriteQuotesSchema, + UserEmailSchema, + UserNameWithoutFilterSchema, + UserNameSchema, + UserSchema, + TypingStatsSchema, + UserProfileSchema, + RewardTypeSchema, + XpRewardSchema, + BadgeRewardSchema, + AllRewardsSchema, + MonkeyMailSchema, + ReportUserReasonSchema, + PasswordSchema, + FriendSchema, +} from "../src/users"; + +// Constants for complex nested objects +const validUserStreak = { + lastResultTimestamp: 1234567890, + length: 10, + maxLength: 100, +}; + +const validUserTag = { + _id: "tag123", + name: "my_tag", + personalBests: { + time: { "10": [] }, + words: { "10": [] }, + quote: { "1": [] }, + custom: { custom: [] }, + zen: { zen: [] }, + }, +}; + +const validUserProfileDetails = { + bio: "Test user bio", + keyboard: "Mechanical", +}; + +const validCustomTheme = { + _id: "theme123", + name: "my_theme", + colors: [ + "#ffffff", + "#000000", + "#ff0000", + "#00ff00", + "#0000ff", + "#ffff00", + "#ff00ff", + "#00ffff", + "#ffffff", + "#000000", + ], +}; + +const validPremiumInfo = { + startTimestamp: 1234567890, + expirationTimestamp: -1, +}; + +const validUserQuoteRatings = { + english: { "1": 5 }, +}; + +const validUserLbMemory = { + time: { "10": { english: 100 } }, +}; + +const validRankAndCount = { + rank: 1, + count: 100, +}; + +const validAllTimeLbs = { + time: { "10": { english: { count: 100 } } }, +}; + +const validBadge = { + id: 1, +}; + +const validUserInventory = { + badges: [{ id: 1 }], +}; + +const validTestActivity = { + testsByDays: [10, 20, 30], + lastDay: 1234567890, +}; + +const validCountByYearAndDay = { + "2023": [1, 2, 3], +}; + +const validFavoriteQuotes = { + english: ["1", "2"], +}; + +const validUserEmail = "user@example.com"; + +const validUserNameWithoutFilter = "john_doe"; + +const validUserSchema = { + name: "john_doe", + email: "user@example.com", + uid: "uid123", + addedAt: 1234567890, + personalBests: { + time: { + "10": [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + words: { + "10": [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + quote: { + "1": [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + custom: { + custom: [ + { + acc: 95, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + zen: { + zen: [ + { + acc: 100, + consistency: 90, + difficulty: "normal", + language: "english", + raw: 120, + wpm: 100, + timestamp: 1234567890, + }, + ], + }, + }, + allTimeLbs: { time: { "10": { english: { count: 100 } } } }, +}; + +const validTypingStats = { + completedTests: 10, + startedTests: 15, + timeTyping: 3600, +}; + +const validUserProfile = { + uid: "uid123", + name: "john_doe", + banned: false, + addedAt: 1234567890, + discordId: "discord123", + discordAvatar: "avatar.png", + xp: 1000, + lbOptOut: false, + isPremium: true, + inventory: { badges: [{ id: 1 }] }, + allTimeLbs: { time: { "10": { english: { count: 100 } } } }, + testActivity: { testsByDays: [10], lastDay: 1234567890 }, + typingStats: { completedTests: 10, startedTests: 15, timeTyping: 3600 }, + personalBests: { time: { "10": [] }, words: { "10": [] } }, + streak: 10, + maxStreak: 100, + details: { bio: "Test" }, +}; + +const validXpReward = { + type: "xp", + item: 1, +}; + +const validBadgeReward = { + type: "badge", + item: { id: 1 }, +}; + +const validMonkeyMail = { + id: "mail123", + subject: "Welcome!", + body: "Welcome to Monkeytype!", + timestamp: 1234567890, + read: false, + rewards: [{ type: "xp", item: 1 }], +}; + +const validPassword = "Password123!"; describe("users schemas", () => { describe("ResultFilterPresetNameSchema", () => { it.each([ - { description: "valid preset name", input: "my_preset" }, + { description: "valid preset name", input: validUserNameWithoutFilter }, { - description: "invalid preset name too long", + description: "exceeds max length", input: "a".repeat(17), - expectedError: "String must contain at most 16 character(s)", + expectedError: "String must contain at most 16 character", }, ] as const)("$description", ({ input, expectedError }) => { - if (expectedError) { + if (expectedError !== undefined) { expect(ResultFilterPresetNameSchema).toReject(input, expectedError); } else { expect(ResultFilterPresetNameSchema).toValidate(input); } }); }); + + describe("ResultFiltersSchema", () => { + const validInput = { + _id: "abc123", + name: "my_preset", + pb: { yes: true, no: false }, + difficulty: { normal: true, expert: false, master: true }, + mode: { time: true, words: true, quote: false, custom: true, zen: false }, + words: { "10": true, "25": false }, + time: { "30": true, "60": false }, + quoteLength: { short: true, medium: false, long: true, thicc: false }, + punctuation: { on: true, off: false }, + numbers: { on: true, off: false }, + date: { + last_day: true, + last_week: false, + last_month: false, + last_3months: false, + all: false, + }, + tags: { abc123: true, none: true }, + language: { english: true, spanish: false }, + funbox: { arrows: true, mirror: false }, + }; + + it.each([ + { description: "valid result filters", input: validInput } as const, + { + description: "missing required field", + input: { _id: "abc123" }, + expectedError: "Required", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(ResultFiltersSchema).toReject(input, expectedError); + } else { + expect(ResultFiltersSchema).toValidate(input); + } + }); + }); + + describe("StreakHourOffsetSchema", () => { + it.each([ + { description: "valid offset 0", input: 0 } as const, + { description: "valid offset 12", input: 12 } as const, + { description: "valid negative offset -11", input: -11 } as const, + { + description: "exceeds max", + input: 13, + expectedError: "Number must be less than or equal to 12", + } as const, + { + description: "below min", + input: -12, + expectedError: "Number must be greater than or equal to -11", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(StreakHourOffsetSchema).toReject(input, expectedError); + } else { + expect(StreakHourOffsetSchema).toValidate(input); + } + }); + }); + + describe("UserStreakSchema", () => { + const validInput = { ...validUserStreak }; + + it.each([ + { description: "valid user streak", input: validInput } as const, + { + description: "invalid - negative length", + input: { ...validInput, length: -1 }, + expectedError: "Number must be greater than or equal to 0", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(UserStreakSchema).toReject(input, expectedError); + } else { + expect(UserStreakSchema).toValidate(input); + } + }); + }); + + describe("TagNameSchema", () => { + it.each([ + { + description: "valid tag name", + input: validUserNameWithoutFilter, + } as const, + { + description: "exceeds max length", + input: "a".repeat(17), + expectedError: "String must contain at most 16 character", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(TagNameSchema).toReject(input, expectedError); + } else { + expect(TagNameSchema).toValidate(input); + } + }); + }); + + describe("UserTagSchema", () => { + const validInput = { ...validUserTag }; + + it.each([ + { description: "valid user tag", input: validInput } as const, + { + description: "invalid - missing name", + input: { + _id: validUserTag._id, + personalBests: validUserTag.personalBests, + }, + expectedError: "Required", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(UserTagSchema).toReject(input, expectedError); + } else { + expect(UserTagSchema).toValidate(input); + } + }); + }); + + describe("TwitterProfileSchema", () => { + it.each([ + { + description: "valid twitter profile", + input: validUserNameWithoutFilter, + } as const, + { + description: "empty string is valid", + input: "", + } as const, + { + description: "exceeds max length (15)", + input: "a".repeat(16), + expectedError: "String must contain at most 15 character", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(TwitterProfileSchema).toReject(input, expectedError); + } else { + expect(TwitterProfileSchema).toValidate(input); + } + }); + }); + + describe("GithubProfileSchema", () => { + it.each([ + { + description: "valid github profile", + input: validUserNameWithoutFilter, + } as const, + { + description: "empty string is valid", + input: "", + } as const, + { + description: "exceeds max length (39)", + input: "a".repeat(40), + expectedError: "String must contain at most 39 character", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(GithubProfileSchema).toReject(input, expectedError); + } else { + expect(GithubProfileSchema).toValidate(input); + } + }); + }); + + describe("WebsiteSchema", () => { + it.each([ + { description: "valid website", input: "https://example.com" } as const, + { + description: "empty string is valid", + input: "", + } as const, + { + description: "exceeds max length (200)", + input: "a".repeat(201), + expectedError: "String must contain at most 200 character", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(WebsiteSchema).toReject(input, expectedError); + } else { + expect(WebsiteSchema).toValidate(input); + } + }); + }); + + describe("UserProfileDetailsSchema", () => { + const validInput = { ...validUserProfileDetails }; + + it.each([ + { description: "valid user profile details", input: validInput } as const, + { + description: "with socialProfiles", + input: { + ...validInput, + socialProfiles: { twitter: validUserNameWithoutFilter }, + }, + } as const, + { + description: "bio exceeds max length", + input: { ...validInput, bio: "a".repeat(251) }, + expectedError: "String must contain at most 250 character", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(UserProfileDetailsSchema).toReject(input, expectedError); + } else { + expect(UserProfileDetailsSchema).toValidate(input); + } + }); + }); + + describe("CustomThemeNameSchema", () => { + it.each([ + { + description: "valid custom theme name", + input: validUserNameWithoutFilter, + } as const, + { + description: "exceeds max length", + input: "a".repeat(17), + expectedError: "String must contain at most 16 character", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(CustomThemeNameSchema).toReject(input, expectedError); + } else { + expect(CustomThemeNameSchema).toValidate(input); + } + }); + }); + + describe("CustomThemeSchema", () => { + const validInput = { ...validCustomTheme }; + + it.each([ + { description: "valid custom theme", input: validInput } as const, + { + description: "invalid - missing _id", + input: { name: validCustomTheme.name, colors: validCustomTheme.colors }, + expectedError: "Required", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(CustomThemeSchema).toReject(input, expectedError); + } else { + expect(CustomThemeSchema).toValidate(input); + } + }); + }); + + describe("PremiumInfoSchema", () => { + it.each([ + { + description: "valid premium info with expiration", + input: validPremiumInfo, + } as const, + { + description: "valid lifetime premium", + input: { startTimestamp: 1234567890, expirationTimestamp: -1 }, + } as const, + { + description: "invalid - negative startTimestamp", + input: { ...validPremiumInfo, startTimestamp: -1 }, + expectedError: "Number must be greater than or equal to 0", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(PremiumInfoSchema).toReject(input, expectedError); + } else { + expect(PremiumInfoSchema).toValidate(input); + } + }); + }); + + describe("UserQuoteRatingsSchema", () => { + it.each([ + { + description: "valid user quote ratings", + input: validUserQuoteRatings, + } as const, + { + description: "invalid - negative rating", + input: { english: { "1": -1 } }, + expectedError: "Number must be greater than or equal to 0", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(UserQuoteRatingsSchema).toReject(input, expectedError); + } else { + expect(UserQuoteRatingsSchema).toValidate(input); + } + }); + }); + + describe("UserLbMemorySchema", () => { + it.each([ + { + description: "valid user lb memory", + input: validUserLbMemory, + } as const, + { + description: "invalid - invalid value", + input: { time: { "10": { english: "not-a-number" } } }, + expectedError: "Expected number, received string", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(UserLbMemorySchema).toReject(input, expectedError); + } else { + expect(UserLbMemorySchema).toValidate(input); + } + }); + }); + + describe("RankAndCountSchema", () => { + it.each([ + { description: "valid rank and count", input: validRankAndCount }, + { + description: "with optional rank", + input: { count: 100 }, + }, + ] as const)("$description", ({ input }) => { + expect(RankAndCountSchema).toValidate(input); + }); + + it("invalid - negative rank", () => { + expect(RankAndCountSchema).toReject( + { ...validRankAndCount, rank: -1 }, + "Number must be greater than or equal to 0", + ); + }); + }); + + describe("AllTimeLbsSchema", () => { + it.each([ + { description: "valid all time lbs", input: validAllTimeLbs } as const, + ])("$description", ({ input }) => { + expect(AllTimeLbsSchema).toValidate(input); + }); + }); + + describe("BadgeSchema", () => { + it.each([ + { description: "valid badge", input: validBadge } as const, + { + description: "with optional selected", + input: { ...validBadge, selected: true }, + } as const, + ])("$description", ({ input }) => { + expect(BadgeSchema).toValidate(input); + }); + }); + + describe("UserInventorySchema", () => { + it.each([ + { + description: "valid user inventory", + input: validUserInventory, + } as const, + ])("$description", ({ input }) => { + expect(UserInventorySchema).toValidate(input); + }); + }); + + describe("QuoteModSchema", () => { + it.each([ + { description: "valid admin for all languages", input: true } as const, + { + description: "valid admin for specific language", + input: "english", + } as const, + ])("$description", ({ input }) => { + expect(QuoteModSchema).toValidate(input); + }); + }); + + describe("TestActivitySchema", () => { + const validInput = { ...validTestActivity }; + + it.each([ + { description: "valid test activity", input: validInput } as const, + { + description: "with null values", + input: { ...validInput, testsByDays: [10, null, 30] }, + } as const, + ])("$description", ({ input }) => { + expect(TestActivitySchema).toValidate(input); + }); + }); + + describe("CountByYearAndDaySchema", () => { + it.each([ + { + description: "valid count by year and day", + input: validCountByYearAndDay, + } as const, + ])("$description", ({ input }) => { + expect(CountByYearAndDaySchema).toValidate(input); + }); + }); + + describe("FavoriteQuotesSchema", () => { + it.each([ + { + description: "valid favorite quotes", + input: validFavoriteQuotes, + } as const, + ])("$description", ({ input }) => { + expect(FavoriteQuotesSchema).toValidate(input); + }); + }); + + describe("UserEmailSchema", () => { + it.each([ + { description: "valid email", input: validUserEmail } as const, + { + description: "invalid email format", + input: "not-an-email", + expectedError: "Invalid email", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(UserEmailSchema).toReject(input, expectedError); + } else { + expect(UserEmailSchema).toValidate(input); + } + }); + }); + + describe("UserNameWithoutFilterSchema", () => { + it.each([ + { + description: "valid username without filter", + input: validUserNameWithoutFilter, + } as const, + { + description: "exceeds max length", + input: "a".repeat(17), + expectedError: "String must contain at most 16 character", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(UserNameWithoutFilterSchema).toReject(input, expectedError); + } else { + expect(UserNameWithoutFilterSchema).toValidate(input); + } + }); + }); + + describe("UserNameSchema", () => { + it.each([ + { + description: "valid username with filter", + input: validUserNameWithoutFilter, + } as const, + ])("$description", ({ input }) => { + expect(UserNameSchema).toValidate(input); + }); + }); + + describe("UserSchema", () => { + const validInput = { ...validUserSchema }; + + it.each([ + { + description: "valid user with all required fields", + input: validInput, + } as const, + { + description: "with optional fields", + input: { ...validInput, xp: 1000, banned: false }, + } as const, + ])("$description", ({ input }) => { + expect(UserSchema).toValidate(input); + }); + + it("invalid - missing required field", () => { + expect(UserSchema).toReject({ name: "test" }, "Required"); + }); + }); + + describe("TypingStatsSchema", () => { + const validInput = { ...validTypingStats }; + + it.each([ + { description: "valid typing stats", input: validInput } as const, + { + description: "with optional fields", + input: { timeTyping: 3600 }, + } as const, + ])("$description", ({ input }) => { + expect(TypingStatsSchema).toValidate(input); + }); + }); + + describe("UserProfileSchema", () => { + const validInput = { ...validUserProfile }; + + it.each([ + { description: "valid user profile", input: validInput } as const, + ])("$description", ({ input }) => { + expect(UserProfileSchema).toValidate(input); + }); + }); + + describe("RewardTypeSchema", () => { + it.each([ + { description: "valid reward type xp", input: "xp" } as const, + { + description: "invalid reward type", + input: "invalid", + expectedError: "Invalid enum value", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(RewardTypeSchema).toReject(input, expectedError); + } else { + expect(RewardTypeSchema).toValidate(input); + } + }); + }); + + describe("XpRewardSchema", () => { + const validInput = { ...validXpReward }; + + it.each([ + { description: "valid xp reward", input: validInput } as const, + { + description: "invalid - invalid item", + input: { ...validXpReward, item: "not-a-number" }, + expectedError: "Expected number, received string", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(XpRewardSchema).toReject(input, expectedError); + } else { + expect(XpRewardSchema).toValidate(input); + } + }); + }); + + describe("BadgeRewardSchema", () => { + const validInput = { ...validBadgeReward }; + + it.each([ + { description: "valid badge reward", input: validInput } as const, + ])("$description", ({ input }) => { + expect(BadgeRewardSchema).toValidate(input); + }); + }); + + describe("AllRewardsSchema", () => { + it.each([ + { description: "valid all rewards (xp)", input: validXpReward } as const, + { + description: "valid all rewards (badge)", + input: validBadgeReward, + } as const, + ])("$description", ({ input }) => { + expect(AllRewardsSchema).toValidate(input); + }); + }); + + describe("MonkeyMailSchema", () => { + const validInput = { ...validMonkeyMail }; + + it.each([ + { description: "valid monkey mail", input: validInput } as const, + { + description: "with read true", + input: { ...validInput, read: true }, + } as const, + ])("$description", ({ input }) => { + expect(MonkeyMailSchema).toValidate(input); + }); + }); + + describe("ReportUserReasonSchema", () => { + it.each([ + { + description: "valid reason inappropriate name", + input: "Inappropriate name", + } as const, + { + description: "invalid reason", + input: "invalid_reason", + expectedError: "Invalid enum value", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(ReportUserReasonSchema).toReject(input, expectedError); + } else { + expect(ReportUserReasonSchema).toValidate(input); + } + }); + }); + + describe("PasswordSchema", () => { + it.each([ + { description: "valid password", input: validPassword } as const, + { + description: "too short", + input: "Pass1!", + expectedError: "must be at least 8 characters", + } as const, + { + description: "no uppercase letter", + input: "password123!", + expectedError: "must contain at least one capital letter", + } as const, + ])("$description", ({ input, expectedError }) => { + if (expectedError !== undefined) { + expect(PasswordSchema).toReject(input, expectedError); + } else { + expect(PasswordSchema).toValidate(input); + } + }); + }); + + describe("FriendSchema", () => { + const validInput = { + uid: "friend123", + name: "friend", + discordId: "discord123", + discordAvatar: "avatar.png", + startedTests: 10, + completedTests: 5, + timeTyping: 3600, + xp: 1000, + banned: false, + lbOptOut: false, + }; + + it.each([ + { description: "valid friend", input: validInput } as const, + { + description: "with optional connectionId", + input: { ...validInput, connectionId: "conn123" }, + } as const, + ])("$description", ({ input }) => { + expect(FriendSchema).toValidate(input); + }); + }); }); From d0a2f6a6f25e9f29d3c38a07fee91f564b554ae8 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 16 Jun 2026 17:04:40 +0200 Subject: [PATCH 12/12] layout --- packages/schemas/__tests__/layouts.spec.ts | 190 ++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) diff --git a/packages/schemas/__tests__/layouts.spec.ts b/packages/schemas/__tests__/layouts.spec.ts index af9f3917aff9..b1e22524ec49 100644 --- a/packages/schemas/__tests__/layouts.spec.ts +++ b/packages/schemas/__tests__/layouts.spec.ts @@ -1,7 +1,195 @@ import { it, expect, describe } from "vitest"; -import { LayoutNameSchema } from "../src/layouts"; +import { LayoutNameSchema, LayoutObjectSchema } from "../src/layouts"; + +const validAnsILayout = { + keymapShowTopRow: true, + matrixShowRightColumn: false, + type: "ansi", + keys: { + row1: [ + ["`", "~"], + ["1", "!"], + ["2", "@"], + ["3", "#"], + ["4", "$"], + ["5", "%"], + ["6", "^"], + ["7", "&"], + ["8", "*"], + ["9", "("], + ["0", ")"], + ["-", "_"], + ["=", "+"], + ], + row2: [ + ["q", "Q"], + ["w", "W"], + ["e", "E"], + ["r", "R"], + ["t", "T"], + ["y", "Y"], + ["u", "U"], + ["i", "I"], + ["o", "O"], + ["p", "P"], + ["[", "{"], + ["]", "}"], + ["\\", "|"], + ], + row3: [ + ["a", "A"], + ["s", "S"], + ["d", "D"], + ["f", "F"], + ["g", "G"], + ["h", "H"], + ["j", "J"], + ["k", "K"], + ["l", "L"], + [";", ":"], + ["'", '"'], + ], + row4: [ + ["z", "Z"], + ["x", "X"], + ["c", "C"], + ["v", "V"], + ["b", "B"], + ["n", "N"], + ["m", "M"], + [",", "<"], + [".", ">"], + ["/", "?"], + ], + row5: [[" "]], + }, +}; + +const validIsoLayout = { + keymapShowTopRow: true, + matrixShowRightColumn: false, + type: "iso", + keys: { + row1: [ + ["\\", "|"], + ["1", "!"], + ["2", '"'], + ["3", "#"], + ["4", "$"], + ["5", "%"], + ["6", "&"], + ["7", "/"], + ["8", "("], + ["9", ")"], + ["0", "="], + ["'", "?"], + ["«", "»"], + ], + row2: [ + ["q", "Q"], + ["w", "W"], + ["e", "E"], + ["r", "R"], + ["t", "T"], + ["y", "Y"], + ["u", "U"], + ["i", "I"], + ["o", "O"], + ["p", "P"], + ["+", "*"], + ["´", "`"], + ], + row3: [ + ["a", "A"], + ["s", "S"], + ["d", "D"], + ["f", "F"], + ["g", "G"], + ["h", "H"], + ["j", "J"], + ["k", "K"], + ["l", "L"], + ["ç", "Ç"], + ["º", "ª"], + ["~", "^"], + ], + row4: [ + ["<", ">"], + ["z", "Z"], + ["x", "X"], + ["c", "C"], + ["v", "V"], + ["b", "B"], + ["n", "N"], + ["m", "M"], + [",", ";"], + [".", ":"], + ["-", "_"], + ], + row5: [[" "]], + }, +}; + +const validMatrixLayout = { + keymapShowTopRow: true, + matrixShowRightColumn: false, + type: "matrix", + keys: { + row1: [], + row2: [ + ["q", "Q"], + ["w", "W"], + ["e", "E"], + ["r", "R"], + ["t", "T"], + ["y", "Y"], + ["u", "U"], + ["i", "I"], + ["o", "O"], + ["p", "P"], + ], + row3: [ + ["a", "A"], + ["s", "S"], + ["d", "D"], + ["f", "F"], + ["g", "G"], + ["h", "H"], + ["j", "J"], + ["k", "K"], + ["l", "L"], + [";", ":"], + ], + row4: [ + ["z", "Z"], + ["x", "X"], + ["c", "C"], + ["v", "V"], + ], + row5: [], + }, +}; describe("layouts schemas", () => { + describe("LayoutObjectSchema", () => { + it.each([ + { description: "valid ansi layout", input: validAnsILayout }, + { description: "valid iso layout", input: validIsoLayout }, + { description: "valid matrix layout", input: validMatrixLayout }, + { + description: "invalid - missing required field", + input: { keymapShowTopRow: true }, + expectedError: "Invalid input", + }, + ] as const)("$description", ({ input, expectedError }) => { + if (expectedError) { + expect(LayoutObjectSchema).toReject(input, expectedError); + } else { + expect(LayoutObjectSchema).toValidate(input); + } + }); + }); + describe("LayoutNameSchema", () => { it.each([ { description: "valid layout qwerty", input: "qwerty" },