All files / is / testing / spec.ts

100.00% Branches 34/34
96.74% Lines 178/184
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
 
 
x3
x3
x3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x3
x3
x41
x231
x77
 
x41
x42
x42
 
x41
x128
x42
 
x41
x81
x1117
x371
x371
x661
x661
x661
 
 
x661
x662
x662
x662
x662
x662
x662
x662
 
x661
x715
x661
x897
x1082
x1082
x1082
x897
x904
x904
x904
x904
x2692
x897
x661
x661
x662
x662
x661
x1855
x371
x81
x3
x3
x3
x12
x3
x3
x9
x3
x3
x9
x9
x9
x9
x9
x9
x9
x9
x12
x12
x12
x9
x3
x3
x9
x9
x3
x3
x9
x3
x3
x15
x27
 
x3
x3
x3
x9
x12
x12
x12
x12
x3
x3
x9
x12
x12
x3
x3
x9
x12
 
x3
x3
x30
x12
x3
x3
x15
x15
x15
x15
x15
x15
x18
x18
x18
x15
 
x3
x3
x3
x39
 
x3
x3
x9
x12
x3
x3
x9
x9
x9
x9
x9
x9
x12
x3
x3
x9
x12
x3
x3
x9
x12
x3
x3
x9
x12
x3
x3
x6
x6
x12
x12
x3
x3
x21
x12
x3
x3
x9
x9
x9
x9
x9
x9
x15
x3
 
 
 
 
 
 
x3
x4
x14
x16
x4
x3
x3
x4
x20
x4
x4
x3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 















































































































































































































































// Imports
import type { testing } from "@libs/testing"
import { is } from "../is.ts"
import { pick } from "@std/collections"
import { expect, inspect, test } from "@libs/testing"

/** Test case definition. */
export type testcase = { a: unknown; b?: unknown; env?: Record<string, string> }

/** Configuration options for {@linkcode spec()}. */
export type SpecOptions = {
  /** Whether to perform a prefault check. */
  prefault?: boolean
  /** Whether to perform a strictness check. */
  strict?: boolean
  /** Setup function to run before tests. */
  setup?: () => void
}

/** Specification check helper. */
export const spec = Object.assign(
  function (name: string, schema: is.ZodType, testsuites: Array<testcase> | Record<PropertyKey, Array<testcase>>, { prefault = true, strict = true, setup }: SpecOptions = {}): void {
    if (Array.isArray(testsuites)) {
      testsuites = { "*": testsuites }
    }
    // Prefault check
    if (prefault) {
      test(`\`${name}.parse()\` has prefault`, () => expect(schema.parse(undefined)).toMatchObject({}))
    }
    // Strictness check
    if (strict) {
      test(`\`${name}.parse()\` is strict`, () => expect(() => schema.parse({ __strictness_test__: true })).toThrow())
    }
    // Parsing checks
    for (const [key, testcases] of Object.entries(testsuites)) {
      for (const testcase of testcases) {
        const a = key === "*" ? testcase.a : { [key]: testcase.a }
        const b = "b" in testcase ? testcase.b : testcase.a
        test(`\`${name}.parse(${inspect(a)})\` ${b === Error ? "throws error" : `is parsed${"b" in testcase ? ` as \`${inspect(b)}\`` : ""}`}${"env" in testcase ? ` with \`${inspect(testcase.env)}\`` : ""}`, () => {
          const env = {} as NonNullable<testcase["env"]>
          try {
            setup?.()

            // Apply environment variables
            if (testcase.env) {
              for (const [key, value] of Object.entries(testcase.env)) {
                if (Deno.env.has(key)) {
                  env[key] = Deno.env.get(key)!
                }
                Deno.env.set(key, value)
              }
            }
            // Apply test
            if (b === Error) {
              expect(() => schema.parse(a)).toThrow()
            } else {
              if ((key === "*") && ((typeof b !== "object") || (b === null))) {
                expect(schema.parse(a)).toEqual(b as Record<PropertyKey, unknown>)
                return
              }
              if ((b instanceof Date) || (b === "2024-01-01T00:00:00.000Z")) {
                const da = schema.parse(a) as Date
                expect((key === "*" ? da : (da as testing)[key]).getTime()).toBe(new Date(b).getTime())
                return
              }
              expect(schema.parse(a)).toMatchObject(key === "*" ? b as Record<PropertyKey, unknown> : { [key]: b })
            }
          } finally {
            for (const [key, value] of Object.entries(env)) {
              Deno.env.set(key, value)
            }
          }
        }, { permissions: { env: testcase.env ? true : false } })
      }
    }
  },
  {
    optional: [
      { a: undefined, b: undefined },
    ],
    nullable: [
      { a: null },
    ],
    primitive: [
      { a: "foo" },
      { a: 123 },
      { a: 123n },
      { a: true },
      { a: undefined },
      { a: null },
      { a: new Date("2024-01-01T00:00:00.000Z") },
      { a: new Error("Testing") },
      { a: Symbol.for("@@testing"), b: Error },
      { a: {}, b: Error },
      { a: [], b: Error },
      { a: function () {}, b: Error },
    ],
    boolean: [
      { a: true },
      { a: false },
    ],
    string: Object.assign([
      { a: "foo" },
    ], {
      arrayable: [
        { a: "a", b: ["a"] },
        { a: ["a", "b"], b: ["a", "b"] },
      ],
    }),
    positive: {
      int: [
        { a: 1 },
        { a: "1", b: 1 },
        { a: -1, b: Error },
        { a: "-1", b: Error },
        { a: "NaN", b: Error },
      ],
      finite: [
        { a: 1 },
        { a: NaN, b: Error },
        { a: Infinity, b: Error },
      ],
      infinity: [
        { a: Infinity },
        { a: "Infinity", b: Infinity },
      ],
    },
    record: Object.assign([
      { a: { foo: "bar", baz: 123 }, b: { foo: "bar", baz: 123 } },
      { a: "foo", b: Error },
    ], {
      primitive: [
        { a: { foo: "bar" } },
        { a: { foo: 123 } },
        { a: { foo: 123n } },
        { a: { foo: true } },
        { a: { foo: undefined } },
        { a: { foo: null } },
        { a: { foo: Symbol.for("@@testing") }, b: Error },
        { a: { foo: {} }, b: Error },
        { a: { foo: [] }, b: Error },
        { a: { foo: function () {} }, b: Error },
      ],
    }),
    array: {
      unique: [
        { a: ["a", "b", "b", "a", "c"], b: ["a", "b", "c"] },
      ],
    },
    ulid: [
      { a: "0".repeat(26) },
      { a: "TEST_INVALID_ULID", b: Error },
    ],
    url: [
      { a: "http://example.com" },
      { a: "https://example.com" },
      { a: "jsr:@scope/test" },
      { a: "npm:@scope/test" },
      { a: "file:///test/path" },
      { a: "data:application/test,null" },
      { a: "test_invalid_url", b: Error },
    ],
    date: [
      { a: new Date("2024-01-01T00:00:00.000Z") },
      { a: "test_invalid_date", b: Error },
    ],
    error: [
      { a: new Error("Test error") },
      { a: `${new Error("Test error")}`, b: Error },
    ],
    expression: [
      { a: "${foo + 1}" },
      { a: "foo + 1", b: Error },
    ],
    callable: [
      { a: () => {} },
      { a: async () => {} },
      { a: null, b: Error },
      { a: {}, b: Error },
    ],
    parser: [
      { a: Object.assign(is.object({ foo: is.string() }), { [Symbol.for("Deno.customInspect")]: () => "schema" }) },
      { a: { parse: () => ({}) } },
    ],
    unknown: [
      { a: null },
      { a: 123 },
      { a: "string" },
      { a: true },
      { a: [] },
      { a: {} },
      { a: { foo: "bar" } },
    ],
    meta(meta: ImportMeta) {
      return [
        { a: pick(meta, ["url", "dirname", "filename"]) },
        { a: {}, b: Error },
      ]
    },
    enum(values: readonly string[]) {
      return [
        ...values.map((a) => ({ a })),
        { a: "TEST_INVALID_ENUM_VALUE", b: Error },
      ]
    },
    default(b: unknown, options?: Omit<testcase, "a" | "b">) {
      return [
        { a: undefined, b, ...options },
      ]
    },
  },
) as ((name: string, schema: is.ZodType, testsuites: Array<testcase> | Record<PropertyKey, Array<testcase>>, options?: SpecOptions) => void) & {
  optional: Array<testcase>
  nullable: Array<testcase>
  primitive: Array<testcase>
  boolean: Array<testcase>
  string: {
    arrayable: Array<testcase>
  }
  positive: {
    int: Array<testcase>
    finite: Array<testcase>
    infinity: Array<testcase>
  }
  record: {
    primitive: Array<testcase>
  } & Array<testcase>
  array: {
    unique: Array<testcase>
  }
  ulid: Array<testcase>
  url: Array<testcase>
  date: Array<testcase>
  error: Array<testcase>
  expression: Array<testcase>
  callable: Array<testcase>
  parser: Array<testcase>
  unknown: Array<testcase>
  meta: (meta: ImportMeta) => Array<testcase>
  enum: (values: readonly string[]) => Array<testcase>
  default: (b: unknown, options?: Omit<testcase, "a" | "b">) => Array<testcase>
}