@schafevormfenster/rest-commons 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +1190 -0
- package/README.md +275 -0
- package/bin/setup.js +10 -0
- package/dist/api-schemas/error.schema.d.ts +20 -0
- package/dist/api-schemas/error.schema.d.ts.map +1 -0
- package/dist/api-schemas/error.schema.js +17 -0
- package/dist/api-schemas/health.schema.d.ts +497 -0
- package/dist/api-schemas/health.schema.d.ts.map +1 -0
- package/dist/api-schemas/health.schema.js +33 -0
- package/dist/api-schemas/okay.schema.d.ts +13 -0
- package/dist/api-schemas/okay.schema.d.ts.map +1 -0
- package/dist/api-schemas/okay.schema.js +5 -0
- package/dist/api-schemas/paginated-results.schema.d.ts +59 -0
- package/dist/api-schemas/paginated-results.schema.d.ts.map +1 -0
- package/dist/api-schemas/paginated-results.schema.js +10 -0
- package/dist/api-schemas/partial-results.schema.d.ts +30 -0
- package/dist/api-schemas/partial-results.schema.d.ts.map +1 -0
- package/dist/api-schemas/partial-results.schema.js +10 -0
- package/dist/api-schemas/result.schema.d.ts +17 -0
- package/dist/api-schemas/result.schema.d.ts.map +1 -0
- package/dist/api-schemas/result.schema.js +5 -0
- package/dist/api-schemas/results.schema.d.ts +21 -0
- package/dist/api-schemas/results.schema.d.ts.map +1 -0
- package/dist/api-schemas/results.schema.js +5 -0
- package/dist/helpers/correlation/get-correlation-id.d.ts +7 -0
- package/dist/helpers/correlation/get-correlation-id.d.ts.map +1 -0
- package/dist/helpers/correlation/get-correlation-id.js +16 -0
- package/dist/helpers/correlation/get-header.d.ts +7 -0
- package/dist/helpers/correlation/get-header.d.ts.map +1 -0
- package/dist/helpers/correlation/get-header.js +11 -0
- package/dist/helpers/detect-mime-type.d.ts +11 -0
- package/dist/helpers/detect-mime-type.d.ts.map +1 -0
- package/dist/helpers/detect-mime-type.js +40 -0
- package/dist/helpers/detect-suspicious-patterns.d.ts +8 -0
- package/dist/helpers/detect-suspicious-patterns.d.ts.map +1 -0
- package/dist/helpers/detect-suspicious-patterns.js +55 -0
- package/dist/helpers/eventify-constants.types.d.ts +32 -0
- package/dist/helpers/eventify-constants.types.d.ts.map +1 -0
- package/dist/helpers/eventify-constants.types.js +40 -0
- package/dist/helpers/hash-binary.d.ts +21 -0
- package/dist/helpers/hash-binary.d.ts.map +1 -0
- package/dist/helpers/hash-binary.js +28 -0
- package/dist/helpers/mime-types/detect-image-mime-type.d.ts +5 -0
- package/dist/helpers/mime-types/detect-image-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-image-mime-type.js +41 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.d.ts +6 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-ole-mime-type.js +34 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts +5 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-pdf-mime-type.js +13 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.d.ts +6 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.d.ts.map +1 -0
- package/dist/helpers/mime-types/detect-zip-mime-type.js +23 -0
- package/dist/helpers/parameter-validation.d.ts +6 -0
- package/dist/helpers/parameter-validation.d.ts.map +1 -0
- package/dist/helpers/parameter-validation.js +19 -0
- package/dist/helpers/parameter-validation.types.d.ts +16 -0
- package/dist/helpers/parameter-validation.types.d.ts.map +1 -0
- package/dist/helpers/parameter-validation.types.js +38 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts +6 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.d.ts.map +1 -0
- package/dist/helpers/response-headers/build-api-unauthorized-headers.js +23 -0
- package/dist/helpers/response-headers/environment.types.d.ts +2 -0
- package/dist/helpers/response-headers/environment.types.d.ts.map +1 -0
- package/dist/helpers/response-headers/environment.types.js +1 -0
- package/dist/helpers/response-headers/resolve-environment.d.ts +8 -0
- package/dist/helpers/response-headers/resolve-environment.d.ts.map +1 -0
- package/dist/helpers/response-headers/resolve-environment.js +18 -0
- package/dist/helpers/slugify.d.ts +15 -0
- package/dist/helpers/slugify.d.ts.map +1 -0
- package/dist/helpers/slugify.js +32 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/normalization/normalize-list.d.ts +11 -0
- package/dist/normalization/normalize-list.d.ts.map +1 -0
- package/dist/normalization/normalize-list.js +19 -0
- package/dist/normalization/normalize-location.d.ts +16 -0
- package/dist/normalization/normalize-location.d.ts.map +1 -0
- package/dist/normalization/normalize-location.js +26 -0
- package/dist/primitives/coordinate-precision.d.ts +10 -0
- package/dist/primitives/coordinate-precision.d.ts.map +1 -0
- package/dist/primitives/coordinate-precision.js +27 -0
- package/dist/primitives/geo-point.schema.d.ts +8 -0
- package/dist/primitives/geo-point.schema.d.ts.map +1 -0
- package/dist/primitives/geo-point.schema.js +10 -0
- package/dist/primitives/geoname-id.schema.d.ts +8 -0
- package/dist/primitives/geoname-id.schema.d.ts.map +1 -0
- package/dist/primitives/geoname-id.schema.js +9 -0
- package/dist/primitives/international-zip.schema.d.ts +76 -0
- package/dist/primitives/international-zip.schema.d.ts.map +1 -0
- package/dist/primitives/international-zip.schema.js +81 -0
- package/dist/primitives/latitude.schema.d.ts +9 -0
- package/dist/primitives/latitude.schema.d.ts.map +1 -0
- package/dist/primitives/latitude.schema.js +13 -0
- package/dist/primitives/location.schema.d.ts +8 -0
- package/dist/primitives/location.schema.d.ts.map +1 -0
- package/dist/primitives/location.schema.js +15 -0
- package/dist/primitives/longitude.schema.d.ts +9 -0
- package/dist/primitives/longitude.schema.d.ts.map +1 -0
- package/dist/primitives/longitude.schema.js +13 -0
- package/dist/primitives/numeric-id.schema.d.ts +8 -0
- package/dist/primitives/numeric-id.schema.d.ts.map +1 -0
- package/dist/primitives/numeric-id.schema.js +10 -0
- package/dist/primitives/slug.schema.d.ts +17 -0
- package/dist/primitives/slug.schema.d.ts.map +1 -0
- package/dist/primitives/slug.schema.js +30 -0
- package/dist/primitives/uuid.schema.d.ts +8 -0
- package/dist/primitives/uuid.schema.d.ts.map +1 -0
- package/dist/primitives/uuid.schema.js +9 -0
- package/dist/primitives/wikidata-id.schema.d.ts +9 -0
- package/dist/primitives/wikidata-id.schema.d.ts.map +1 -0
- package/dist/primitives/wikidata-id.schema.js +10 -0
- package/dist/time/boundary-enforcement.d.ts +11 -0
- package/dist/time/boundary-enforcement.d.ts.map +1 -0
- package/dist/time/boundary-enforcement.js +43 -0
- package/dist/time/bounded-time.schema.d.ts +31 -0
- package/dist/time/bounded-time.schema.d.ts.map +1 -0
- package/dist/time/bounded-time.schema.js +77 -0
- package/dist/time/flexible-time-parser.d.ts +12 -0
- package/dist/time/flexible-time-parser.d.ts.map +1 -0
- package/dist/time/flexible-time-parser.js +94 -0
- package/dist/time/flexible-time.schema.d.ts +31 -0
- package/dist/time/flexible-time.schema.d.ts.map +1 -0
- package/dist/time/flexible-time.schema.js +31 -0
- package/dist/time/get-week-end.d.ts +10 -0
- package/dist/time/get-week-end.d.ts.map +1 -0
- package/dist/time/get-week-end.js +25 -0
- package/dist/time/get-week-start.d.ts +10 -0
- package/dist/time/get-week-start.d.ts.map +1 -0
- package/dist/time/get-week-start.js +25 -0
- package/dist/time/is-relative-time.d.ts +8 -0
- package/dist/time/is-relative-time.d.ts.map +1 -0
- package/dist/time/is-relative-time.js +9 -0
- package/dist/time/iso8601.schema.d.ts +14 -0
- package/dist/time/iso8601.schema.d.ts.map +1 -0
- package/dist/time/iso8601.schema.js +17 -0
- package/dist/time/iso8601.types.d.ts +6 -0
- package/dist/time/iso8601.types.d.ts.map +1 -0
- package/dist/time/iso8601.types.js +11 -0
- package/dist/time/parse-relative-time.d.ts +9 -0
- package/dist/time/parse-relative-time.d.ts.map +1 -0
- package/dist/time/parse-relative-time.js +36 -0
- package/dist/time/relative-time.schema.d.ts +23 -0
- package/dist/time/relative-time.schema.d.ts.map +1 -0
- package/dist/time/relative-time.schema.js +25 -0
- package/dist/time/since-parameter.schema.d.ts +8 -0
- package/dist/time/since-parameter.schema.d.ts.map +1 -0
- package/dist/time/since-parameter.schema.js +56 -0
- package/dist/time/time-helpers.d.ts +19 -0
- package/dist/time/time-helpers.d.ts.map +1 -0
- package/dist/time/time-helpers.js +56 -0
- package/dist/time/time-schemas.d.ts +20 -0
- package/dist/time/time-schemas.d.ts.map +1 -0
- package/dist/time/time-schemas.js +25 -0
- package/dist/time/timezone.types.d.ts +17 -0
- package/dist/time/timezone.types.d.ts.map +1 -0
- package/dist/time/timezone.types.js +15 -0
- package/dist/validation/zod-error-handler.d.ts +3 -0
- package/dist/validation/zod-error-handler.d.ts.map +1 -0
- package/dist/validation/zod-error-handler.js +189 -0
- package/dist/validation/zod-utils.d.ts +9 -0
- package/dist/validation/zod-utils.d.ts.map +1 -0
- package/dist/validation/zod-utils.js +23 -0
- package/eslint.config.mjs +16 -0
- package/package.json +44 -0
- package/src/api-schemas/error.schema.test.ts +27 -0
- package/src/api-schemas/error.schema.ts +23 -0
- package/src/api-schemas/health.schema.test.ts +104 -0
- package/src/api-schemas/health.schema.ts +63 -0
- package/src/api-schemas/okay.schema.test.ts +15 -0
- package/src/api-schemas/okay.schema.ts +8 -0
- package/src/api-schemas/paginated-results.schema.ts +17 -0
- package/src/api-schemas/partial-results.schema.ts +13 -0
- package/src/api-schemas/result.schema.test.ts +19 -0
- package/src/api-schemas/result.schema.ts +9 -0
- package/src/api-schemas/results.schema.test.ts +15 -0
- package/src/api-schemas/results.schema.ts +9 -0
- package/src/helpers/correlation/get-correlation-id.test.ts +126 -0
- package/src/helpers/correlation/get-correlation-id.ts +22 -0
- package/src/helpers/correlation/get-header.test.ts +179 -0
- package/src/helpers/correlation/get-header.ts +21 -0
- package/src/helpers/detect-mime-type.test.ts +100 -0
- package/src/helpers/detect-mime-type.ts +46 -0
- package/src/helpers/detect-suspicious-patterns.test.ts +45 -0
- package/src/helpers/detect-suspicious-patterns.ts +57 -0
- package/src/helpers/eventify-constants.test.ts +52 -0
- package/src/helpers/eventify-constants.types.test.ts +52 -0
- package/src/helpers/eventify-constants.types.ts +51 -0
- package/src/helpers/hash-binary.test.ts +60 -0
- package/src/helpers/hash-binary.ts +30 -0
- package/src/helpers/mime-types/detect-image-mime-type.test.ts +73 -0
- package/src/helpers/mime-types/detect-image-mime-type.ts +50 -0
- package/src/helpers/mime-types/detect-ole-mime-type.test.ts +86 -0
- package/src/helpers/mime-types/detect-ole-mime-type.ts +44 -0
- package/src/helpers/mime-types/detect-pdf-mime-type.test.ts +39 -0
- package/src/helpers/mime-types/detect-pdf-mime-type.ts +15 -0
- package/src/helpers/mime-types/detect-zip-mime-type.test.ts +88 -0
- package/src/helpers/mime-types/detect-zip-mime-type.ts +28 -0
- package/src/helpers/parameter-validation.test.ts +35 -0
- package/src/helpers/parameter-validation.ts +32 -0
- package/src/helpers/process-eventify-request.ts +146 -0
- package/src/helpers/response-headers/build-api-unauthorized-headers.ts +30 -0
- package/src/helpers/response-headers/environment.types.ts +1 -0
- package/src/helpers/response-headers/resolve-environment.ts +17 -0
- package/src/helpers/slugify.test.ts +77 -0
- package/src/helpers/slugify.ts +34 -0
- package/src/index.ts +46 -0
- package/src/normalization/normalize-list.test.ts +43 -0
- package/src/normalization/normalize-list.ts +21 -0
- package/src/normalization/normalize-location.test.ts +91 -0
- package/src/normalization/normalize-location.ts +29 -0
- package/src/primitives/coordinate-precision.test.ts +46 -0
- package/src/primitives/coordinate-precision.ts +30 -0
- package/src/primitives/geo-point.schema.test.ts +70 -0
- package/src/primitives/geo-point.schema.ts +14 -0
- package/src/primitives/geoname-id.schema.test.ts +60 -0
- package/src/primitives/geoname-id.schema.ts +12 -0
- package/src/primitives/international-zip.schema.test.ts +212 -0
- package/src/primitives/international-zip.schema.ts +103 -0
- package/src/primitives/latitude.schema.test.ts +77 -0
- package/src/primitives/latitude.schema.ts +20 -0
- package/src/primitives/location.schema.test.ts +21 -0
- package/src/primitives/location.schema.ts +22 -0
- package/src/primitives/longitude.schema.test.ts +77 -0
- package/src/primitives/longitude.schema.ts +20 -0
- package/src/primitives/numeric-id.schema.test.ts +32 -0
- package/src/primitives/numeric-id.schema.ts +13 -0
- package/src/primitives/slug.schema.test.ts +101 -0
- package/src/primitives/slug.schema.ts +41 -0
- package/src/primitives/uuid.schema.test.ts +45 -0
- package/src/primitives/uuid.schema.ts +12 -0
- package/src/primitives/wikidata-id.schema.test.ts +51 -0
- package/src/primitives/wikidata-id.schema.ts +16 -0
- package/src/time/README.md +220 -0
- package/src/time/boundary-enforcement.test.ts +130 -0
- package/src/time/boundary-enforcement.ts +59 -0
- package/src/time/bounded-time.schema.test.ts +294 -0
- package/src/time/bounded-time.schema.ts +111 -0
- package/src/time/flexible-time-parser.test.ts +586 -0
- package/src/time/flexible-time-parser.ts +122 -0
- package/src/time/flexible-time.schema.test.ts +243 -0
- package/src/time/flexible-time.schema.ts +43 -0
- package/src/time/is-relative-time.test.ts +23 -0
- package/src/time/is-relative-time.ts +9 -0
- package/src/time/iso8601.schema.ts +29 -0
- package/src/time/iso8601.types.test.ts +112 -0
- package/src/time/iso8601.types.ts +21 -0
- package/src/time/parse-relative-time.test.ts +49 -0
- package/src/time/parse-relative-time.ts +50 -0
- package/src/time/relative-time.schema.test.ts +23 -0
- package/src/time/relative-time.schema.ts +38 -0
- package/src/time/since-parameter.schema.test.ts +59 -0
- package/src/time/since-parameter.schema.ts +69 -0
- package/src/time/time-helpers.test.ts +263 -0
- package/src/time/time-helpers.ts +78 -0
- package/src/time/time-schemas.test.ts +181 -0
- package/src/time/time-schemas.ts +42 -0
- package/src/time/time.schema.test.ts +237 -0
- package/src/time/timezone-independence.test.ts +188 -0
- package/src/time/timezone.types.test.ts +55 -0
- package/src/time/timezone.types.ts +22 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
InternationalZipSchema,
|
|
5
|
+
ZipSchemaByCountry,
|
|
6
|
+
getZipSchemaForCountry,
|
|
7
|
+
} from "./international-zip.schema";
|
|
8
|
+
|
|
9
|
+
describe("InternationalZipSchema", () => {
|
|
10
|
+
it("should accept various international postal code formats", () => {
|
|
11
|
+
expect(InternationalZipSchema.parse("12345")).toBe("12345");
|
|
12
|
+
expect(InternationalZipSchema.parse("12-345")).toBe("12-345");
|
|
13
|
+
expect(InternationalZipSchema.parse("1234 AB")).toBe("1234 AB");
|
|
14
|
+
expect(InternationalZipSchema.parse("SW1A 1AA")).toBe("SW1A 1AA");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should reject codes that are too short", () => {
|
|
18
|
+
expect(() => InternationalZipSchema.parse("12")).toThrow(
|
|
19
|
+
"Postal code must be at least 3 characters long"
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should reject codes that are too long", () => {
|
|
24
|
+
expect(() => InternationalZipSchema.parse("1234567890123")).toThrow(
|
|
25
|
+
"Postal code must not exceed 12 characters"
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should reject codes with invalid characters", () => {
|
|
30
|
+
expect(() => InternationalZipSchema.parse("12345@")).toThrow(
|
|
31
|
+
"Postal code must contain only alphanumeric characters, spaces, and hyphens"
|
|
32
|
+
);
|
|
33
|
+
expect(() => InternationalZipSchema.parse("ABC.DEF")).toThrow(
|
|
34
|
+
"Postal code must contain only alphanumeric characters, spaces, and hyphens"
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Central European Countries (Priority)
|
|
40
|
+
describe("ZipSchemaByCountry.DE (Germany)", () => {
|
|
41
|
+
it("should accept valid German postal codes", () => {
|
|
42
|
+
expect(ZipSchemaByCountry.DE.parse("12345")).toBe("12345");
|
|
43
|
+
expect(ZipSchemaByCountry.DE.parse("80331")).toBe("80331"); // Munich
|
|
44
|
+
expect(ZipSchemaByCountry.DE.parse("10115")).toBe("10115"); // Berlin
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should reject invalid German postal codes", () => {
|
|
48
|
+
expect(() => ZipSchemaByCountry.DE.parse("1234")).toThrow();
|
|
49
|
+
expect(() => ZipSchemaByCountry.DE.parse("123456")).toThrow();
|
|
50
|
+
expect(() => ZipSchemaByCountry.DE.parse("ABCDE")).toThrow();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("ZipSchemaByCountry.PL (Poland)", () => {
|
|
55
|
+
it("should accept valid Polish postal codes", () => {
|
|
56
|
+
expect(ZipSchemaByCountry.PL.parse("00-001")).toBe("00-001"); // Warsaw
|
|
57
|
+
expect(ZipSchemaByCountry.PL.parse("30-001")).toBe("30-001"); // Krakow
|
|
58
|
+
expect(ZipSchemaByCountry.PL.parse("80-001")).toBe("80-001"); // Gdansk
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should reject invalid Polish postal codes", () => {
|
|
62
|
+
expect(() => ZipSchemaByCountry.PL.parse("12345")).toThrow();
|
|
63
|
+
expect(() => ZipSchemaByCountry.PL.parse("1-234")).toThrow();
|
|
64
|
+
expect(() => ZipSchemaByCountry.PL.parse("123-45")).toThrow();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("ZipSchemaByCountry.FR (France)", () => {
|
|
69
|
+
it("should accept valid French postal codes", () => {
|
|
70
|
+
expect(ZipSchemaByCountry.FR.parse("75001")).toBe("75001"); // Paris
|
|
71
|
+
expect(ZipSchemaByCountry.FR.parse("69001")).toBe("69001"); // Lyon
|
|
72
|
+
expect(ZipSchemaByCountry.FR.parse("13001")).toBe("13001"); // Marseille
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should reject invalid French postal codes", () => {
|
|
76
|
+
expect(() => ZipSchemaByCountry.FR.parse("1234")).toThrow();
|
|
77
|
+
expect(() => ZipSchemaByCountry.FR.parse("123456")).toThrow();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("ZipSchemaByCountry.AT (Austria)", () => {
|
|
82
|
+
it("should accept valid Austrian postal codes", () => {
|
|
83
|
+
expect(ZipSchemaByCountry.AT.parse("1010")).toBe("1010"); // Vienna
|
|
84
|
+
expect(ZipSchemaByCountry.AT.parse("5020")).toBe("5020"); // Salzburg
|
|
85
|
+
expect(ZipSchemaByCountry.AT.parse("6020")).toBe("6020"); // Innsbruck
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should reject invalid Austrian postal codes", () => {
|
|
89
|
+
expect(() => ZipSchemaByCountry.AT.parse("123")).toThrow();
|
|
90
|
+
expect(() => ZipSchemaByCountry.AT.parse("12345")).toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("ZipSchemaByCountry.NL (Netherlands)", () => {
|
|
95
|
+
it("should accept valid Dutch postal codes", () => {
|
|
96
|
+
expect(ZipSchemaByCountry.NL.parse("1012 AB")).toBe("1012 AB"); // Amsterdam
|
|
97
|
+
expect(ZipSchemaByCountry.NL.parse("1012AB")).toBe("1012AB");
|
|
98
|
+
expect(ZipSchemaByCountry.NL.parse("3011 AB")).toBe("3011 AB"); // Rotterdam
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should reject invalid Dutch postal codes", () => {
|
|
102
|
+
expect(() => ZipSchemaByCountry.NL.parse("12345")).toThrow();
|
|
103
|
+
expect(() => ZipSchemaByCountry.NL.parse("ABCD EF")).toThrow();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Other European Countries
|
|
108
|
+
describe("ZipSchemaByCountry.CH (Switzerland)", () => {
|
|
109
|
+
it("should accept valid Swiss postal codes", () => {
|
|
110
|
+
expect(ZipSchemaByCountry.CH.parse("8001")).toBe("8001"); // Zurich
|
|
111
|
+
expect(ZipSchemaByCountry.CH.parse("1200")).toBe("1200"); // Geneva
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should reject invalid Swiss postal codes", () => {
|
|
115
|
+
expect(() => ZipSchemaByCountry.CH.parse("123")).toThrow();
|
|
116
|
+
expect(() => ZipSchemaByCountry.CH.parse("12345")).toThrow();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("ZipSchemaByCountry.IT (Italy)", () => {
|
|
121
|
+
it("should accept valid Italian CAP codes", () => {
|
|
122
|
+
expect(ZipSchemaByCountry.IT.parse("00100")).toBe("00100"); // Rome
|
|
123
|
+
expect(ZipSchemaByCountry.IT.parse("20121")).toBe("20121"); // Milan
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should reject invalid Italian CAP codes", () => {
|
|
127
|
+
expect(() => ZipSchemaByCountry.IT.parse("1234")).toThrow();
|
|
128
|
+
expect(() => ZipSchemaByCountry.IT.parse("123456")).toThrow();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("ZipSchemaByCountry.ES (Spain)", () => {
|
|
133
|
+
it("should accept valid Spanish postal codes", () => {
|
|
134
|
+
expect(ZipSchemaByCountry.ES.parse("28001")).toBe("28001"); // Madrid
|
|
135
|
+
expect(ZipSchemaByCountry.ES.parse("08001")).toBe("08001"); // Barcelona
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should reject invalid Spanish postal codes", () => {
|
|
139
|
+
expect(() => ZipSchemaByCountry.ES.parse("1234")).toThrow();
|
|
140
|
+
expect(() => ZipSchemaByCountry.ES.parse("123456")).toThrow();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("ZipSchemaByCountry.BE (Belgium)", () => {
|
|
145
|
+
it("should accept valid Belgian postal codes", () => {
|
|
146
|
+
expect(ZipSchemaByCountry.BE.parse("1000")).toBe("1000"); // Brussels
|
|
147
|
+
expect(ZipSchemaByCountry.BE.parse("2000")).toBe("2000"); // Antwerp
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should reject invalid Belgian postal codes", () => {
|
|
151
|
+
expect(() => ZipSchemaByCountry.BE.parse("123")).toThrow();
|
|
152
|
+
expect(() => ZipSchemaByCountry.BE.parse("12345")).toThrow();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("ZipSchemaByCountry.CZ (Czech Republic)", () => {
|
|
157
|
+
it("should accept valid Czech postal codes", () => {
|
|
158
|
+
expect(ZipSchemaByCountry.CZ.parse("110 00")).toBe("110 00"); // Prague
|
|
159
|
+
expect(ZipSchemaByCountry.CZ.parse("11000")).toBe("11000");
|
|
160
|
+
expect(ZipSchemaByCountry.CZ.parse("602 00")).toBe("602 00"); // Brno
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should reject invalid Czech postal codes", () => {
|
|
164
|
+
expect(() => ZipSchemaByCountry.CZ.parse("123456")).toThrow();
|
|
165
|
+
expect(() => ZipSchemaByCountry.CZ.parse("1234")).toThrow();
|
|
166
|
+
expect(() => ZipSchemaByCountry.CZ.parse("12 345")).toThrow();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("ZipSchemaByCountry.UK (United Kingdom)", () => {
|
|
171
|
+
it("should accept valid UK postcodes", () => {
|
|
172
|
+
expect(ZipSchemaByCountry.UK.parse("SW1A 1AA")).toBe("SW1A 1AA");
|
|
173
|
+
expect(ZipSchemaByCountry.UK.parse("SW1A1AA")).toBe("SW1A1AA");
|
|
174
|
+
expect(ZipSchemaByCountry.UK.parse("M1 1AA")).toBe("M1 1AA");
|
|
175
|
+
expect(ZipSchemaByCountry.UK.parse("EC1A 1BB")).toBe("EC1A 1BB");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should reject invalid UK postcodes", () => {
|
|
179
|
+
expect(() => ZipSchemaByCountry.UK.parse("12345")).toThrow();
|
|
180
|
+
expect(() => ZipSchemaByCountry.UK.parse("AAA")).toThrow();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("getZipSchemaForCountry", () => {
|
|
185
|
+
it("should return correct schema for Central European countries", () => {
|
|
186
|
+
expect(getZipSchemaForCountry("DE")).toBe(ZipSchemaByCountry.DE);
|
|
187
|
+
expect(getZipSchemaForCountry("de")).toBe(ZipSchemaByCountry.DE);
|
|
188
|
+
expect(getZipSchemaForCountry("PL")).toBe(ZipSchemaByCountry.PL);
|
|
189
|
+
expect(getZipSchemaForCountry("FR")).toBe(ZipSchemaByCountry.FR);
|
|
190
|
+
expect(getZipSchemaForCountry("AT")).toBe(ZipSchemaByCountry.AT);
|
|
191
|
+
expect(getZipSchemaForCountry("NL")).toBe(ZipSchemaByCountry.NL);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should return InternationalZipSchema for unknown countries", () => {
|
|
195
|
+
expect(getZipSchemaForCountry("XX")).toBe(InternationalZipSchema);
|
|
196
|
+
expect(getZipSchemaForCountry("ZZ")).toBe(InternationalZipSchema);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should validate postal codes using returned schema", () => {
|
|
200
|
+
const deSchema = getZipSchemaForCountry("DE");
|
|
201
|
+
expect(deSchema.parse("12345")).toBe("12345");
|
|
202
|
+
expect(() => deSchema.parse("ABCDE")).toThrow();
|
|
203
|
+
|
|
204
|
+
const plSchema = getZipSchemaForCountry("PL");
|
|
205
|
+
expect(plSchema.parse("00-001")).toBe("00-001");
|
|
206
|
+
expect(() => plSchema.parse("12345")).toThrow();
|
|
207
|
+
|
|
208
|
+
const unknownSchema = getZipSchemaForCountry("XX");
|
|
209
|
+
expect(unknownSchema.parse("12345")).toBe("12345");
|
|
210
|
+
expect(unknownSchema.parse("ABCDE")).toBe("ABCDE");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for validating international postal/ZIP codes.
|
|
5
|
+
* Focuses on Central European formats with support for other common formats.
|
|
6
|
+
* - Germany: 12345
|
|
7
|
+
* - Poland: 12-345
|
|
8
|
+
* - France: 12345
|
|
9
|
+
* - Austria: 1234
|
|
10
|
+
* - Netherlands: 1234 AB
|
|
11
|
+
* - And more
|
|
12
|
+
*/
|
|
13
|
+
export const InternationalZipSchema = z
|
|
14
|
+
.string()
|
|
15
|
+
.min(3, "Postal code must be at least 3 characters long")
|
|
16
|
+
.max(12, "Postal code must not exceed 12 characters")
|
|
17
|
+
.regex(
|
|
18
|
+
/^[A-Z0-9\s-]+$/i,
|
|
19
|
+
"Postal code must contain only alphanumeric characters, spaces, and hyphens"
|
|
20
|
+
)
|
|
21
|
+
.describe("International postal/ZIP code");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Country-specific ZIP code schemas for stricter validation.
|
|
25
|
+
* Focuses on Central European countries (DE, PL, FR, AT, NL) with additional support for common formats.
|
|
26
|
+
*/
|
|
27
|
+
export const ZipSchemaByCountry = {
|
|
28
|
+
/**
|
|
29
|
+
* Germany PLZ: 12345 (5 digits)
|
|
30
|
+
* Most important for Central Europe
|
|
31
|
+
*/
|
|
32
|
+
DE: z.string().regex(/^\d{5}$/, "Must be a valid German postal code (12345)"),
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Poland kod pocztowy: 12-345 (2 digits, hyphen, 3 digits)
|
|
36
|
+
* Most important for Central Europe
|
|
37
|
+
*/
|
|
38
|
+
PL: z.string().regex(/^\d{2}-\d{3}$/, "Must be a valid Polish postal code (12-345)"),
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* France code postal: 12345 (5 digits)
|
|
42
|
+
* Most important for Central Europe
|
|
43
|
+
*/
|
|
44
|
+
FR: z.string().regex(/^\d{5}$/, "Must be a valid French postal code (12345)"),
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Austria PLZ: 1234 (4 digits)
|
|
48
|
+
* Most important for Central Europe
|
|
49
|
+
*/
|
|
50
|
+
AT: z.string().regex(/^\d{4}$/, "Must be a valid Austrian postal code (1234)"),
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Netherlands postcode: 1234 AB (4 digits, space, 2 letters)
|
|
54
|
+
* Most important for Central Europe
|
|
55
|
+
*/
|
|
56
|
+
NL: z.string().regex(/^\d{4}\s?[A-Z]{2}$/i, "Must be a valid Dutch postal code (1234 AB)"),
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Switzerland PLZ: 1234 (4 digits)
|
|
60
|
+
*/
|
|
61
|
+
CH: z.string().regex(/^\d{4}$/, "Must be a valid Swiss postal code (1234)"),
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Italy CAP: 12345 (5 digits)
|
|
65
|
+
*/
|
|
66
|
+
IT: z.string().regex(/^\d{5}$/, "Must be a valid Italian CAP (12345)"),
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Spain código postal: 12345 (5 digits)
|
|
70
|
+
*/
|
|
71
|
+
ES: z.string().regex(/^\d{5}$/, "Must be a valid Spanish postal code (12345)"),
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Belgium postcode: 1234 (4 digits)
|
|
75
|
+
*/
|
|
76
|
+
BE: z.string().regex(/^\d{4}$/, "Must be a valid Belgian postal code (1234)"),
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Czech Republic PSČ: 123 45 (3 digits, space, 2 digits)
|
|
80
|
+
*/
|
|
81
|
+
CZ: z.string().regex(/^\d{3}\s?\d{2}$/, "Must be a valid Czech postal code (123 45)"),
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* UK postcode: SW1A 1AA
|
|
85
|
+
*/
|
|
86
|
+
UK: z.string().regex(
|
|
87
|
+
/^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$/i,
|
|
88
|
+
"Must be a valid UK postcode (SW1A 1AA)"
|
|
89
|
+
),
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
92
|
+
export type InternationalZip = z.infer<typeof InternationalZipSchema>;
|
|
93
|
+
export type CountryCode = keyof typeof ZipSchemaByCountry;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Helper function to get the appropriate ZIP schema for a country
|
|
97
|
+
* @param countryCode - Two-letter ISO country code
|
|
98
|
+
* @returns Zod schema for the country's postal code format, or InternationalZipSchema as fallback
|
|
99
|
+
*/
|
|
100
|
+
export function getZipSchemaForCountry(countryCode: string): z.ZodString {
|
|
101
|
+
const upperCode = countryCode.toUpperCase() as CountryCode;
|
|
102
|
+
return ZipSchemaByCountry[upperCode] || InternationalZipSchema;
|
|
103
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { LatitudeSchema } from "./latitude.schema";
|
|
4
|
+
|
|
5
|
+
describe("LatitudeSchema", () => {
|
|
6
|
+
it("should accept valid latitude values", () => {
|
|
7
|
+
expect(LatitudeSchema.parse(0)).toBe(0);
|
|
8
|
+
expect(LatitudeSchema.parse(45)).toBe(45);
|
|
9
|
+
expect(LatitudeSchema.parse(-45)).toBe(-45);
|
|
10
|
+
expect(LatitudeSchema.parse(52.52)).toBe(52.52); // Berlin
|
|
11
|
+
expect(LatitudeSchema.parse(40.7128)).toBe(40.7128); // New York
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should accept boundary values", () => {
|
|
15
|
+
expect(LatitudeSchema.parse(90)).toBe(90); // North Pole
|
|
16
|
+
expect(LatitudeSchema.parse(-90)).toBe(-90); // South Pole
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should reject values above 90", () => {
|
|
20
|
+
expect(() => LatitudeSchema.parse(90.1)).toThrow(
|
|
21
|
+
"Latitude must be less than or equal to 90"
|
|
22
|
+
);
|
|
23
|
+
expect(() => LatitudeSchema.parse(100)).toThrow(
|
|
24
|
+
"Latitude must be less than or equal to 90"
|
|
25
|
+
);
|
|
26
|
+
expect(() => LatitudeSchema.parse(180)).toThrow(
|
|
27
|
+
"Latitude must be less than or equal to 90"
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should reject values below -90", () => {
|
|
32
|
+
expect(() => LatitudeSchema.parse(-90.1)).toThrow(
|
|
33
|
+
"Latitude must be greater than or equal to -90"
|
|
34
|
+
);
|
|
35
|
+
expect(() => LatitudeSchema.parse(-100)).toThrow(
|
|
36
|
+
"Latitude must be greater than or equal to -90"
|
|
37
|
+
);
|
|
38
|
+
expect(() => LatitudeSchema.parse(-180)).toThrow(
|
|
39
|
+
"Latitude must be greater than or equal to -90"
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should reject non-numeric values", () => {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
expect(() => LatitudeSchema.parse("45" as any)).toThrow();
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/no-null
|
|
47
|
+
expect(() => LatitudeSchema.parse(null as any)).toThrow();
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
expect(() => LatitudeSchema.parse(undefined as any)).toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should accept values with up to 8 decimal places", () => {
|
|
53
|
+
expect(LatitudeSchema.parse(52.52)).toBe(52.52); // 2 decimal places
|
|
54
|
+
expect(LatitudeSchema.parse(40.7128)).toBe(40.7128); // 4 decimal places
|
|
55
|
+
expect(LatitudeSchema.parse(52.520_008)).toBe(52.520_008); // 6 decimal places
|
|
56
|
+
expect(LatitudeSchema.parse(52.520_008_07)).toBe(52.520_008_07); // 8 decimal places
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should reject values with more than 8 decimal places", () => {
|
|
60
|
+
expect(() => LatitudeSchema.parse(52.520_008_071)).toThrow(
|
|
61
|
+
"Latitude must have at most 8 decimal places"
|
|
62
|
+
);
|
|
63
|
+
expect(() => LatitudeSchema.parse(40.712_812_345_678)).toThrow(
|
|
64
|
+
"Latitude must have at most 8 decimal places"
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should handle scientific notation correctly", () => {
|
|
69
|
+
// Small numbers with few actual decimal places should pass
|
|
70
|
+
expect(LatitudeSchema.parse(1.23e-4)).toBe(1.23e-4); // 0.000123 = 6 decimals
|
|
71
|
+
|
|
72
|
+
// Numbers with more than 8 decimal places should fail
|
|
73
|
+
expect(() => LatitudeSchema.parse(1.234_567_89e-1)).toThrow(
|
|
74
|
+
"Latitude must have at most 8 decimal places"
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { hasValidDecimalPrecision } from "./coordinate-precision";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Zod schema for validating latitude coordinates.
|
|
7
|
+
* Latitude must be a number between -90 and 90 degrees inclusive.
|
|
8
|
+
* Maximum precision is 8 decimal places (~1.1mm accuracy).
|
|
9
|
+
*/
|
|
10
|
+
export const LatitudeSchema = z
|
|
11
|
+
.number()
|
|
12
|
+
.min(-90, "Latitude must be greater than or equal to -90")
|
|
13
|
+
.max(90, "Latitude must be less than or equal to 90")
|
|
14
|
+
.refine(
|
|
15
|
+
(value) => hasValidDecimalPrecision(value, 8),
|
|
16
|
+
{ message: "Latitude must have at most 8 decimal places" }
|
|
17
|
+
)
|
|
18
|
+
.describe("Latitude coordinate in degrees");
|
|
19
|
+
|
|
20
|
+
export type Latitude = z.infer<typeof LatitudeSchema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { LocationSchema } from "./location.schema";
|
|
4
|
+
|
|
5
|
+
describe("LocationSchema", () => {
|
|
6
|
+
it("should accept valid location strings", () => {
|
|
7
|
+
expect(LocationSchema.parse("Berlin")).toBe("Berlin");
|
|
8
|
+
expect(LocationSchema.parse("New York, NY")).toBe("New York, NY");
|
|
9
|
+
expect(LocationSchema.parse("123 Main Street, Anytown")).toBe("123 Main Street, Anytown");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should reject strings shorter than 2 characters", () => {
|
|
13
|
+
expect(() => LocationSchema.parse("a")).toThrow("Location must be at least 2 characters long");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should reject strings with suspicious patterns", () => {
|
|
17
|
+
expect(() => LocationSchema.parse("<script>alert(1)</script>")).toThrow(
|
|
18
|
+
"Parameter 'location' contains suspicious patterns: html.script_tag"
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { validateNoSuspiciousPatterns } from "../helpers/parameter-validation";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Zod schema for validating location parameter in findbyaddress endpoint.
|
|
7
|
+
* Checks for minimum length and suspicious patterns.
|
|
8
|
+
*/
|
|
9
|
+
export const LocationSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(2, "Location must be at least 2 characters long")
|
|
12
|
+
.refine(
|
|
13
|
+
(value) => {
|
|
14
|
+
validateNoSuspiciousPatterns(value, "location");
|
|
15
|
+
return true;
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
message: "Location parameter contains suspicious patterns that are not allowed",
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export type Location = z.infer<typeof LocationSchema>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { LongitudeSchema } from "./longitude.schema";
|
|
4
|
+
|
|
5
|
+
describe("LongitudeSchema", () => {
|
|
6
|
+
it("should accept valid longitude values", () => {
|
|
7
|
+
expect(LongitudeSchema.parse(0)).toBe(0);
|
|
8
|
+
expect(LongitudeSchema.parse(90)).toBe(90);
|
|
9
|
+
expect(LongitudeSchema.parse(-90)).toBe(-90);
|
|
10
|
+
expect(LongitudeSchema.parse(13.405)).toBe(13.405); // Berlin
|
|
11
|
+
expect(LongitudeSchema.parse(-74.006)).toBe(-74.006); // New York
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should accept boundary values", () => {
|
|
15
|
+
expect(LongitudeSchema.parse(180)).toBe(180); // International Date Line
|
|
16
|
+
expect(LongitudeSchema.parse(-180)).toBe(-180); // International Date Line
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should reject values above 180", () => {
|
|
20
|
+
expect(() => LongitudeSchema.parse(180.1)).toThrow(
|
|
21
|
+
"Longitude must be less than or equal to 180"
|
|
22
|
+
);
|
|
23
|
+
expect(() => LongitudeSchema.parse(200)).toThrow(
|
|
24
|
+
"Longitude must be less than or equal to 180"
|
|
25
|
+
);
|
|
26
|
+
expect(() => LongitudeSchema.parse(360)).toThrow(
|
|
27
|
+
"Longitude must be less than or equal to 180"
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should reject values below -180", () => {
|
|
32
|
+
expect(() => LongitudeSchema.parse(-180.1)).toThrow(
|
|
33
|
+
"Longitude must be greater than or equal to -180"
|
|
34
|
+
);
|
|
35
|
+
expect(() => LongitudeSchema.parse(-200)).toThrow(
|
|
36
|
+
"Longitude must be greater than or equal to -180"
|
|
37
|
+
);
|
|
38
|
+
expect(() => LongitudeSchema.parse(-360)).toThrow(
|
|
39
|
+
"Longitude must be greater than or equal to -180"
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should reject non-numeric values", () => {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
expect(() => LongitudeSchema.parse("45" as any)).toThrow();
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/no-null
|
|
47
|
+
expect(() => LongitudeSchema.parse(null as any)).toThrow();
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
expect(() => LongitudeSchema.parse(undefined as any)).toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should accept values with up to 8 decimal places", () => {
|
|
53
|
+
expect(LongitudeSchema.parse(13.405)).toBe(13.405); // 3 decimal places
|
|
54
|
+
expect(LongitudeSchema.parse(-74.006)).toBe(-74.006); // 3 decimal places
|
|
55
|
+
expect(LongitudeSchema.parse(151.2093)).toBe(151.2093); // 4 decimal places
|
|
56
|
+
expect(LongitudeSchema.parse(13.405_000_01)).toBe(13.405_000_01); // 8 decimal places
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should reject values with more than 8 decimal places", () => {
|
|
60
|
+
expect(() => LongitudeSchema.parse(13.405_000_012)).toThrow(
|
|
61
|
+
"Longitude must have at most 8 decimal places"
|
|
62
|
+
);
|
|
63
|
+
expect(() => LongitudeSchema.parse(-74.006_123_456_789)).toThrow(
|
|
64
|
+
"Longitude must have at most 8 decimal places"
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should handle scientific notation correctly", () => {
|
|
69
|
+
// Small numbers with few actual decimal places should pass
|
|
70
|
+
expect(LongitudeSchema.parse(1.23e-4)).toBe(1.23e-4); // 0.000123 = 6 decimals
|
|
71
|
+
|
|
72
|
+
// Numbers with more than 8 decimal places should fail
|
|
73
|
+
expect(() => LongitudeSchema.parse(1.234_567_89e-1)).toThrow(
|
|
74
|
+
"Longitude must have at most 8 decimal places"
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { hasValidDecimalPrecision } from "./coordinate-precision";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Zod schema for validating longitude coordinates.
|
|
7
|
+
* Longitude must be a number between -180 and 180 degrees inclusive.
|
|
8
|
+
* Maximum precision is 8 decimal places (~1.1mm accuracy).
|
|
9
|
+
*/
|
|
10
|
+
export const LongitudeSchema = z
|
|
11
|
+
.number()
|
|
12
|
+
.min(-180, "Longitude must be greater than or equal to -180")
|
|
13
|
+
.max(180, "Longitude must be less than or equal to 180")
|
|
14
|
+
.refine(
|
|
15
|
+
(value) => hasValidDecimalPrecision(value, 8),
|
|
16
|
+
{ message: "Longitude must have at most 8 decimal places" }
|
|
17
|
+
)
|
|
18
|
+
.describe("Longitude coordinate in degrees");
|
|
19
|
+
|
|
20
|
+
export type Longitude = z.infer<typeof LongitudeSchema>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { NumericIdSchema } from "./numeric-id.schema";
|
|
4
|
+
|
|
5
|
+
describe("NumericIdSchema", () => {
|
|
6
|
+
it("should accept valid numeric ID strings", () => {
|
|
7
|
+
expect(NumericIdSchema.parse("123")).toBe("123");
|
|
8
|
+
expect(NumericIdSchema.parse("1234567890")).toBe("1234567890");
|
|
9
|
+
expect(NumericIdSchema.parse("12345678901234567890")).toBe("12345678901234567890");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should reject strings shorter than 3 characters", () => {
|
|
13
|
+
expect(() => NumericIdSchema.parse("12")).toThrow("ID must be at least 3 characters long");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should reject strings longer than 20 characters", () => {
|
|
17
|
+
expect(() => NumericIdSchema.parse("123456789012345678901")).toThrow("ID must not exceed 20 characters");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should reject non-numeric strings", () => {
|
|
21
|
+
expect(() => NumericIdSchema.parse("123abc")).toThrow("ID must contain only numeric characters");
|
|
22
|
+
expect(() => NumericIdSchema.parse("abc123")).toThrow("ID must contain only numeric characters");
|
|
23
|
+
expect(() => NumericIdSchema.parse("12-34")).toThrow("ID must contain only numeric characters");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should reject numeric strings with suspicious patterns", () => {
|
|
27
|
+
// This test ensures regex validation happens first for non-numeric characters
|
|
28
|
+
expect(() => NumericIdSchema.parse("123<script>")).toThrow(
|
|
29
|
+
"ID must contain only numeric characters"
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for validating ID parameter in community ID endpoint.
|
|
5
|
+
* Checks that ID contains only numbers and is between 3-20 characters long.
|
|
6
|
+
*/
|
|
7
|
+
export const NumericIdSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.min(3, "ID must be at least 3 characters long")
|
|
10
|
+
.max(20, "ID must not exceed 20 characters")
|
|
11
|
+
.regex(/^\d+$/, "ID must contain only numeric characters");
|
|
12
|
+
|
|
13
|
+
export type NumericId = z.infer<typeof NumericIdSchema>;
|