From 0525a5c0a4237548124cedfe0bd17d61a8ec7950 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Sat, 12 Sep 2026 09:46:04 -0300 Subject: [PATCH] test: property tests, signature pins and benchmarks in every test file Every util's test file ends with a describe("properties") block of fast-check properties that hold by specification (a generated value is valid, format/parse round-trip, masks never change the verdict, arbitrary input never throws, data tables agree with their validators), a describe(" types") block that pins the public signature with expectTypeOf (checked statically by vp check), and, on hot paths, a describe(" benchmarks") block of bench cases. expectTypeOf and bench come from the multi-runtime shim (real in vitest, a no-op chain on Bun and Deno; bench registers a todo outside benchmark mode and runs with `npx vp test bench --run`). Shared arbitraries and property runners live in src/_internals/test so jscpd stays at zero clones. Also makes the randomized Pix-key CNPJ test deterministic (a bare CNPJ starting with 0055 is read as a phone, as documented). --- .github/workflows/tests.yml | 8 +- CONTRIBUTING.md | 11 +- package-lock.json | 41 ++++ package.json | 1 + src/_internals/test/arbitraries.ts | 132 ++++++++++++ src/_internals/test/globals.d.ts | 6 + src/_internals/test/noop.ts | 18 ++ src/_internals/test/properties.ts | 202 ++++++++++++++++++ src/_internals/test/runtime-bun.ts | 1 + src/_internals/test/runtime-deno.ts | 1 + src/_internals/test/runtime-vitest.ts | 28 ++- src/_internals/test/runtime.ts | 7 +- .../add-business-days.test.ts | 63 +++++- src/capitalize/capitalize.test.ts | 44 +++- .../convert-currency-to-words.test.ts | 58 ++++- .../convert-date-to-words.test.ts | 62 +++++- .../convert-license-plate-to-mercosul.test.ts | 48 ++++- .../convert-number-to-words.test.ts | 72 ++++++- .../difference-in-business-days.test.ts | 59 ++++- src/format-boleto/format-boleto.test.ts | 67 +++++- src/format-caepf/format-caepf.test.ts | 44 +++- src/format-cei/format-cei.test.ts | 44 +++- src/format-cep/format-cep.test.ts | 44 +++- src/format-certidao/format-certidao.test.ts | 56 ++++- src/format-cnae/format-cnae.test.ts | 31 ++- src/format-cnh/format-cnh.test.ts | 44 +++- src/format-cno/format-cno.test.ts | 36 +++- src/format-cnpj/format-cnpj.test.ts | 68 +++++- src/format-cns/format-cns.test.ts | 44 +++- src/format-cpf/format-cpf.test.ts | 54 ++++- src/format-currency/format-currency.test.ts | 55 ++++- src/format-iban/format-iban.test.ts | 44 +++- .../format-legal-nature.test.ts | 32 ++- .../format-license-plate.test.ts | 46 +++- src/format-ncm/format-ncm.test.ts | 31 ++- src/format-nfe-key/format-nfe-key.test.ts | 47 +++- src/format-passport/format-passport.test.ts | 34 ++- src/format-phone/format-phone.test.ts | 74 ++++++- src/format-pis/format-pis.test.ts | 44 +++- .../format-processo-juridico.test.ts | 56 ++++- src/format-voter-id/format-voter-id.test.ts | 46 +++- src/generate-boleto/generate-boleto.test.ts | 59 ++++- src/generate-cep/generate-cep.test.ts | 29 ++- src/generate-cnh/generate-cnh.test.ts | 29 ++- src/generate-cnpj/generate-cnpj.test.ts | 43 +++- src/generate-cpf/generate-cpf.test.ts | 44 +++- .../generate-legal-nature.test.ts | 28 ++- .../generate-license-plate.test.ts | 44 +++- .../generate-passport.test.ts | 28 ++- src/generate-phone/generate-phone.test.ts | 69 +++++- src/generate-pis/generate-pis.test.ts | 29 ++- .../generate-pix-payload.test.ts | 119 ++++++++++- .../generate-processo-juridico.test.ts | 67 +++++- .../generate-voter-id.test.ts | 30 ++- .../get-address-info-by-cep.test.ts | 40 +++- .../get-area-code-info.test.ts | 51 ++++- .../get-area-codes-by-state.test.ts | 45 +++- src/get-bank-by-code/get-bank-by-code.test.ts | 47 +++- src/get-bank-by-ispb/get-bank-by-ispb.test.ts | 44 +++- src/get-banks/get-banks.test.ts | 41 +++- src/get-boleto-info/get-boleto-info.test.ts | 78 ++++++- src/get-cbo/get-cbo.test.ts | 46 +++- .../get-cep-info-by-address.test.ts | 30 ++- src/get-cfop/get-cfop.test.ts | 38 +++- src/get-cities/get-cities.test.ts | 32 ++- src/get-cnae/get-cnae.test.ts | 39 +++- .../get-format-license-plate.test.ts | 41 +++- src/get-holidays/get-holidays.test.ts | 83 ++++++- src/get-legal-nature/get-legal-nature.test.ts | 55 ++++- .../get-legal-natures.test.ts | 44 +++- .../get-municipalities.test.ts | 52 ++++- .../get-municipality-by-code.test.ts | 34 ++- src/get-municipality/get-municipality.test.ts | 26 ++- .../get-state-by-ibge-code.test.ts | 38 +++- .../get-state-code-by-name.test.ts | 32 ++- .../get-state-name-by-code.test.ts | 32 ++- src/get-states/get-states.test.ts | 44 +++- .../get-timezone-by-state.test.ts | 31 ++- src/index.test.ts | 2 + src/is-business-day/is-business-day.test.ts | 69 +++++- src/is-holiday/is-holiday.test.ts | 49 ++++- .../is-valid-bank-account.test.ts | 105 ++++++++- src/is-valid-boleto/is-valid-boleto.test.ts | 55 ++++- src/is-valid-caepf/is-valid-caepf.test.ts | 50 ++++- src/is-valid-cbo/is-valid-cbo.test.ts | 39 +++- src/is-valid-cei/is-valid-cei.test.ts | 60 +++++- src/is-valid-cep/is-valid-cep.test.ts | 34 ++- .../is-valid-certidao.test.ts | 73 ++++++- src/is-valid-cfop/is-valid-cfop.test.ts | 31 ++- src/is-valid-cnae/is-valid-cnae.test.ts | 39 +++- src/is-valid-cnh/is-valid-cnh.test.ts | 35 ++- src/is-valid-cno/is-valid-cno.test.ts | 46 +++- src/is-valid-cnpj/is-valid-cnpj.test.ts | 73 ++++++- src/is-valid-cns/is-valid-cns.test.ts | 74 ++++++- src/is-valid-cpf/is-valid-cpf.test.ts | 52 ++++- .../is-valid-credit-card.test.ts | 57 ++++- src/is-valid-csosn/is-valid-csosn.test.ts | 30 ++- src/is-valid-cst/is-valid-cst.test.ts | 60 +++++- src/is-valid-email/is-valid-email.test.ts | 51 ++++- src/is-valid-iban/is-valid-iban.test.ts | 62 +++++- src/is-valid-ie/is-valid-ie.test.ts | 50 ++++- .../is-valid-landline-phone.test.ts | 43 +++- .../is-valid-legal-nature.test.ts | 36 +++- .../is-valid-license-plate.test.ts | 53 ++++- .../is-valid-mobile-phone.test.ts | 66 +++++- src/is-valid-ncm/is-valid-ncm.test.ts | 44 +++- src/is-valid-nfe-key/is-valid-nfe-key.test.ts | 58 ++++- .../is-valid-passport.test.ts | 37 +++- src/is-valid-phone/is-valid-phone.test.ts | 72 ++++++- src/is-valid-pis/is-valid-pis.test.ts | 38 +++- src/is-valid-pix-key/is-valid-pix-key.test.ts | 63 +++++- .../is-valid-pix-payload.test.ts | 54 ++++- .../is-valid-processo-juridico.test.ts | 38 +++- .../is-valid-registro-profissional.test.ts | 86 +++++++- src/is-valid-renavam/is-valid-renavam.test.ts | 54 ++++- .../is-valid-service-phone.test.ts | 50 ++++- src/is-valid-vin/is-valid-vin.test.ts | 65 +++++- .../is-valid-voter-id.test.ts | 34 ++- src/parse-boleto/parse-boleto.test.ts | 73 ++++++- src/parse-cep/parse-cep.test.ts | 29 ++- src/parse-certidao/parse-certidao.test.ts | 92 +++++++- src/parse-cnh/parse-cnh.test.ts | 29 ++- src/parse-cnpj/parse-cnpj.test.ts | 47 +++- src/parse-cpf/parse-cpf.test.ts | 29 ++- src/parse-currency/parse-currency.test.ts | 33 ++- src/parse-iban/parse-iban.test.ts | 67 +++++- .../parse-legal-nature.test.ts | 29 ++- .../parse-license-plate.test.ts | 39 +++- src/parse-nfe-key/parse-nfe-key.test.ts | 80 ++++++- src/parse-passport/parse-passport.test.ts | 34 ++- src/parse-phone/parse-phone.test.ts | 56 ++++- src/parse-pis/parse-pis.test.ts | 29 ++- src/parse-pix-key/parse-pix-key.test.ts | 87 +++++++- .../parse-pix-payload.test.ts | 68 +++++- .../parse-processo-juridico.test.ts | 29 ++- src/parse-voter-id/parse-voter-id.test.ts | 35 ++- src/remove-accents/remove-accents.test.ts | 57 ++++- vite.config.ts | 6 +- 138 files changed, 6414 insertions(+), 185 deletions(-) create mode 100644 src/_internals/test/arbitraries.ts create mode 100644 src/_internals/test/noop.ts create mode 100644 src/_internals/test/properties.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d1427a1d..67a5b57b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,6 +64,9 @@ jobs: with: persist-credentials: false + - name: Setup + uses: ./.github/actions/setup + - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -81,13 +84,16 @@ jobs: with: persist-credentials: false + - name: Setup + uses: ./.github/actions/setup + - name: Setup Deno uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 with: deno-version: v2.x - name: Run tests - run: deno test --unstable-sloppy-imports --allow-net src + run: deno test --unstable-sloppy-imports --allow-net --allow-env src test-browsers: name: Test on Browsers (${{ matrix.browser }}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fae7e44..68b506b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,16 @@ example `formatSomething`): or `src/is-valid-service-phone/is-valid-service-phone.ts` for examples). 3. Add tests alongside it in `src/format-something/format-something.test.ts`. Cover valid input, invalid/edge-case input, and options, if any. Tests must pass on Node, Bun and Deno (see - `npm test:bun` / `npm test:deno` under Useful scripts). + `npm test:bun` / `npm test:deno` under Useful scripts). Expectations are hand-written literals, + never values computed by the code under test. Close the file with a `describe("properties")` + block of [fast-check](https://fast-check.dev) properties that hold by specification (a + generated value is valid, format/parse round-trip, masks never change the verdict, arbitrary + input never throws) and a `describe(" types")` block that pins the public signature with + `expectTypeOf` (parameters, options and return type; `vp check` fails on a wrong assertion). A + hot path may also get a `describe(" benchmarks")` block of `bench` cases: they are todo + entries in a normal run and execute with `npx vp test bench --run`. `describe`, `test`, + `expect`, `expectTypeOf` and `bench` all come from `src/_internals/test/runtime`, which maps + them to vitest, Bun or Deno. 4. Export the new function (and any exported types) from `src/index.ts`, keeping the existing alphabetical ordering. Then add the function name to the `PUBLIC` list and the type(s) to the `publicTypes` map in `src/index.test.ts`, alphabetically. These two make up the package's diff --git a/package-lock.json b/package-lock.json index 8ae1a2d5..2ab2069a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-v8": "4.1.11", "esbuild": "0.27.2", + "fast-check": "4.10.0", "jscpd": "5.2.0", "knip": "6.35.1", "lockfile-lint": "5.0.1", @@ -7720,6 +7721,29 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-check": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.0.tgz", + "integrity": "sha512-hhqQL+IJllZi3aM4TKvmCj3bywLEcycNTTLZeLhA9ttMxBrCqM07q7Di4kl+j9EWSTXvJH1+EpIgsDbF/+8H5Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, "node_modules/fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", @@ -10301,6 +10325,23 @@ "once": "^1.3.1" } }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", diff --git a/package.json b/package.json index b6f7bc7e..716876d3 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-v8": "4.1.11", "esbuild": "0.27.2", + "fast-check": "4.10.0", "jscpd": "5.2.0", "knip": "6.35.1", "lockfile-lint": "5.0.1", diff --git a/src/_internals/test/arbitraries.ts b/src/_internals/test/arbitraries.ts new file mode 100644 index 00000000..4413d466 --- /dev/null +++ b/src/_internals/test/arbitraries.ts @@ -0,0 +1,132 @@ +import * as fc from "fast-check"; + +import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../constants/holidays"; +import { DATA as STATES, type StateCode } from "../constants/states"; + +/** + * Spreads `separators` around every character of `value`: one before the first character, one + * between each pair and one after the last, so `separators` must hold `value.length + 1` entries. + * @param {string} value The characters to spread the separators around. + * @param {string[]} separators The separators to place around each character. + * @returns {string} The interleaved value. + */ +const interleave = (value: string, separators: string[]): string => { + let result = separators[0]; + + for (let index = 0; index < value.length; index++) { + result += `${value[index]}${separators[index + 1]}`; + } + + return result; +}; + +const anyPrimitive: fc.Arbitrary = fc.oneof(fc.string(), fc.integer(), fc.boolean()); + +/** Any grapheme string: the widest text a util is expected to survive. */ +export const anyText: fc.Arbitrary = fc.string({ unit: "grapheme" }); + +/** Text, numbers, booleans, `undefined`, `null` and arrays: what a caller may pass by mistake. */ +export const anyValue: fc.Arbitrary = fc.oneof( + anyText, + fc.double(), + fc.boolean(), + fc.constantFrom(undefined, null), + fc.array(fc.string()), +); + +/** ASCII alphanumeric text, at most twelve characters long. */ +export const asciiAlphanumericText: fc.Arbitrary = fc.stringMatching(/^[0-9A-Za-z]{0,12}$/); + +/** Booleans, `null`, numbers, strings, arrays and plain objects, including nested primitives. */ +export const anyGarbage: fc.Arbitrary = fc.oneof( + fc.boolean(), + fc.constant(null), + fc.double(), + fc.string(), + fc.array(anyPrimitive), + fc.object({ key: fc.constantFrom("a", "b", "c") }), +); + +/** + * @param {number} length How many digits the generated value holds. + * @returns {fc.Arbitrary} Strings of exactly `length` digits. + */ +export const digits = (length: number): fc.Arbitrary => + fc.stringMatching(new RegExp(`^[0-9]{${length}}$`)); + +/** + * @param {number} maxLength The largest number of digits the generated value holds. + * @returns {fc.Arbitrary} Strings of zero up to `maxLength` digits. + */ +export const digitsUpTo = (maxLength: number): fc.Arbitrary => + fc.stringMatching(new RegExp(`^[0-9]{0,${maxLength}}$`)); + +/** + * @param {number} maxLength The largest number of digits the generated value holds. + * @param {number[]} lengths The lengths to leave out. + * @returns {fc.Arbitrary} Digit strings whose length is none of `lengths`. + */ +export const digitsOfOtherLength = (maxLength: number, lengths: number[]): fc.Arbitrary => + digitsUpTo(maxLength).filter((value) => !lengths.includes(value.length)); + +/** + * @param {string[]} maskChars The characters a separator is built from. + * @param {number} count How many separators the generated array holds. + * @param {number} maxLength The largest length of a single separator. + * @returns {fc.Arbitrary} Arrays of exactly `count` separators. + */ +export const maskSeparators = ( + maskChars: string[], + count: number, + maxLength: number, +): fc.Arbitrary => + fc.array(fc.string({ unit: fc.constantFrom(...maskChars), maxLength }), { + minLength: count, + maxLength: count, + }); + +/** + * @param {fc.Arbitrary} source The values to spread the mask over. + * @param {string[]} maskChars The characters a separator is built from. + * @param {number} maxLength The largest length of a single separator. + * @returns {fc.Arbitrary} Values of `source` with a separator around every character. + */ +export const maskedValues = ( + source: fc.Arbitrary, + maskChars: string[], + maxLength: number, +): fc.Arbitrary => + source.chain((value) => + maskSeparators(maskChars, value.length + 1, maxLength).map((separators) => + interleave(value, separators), + ), + ); + +/** The two letter code of every Brazilian state. */ +export const stateCodes: fc.Arbitrary = fc.constantFrom( + ...STATES.map((state) => state.code), +); + +/** A year covered by the bundled holiday tables. */ +export const holidayYears: fc.Arbitrary = fc.integer({ + min: HOLIDAYS_MIN_YEAR, + max: HOLIDAYS_MAX_YEAR, +}); + +/** A zero based month index, as `Date` numbers them. */ +export const monthIndexes: fc.Arbitrary = fc.integer({ min: 0, max: 11 }); + +/** A day of the month that exists in every month, February included. */ +export const monthDays: fc.Arbitrary = fc.integer({ min: 1, max: 28 }); + +/** A valid date inside the range the business day utils are exercised over. */ +export const businessDayDates: fc.Arbitrary = fc.date({ + min: new Date(1950, 0, 1), + max: new Date(2050, 11, 31), + noInvalidDate: true, +}); + +/** An amount with at most two decimals, the precision currency formatting round-trips. */ +export const twoDecimalAmounts: fc.Arbitrary = fc + .integer({ min: -1_000_000_000, max: 1_000_000_000 }) + .map((cents) => cents / 100); diff --git a/src/_internals/test/globals.d.ts b/src/_internals/test/globals.d.ts index acb9a676..5107e30e 100644 --- a/src/_internals/test/globals.d.ts +++ b/src/_internals/test/globals.d.ts @@ -12,6 +12,12 @@ declare const Deno: { }) => void; }; +interface ImportMeta { + readonly env: { + readonly MODE: string; + }; +} + declare global { var RUN_LIVE_CEP_TESTS: string | number | undefined; } diff --git a/src/_internals/test/noop.ts b/src/_internals/test/noop.ts new file mode 100644 index 00000000..fd6a2786 --- /dev/null +++ b/src/_internals/test/noop.ts @@ -0,0 +1,18 @@ +import { type bench as vitestBench, type expectTypeOf as vitestExpectTypeOf } from "vite-plus/test"; + +const chain: unknown = new Proxy(() => chain, { apply: () => chain, get: () => chain }); + +/** + * Runtime stand-in for vitest's `expectTypeOf` on Bun and Deno: every call and property access + * returns the same chainable no-op, so a `describe(" types")` block runs without effect + * there. The assertions themselves are checked statically by `vp check` and by + * `npm run test:types`. + */ +export const expectTypeOf = chain as typeof vitestExpectTypeOf; + +/** + * Runtime stand-in for vitest's `bench` outside benchmark mode (vitest in test mode, Bun, Deno): + * a `describe(" benchmarks")` block registers nothing there. `npm run bench` runs vitest in + * benchmark mode, where the real `bench` is used and the `test` blocks are ignored instead. + */ +export const bench = chain as typeof vitestBench; diff --git a/src/_internals/test/properties.ts b/src/_internals/test/properties.ts new file mode 100644 index 00000000..1cd6b9b6 --- /dev/null +++ b/src/_internals/test/properties.ts @@ -0,0 +1,202 @@ +import * as fc from "fast-check"; + +import { expect } from "./runtime"; + +type UnknownInputFunction = (value: never) => unknown; + +/** + * Asserts the util never throws, whatever the arbitrary produces. + * @param {UnknownInputFunction} fn The util under test. + * @param {fc.Arbitrary} arbitrary The values to feed it. + * @returns {void} Nothing. + */ +export const expectNeverThrows = ( + fn: UnknownInputFunction, + arbitrary: fc.Arbitrary, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + expect(() => fn(value as never)).not.toThrow(); + }), + ); +}; + +/** + * Asserts the util never throws for any pair of value and options the arbitraries produce. + * @param {Function} fn The util under test. + * @param {fc.Arbitrary} values The values to feed it. + * @param {fc.Arbitrary} options The options to feed it. + * @returns {void} Nothing. + */ +export const expectNeverThrowsWithOptions = ( + fn: (value: never, options: never) => unknown, + values: fc.Arbitrary, + options: fc.Arbitrary, +): void => { + fc.assert( + fc.property(values, options, (value, currentOptions) => { + expect(() => fn(value as never, currentOptions as never)).not.toThrow(); + }), + ); +}; + +/** + * Asserts the util always returns a value of `expectedType`, whatever the arbitrary produces. + * @param {UnknownInputFunction} fn The util under test. + * @param {string} expectedType The `typeof` the util is expected to return. + * @param {fc.Arbitrary} arbitrary The values to feed it. + * @returns {void} Nothing. + */ +export const expectAlwaysReturnsType = ( + fn: UnknownInputFunction, + expectedType: "boolean" | "number" | "string", + arbitrary: fc.Arbitrary, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + expect(typeof fn(value as never)).toBe(expectedType); + }), + ); +}; + +/** + * Asserts the validator answers `verdict` for every value the arbitrary produces. + * @param {Function} fn The validator under test. + * @param {fc.Arbitrary} arbitrary The values to feed it. + * @param {boolean} verdict The answer it must give. + * @returns {void} Nothing. + */ +const expectVerdict = ( + fn: (value: string) => boolean, + arbitrary: fc.Arbitrary, + verdict: boolean, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + expect(fn(value)).toBe(verdict); + }), + ); +}; + +/** + * Asserts the validator accepts every value the arbitrary produces. + * @param {Function} fn The validator under test. + * @param {fc.Arbitrary} arbitrary The values it must accept. + * @returns {void} Nothing. + */ +export const expectAccepted = ( + fn: (value: string) => boolean, + arbitrary: fc.Arbitrary, +): void => { + expectVerdict(fn, arbitrary, true); +}; + +/** + * Asserts the validator rejects every value the arbitrary produces. + * @param {Function} fn The validator under test. + * @param {fc.Arbitrary} arbitrary The values it must reject. + * @returns {void} Nothing. + */ +export const expectRejected = ( + fn: (value: string) => boolean, + arbitrary: fc.Arbitrary, +): void => { + expectVerdict(fn, arbitrary, false); +}; + +/** + * Asserts the util's output always matches `pattern`. + * @param {Function} fn The util under test. + * @param {RegExp} pattern The shape the output must have. + * @param {fc.Arbitrary} arbitrary The values to feed it. + * @returns {void} Nothing. + */ +export const expectMatchesPattern = ( + fn: (value: string) => string, + pattern: RegExp, + arbitrary: fc.Arbitrary, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + expect(fn(value)).toMatch(pattern); + }), + ); +}; + +/** + * Asserts applying the util twice gives the same result as applying it once. + * @param {Function} fn The util under test. + * @param {fc.Arbitrary} arbitrary The values to feed it. + * @returns {void} Nothing. + */ +export const expectIdempotent = ( + fn: (value: string) => string, + arbitrary: fc.Arbitrary, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + const once = fn(value); + + expect(fn(once)).toBe(once); + }), + ); +}; + +/** + * Asserts the util treats an uppercased value exactly like the value it was given. + * @param {Function} fn The util under test. + * @param {fc.Arbitrary} arbitrary The values to feed it. + * @returns {void} Nothing. + */ +export const expectCaseInsensitive = ( + fn: (value: string) => string, + arbitrary: fc.Arbitrary, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + expect(fn(value.toUpperCase())).toBe(fn(value)); + }), + ); +}; + +/** + * Asserts `parse` undoes `format` for every value the arbitrary produces. + * @param {Function} format The formatter under test. + * @param {Function} parse The parser that must undo it. + * @param {fc.Arbitrary} arbitrary The values to feed them. + * @returns {void} Nothing. + */ +export const expectRoundTrip = ( + format: (value: T) => string, + parse: (value: string) => T, + arbitrary: fc.Arbitrary, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + expect(parse(format(value))).toBe(value); + }), + ); +}; + +/** + * Asserts the formatter left pads a shorter value with zeros up to `length` when asked to. + * @param {Function} format The formatter under test. + * @param {Function} parse The parser that strips the mask back off. + * @param {fc.Arbitrary} arbitrary The values to feed them. + * @param {number} length The length the padded value must reach. + * @returns {void} Nothing. + */ +export const expectPadsToLength = ( + format: (value: string, options: { pad: boolean }) => string, + parse: (value: string) => string, + arbitrary: fc.Arbitrary, + length: number, +): void => { + fc.assert( + fc.property(arbitrary, (value) => { + const padded = format(value, { pad: true }); + + expect(parse(padded)).toBe(value.padStart(length, "0")); + }), + ); +}; diff --git a/src/_internals/test/runtime-bun.ts b/src/_internals/test/runtime-bun.ts index c1da6d3f..16453705 100644 --- a/src/_internals/test/runtime-bun.ts +++ b/src/_internals/test/runtime-bun.ts @@ -1,6 +1,7 @@ import { jest } from "bun:test"; export { afterEach, beforeEach, describe, expect, it, test } from "bun:test"; +export { bench, expectTypeOf } from "./noop"; export const vi = { fn: jest.fn, diff --git a/src/_internals/test/runtime-deno.ts b/src/_internals/test/runtime-deno.ts index 5d30a779..db72f70d 100644 --- a/src/_internals/test/runtime-deno.ts +++ b/src/_internals/test/runtime-deno.ts @@ -565,3 +565,4 @@ export const vi = { }; export { describe }; +export { bench, expectTypeOf } from "./noop"; diff --git a/src/_internals/test/runtime-vitest.ts b/src/_internals/test/runtime-vitest.ts index a4b05c06..0d679388 100644 --- a/src/_internals/test/runtime-vitest.ts +++ b/src/_internals/test/runtime-vitest.ts @@ -1 +1,27 @@ -export { afterEach, beforeEach, describe, expect, it, test, vi } from "vite-plus/test"; +import { bench as vitestBench, test } from "vite-plus/test"; + +import { bench as noopBench } from "./noop"; + +export { + afterEach, + beforeEach, + describe, + expect, + expectTypeOf, + it, + test, + vi, +} from "vite-plus/test"; + +const isBenchmarkMode = (): boolean => import.meta.env.MODE === "benchmark"; + +/** + * In benchmark mode (`npm run bench`) this is vitest's own `bench`. In test mode every benchmark + * is registered as a todo test instead, so a `describe(" benchmarks")` block is never an + * empty suite (which vitest reports as a failure) and shows up in the run as todo. + */ +export const bench: typeof vitestBench = isBenchmarkMode() + ? vitestBench + : Object.assign((name: string): void => { + test.todo(name); + }, noopBench); diff --git a/src/_internals/test/runtime.ts b/src/_internals/test/runtime.ts index cb8960c3..85a88f94 100644 --- a/src/_internals/test/runtime.ts +++ b/src/_internals/test/runtime.ts @@ -1,10 +1,14 @@ +import { type bench as vitestBench, type expectTypeOf as vitestExpectTypeOf } from "vite-plus/test"; + type RuntimeModule = { afterEach: (callback: () => void | Promise) => void; + bench: typeof vitestBench; beforeEach: (callback: () => void | Promise) => void; describe: ((name: string, callback: () => void) => void) & { skip: (name: string, callback: () => void) => void; }; expect: (actual: unknown) => any; + expectTypeOf: typeof vitestExpectTypeOf; it: (name: string, callback: () => void | Promise, timeout?: number) => void; test: (name: string, callback: () => void | Promise, timeout?: number) => void; vi: { fn: (...args: any[]) => any; restoreAllMocks: () => void }; @@ -17,4 +21,5 @@ const runtimeModule: RuntimeModule = ? await import("./runtime-deno") : await import("./runtime-vitest"); -export const { afterEach, beforeEach, describe, expect, it, test, vi } = runtimeModule; +export const { afterEach, bench, beforeEach, describe, expect, expectTypeOf, it, test, vi } = + runtimeModule; diff --git a/src/add-business-days/add-business-days.test.ts b/src/add-business-days/add-business-days.test.ts index d2f3555e..9bcbc3ae 100644 --- a/src/add-business-days/add-business-days.test.ts +++ b/src/add-business-days/add-business-days.test.ts @@ -1,5 +1,11 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { addBusinessDays } from "./add-business-days"; +import * as fc from "fast-check"; + +import { type StateCode } from "../_internals/constants/states"; +import { businessDayDates } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { isBusinessDay } from "../is-business-day/is-business-day"; +import { addBusinessDays, type AddBusinessDaysParams } from "./add-business-days"; describe("addBusinessDays", () => { it("should match the date-fns addBusinessDays example (10 business days from 2014-09-01 lands on 2014-09-15, https://date-fns.org/docs/addBusinessDays)", () => { @@ -205,4 +211,57 @@ describe("addBusinessDays", () => { expect(result?.getSeconds()).toBe(15); expect(result?.getMilliseconds()).toBe(500); }); + + describe("properties", () => { + const daysArbitrary = fc.integer({ min: -200, max: 200 }); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(addBusinessDays, fc.anything()); + }); + + test("should land on a business day whenever a non-zero number of days is requested", () => { + fc.assert( + fc.property(businessDayDates, daysArbitrary, (date, days) => { + if (days === 0) return; + + const result = addBusinessDays({ date, days }); + + if (result !== null) { + expect(isBusinessDay(result)).toBe(true); + } + }), + ); + }); + + test("should move the date forward for positive days and backward for negative days", () => { + fc.assert( + fc.property(businessDayDates, daysArbitrary, (date, days) => { + const result = addBusinessDays({ date, days }); + + if (result === null) return; + + if (days > 0) { + expect(result.getTime()).toBeGreaterThan(date.getTime()); + } else if (days < 0) { + expect(result.getTime()).toBeLessThan(date.getTime()); + } else { + expect(result.getTime()).toBe(date.getTime()); + } + }), + ); + }); + }); +}); + +describe("addBusinessDays types", () => { + test("should take an AddBusinessDaysParams and return a Date or null", () => { + expectTypeOf(addBusinessDays).parameter(0).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + date: Date; + days: number; + stateCode?: StateCode; + includeOptional?: boolean; + }>(); + expectTypeOf(addBusinessDays).returns.toEqualTypeOf(); + }); }); diff --git a/src/capitalize/capitalize.test.ts b/src/capitalize/capitalize.test.ts index 964dedc5..8aa6fc9d 100644 --- a/src/capitalize/capitalize.test.ts +++ b/src/capitalize/capitalize.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { capitalize } from "./capitalize"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { capitalize, type CapitalizeOptions } from "./capitalize"; describe("capitalize", () => { describe("should capitalize", () => { @@ -80,4 +82,42 @@ describe("capitalize", () => { // @ts-expect-error: intentionally invalid input expect(capitalize(123)).toBe(""); }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(() => capitalize(value as never)).not.toThrow(); + }), + ); + }); + + test("should be idempotent on its own output", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const once = capitalize(value); + + expect(capitalize(once)).toBe(once); + }), + ); + }); + + test("should never produce leading, trailing or doubled whitespace", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + expect(capitalize(value)).not.toMatch(/^\s|\s$|\s{2}/); + }), + ); + }); + }); +}); + +describe("capitalize types", () => { + test("should take a string and options and return a string", () => { + expectTypeOf(capitalize).parameter(0).toEqualTypeOf(); + expectTypeOf(capitalize).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(capitalize).returns.toEqualTypeOf(); + }); }); diff --git a/src/convert-currency-to-words/convert-currency-to-words.test.ts b/src/convert-currency-to-words/convert-currency-to-words.test.ts index 07f7fa8d..ab493294 100644 --- a/src/convert-currency-to-words/convert-currency-to-words.test.ts +++ b/src/convert-currency-to-words/convert-currency-to-words.test.ts @@ -1,6 +1,14 @@ -import { NUMBER_TO_WORDS_MAX_VALUE } from "../_internals/number-to-words/number-to-words"; -import { describe, expect, test } from "../_internals/test/runtime"; -import { convertCurrencyToWords } from "./convert-currency-to-words"; +import * as fc from "fast-check"; + +import { + NUMBER_TO_WORDS_MAX_VALUE, + type WordsCase, +} from "../_internals/number-to-words/number-to-words"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { + convertCurrencyToWords, + type ConvertCurrencyToWordsOptions, +} from "./convert-currency-to-words"; function expectAmounts(cases: readonly (readonly [number, string])[]): void { const failures = cases @@ -386,4 +394,48 @@ describe("convertCurrencyToWords", () => { expectAmounts(cases); }); }); + + describe("properties", () => { + const safeCentsArbitrary = fc.integer({ min: -100_000_000_000, max: 100_000_000_000 }); + + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.double(), (value) => { + expect(() => convertCurrencyToWords(value)).not.toThrow(); + }), + ); + }); + + test("should never return an empty string for a finite amount within the supported range", () => { + fc.assert( + fc.property(safeCentsArbitrary, (cents) => { + expect(convertCurrencyToWords(cents / 100)).not.toBe(""); + }), + ); + }); + + test("should uppercase the result the same way as the lower case result, for the 'upper' case option", () => { + fc.assert( + fc.property(safeCentsArbitrary, (cents) => { + const value = cents / 100; + const lower = convertCurrencyToWords(value); + + expect(convertCurrencyToWords(value, { case: "upper" })).toBe( + lower.toLocaleUpperCase("pt-BR"), + ); + }), + ); + }); + }); +}); + +describe("convertCurrencyToWords types", () => { + test("should take a number, options, and return a string", () => { + expectTypeOf(convertCurrencyToWords).parameter(0).toEqualTypeOf(); + expectTypeOf(convertCurrencyToWords) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(convertCurrencyToWords).returns.toEqualTypeOf(); + }); }); diff --git a/src/convert-date-to-words/convert-date-to-words.test.ts b/src/convert-date-to-words/convert-date-to-words.test.ts index bcb265b2..63d6e816 100644 --- a/src/convert-date-to-words/convert-date-to-words.test.ts +++ b/src/convert-date-to-words/convert-date-to-words.test.ts @@ -1,5 +1,8 @@ +import * as fc from "fast-check"; + import { MONTH_NAMES, WEEKDAY_NAMES } from "../_internals/constants/number-words"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { type WordsCase } from "../_internals/number-to-words/number-to-words"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { convertDateToWords, type ConvertDateToWordsOptions } from "./convert-date-to-words"; function expectDates( @@ -13,6 +16,13 @@ function expectDates( expect(mismatches).toEqual([]); } +function toBrDateString(date: Date): string { + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + + return `${day}/${month}/${date.getFullYear()}`; +} + describe("convertDateToWords", () => { test("should return 'primeiro' for day 1", () => { expect(convertDateToWords("01/01/2024")).toBe( @@ -414,4 +424,54 @@ describe("convertDateToWords", () => { expectDates(cases); }); }); + + describe("properties", () => { + const dateArbitrary = fc.date({ + min: new Date(2000, 0, 1), + max: new Date(2100, 11, 31), + noInvalidDate: true, + }); + + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(() => convertDateToWords(value as never)).not.toThrow(); + }), + ); + }); + + test("should give the same result for a Date and its equivalent 'dd/mm/yyyy' string", () => { + fc.assert( + fc.property(dateArbitrary, (date) => { + expect(convertDateToWords(date)).toBe(convertDateToWords(toBrDateString(date))); + }), + ); + }); + + test("should prefix the pt-BR weekday name when 'weekday' is true", () => { + fc.assert( + fc.property(dateArbitrary, (date) => { + const plain = convertDateToWords(date); + const withWeekday = convertDateToWords(date, { weekday: true }); + + expect(withWeekday).toBe(`${WEEKDAY_NAMES[date.getDay()]}, ${plain}`); + }), + ); + }); + }); +}); + +describe("convertDateToWords types", () => { + test("should take a Date or string, options, and return a string", () => { + expectTypeOf(convertDateToWords).parameter(0).toEqualTypeOf(); + expectTypeOf(convertDateToWords) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + "full" | "month" | undefined + >(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(convertDateToWords).returns.toEqualTypeOf(); + }); }); diff --git a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts index 948f116d..f2dd52ba 100644 --- a/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts +++ b/src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generateLicensePlate } from "../generate-license-plate/generate-license-plate"; +import { getFormatLicensePlate } from "../get-format-license-plate/get-format-license-plate"; +import { DIGIT_TO_MERCOSUL_LETTER } from "./constants"; import { convertLicensePlateToMercosul } from "./convert-license-plate-to-mercosul"; describe("convertLicensePlateToMercosul", () => { @@ -61,4 +66,45 @@ describe("convertLicensePlateToMercosul", () => { expect(convertLicensePlateToMercosul("ABC-1234")).toBe("ABC1C34"); }); }); + + describe("properties", () => { + test("should turn every old format plate into a Mercosul one", () => { + fc.assert( + fc.property(fc.constant("LLLNNNN" as const), (format) => { + const plate = generateLicensePlate(format); + const converted = convertLicensePlateToMercosul(plate); + + expect(getFormatLicensePlate(converted)).toBe("LLLNLNN"); + expect(converted.slice(0, 4)).toBe(plate.slice(0, 4)); + expect(converted.charAt(4)).toBe(DIGIT_TO_MERCOSUL_LETTER[plate.charAt(4)]); + expect(converted.slice(5)).toBe(plate.slice(5)); + }), + ); + }); + + test("should convert a plate only once", () => { + fc.assert( + fc.property(fc.constantFrom("LLLNNNN", "LLLNLNN"), (format) => { + const converted = convertLicensePlateToMercosul(generateLicensePlate(format)); + + expect(convertLicensePlateToMercosul(converted)).toBe(""); + }), + ); + }); + + test("should never throw and always return the converted plate as a string", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof convertLicensePlateToMercosul(value as string)).toBe("string"); + }), + ); + }); + }); +}); + +describe("convertLicensePlateToMercosul types", () => { + test("should take a string and return a string", () => { + expectTypeOf(convertLicensePlateToMercosul).parameter(0).toEqualTypeOf(); + expectTypeOf(convertLicensePlateToMercosul).returns.toEqualTypeOf(); + }); }); diff --git a/src/convert-number-to-words/convert-number-to-words.test.ts b/src/convert-number-to-words/convert-number-to-words.test.ts index 0be44ed6..33c1dc7a 100644 --- a/src/convert-number-to-words/convert-number-to-words.test.ts +++ b/src/convert-number-to-words/convert-number-to-words.test.ts @@ -1,5 +1,11 @@ -import { NUMBER_TO_WORDS_MAX_VALUE } from "../_internals/number-to-words/number-to-words"; -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { + NUMBER_TO_WORDS_MAX_VALUE, + type NumberToWordsGender, + type WordsCase, +} from "../_internals/number-to-words/number-to-words"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { convertNumberToWords, type ConvertNumberToWordsOptions } from "./convert-number-to-words"; function expectWords( @@ -557,4 +563,66 @@ describe("convertNumberToWords", () => { expectWords(cases, { gender: "feminine" }); }); }); + + describe("properties", () => { + const inRangeIntegerArbitrary = fc.integer({ + min: -NUMBER_TO_WORDS_MAX_VALUE, + max: NUMBER_TO_WORDS_MAX_VALUE, + }); + + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.double(), (value) => { + expect(() => convertNumberToWords(value)).not.toThrow(); + }), + ); + }); + + test("should return a non-empty string for every integer within the supported range", () => { + fc.assert( + fc.property(inRangeIntegerArbitrary, (value) => { + expect(convertNumberToWords(value)).not.toBe(""); + }), + ); + }); + + test("should return an empty string outside the supported range", () => { + const outOfRangeArbitrary = fc.oneof( + fc.integer({ min: NUMBER_TO_WORDS_MAX_VALUE + 1, max: Number.MAX_SAFE_INTEGER }), + fc.integer({ min: -Number.MAX_SAFE_INTEGER, max: -NUMBER_TO_WORDS_MAX_VALUE - 1 }), + ); + + fc.assert( + fc.property(outOfRangeArbitrary, (value) => { + expect(convertNumberToWords(value)).toBe(""); + }), + ); + }); + + test("should uppercase the result the same way as the lower case result, for the 'upper' case option", () => { + fc.assert( + fc.property(inRangeIntegerArbitrary, (value) => { + const lower = convertNumberToWords(value); + + expect(convertNumberToWords(value, { case: "upper" })).toBe( + lower.toLocaleUpperCase("pt-BR"), + ); + }), + ); + }); + }); +}); + +describe("convertNumberToWords types", () => { + test("should take a number, options, and return a string", () => { + expectTypeOf(convertNumberToWords).parameter(0).toEqualTypeOf(); + expectTypeOf(convertNumberToWords) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + NumberToWordsGender | undefined + >(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(convertNumberToWords).returns.toEqualTypeOf(); + }); }); diff --git a/src/difference-in-business-days/difference-in-business-days.test.ts b/src/difference-in-business-days/difference-in-business-days.test.ts index bc19e5e7..3f3b92da 100644 --- a/src/difference-in-business-days/difference-in-business-days.test.ts +++ b/src/difference-in-business-days/difference-in-business-days.test.ts @@ -1,5 +1,15 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { differenceInBusinessDays } from "./difference-in-business-days"; +import * as fc from "fast-check"; + +import { type StateCode } from "../_internals/constants/states"; +import { businessDayDates } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { addBusinessDays } from "../add-business-days/add-business-days"; +import { isBusinessDay } from "../is-business-day/is-business-day"; +import { + differenceInBusinessDays, + type DifferenceInBusinessDaysParams, +} from "./difference-in-business-days"; describe("differenceInBusinessDays", () => { it("should return 0 for the same calendar day", () => { @@ -217,4 +227,49 @@ describe("differenceInBusinessDays", () => { expect(result).toBe(1); }); }); + + describe("properties", () => { + const daysArbitrary = fc.integer({ min: -100, max: 100 }); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(differenceInBusinessDays, fc.anything()); + }); + + test("should return 0 for the same calendar day", () => { + fc.assert( + fc.property(businessDayDates, (date) => { + expect(differenceInBusinessDays({ from: date, to: date })).toBe(0); + }), + ); + }); + + test("should undo addBusinessDays when starting from a business day", () => { + fc.assert( + fc.property(businessDayDates, daysArbitrary, (from, days) => { + if (!isBusinessDay(from)) return; + + const to = addBusinessDays({ date: from, days }); + + if (to === null) return; + + expect(differenceInBusinessDays({ from, to })).toBe(days); + }), + ); + }); + }); +}); + +describe("differenceInBusinessDays types", () => { + test("should take a DifferenceInBusinessDaysParams and return a number or null", () => { + expectTypeOf(differenceInBusinessDays) + .parameter(0) + .toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + from: Date; + to: Date; + stateCode?: StateCode; + includeOptional?: boolean; + }>(); + expectTypeOf(differenceInBusinessDays).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-boleto/format-boleto.test.ts b/src/format-boleto/format-boleto.test.ts index c0e4c902..68925f64 100644 --- a/src/format-boleto/format-boleto.test.ts +++ b/src/format-boleto/format-boleto.test.ts @@ -1,7 +1,10 @@ +import * as fc from "fast-check"; + import { ARRECADACAO_LINE_LENGTH } from "../_internals/constants/arrecadacao"; import { BOLETO_LENGTH } from "../_internals/constants/boleto"; -import { describe, expect, test } from "../_internals/test/runtime"; -import { formatBoleto } from "./format-boleto"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generateBoleto } from "../generate-boleto/generate-boleto"; +import { type FormatBoletoOptions, formatBoleto } from "./format-boleto"; describe("formatBoleto", () => { test("should format boleto with mask", () => { @@ -160,4 +163,64 @@ describe("formatBoleto", () => { ); }); }); + + describe("properties", () => { + const bancarioMask = /^\d{5}\.\d{5} \d{5}\.\d{6} \d{5}\.\d{6} \d \d{14}$/; + + const arrecadacaoMask = /^\d{11}-\d \d{11}-\d \d{11}-\d \d{11}-\d$/; + + test("should print a generated bank slip in the mask of its kind", () => { + fc.assert( + fc.property(fc.constantFrom("bancario", "arrecadacao"), (type) => { + const value = generateBoleto({ type }); + const mask = type === "arrecadacao" ? arrecadacaoMask : bancarioMask; + + expect(mask.test(formatBoleto(value))).toBe(true); + }), + ); + }); + + test("should never emit a digit it was not given", () => { + fc.assert( + fc.property(fc.string(), (value) => { + const digits = value.replaceAll(/\D/g, ""); + const formatted = formatBoleto(value).replaceAll(/\D/g, ""); + + expect(digits.startsWith(formatted)).toBe(true); + expect(formatted.length).toBeLessThanOrEqual(ARRECADACAO_LINE_LENGTH); + }), + ); + }); + + test("should pad a short value up to the bank slip length", () => { + fc.assert( + fc.property(fc.integer({ min: 0, max: 999_999 }), (value) => { + const padded = formatBoleto(value, { pad: true }).replaceAll(/\D/g, ""); + + expect(padded.length).toBe(BOLETO_LENGTH); + }), + ); + }); + + test("should never throw and always return the bank slip as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof formatBoleto(text)).toBe("string"); + expect(typeof formatBoleto(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatBoleto types", () => { + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatBoleto).parameter(0).toEqualTypeOf(); + expectTypeOf(formatBoleto).parameter(1).toEqualTypeOf(); + expectTypeOf(formatBoleto).returns.toEqualTypeOf(); + }); + + test("should restrict pad to a boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-caepf/format-caepf.test.ts b/src/format-caepf/format-caepf.test.ts index fe440b72..b1eb6bb5 100644 --- a/src/format-caepf/format-caepf.test.ts +++ b/src/format-caepf/format-caepf.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { formatCaepf } from "./format-caepf"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { formatCaepf, type FormatCaepfOptions } from "./format-caepf"; describe("formatCaepf", () => { test("should format a full 14 digit value", () => { @@ -48,4 +50,42 @@ describe("formatCaepf", () => { // @ts-expect-error: intentionally invalid input expect(formatCaepf()).toBe(""); }); + + describe("properties", () => { + test("should print a full registration in the official mask", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{14}$/), (value) => { + expect(/^\d{3}\.\d{3}\.\d{3}\/\d{3}-\d{2}$/.test(formatCaepf(value))).toBe(true); + }), + ); + }); + + test("should keep only the digits of the registration it formats", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const digits = value.replaceAll(/\D/g, "").slice(0, 14); + + expect(formatCaepf(value).replaceAll(/\D/g, "")).toBe(digits); + }), + ); + }); + + test("should never throw and always return the CAEPF number as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof formatCaepf(text)).toBe("string"); + expect(typeof formatCaepf(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatCaepf types", () => { + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatCaepf).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCaepf).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(formatCaepf).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-cei/format-cei.test.ts b/src/format-cei/format-cei.test.ts index 87f2b8ef..c11157f9 100644 --- a/src/format-cei/format-cei.test.ts +++ b/src/format-cei/format-cei.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { formatCei } from "./format-cei"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { formatCei, type FormatCeiOptions } from "./format-cei"; describe("formatCei", () => { test("should format a full 12 digit value", () => { @@ -44,4 +46,42 @@ describe("formatCei", () => { // @ts-expect-error: intentionally invalid input expect(formatCei()).toBe(""); }); + + describe("properties", () => { + test("should print a full number in the official mask", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{12}$/), (value) => { + expect(/^\d{2}\.\d{3}\.\d{5}\/\d{2}$/.test(formatCei(value))).toBe(true); + }), + ); + }); + + test("should left pad a shorter value up to the CEI length", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,12}$/), (value) => { + const padded = formatCei(value, { pad: true }).replaceAll(/\D/g, ""); + + expect(padded).toBe(value.padStart(12, "0")); + }), + ); + }); + + test("should never throw and always return the CEI number as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof formatCei(text)).toBe("string"); + expect(typeof formatCei(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatCei types", () => { + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatCei).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCei).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(formatCei).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-cep/format-cep.test.ts b/src/format-cep/format-cep.test.ts index 331cec21..228c7266 100644 --- a/src/format-cep/format-cep.test.ts +++ b/src/format-cep/format-cep.test.ts @@ -1,6 +1,14 @@ import { CEP_LENGTH } from "../_internals/constants/cep"; -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatCep } from "./format-cep"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectMatchesPattern, + expectPadsToLength, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCep } from "../parse-cep/parse-cep"; +import { formatCep, type FormatCepOptions } from "./format-cep"; describe("formatCep", () => { it("should format CEP with mask", () => { @@ -29,4 +37,36 @@ describe("formatCep", () => { // @ts-expect-error: intentionally invalid input expect(formatCep()).toBe(""); }); + + describe("properties", () => { + const upToACep = digitsUpTo(8); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatCep, parseCep, upToACep); + }); + + test("should produce the documented mask shape for a full CEP", () => { + expectMatchesPattern(formatCep, /^\d{5}-\d{3}$/, digits(8)); + }); + + test("should left pad a shorter value up to the CEP length", () => { + expectPadsToLength(formatCep, parseCep, upToACep, CEP_LENGTH); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatCep, "string", anyValue); + }); + }); +}); + +describe("formatCep types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(formatCep).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCep).parameter(1).toEqualTypeOf(); + expectTypeOf(formatCep).returns.toEqualTypeOf(); + }); + + test("should type the pad option as an optional boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-certidao/format-certidao.test.ts b/src/format-certidao/format-certidao.test.ts index 4749b3ef..96b7f4e8 100644 --- a/src/format-certidao/format-certidao.test.ts +++ b/src/format-certidao/format-certidao.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { formatCertidao } from "./format-certidao"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { formatCertidao, type FormatCertidaoOptions } from "./format-certidao"; describe("formatCertidao", () => { describe("should return an empty string", () => { @@ -63,4 +65,54 @@ describe("formatCertidao", () => { expect(formatCertidao(104_539_015_520)).toBe("104539 01 55 20"); }); }); + + describe("properties", () => { + test("should print a full matrícula in the groups of the Provimento", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{32}$/), (value) => { + const mask = /^\d{6} \d{2} \d{2} \d{4} \d \d{5} \d{3} \d{7} \d{2}$/; + + expect(mask.test(formatCertidao(value))).toBe(true); + }), + ); + }); + + test("should keep only the digits of the matrícula it formats", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const digits = value.replaceAll(/\D/g, "").slice(0, 32); + + expect(formatCertidao(value).replaceAll(/\D/g, "")).toBe(digits); + }), + ); + }); + + test("should left pad a shorter value up to the matrícula length", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,32}$/), (value) => { + const padded = formatCertidao(value, { pad: true }).replaceAll(/\D/g, ""); + + expect(padded).toBe(value.padStart(32, "0")); + }), + ); + }); + + test("should never throw and always return the matrícula as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof formatCertidao(text)).toBe("string"); + expect(typeof formatCertidao(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatCertidao types", () => { + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatCertidao).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCertidao).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(formatCertidao).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-cnae/format-cnae.test.ts b/src/format-cnae/format-cnae.test.ts index 2d444c67..4e2b5d50 100644 --- a/src/format-cnae/format-cnae.test.ts +++ b/src/format-cnae/format-cnae.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyGarbage, digits } from "../_internals/test/arbitraries"; +import { + expectIdempotent, + expectMatchesPattern, + expectNeverThrows, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { formatCnae } from "./format-cnae"; describe("formatCnae", () => { @@ -33,4 +39,27 @@ describe("formatCnae", () => { // @ts-expect-error not a string or number expect(formatCnae()).toBe(""); }); + + describe("properties", () => { + const sevenDigitArbitrary = digits(7); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(formatCnae, anyGarbage); + }); + + test("should format every 7 digit value in the NNNN-N/NN pattern", () => { + expectMatchesPattern(formatCnae, /^\d{4}-\d\/\d{2}$/, sevenDigitArbitrary); + }); + + test("should be idempotent on a full 7 digit code", () => { + expectIdempotent(formatCnae, sevenDigitArbitrary); + }); + }); +}); + +describe("formatCnae types", () => { + test("should take a string or number and return a string", () => { + expectTypeOf(formatCnae).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCnae).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-cnh/format-cnh.test.ts b/src/format-cnh/format-cnh.test.ts index 28607917..51e0c9e5 100644 --- a/src/format-cnh/format-cnh.test.ts +++ b/src/format-cnh/format-cnh.test.ts @@ -1,5 +1,13 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatCnh } from "./format-cnh"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectMatchesPattern, + expectPadsToLength, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCnh } from "../parse-cnh/parse-cnh"; +import { formatCnh, type FormatCnhOptions } from "./format-cnh"; describe("formatCnh", () => { it("should format CNH values", () => { @@ -27,4 +35,36 @@ describe("formatCnh", () => { // @ts-expect-error: intentionally invalid input expect(formatCnh()).toBe(""); }); + + describe("properties", () => { + const upToACnh = digitsUpTo(11); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatCnh, parseCnh, upToACnh); + }); + + test("should produce the documented mask shape for a full CNH", () => { + expectMatchesPattern(formatCnh, /^\d{9}-\d{2}$/, digits(11)); + }); + + test("should left pad a shorter value up to the CNH length", () => { + expectPadsToLength(formatCnh, parseCnh, upToACnh, 11); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatCnh, "string", anyValue); + }); + }); +}); + +describe("formatCnh types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(formatCnh).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCnh).parameter(1).toEqualTypeOf(); + expectTypeOf(formatCnh).returns.toEqualTypeOf(); + }); + + test("should type the pad option as an optional boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-cno/format-cno.test.ts b/src/format-cno/format-cno.test.ts index 7972e798..7de89270 100644 --- a/src/format-cno/format-cno.test.ts +++ b/src/format-cno/format-cno.test.ts @@ -1,5 +1,8 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { formatCno } from "./format-cno"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { formatCei } from "../format-cei/format-cei"; +import { formatCno, type FormatCnoOptions } from "./format-cno"; describe("formatCno", () => { test("should format a full 12 digit value", () => { @@ -44,4 +47,33 @@ describe("formatCno", () => { // @ts-expect-error: intentionally invalid input expect(formatCno()).toBe(""); }); + + describe("properties", () => { + test("should print what formatCei prints, since both share the mask", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,16}$/), (value) => { + expect(formatCno(value)).toBe(formatCei(value)); + expect(formatCno(value, { pad: true })).toBe(formatCei(value, { pad: true })); + }), + ); + }); + + test("should never throw and always return the CNO number as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof formatCno(text)).toBe("string"); + expect(typeof formatCno(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatCno types", () => { + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatCno).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCno).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(formatCno).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-cnpj/format-cnpj.test.ts b/src/format-cnpj/format-cnpj.test.ts index 5fcbcd7b..55e7b54a 100644 --- a/src/format-cnpj/format-cnpj.test.ts +++ b/src/format-cnpj/format-cnpj.test.ts @@ -1,6 +1,15 @@ +import * as fc from "fast-check"; + import { CNPJ_LENGTH } from "../_internals/constants/cnpj"; -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatCnpj } from "./format-cnpj"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectPadsToLength, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCnpj } from "../parse-cnpj/parse-cnpj"; +import { formatCnpj, type FormatCnpjOptions } from "./format-cnpj"; describe("formatCnpj", () => { it("should return an empty string for null or undefined", () => { @@ -138,4 +147,59 @@ describe("formatCnpj", () => { "Q0.SLF.MBD/7VX4-39", ); }); + + describe("properties", () => { + const upToACnpj = digitsUpTo(14); + const fullCnpj = digits(14); + const alphanumericCnpj = fc.stringMatching(/^[0-9A-Z]{14}$/); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatCnpj, parseCnpj, upToACnpj); + }); + + test("should produce the documented mask shape for a full CNPJ", () => { + fc.assert( + fc.property(fullCnpj, (value) => { + const obfuscated = formatCnpj(value, { obfuscate: true }); + + expect(formatCnpj(value)).toMatch(/^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$/); + expect(obfuscated).toMatch(/^\*{2}\.\d{3}\.\d{3}\/\d{4}-\*{2}$/); + }), + ); + }); + + test("should uppercase and keep every character of an alphanumeric CNPJ", () => { + fc.assert( + fc.property(alphanumericCnpj, (value) => { + const formatted = formatCnpj(value.toLowerCase(), { version: 2 }); + const shape = /^[\dA-Z]{2}\.[\dA-Z]{3}\.[\dA-Z]{3}\/[\dA-Z]{4}-[\dA-Z]{2}$/; + + expect(formatted).toMatch(shape); + expect(parseCnpj(formatted, { version: 2 })).toBe(value); + }), + ); + }); + + test("should left pad a shorter value up to the CNPJ length", () => { + expectPadsToLength(formatCnpj, parseCnpj, upToACnpj, CNPJ_LENGTH); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatCnpj, "string", anyValue); + }); + }); +}); + +describe("formatCnpj types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(formatCnpj).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCnpj).parameter(1).toEqualTypeOf(); + expectTypeOf(formatCnpj).returns.toEqualTypeOf(); + }); + + test("should type the pad, version and obfuscate options", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<1 | 2 | undefined>(); + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-cns/format-cns.test.ts b/src/format-cns/format-cns.test.ts index 1c93168d..687cbbef 100644 --- a/src/format-cns/format-cns.test.ts +++ b/src/format-cns/format-cns.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatCns } from "./format-cns"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCns, type FormatCnsOptions } from "./format-cns"; describe("formatCns", () => { it("should format a CNS with the 3-4-4-4 space mask", () => { @@ -34,4 +36,42 @@ describe("formatCns", () => { // @ts-expect-error: intentionally invalid input expect(formatCns()).toBe(""); }); + + describe("properties", () => { + test("should print a full card number in groups of three, four, four and four", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{15}$/), (value) => { + expect(/^\d{3} \d{4} \d{4} \d{4}$/.test(formatCns(value))).toBe(true); + }), + ); + }); + + test("should left pad a shorter value up to the card length", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,15}$/), (value) => { + const padded = formatCns(value, { pad: true }).replaceAll(" ", ""); + + expect(padded).toBe(value.padStart(15, "0")); + }), + ); + }); + + test("should never throw and always return the health card as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof formatCns(text)).toBe("string"); + expect(typeof formatCns(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatCns types", () => { + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatCns).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCns).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(formatCns).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-cpf/format-cpf.test.ts b/src/format-cpf/format-cpf.test.ts index 7d40adce..ea39c628 100644 --- a/src/format-cpf/format-cpf.test.ts +++ b/src/format-cpf/format-cpf.test.ts @@ -1,6 +1,15 @@ +import * as fc from "fast-check"; + import { CPF_LENGTH } from "../_internals/constants/cpf"; -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatCpf } from "./format-cpf"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectPadsToLength, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCpf } from "../parse-cpf/parse-cpf"; +import { formatCpf, type FormatCpfOptions } from "./format-cpf"; describe("formatCpf", () => { it("should return an empty string for null or undefined", () => { @@ -95,4 +104,45 @@ describe("formatCpf", () => { expect(formatCpf("94389575104")).toBe("943.895.751-04"); expect(formatCpf("943", { pad: true, obfuscate: false })).toBe("000.000.009-43"); }); + + describe("properties", () => { + const upToACpf = digitsUpTo(11); + const fullCpf = digits(11); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatCpf, parseCpf, upToACpf); + }); + + test("should produce the documented mask shape for a full CPF", () => { + fc.assert( + fc.property(fullCpf, (value) => { + const obfuscated = formatCpf(value, { obfuscate: true }); + + expect(formatCpf(value)).toMatch(/^\d{3}\.\d{3}\.\d{3}-\d{2}$/); + expect(obfuscated).toMatch(/^\*{3}\.\d{3}\.\d{3}-\*{2}$/); + }), + ); + }); + + test("should left pad a shorter value up to the CPF length", () => { + expectPadsToLength(formatCpf, parseCpf, upToACpf, CPF_LENGTH); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatCpf, "string", anyValue); + }); + }); +}); + +describe("formatCpf types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(formatCpf).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCpf).parameter(1).toEqualTypeOf(); + expectTypeOf(formatCpf).returns.toEqualTypeOf(); + }); + + test("should type the pad and obfuscate options as optional booleans", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-currency/format-currency.test.ts b/src/format-currency/format-currency.test.ts index 1a91d24d..a96880fc 100644 --- a/src/format-currency/format-currency.test.ts +++ b/src/format-currency/format-currency.test.ts @@ -1,5 +1,14 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatCurrency } from "./format-currency"; +import * as fc from "fast-check"; + +import { anyGarbage, twoDecimalAmounts } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectNeverThrowsWithOptions, + expectRoundTrip, +} from "../_internals/test/properties"; +import { bench, describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCurrency } from "../parse-currency/parse-currency"; +import { formatCurrency, type FormatCurrencyOptions } from "./format-currency"; describe("formatCurrency", () => { it("should formatCurrency positive currency into BRL", () => { @@ -118,4 +127,46 @@ describe("formatCurrency", () => { expect(formatCurrency(1234.56, { symbol: true })).toBe("R$ 1.234,56"); expect(formatCurrency(1234.56, { symbol: true })).not.toContain("\u00A0"); }); + + describe("properties", () => { + const optionsArbitrary = fc + .option(fc.record({ symbol: fc.boolean(), precision: fc.double() }, { requiredKeys: [] })) + .map((options) => options ?? undefined); + + test("should round-trip with parseCurrency for any value with 2 decimals", () => { + expectRoundTrip(formatCurrency, parseCurrency, twoDecimalAmounts); + }); + + test("should never throw, regardless of the input", () => { + expectNeverThrowsWithOptions(formatCurrency, anyGarbage, optionsArbitrary); + }); + + test("should always return a string", () => { + expectAlwaysReturnsType(formatCurrency, "string", anyGarbage); + }); + }); +}); + +describe("formatCurrency types", () => { + test("should take a string or number, options, and return a string", () => { + expectTypeOf(formatCurrency).parameter(0).toEqualTypeOf(); + expectTypeOf(formatCurrency).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(formatCurrency).returns.toEqualTypeOf(); + }); +}); + +describe("formatCurrency benchmarks", () => { + bench("formatCurrency(number)", () => { + formatCurrency(1_234_567.89); + }); + + bench("formatCurrency(string with separators)", () => { + formatCurrency("1.234.567,89"); + }); + + bench("parseCurrency", () => { + parseCurrency("R$ 1.234.567,89"); + }); }); diff --git a/src/format-iban/format-iban.test.ts b/src/format-iban/format-iban.test.ts index e5efa047..eda82079 100644 --- a/src/format-iban/format-iban.test.ts +++ b/src/format-iban/format-iban.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { formatIban } from "./format-iban"; describe("formatIban", () => { @@ -50,4 +52,44 @@ describe("formatIban", () => { // @ts-expect-error: intentionally invalid input expect(formatIban([])).toBe(""); }); + + describe("properties", () => { + const values = fc.stringMatching(/^[A-Za-z0-9]{0,40}$/); + + test("should uppercase and group in blocks of four, keeping every character", () => { + fc.assert( + fc.property(values, (value) => { + const formatted = formatIban(value); + + expect(/^(?:[A-Z0-9]{4} )*[A-Z0-9]{0,4}$/.test(formatted)).toBe(true); + expect(formatted.replaceAll(" ", "")).toBe(value.toUpperCase().slice(0, 29)); + }), + ); + }); + + test("should be stable under its own output", () => { + fc.assert( + fc.property(values, (value) => { + const formatted = formatIban(value); + + expect(formatIban(formatted)).toBe(formatted); + }), + ); + }); + + test("should never throw and always return the IBAN as a string", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof formatIban(value as string)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatIban types", () => { + test("should take a string and return a string", () => { + expectTypeOf(formatIban).parameter(0).toEqualTypeOf(); + expectTypeOf(formatIban).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-legal-nature/format-legal-nature.test.ts b/src/format-legal-nature/format-legal-nature.test.ts index 90750f3c..d13ed4e2 100644 --- a/src/format-legal-nature/format-legal-nature.test.ts +++ b/src/format-legal-nature/format-legal-nature.test.ts @@ -1,4 +1,11 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseLegalNature } from "../parse-legal-nature/parse-legal-nature"; import { formatLegalNature } from "./format-legal-nature"; describe("formatLegalNature", () => { @@ -16,4 +23,27 @@ describe("formatLegalNature", () => { // @ts-expect-error: intentionally invalid input expect(formatLegalNature()).toBe(""); }); + + describe("properties", () => { + const upToACode = digitsUpTo(4); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatLegalNature, parseLegalNature, upToACode); + }); + + test("should produce the documented mask shape for a full code", () => { + expectMatchesPattern(formatLegalNature, /^\d{3}-\d$/, digits(4)); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatLegalNature, "string", anyValue); + }); + }); +}); + +describe("formatLegalNature types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(formatLegalNature).parameter(0).toEqualTypeOf(); + expectTypeOf(formatLegalNature).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-license-plate/format-license-plate.test.ts b/src/format-license-plate/format-license-plate.test.ts index af593882..6ce402dc 100644 --- a/src/format-license-plate/format-license-plate.test.ts +++ b/src/format-license-plate/format-license-plate.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { generateLicensePlate } from "../generate-license-plate/generate-license-plate"; +import { parseLicensePlate } from "../parse-license-plate/parse-license-plate"; import { formatLicensePlate } from "./format-license-plate"; const expectFormattedPrefixes = () => { @@ -40,4 +44,44 @@ describe("formatLicensePlate", () => { expect(formatLicensePlate("1ab4")).toBe(""); expect(formatLicensePlate("ab14")).toBe(""); }); + + describe("properties", () => { + test("should hyphenate an old format plate and leave a Mercosul one alone", () => { + fc.assert( + fc.property(fc.constantFrom("LLLNNNN", "LLLNLNN"), (format) => { + const plate = generateLicensePlate(format); + const expected = format === "LLLNNNN" ? `${plate.slice(0, 3)}-${plate.slice(3)}` : plate; + + expect(formatLicensePlate(plate.toLowerCase())).toBe(expected); + }), + ); + }); + + test("should never change the characters of the plate it formats", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const formatted = formatLicensePlate(value); + + fc.pre(formatted !== ""); + + expect(parseLicensePlate(formatted)).toBe(parseLicensePlate(value)); + }), + ); + }); + + test("should never throw and always return the plate as a string", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof formatLicensePlate(value as string)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatLicensePlate types", () => { + test("should take a string and return a string", () => { + expectTypeOf(formatLicensePlate).parameter(0).toEqualTypeOf(); + expectTypeOf(formatLicensePlate).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-ncm/format-ncm.test.ts b/src/format-ncm/format-ncm.test.ts index 260984b2..9426e236 100644 --- a/src/format-ncm/format-ncm.test.ts +++ b/src/format-ncm/format-ncm.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyGarbage, digits } from "../_internals/test/arbitraries"; +import { + expectIdempotent, + expectMatchesPattern, + expectNeverThrows, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { formatNcm } from "./format-ncm"; describe("formatNcm", () => { @@ -38,4 +44,27 @@ describe("formatNcm", () => { // @ts-expect-error not a string or number expect(formatNcm()).toBe(""); }); + + describe("properties", () => { + const eightDigitArbitrary = digits(8); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(formatNcm, anyGarbage); + }); + + test("should format every 8 digit value in the NNNN.NN.NN pattern", () => { + expectMatchesPattern(formatNcm, /^\d{4}\.\d{2}\.\d{2}$/, eightDigitArbitrary); + }); + + test("should be idempotent on a full 8 digit code", () => { + expectIdempotent(formatNcm, eightDigitArbitrary); + }); + }); +}); + +describe("formatNcm types", () => { + test("should take a string or number and return a string", () => { + expectTypeOf(formatNcm).parameter(0).toEqualTypeOf(); + expectTypeOf(formatNcm).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-nfe-key/format-nfe-key.test.ts b/src/format-nfe-key/format-nfe-key.test.ts index 7adfa848..68ae4622 100644 --- a/src/format-nfe-key/format-nfe-key.test.ts +++ b/src/format-nfe-key/format-nfe-key.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { formatNfeKey } from "./format-nfe-key"; const KEY = "35170458716523000119550010000000121000123458"; @@ -43,4 +45,47 @@ describe("formatNfeKey", () => { // @ts-expect-error: intentionally invalid input expect(formatNfeKey(true)).toBe(""); }); + + describe("properties", () => { + test("should group a full access key into eleven blocks of four digits", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{44}$/), (key) => { + const formatted = formatNfeKey(key); + + expect(/^(?:\d{4} ){10}\d{4}$/.test(formatted)).toBe(true); + expect(formatted.replaceAll(" ", "")).toBe(key); + }), + ); + }); + + test("should keep only the digits of the access key it formats", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const digits = value.replaceAll(/\D/g, "").slice(0, 44); + + expect(formatNfeKey(value).replaceAll(/\D/g, "")).toBe(digits); + }), + ); + }); + + test("should never throw and always return the access key as a string", () => { + fc.assert( + fc.property( + fc.string({ unit: "grapheme" }), + fc.stringMatching(/^[0-9]{0,60}$/), + (text, digits) => { + expect(typeof formatNfeKey(text)).toBe("string"); + expect(typeof formatNfeKey(digits)).toBe("string"); + }, + ), + ); + }); + }); +}); + +describe("formatNfeKey types", () => { + test("should take a string and return a string", () => { + expectTypeOf(formatNfeKey).parameter(0).toEqualTypeOf(); + expectTypeOf(formatNfeKey).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-passport/format-passport.test.ts b/src/format-passport/format-passport.test.ts index 22488337..0bde5c79 100644 --- a/src/format-passport/format-passport.test.ts +++ b/src/format-passport/format-passport.test.ts @@ -1,4 +1,11 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import { anyText, anyValue, asciiAlphanumericText } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectCaseInsensitive, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { formatPassport } from "./format-passport"; describe("formatPassport", () => { @@ -46,4 +53,29 @@ describe("formatPassport", () => { expect(formatPassport(123)).toBe(""); }); }); + + describe("properties", () => { + test("should always return at most 8 uppercase alphanumeric characters", () => { + expectMatchesPattern(formatPassport, /^[0-9A-Z]{0,8}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(formatPassport, anyText); + }); + + test("should ignore the case of an ascii alphanumeric value", () => { + expectCaseInsensitive(formatPassport, asciiAlphanumericText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatPassport, "string", anyValue); + }); + }); +}); + +describe("formatPassport types", () => { + test("should take a string and return a string", () => { + expectTypeOf(formatPassport).parameter(0).toEqualTypeOf(); + expectTypeOf(formatPassport).returns.toEqualTypeOf(); + }); }); diff --git a/src/format-phone/format-phone.test.ts b/src/format-phone/format-phone.test.ts index cc0f55f0..3d4a0391 100644 --- a/src/format-phone/format-phone.test.ts +++ b/src/format-phone/format-phone.test.ts @@ -1,5 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatPhone } from "./format-phone"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { generatePhone } from "../generate-phone/generate-phone"; +import { parsePhone } from "../parse-phone/parse-phone"; +import { type FormatPhoneOptions, type PhoneMask, formatPhone } from "./format-phone"; describe("formatPhone", () => { it("should format a service number written with an explicit country code", () => { @@ -147,4 +151,70 @@ describe("formatPhone", () => { // @ts-expect-error: intentionally invalid input expect(formatPhone(undefined, { mask: "service" })).toBe(""); }); + + describe("properties", () => { + const geographic = ["mobile", "landline"] as const; + + test("should print a generated number in E.164 and read it back", () => { + fc.assert( + fc.property(fc.constantFrom(...geographic), (type) => { + const phone = generatePhone(type); + const formatted = formatPhone(phone, { mask: "e164" }); + + expect(formatted).toBe(`+55${phone}`); + expect(parsePhone(formatted)).toBe(phone); + }), + ); + }); + + test("should keep every digit of a generated number under the auto mask", () => { + fc.assert( + fc.property(fc.constantFrom(...geographic), (type) => { + const phone = generatePhone(type); + const international = formatPhone(phone, { mask: "international" }); + + expect(parsePhone(formatPhone(phone, { mask: "auto" }))).toBe(phone); + expect(international.startsWith("+55 ")).toBe(true); + expect(parsePhone(international)).toBe(phone); + }), + ); + }); + + test("should keep the digits a national mask has room for", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const digits = value.replaceAll(/\D/g, ""); + + expect(formatPhone(value).replaceAll(/\D/g, "")).toBe(digits.slice(0, 9)); + expect(formatPhone(value, { mask: "nanp" }).replaceAll(/\D/g, "")).toBe( + digits.slice(0, 11), + ); + }), + ); + }); + + test("should never throw and always return the phone number as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof formatPhone(text)).toBe("string"); + expect(typeof formatPhone(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("formatPhone types", () => { + test("should take a string or number, optional options, and return a string", () => { + expectTypeOf(formatPhone).parameter(0).toEqualTypeOf(); + expectTypeOf(formatPhone).parameter(1).toEqualTypeOf(); + expectTypeOf(formatPhone).returns.toEqualTypeOf(); + }); + + test("should restrict mask to the supported phone masks", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + "auto" | "e164" | "international" | "service" | "sn" | "nanp" + >(); + }); }); diff --git a/src/format-pis/format-pis.test.ts b/src/format-pis/format-pis.test.ts index 0b24f4ff..188b3d52 100644 --- a/src/format-pis/format-pis.test.ts +++ b/src/format-pis/format-pis.test.ts @@ -1,6 +1,14 @@ import { PIS_LENGTH } from "../_internals/constants/pis"; -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatPis } from "./format-pis"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectMatchesPattern, + expectPadsToLength, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parsePis } from "../parse-pis/parse-pis"; +import { formatPis, type FormatPisOptions } from "./format-pis"; describe("formatPis", () => { it("when it is a no formatted string", () => { @@ -82,4 +90,36 @@ describe("formatPis", () => { // @ts-expect-error: intentionally invalid input expect(formatPis()).toBe(""); }); + + describe("properties", () => { + const upToAPis = digitsUpTo(11); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatPis, parsePis, upToAPis); + }); + + test("should produce the documented mask shape for a full PIS", () => { + expectMatchesPattern(formatPis, /^\d{3}\.\d{5}\.\d{2}-\d$/, digits(11)); + }); + + test("should left pad a shorter value up to the PIS length", () => { + expectPadsToLength(formatPis, parsePis, upToAPis, PIS_LENGTH); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatPis, "string", anyValue); + }); + }); +}); + +describe("formatPis types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(formatPis).parameter(0).toEqualTypeOf(); + expectTypeOf(formatPis).parameter(1).toEqualTypeOf(); + expectTypeOf(formatPis).returns.toEqualTypeOf(); + }); + + test("should type the pad option as an optional boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-processo-juridico/format-processo-juridico.test.ts b/src/format-processo-juridico/format-processo-juridico.test.ts index 33901ec5..0343729d 100644 --- a/src/format-processo-juridico/format-processo-juridico.test.ts +++ b/src/format-processo-juridico/format-processo-juridico.test.ts @@ -1,6 +1,17 @@ import { PROCESSO_JURIDICO_LENGTH } from "../_internals/constants/processo-juridico"; -import { describe, expect, it } from "../_internals/test/runtime"; -import { formatProcessoJuridico } from "./format-processo-juridico"; +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectMatchesPattern, + expectPadsToLength, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseProcessoJuridico } from "../parse-processo-juridico/parse-processo-juridico"; +import { + formatProcessoJuridico, + type FormatProcessoJuridicoOptions, +} from "./format-processo-juridico"; describe("formatProcessoJuridico", () => { it("should format processo juridico with mask", () => { @@ -47,4 +58,45 @@ describe("formatProcessoJuridico", () => { // @ts-expect-error: intentionally invalid input expect(formatProcessoJuridico()).toBe(""); }); + + describe("properties", () => { + const upToANumber = digitsUpTo(20); + + test("should only add the mask, never change the digits", () => { + expectRoundTrip(formatProcessoJuridico, parseProcessoJuridico, upToANumber); + }); + + test("should produce the documented mask shape for a full number", () => { + const shape = /^\d{7}-\d{2}\.\d{4}\.\d\.\d{2}\.\d{4}$/; + + expectMatchesPattern(formatProcessoJuridico, shape, digits(20)); + }); + + test("should left pad a shorter value up to the documented length", () => { + expectPadsToLength( + formatProcessoJuridico, + parseProcessoJuridico, + upToANumber, + PROCESSO_JURIDICO_LENGTH, + ); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatProcessoJuridico, "string", anyValue); + }); + }); +}); + +describe("formatProcessoJuridico types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(formatProcessoJuridico).parameter(0).toEqualTypeOf(); + expectTypeOf(formatProcessoJuridico) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf(formatProcessoJuridico).returns.toEqualTypeOf(); + }); + + test("should type the pad option as an optional boolean", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/format-voter-id/format-voter-id.test.ts b/src/format-voter-id/format-voter-id.test.ts index a3d78826..c766ef71 100644 --- a/src/format-voter-id/format-voter-id.test.ts +++ b/src/format-voter-id/format-voter-id.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { anyValue, digits, digitsUpTo } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { formatVoterId } from "./format-voter-id"; describe("formatVoterId", () => { @@ -38,4 +42,44 @@ describe("formatVoterId", () => { // @ts-expect-error: intentionally invalid input expect(formatVoterId()).toBe(""); }); + + describe("properties", () => { + const upToAVoterId = digitsUpTo(13); + + test("should only add spaces, never change the digits", () => { + fc.assert( + fc.property(upToAVoterId, (value) => { + expect(formatVoterId(value).replaceAll(" ", "")).toBe(value); + }), + ); + }); + + test("should use the grouping documented for each of the two lengths", () => { + fc.assert( + fc.property(digits(12), digits(9), fc.constantFrom("01", "02"), (short, sequential, uf) => { + expect(formatVoterId(short)).toMatch(/^\d{4} \d{4} \d{2} \d{2}$/); + expect(formatVoterId(`${sequential}${uf}00`)).toMatch(/^\d{4} \d{4} \d \d{2} \d{2}$/); + }), + ); + }); + + test("should keep the 12-digit grouping for 13 digits when the UF is not 01 or 02", () => { + fc.assert( + fc.property(digits(9), fc.constantFrom("03", "10", "28", "99"), (sequential, uf) => { + expect(formatVoterId(`${sequential}${uf}00`)).toMatch(/^\d{4} \d{4} \d{2} \d{2}$/); + }), + ); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(formatVoterId, "string", anyValue); + }); + }); +}); + +describe("formatVoterId types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(formatVoterId).parameter(0).toEqualTypeOf(); + expectTypeOf(formatVoterId).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-boleto/generate-boleto.test.ts b/src/generate-boleto/generate-boleto.test.ts index 24b05868..c1547af7 100644 --- a/src/generate-boleto/generate-boleto.test.ts +++ b/src/generate-boleto/generate-boleto.test.ts @@ -1,11 +1,13 @@ +import * as fc from "fast-check"; + import { ARRECADACAO_LINE_LENGTH } from "../_internals/constants/arrecadacao"; import { BOLETO_LENGTH } from "../_internals/constants/boleto"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { formatBoleto } from "../format-boleto/format-boleto"; import { getBoletoInfo } from "../get-boleto-info/get-boleto-info"; import { isValidBoleto } from "../is-valid-boleto/is-valid-boleto"; import { parseBoleto } from "../parse-boleto/parse-boleto"; -import { generateBoleto } from "./generate-boleto"; +import { type GenerateBoletoOptions, generateBoleto } from "./generate-boleto"; const drawArrecadacaoSegment = (): number => getBoletoInfo(generateBoleto({ type: "arrecadacao" }))?.segment ?? 0; @@ -105,4 +107,57 @@ describe("generateBoleto", () => { } }); }); + + describe("properties", () => { + const types = ["bancario", "arrecadacao"] as const; + + test("should always generate a bank slip its own validator accepts", () => { + fc.assert( + fc.property(fc.constantFrom(...types), (type) => { + const value = generateBoleto({ type }); + const length = type === "arrecadacao" ? ARRECADACAO_LINE_LENGTH : BOLETO_LENGTH; + + expect(value.length).toBe(length); + expect(isValidBoleto(value)).toBe(true); + }), + ); + }); + + test("should always generate a bank slip that survives formatting and parsing", () => { + fc.assert( + fc.property(fc.constantFrom(...types), (type) => { + const value = generateBoleto({ type }); + const formatted = formatBoleto(value); + + expect(parseBoleto(formatted)).toBe(value); + expect(isValidBoleto(formatted)).toBe(true); + }), + ); + }); + + test("should always generate a bank slip getBoletoInfo can read", () => { + fc.assert( + fc.property(fc.constantFrom(...types), (type) => { + const value = generateBoleto({ type }); + const info = getBoletoInfo(value); + + expect(info).toBeDefined(); + expect(info?.bankCode).toBe(type === "arrecadacao" ? "" : value.slice(0, 3)); + }), + ); + }); + }); +}); + +describe("generateBoleto types", () => { + test("should take optional options and return a string", () => { + expectTypeOf(generateBoleto).parameter(0).toEqualTypeOf(); + expectTypeOf(generateBoleto).returns.toEqualTypeOf(); + }); + + test("should restrict type to the supported boleto kinds", () => { + expectTypeOf().toEqualTypeOf< + "bancario" | "arrecadacao" | undefined + >(); + }); }); diff --git a/src/generate-cep/generate-cep.test.ts b/src/generate-cep/generate-cep.test.ts index 3355c927..3a88457e 100644 --- a/src/generate-cep/generate-cep.test.ts +++ b/src/generate-cep/generate-cep.test.ts @@ -1,8 +1,35 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { isValidCep } from "../is-valid-cep/is-valid-cep"; import { generateCep } from "./generate-cep"; describe("generateCep", () => { it("should generate a valid CEP", () => { expect(generateCep()).toMatch(/^\d{8}$/); }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 20 }); + + test("should generate 8 digit CEPs its own validator accepts", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const cep = generateCep(); + + expect(cep).toMatch(/^\d{8}$/); + expect(isValidCep(cep)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generateCep types", () => { + test("should take no parameters and return a string", () => { + expectTypeOf(generateCep).parameters.toEqualTypeOf<[]>(); + expectTypeOf(generateCep).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-cnh/generate-cnh.test.ts b/src/generate-cnh/generate-cnh.test.ts index 9b154e23..2b5fc376 100644 --- a/src/generate-cnh/generate-cnh.test.ts +++ b/src/generate-cnh/generate-cnh.test.ts @@ -1,6 +1,8 @@ +import * as fc from "fast-check"; + import { calculateCnhFirstVerifier } from "../_internals/calculate-cnh-first-verifier/calculate-cnh-first-verifier"; import { calculateCnhSecondVerifier } from "../_internals/calculate-cnh-second-verifier/calculate-cnh-second-verifier"; -import { describe, expect, it } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidCnh } from "../is-valid-cnh/is-valid-cnh"; import { generateCnh } from "./generate-cnh"; @@ -44,4 +46,29 @@ describe("generateCnh", () => { expect(isValidCnh(cnh)).toBe(true); }); }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 20 }); + + test("should generate 11 digit CNHs its own validator accepts", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const cnh = generateCnh(); + + expect(cnh).toMatch(/^\d{11}$/); + expect(/^(\d)\1{8}/.test(cnh)).toBe(false); + expect(isValidCnh(cnh)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generateCnh types", () => { + test("should take no parameters and return a string", () => { + expectTypeOf(generateCnh).parameters.toEqualTypeOf<[]>(); + expectTypeOf(generateCnh).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-cnpj/generate-cnpj.test.ts b/src/generate-cnpj/generate-cnpj.test.ts index b176c6f3..5acd2a2e 100644 --- a/src/generate-cnpj/generate-cnpj.test.ts +++ b/src/generate-cnpj/generate-cnpj.test.ts @@ -1,5 +1,7 @@ +import * as fc from "fast-check"; + import { CNPJ_LENGTH } from "../_internals/constants/cnpj"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCnpj } from "../is-valid-cnpj/is-valid-cnpj"; import { generateCnpj } from "./generate-cnpj"; @@ -134,4 +136,43 @@ describe("generateCnpj", () => { } }); }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 10 }); + + test("should generate numeric CNPJs both versions accept", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const cnpj = generateCnpj(1); + + expect(cnpj).toMatch(/^\d{14}$/); + expect(isValidCnpj(cnpj)).toBe(true); + expect(isValidCnpj(cnpj, { version: 2 })).toBe(true); + } + }), + ); + }); + + test("should generate alphanumeric CNPJs with numeric check digits", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const cnpj = generateCnpj(2); + + expect(cnpj).toHaveLength(CNPJ_LENGTH); + expect(cnpj).toMatch(/^[0-9A-Z]{12}\d{2}$/); + expect(isValidCnpj(cnpj, { version: 2 })).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generateCnpj types", () => { + test("should take an optional version and return a string", () => { + expectTypeOf(generateCnpj).parameter(0).toEqualTypeOf<1 | 2 | undefined>(); + expectTypeOf(generateCnpj).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-cpf/generate-cpf.test.ts b/src/generate-cpf/generate-cpf.test.ts index 08b13ae6..83498bf3 100644 --- a/src/generate-cpf/generate-cpf.test.ts +++ b/src/generate-cpf/generate-cpf.test.ts @@ -1,6 +1,8 @@ +import * as fc from "fast-check"; + import { CPF_LENGTH } from "../_internals/constants/cpf"; -import { DATA } from "../_internals/constants/states"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { DATA, type StateCode } from "../_internals/constants/states"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCpf } from "../is-valid-cpf/is-valid-cpf"; import { STATE_CODES } from "./constants"; import { generateCpf } from "./generate-cpf"; @@ -55,4 +57,42 @@ describe("generateCpf", () => { expect(cpf).toHaveLength(CPF_LENGTH); expect(isValidCpf(cpf)).toBe(true); }); + + describe("properties", () => { + const stateCode = fc.constantFrom(...DATA.map((state) => state.code)); + const batchSize = fc.integer({ min: 1, max: 10 }); + + test("should generate a valid CPF carrying the state digit of every state", () => { + fc.assert( + fc.property(stateCode, (state) => { + const cpf = generateCpf(state); + + expect(cpf).toHaveLength(CPF_LENGTH); + expect(cpf[8]).toBe(STATE_CODES[state]); + expect(isValidCpf(cpf)).toBe(true); + }), + ); + }); + + test("should never draw a base made of a single repeated digit", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const cpf = generateCpf(); + + expect(cpf).toMatch(/^\d{11}$/); + expect(/^(\d)\1{8}/.test(cpf)).toBe(false); + expect(isValidCpf(cpf)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generateCpf types", () => { + test("should take an optional state code and return a string", () => { + expectTypeOf(generateCpf).parameter(0).toEqualTypeOf(); + expectTypeOf(generateCpf).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-legal-nature/generate-legal-nature.test.ts b/src/generate-legal-nature/generate-legal-nature.test.ts index 469e4278..ca395b04 100644 --- a/src/generate-legal-nature/generate-legal-nature.test.ts +++ b/src/generate-legal-nature/generate-legal-nature.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidLegalNature } from "../is-valid-legal-nature/is-valid-legal-nature"; import { generateLegalNature } from "./generate-legal-nature"; @@ -20,4 +22,28 @@ describe("generateLegalNature", () => { Math.random = originalRandom; } }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 20 }); + + test("should only draw 4 digit codes its own validator accepts", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const code = generateLegalNature(); + + expect(code).toMatch(/^\d{4}$/); + expect(isValidLegalNature(code)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generateLegalNature types", () => { + test("should take no parameters and return a string", () => { + expectTypeOf(generateLegalNature).parameters.toEqualTypeOf<[]>(); + expectTypeOf(generateLegalNature).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-license-plate/generate-license-plate.test.ts b/src/generate-license-plate/generate-license-plate.test.ts index 9dde61ca..4d1f4edf 100644 --- a/src/generate-license-plate/generate-license-plate.test.ts +++ b/src/generate-license-plate/generate-license-plate.test.ts @@ -1,6 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getFormatLicensePlate } from "../get-format-license-plate/get-format-license-plate"; import { isValidLicensePlate } from "../is-valid-license-plate/is-valid-license-plate"; -import { generateLicensePlate } from "./generate-license-plate"; +import { type GenerateLicensePlateFormat, generateLicensePlate } from "./generate-license-plate"; const runWithForcedRandom = (forced: number, run: () => void) => { const originalRandom = Math.random; @@ -43,4 +46,41 @@ describe("generateLicensePlate", () => { expect(generateLicensePlate("LLLNLNN")).toBe("NNN5N55"); }); }); + + describe("properties", () => { + const formats = ["LLLNNNN", "LLLNLNN"] as const; + + test("should always generate a plate of the requested format", () => { + fc.assert( + fc.property(fc.constantFrom(...formats), (format) => { + const plate = generateLicensePlate(format); + + expect(plate.length).toBe(7); + expect(getFormatLicensePlate(plate)).toBe(format); + expect(isValidLicensePlate(plate)).toBe(true); + }), + ); + }); + + test("should always generate a Mercosul plate when no format is given", () => { + fc.assert( + fc.property(fc.constant(null), () => { + expect(getFormatLicensePlate(generateLicensePlate())).toBe("LLLNLNN"); + }), + ); + }); + }); +}); + +describe("generateLicensePlate types", () => { + test("should take an optional format and return a string", () => { + expectTypeOf(generateLicensePlate) + .parameter(0) + .toEqualTypeOf(); + expectTypeOf(generateLicensePlate).returns.toEqualTypeOf(); + }); + + test("should restrict the format to the supported license plate formats", () => { + expectTypeOf().toEqualTypeOf<"LLLNNNN" | "LLLNLNN">(); + }); }); diff --git a/src/generate-passport/generate-passport.test.ts b/src/generate-passport/generate-passport.test.ts index af55a1fb..7fb3176a 100644 --- a/src/generate-passport/generate-passport.test.ts +++ b/src/generate-passport/generate-passport.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidPassport } from "../is-valid-passport/is-valid-passport"; import { generatePassport } from "./generate-passport"; @@ -20,4 +22,28 @@ describe("generatePassport", () => { Math.random = originalRandom; } }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 20 }); + + test("should generate passports its own validator accepts", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const passport = generatePassport(); + + expect(passport).toMatch(/^[A-Z]{2}\d{6}$/); + expect(isValidPassport(passport)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generatePassport types", () => { + test("should take no parameters and return a string", () => { + expectTypeOf(generatePassport).parameters.toEqualTypeOf<[]>(); + expectTypeOf(generatePassport).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-phone/generate-phone.test.ts b/src/generate-phone/generate-phone.test.ts index e25f2270..4b755117 100644 --- a/src/generate-phone/generate-phone.test.ts +++ b/src/generate-phone/generate-phone.test.ts @@ -1,9 +1,12 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { VALID_AREA_CODES } from "../_internals/constants/area-codes"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidLandlinePhone } from "../is-valid-landline-phone/is-valid-landline-phone"; import { isValidMobilePhone } from "../is-valid-mobile-phone/is-valid-mobile-phone"; import { isValidPhone } from "../is-valid-phone/is-valid-phone"; import { isValidServicePhone } from "../is-valid-service-phone/is-valid-service-phone"; -import { generatePhone } from "./generate-phone"; +import { type GeneratePhoneType, generatePhone } from "./generate-phone"; const drawAreaCode = (): string => generatePhone("mobile").slice(0, 2); @@ -104,4 +107,66 @@ describe("generatePhone", () => { expect(lengths.has(11)).toBe(true); expect(lengths.has(10)).toBe(true); }); + + describe("properties", () => { + test("should always generate a mobile number with a valid area code", () => { + fc.assert( + fc.property(fc.constant("mobile" as const), (type) => { + const phone = generatePhone(type); + + const areaCode = Number(phone.slice(0, 2)); + + expect(phone.length).toBe(11); + expect(phone.charAt(2)).toBe("9"); + expect(VALID_AREA_CODES.includes(areaCode)).toBe(true); + expect(isValidMobilePhone(phone, { version: 2 })).toBe(true); + }), + ); + }); + + test("should always generate a landline number with a valid area code", () => { + fc.assert( + fc.property(fc.constant("landline" as const), (type) => { + const phone = generatePhone(type); + + const areaCode = Number(phone.slice(0, 2)); + + expect(phone.length).toBe(10); + expect(/^[2-6]$/.test(phone.charAt(2))).toBe(true); + expect(VALID_AREA_CODES.includes(areaCode)).toBe(true); + expect(isValidLandlinePhone(phone)).toBe(true); + }), + ); + }); + + test("should never generate a service number unless asked to", () => { + fc.assert( + fc.property(fc.constant(null), () => { + const phone = generatePhone(); + + expect(isValidPhone(phone)).toBe(true); + expect(isValidServicePhone(phone)).toBe(false); + }), + ); + }); + + test("should always generate a service number its own validator accepts", () => { + fc.assert( + fc.property(fc.constant("service" as const), (type) => { + expect(isValidServicePhone(generatePhone(type))).toBe(true); + }), + ); + }); + }); +}); + +describe("generatePhone types", () => { + test("should take an optional phone type and return a string", () => { + expectTypeOf(generatePhone).parameter(0).toEqualTypeOf(); + expectTypeOf(generatePhone).returns.toEqualTypeOf(); + }); + + test("should restrict the type to the supported phone kinds", () => { + expectTypeOf().toEqualTypeOf<"mobile" | "landline" | "service">(); + }); }); diff --git a/src/generate-pis/generate-pis.test.ts b/src/generate-pis/generate-pis.test.ts index e9112dd5..cd083015 100644 --- a/src/generate-pis/generate-pis.test.ts +++ b/src/generate-pis/generate-pis.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidPis } from "../is-valid-pis/is-valid-pis"; import { generatePis } from "./generate-pis"; @@ -30,4 +32,29 @@ describe("generatePis", () => { Math.random = originalRandom; } }); + + describe("properties", () => { + const batchSize = fc.integer({ min: 1, max: 20 }); + + test("should generate 11 digit PIS numbers its own validator accepts", () => { + fc.assert( + fc.property(batchSize, (size) => { + for (let index = 0; index < size; index++) { + const pis = generatePis(); + + expect(pis).toMatch(/^\d{11}$/); + expect(/^(\d)\1{9}/.test(pis)).toBe(false); + expect(isValidPis(pis)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("generatePis types", () => { + test("should take no parameters and return a string", () => { + expectTypeOf(generatePis).parameters.toEqualTypeOf<[]>(); + expectTypeOf(generatePis).returns.toEqualTypeOf(); + }); }); diff --git a/src/generate-pix-payload/generate-pix-payload.test.ts b/src/generate-pix-payload/generate-pix-payload.test.ts index a6500444..4b0bfd2c 100644 --- a/src/generate-pix-payload/generate-pix-payload.test.ts +++ b/src/generate-pix-payload/generate-pix-payload.test.ts @@ -1,10 +1,12 @@ +import * as fc from "fast-check"; + import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { isValidPixPayload } from "../is-valid-pix-payload/is-valid-pix-payload"; import { parsePixPayload } from "../parse-pix-payload/parse-pix-payload"; -import { generatePixPayload } from "./generate-pix-payload"; +import { type GeneratePixPayloadParams, generatePixPayload } from "./generate-pix-payload"; const BASE = { key: "123e4567-e12b-12d1-a456-426655440000", @@ -401,4 +403,117 @@ describe("generatePixPayload", () => { } }); }); + + describe("properties", () => { + const names = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{0,24}$/); + + const cities = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{0,14}$/); + + const urls = fc.stringMatching(/^[a-z]{2,8}\.[a-z]{2,6}\/[a-z0-9]{1,20}$/); + + const txids = fc.stringMatching(/^[A-Za-z0-9]{1,25}$/); + + const cents = fc.integer({ min: 1, max: 9_999_999 }); + + test("should round-trip a static payload through parsePixPayload", () => { + fc.assert( + fc.property(names, cities, (merchantName, merchantCity) => { + const key = generateCpf(); + const payload = generatePixPayload({ key, merchantName, merchantCity }); + const parsed = parsePixPayload(payload ?? ""); + + expect(isValidPixPayload(payload ?? "")).toBe(true); + expect(parsed?.key).toBe(key); + expect(parsed?.merchantName).toBe(merchantName); + expect(parsed?.merchantCity).toBe(merchantCity); + expect(parsed?.amount).toBeUndefined(); + expect(parsed?.txid).toBeUndefined(); + }), + ); + }); + + test("should carry the amount and the txid it was given", () => { + fc.assert( + fc.property(names, cents, txids, (merchantName, amountInCents, txid) => { + const amount = amountInCents / 100; + const key = generateCnpj(); + const payload = generatePixPayload({ + key, + merchantName, + merchantCity: "BRASILIA", + amount, + txid, + }); + const parsed = parsePixPayload(payload ?? ""); + + expect(parsed?.amount).toBe(Number(amount.toFixed(2))); + expect(parsed?.txid).toBe(txid); + }), + ); + }); + + test("should round-trip a dynamic payload", () => { + fc.assert( + fc.property(names, urls, (merchantName, url) => { + const payload = generatePixPayload({ url, merchantName, merchantCity: "BRASILIA" }); + const parsed = parsePixPayload(payload ?? ""); + + expect(parsed?.url).toBe(url); + expect(parsed?.pointOfInitiation).toBe("dynamic"); + expect(parsed?.key).toBeUndefined(); + }), + ); + }); + + test("should return null unless exactly one of the key and the url is given", () => { + fc.assert( + fc.property(names, urls, (merchantName, url) => { + const key = generateCpf(); + const merchantCity = "BRASILIA"; + + expect(generatePixPayload({ key, url, merchantName, merchantCity })).toBeNull(); + expect(generatePixPayload({ merchantName, merchantCity })).toBeNull(); + }), + ); + }); + + test("should fold the merchant name and city down to the lengths the BR Code allows", () => { + fc.assert( + fc.property( + fc.string({ minLength: 1, unit: "grapheme" }), + fc.string({ minLength: 1, unit: "grapheme" }), + (merchantName, merchantCity) => { + const key = generateCpf(); + const payload = generatePixPayload({ key, merchantName, merchantCity }); + + fc.pre(payload !== null); + + const parsed = parsePixPayload(payload ?? ""); + + expect(/^[\u0020-\u007E]{1,25}$/.test(parsed?.merchantName ?? "")).toBe(true); + expect(/^[\u0020-\u007E]{1,15}$/.test(parsed?.merchantCity ?? "")).toBe(true); + }, + ), + ); + }); + }); +}); + +describe("generatePixPayload types", () => { + test("should take Pix payload params and return a string or null", () => { + expectTypeOf(generatePixPayload).parameter(0).toEqualTypeOf(); + expectTypeOf(generatePixPayload).returns.toEqualTypeOf(); + }); + + test("should restrict the params to the documented fields", () => { + expectTypeOf().toEqualTypeOf<{ + key?: string; + url?: string; + merchantName: string; + merchantCity: string; + amount?: number; + txid?: string; + description?: string; + }>(); + }); }); diff --git a/src/generate-processo-juridico/generate-processo-juridico.test.ts b/src/generate-processo-juridico/generate-processo-juridico.test.ts index 772ea8bd..21bae3e9 100644 --- a/src/generate-processo-juridico/generate-processo-juridico.test.ts +++ b/src/generate-processo-juridico/generate-processo-juridico.test.ts @@ -1,7 +1,12 @@ +import * as fc from "fast-check"; + import { PROCESSO_JURIDICO_LENGTH } from "../_internals/constants/processo-juridico"; -import { describe, expect, it } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidProcessoJuridico } from "../is-valid-processo-juridico/is-valid-processo-juridico"; -import { generateProcessoJuridico } from "./generate-processo-juridico"; +import { + generateProcessoJuridico, + type GenerateProcessoJuridicoOptions, +} from "./generate-processo-juridico"; const currentYear = new Date().getFullYear(); @@ -77,4 +82,62 @@ describe("generateProcessoJuridico", () => { Math.random = originalRandom; } }); + + describe("properties", () => { + const year = fc.integer({ min: currentYear, max: 9999 }); + const court = fc.integer({ min: 1, max: 9 }); + + test("should embed every accepted year and court in a valid number", () => { + fc.assert( + fc.property(year, court, (chosenYear, chosenCourt) => { + const value = generateProcessoJuridico({ + year: chosenYear, + court: chosenCourt, + }) as string; + + expect(value).toHaveLength(PROCESSO_JURIDICO_LENGTH); + expect(value.slice(9, 13)).toBe(String(chosenYear)); + expect(value.charAt(13)).toBe(String(chosenCourt)); + expect(isValidProcessoJuridico(value)).toBe(true); + }), + ); + }); + + test("should return null for every year outside the accepted range", () => { + const tooEarly = fc.integer({ min: -9999, max: currentYear - 1 }); + const tooLate = fc.integer({ min: 10_000, max: 999_999 }); + + fc.assert( + fc.property(fc.oneof(tooEarly, tooLate), (invalidYear) => { + expect(generateProcessoJuridico({ year: invalidYear })).toBe(null); + }), + ); + }); + + test("should return null for every court outside 1 to 9", () => { + const invalidCourts = fc + .integer({ min: -100, max: 100 }) + .filter((value) => value < 1 || value > 9); + + fc.assert( + fc.property(invalidCourts, (invalidCourt) => { + expect(generateProcessoJuridico({ court: invalidCourt })).toBe(null); + }), + ); + }); + }); +}); + +describe("generateProcessoJuridico types", () => { + test("should take options and return a string or null", () => { + expectTypeOf(generateProcessoJuridico) + .parameter(0) + .toEqualTypeOf(); + expectTypeOf(generateProcessoJuridico).returns.toEqualTypeOf(); + }); + + test("should type the year and court options as optional numbers", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/generate-voter-id/generate-voter-id.test.ts b/src/generate-voter-id/generate-voter-id.test.ts index 9a06cedd..cead4fe4 100644 --- a/src/generate-voter-id/generate-voter-id.test.ts +++ b/src/generate-voter-id/generate-voter-id.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { type StateCode } from "../_internals/constants/states"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { UF_TO_VOTER_ID_CODE } from "../is-valid-voter-id/constants"; import { isValidVoterId } from "../is-valid-voter-id/is-valid-voter-id"; import { generateVoterId } from "./generate-voter-id"; @@ -21,4 +25,28 @@ describe("generateVoterId", () => { expect(voterId.slice(8, 10)).toBe("28"); expect(isValidVoterId(voterId)).toBe(true); }); + + describe("properties", () => { + const states = Object.keys(UF_TO_VOTER_ID_CODE) as (StateCode | "ZZ")[]; + const stateCode = fc.constantFrom(...states); + + test("should carry the federative union code of every state it supports", () => { + fc.assert( + fc.property(stateCode, (state) => { + const voterId = generateVoterId(state); + + expect(voterId).toMatch(/^\d{12}$/); + expect(voterId.slice(8, 10)).toBe(UF_TO_VOTER_ID_CODE[state]); + expect(isValidVoterId(voterId)).toBe(true); + }), + ); + }); + }); +}); + +describe("generateVoterId types", () => { + test("should take an optional state code or ZZ and return a string", () => { + expectTypeOf(generateVoterId).parameter(0).toEqualTypeOf(); + expectTypeOf(generateVoterId).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts index 7f855edf..4c37a759 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.test.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.test.ts @@ -1,6 +1,16 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "../_internals/test/runtime"; +import { + afterEach, + beforeEach, + describe, + expect, + expectTypeOf, + it, + vi, +} from "../_internals/test/runtime"; import { type AddressInfo, + type CepProvider, + type GetAddressInfoByCepOptions, GetAddressInfoByCepError, GetAddressInfoByCepNotFoundError, GetAddressInfoByCepServiceError, @@ -735,3 +745,31 @@ describe("getAddressInfoByCep", () => { ); }); }); + +describe("getAddressInfoByCep types", () => { + it("should take a string or number CEP, optional providers, and resolve to an AddressInfo", () => { + expectTypeOf(getAddressInfoByCep).parameter(0).toEqualTypeOf(); + expectTypeOf(getAddressInfoByCep) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + CepProvider[] | undefined + >(); + expectTypeOf().toEqualTypeOf<"viacep" | "widenet" | "brasilapi">(); + expectTypeOf(getAddressInfoByCep).returns.resolves.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + cep: string; + state: string; + city: string; + neighborhood: string; + street: string; + }>(); + }); + + it("should expose error classes that extend the base error", () => { + expectTypeOf(new GetAddressInfoByCepNotFoundError("m")).toExtend(); + expectTypeOf(new GetAddressInfoByCepServiceError("m")).toExtend(); + expectTypeOf(new GetAddressInfoByCepValidationError("m")).toExtend(); + expectTypeOf(new GetAddressInfoByCepError("m")).toExtend(); + }); +}); diff --git a/src/get-area-code-info/get-area-code-info.test.ts b/src/get-area-code-info/get-area-code-info.test.ts index 5b83c377..da452f30 100644 --- a/src/get-area-code-info/get-area-code-info.test.ts +++ b/src/get-area-code-info/get-area-code-info.test.ts @@ -1,5 +1,12 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { getAreaCodeInfo } from "./get-area-code-info"; +import * as fc from "fast-check"; + +import { VALID_AREA_CODES } from "../_internals/constants/area-codes"; +import { type StateCode, type StateName } from "../_internals/constants/states"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getAreaCodesByState } from "../get-area-codes-by-state/get-area-codes-by-state"; +import { getAreaCodeInfo, type AreaCodeInfo } from "./get-area-code-info"; describe("getAreaCodeInfo", () => { it("should resolve DDD 11 to São Paulo, Sudeste, from a string", () => { @@ -94,4 +101,44 @@ describe("getAreaCodeInfo", () => { // @ts-expect-error: intentionally invalid input expect(getAreaCodeInfo()).toBeNull(); }); + + describe("properties", () => { + const areaCodeArbitrary = fc.constantFrom(...VALID_AREA_CODES); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(getAreaCodeInfo, anyGarbage); + }); + + test("should resolve every valid DDD back to a state that lists it", () => { + fc.assert( + fc.property(areaCodeArbitrary, (areaCode) => { + const info = getAreaCodeInfo(areaCode); + + expect(info).not.toBeNull(); + expect(getAreaCodesByState(info?.stateCode ?? "")).toContain(areaCode); + }), + ); + }); + + test("should resolve the same DDD whether given as a string or a number", () => { + fc.assert( + fc.property(areaCodeArbitrary, (areaCode) => { + expect(getAreaCodeInfo(String(areaCode))).toEqual(getAreaCodeInfo(areaCode)); + }), + ); + }); + }); +}); + +describe("getAreaCodeInfo types", () => { + test("should take a string or number and return an AreaCodeInfo or null", () => { + expectTypeOf(getAreaCodeInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getAreaCodeInfo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + areaCode: number; + stateCode: StateCode; + stateName: StateName; + region: "Norte" | "Nordeste" | "Centro-Oeste" | "Sudeste" | "Sul"; + }>(); + }); }); diff --git a/src/get-area-codes-by-state/get-area-codes-by-state.test.ts b/src/get-area-codes-by-state/get-area-codes-by-state.test.ts index 96d89708..80d636ed 100644 --- a/src/get-area-codes-by-state/get-area-codes-by-state.test.ts +++ b/src/get-area-codes-by-state/get-area-codes-by-state.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { stateCodes } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { getAreaCodeInfo } from "../get-area-code-info/get-area-code-info"; import { getAreaCodesByState } from "./get-area-codes-by-state"; describe("getAreaCodesByState", () => { @@ -57,4 +62,42 @@ describe("getAreaCodesByState", () => { expect(getAreaCodesByState(11)).toEqual([]); }); }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrows(getAreaCodesByState, fc.anything()); + }); + + test("should return every DDD sorted ascending, each resolving back to the same state", () => { + fc.assert( + fc.property(stateCodes, (stateCode) => { + const areaCodes = getAreaCodesByState(stateCode); + const sorted = [...areaCodes].sort((a, b) => a - b); + + expect(areaCodes).toEqual(sorted); + + for (const areaCode of areaCodes) { + expect(getAreaCodeInfo(areaCode)?.stateCode).toBe(stateCode); + } + }), + ); + }); + + test("should be case-insensitive", () => { + fc.assert( + fc.property(stateCodes, (stateCode) => { + expect(getAreaCodesByState(stateCode.toLowerCase())).toEqual( + getAreaCodesByState(stateCode), + ); + }), + ); + }); + }); +}); + +describe("getAreaCodesByState types", () => { + test("should take a string and return an array of numbers", () => { + expectTypeOf(getAreaCodesByState).parameter(0).toEqualTypeOf(); + expectTypeOf(getAreaCodesByState).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-bank-by-code/get-bank-by-code.test.ts b/src/get-bank-by-code/get-bank-by-code.test.ts index 8bd70d3c..b9bfa9c3 100644 --- a/src/get-bank-by-code/get-bank-by-code.test.ts +++ b/src/get-bank-by-code/get-bank-by-code.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { BANKS, type Bank } from "../_internals/constants/banks"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { getBankByCode } from "./get-bank-by-code"; describe("getBankByCode", () => { @@ -104,4 +107,46 @@ describe("getBankByCode", () => { expect(getBankByCode([1])).toBeNull(); }); }); + + describe("properties", () => { + const banks = fc.constantFrom(...BANKS); + + test("should find every bank of the table by its COMPE code", () => { + fc.assert( + fc.property(banks, (bank) => { + expect(getBankByCode(bank.code)?.code).toBe(bank.code); + expect(getBankByCode(Number(bank.code))?.code).toBe(bank.code); + }), + ); + }); + + test("should hand out a copy that cannot change the table", () => { + fc.assert( + fc.property(banks, (bank) => { + const found = getBankByCode(bank.code); + + expect(found).not.toBe(bank); + expect(found?.name).toBe(bank.name); + }), + ); + }); + + test("should never throw and always return a bank or null for a code", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const found = getBankByCode(value as string); + + expect(found === null || typeof found.code === "string").toBe(true); + }), + ); + }); + }); +}); + +describe("getBankByCode types", () => { + test("should take a string or number and return a bank or null", () => { + expectTypeOf(getBankByCode).parameter(0).toEqualTypeOf(); + expectTypeOf(getBankByCode).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ code: string; ispb: string; name: string }>(); + }); }); diff --git a/src/get-bank-by-ispb/get-bank-by-ispb.test.ts b/src/get-bank-by-ispb/get-bank-by-ispb.test.ts index 55187c08..4d321180 100644 --- a/src/get-bank-by-ispb/get-bank-by-ispb.test.ts +++ b/src/get-bank-by-ispb/get-bank-by-ispb.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { BANKS, type Bank } from "../_internals/constants/banks"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { getBankByIspb } from "./get-bank-by-ispb"; describe("getBankByIspb", () => { @@ -108,4 +111,43 @@ describe("getBankByIspb", () => { expect(getBankByIspb([0])).toBeNull(); }); }); + + describe("properties", () => { + const banks = fc.constantFrom(...BANKS); + + test("should find a participant for the ISPB of every bank of the table", () => { + fc.assert( + fc.property(banks, (bank) => { + expect(getBankByIspb(bank.ispb)?.ispb).toBe(bank.ispb); + expect(getBankByIspb(Number(bank.ispb))?.ispb).toBe(bank.ispb); + }), + ); + }); + + test("should return null for an ISPB longer than the eight digits of the register", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{9,20}$/), (value) => { + expect(getBankByIspb(value)).toBeNull(); + }), + ); + }); + + test("should never throw and always return a bank or null for an ISPB", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const found = getBankByIspb(value as string); + + expect(found === null || typeof found.ispb === "string").toBe(true); + }), + ); + }); + }); +}); + +describe("getBankByIspb types", () => { + test("should take a string or number and return a bank or null", () => { + expectTypeOf(getBankByIspb).parameter(0).toEqualTypeOf(); + expectTypeOf(getBankByIspb).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ code: string; ispb: string; name: string }>(); + }); }); diff --git a/src/get-banks/get-banks.test.ts b/src/get-banks/get-banks.test.ts index aa2db929..8f879461 100644 --- a/src/get-banks/get-banks.test.ts +++ b/src/get-banks/get-banks.test.ts @@ -1,5 +1,7 @@ -import { BANKS } from "../_internals/constants/banks"; -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { BANKS, type Bank } from "../_internals/constants/banks"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { getBanks } from "./get-banks"; describe("getBanks", () => { @@ -40,4 +42,39 @@ describe("getBanks", () => { expect(getBanks().at(0)?.name).not.toBe("mutated"); }); + + describe("properties", () => { + const indexes = fc.nat({ max: BANKS.length - 1 }); + + test("should describe every bank with a COMPE code, an ISPB and a name", () => { + fc.assert( + fc.property(indexes, (index) => { + const bank = getBanks()[index]; + + expect(/^\d{3}$/.test(bank.code)).toBe(true); + expect(/^\d{8}$/.test(bank.ispb)).toBe(true); + expect(bank.name.length).toBeGreaterThan(0); + }), + ); + }); + + test("should hand out a fresh copy on every call", () => { + fc.assert( + fc.property(indexes, (index) => { + const bank = getBanks()[index]; + + bank.name = "changed"; + + expect(getBanks()[index].name).toBe(BANKS[index].name); + }), + ); + }); + }); +}); + +describe("getBanks types", () => { + test("should take no parameters and return an array of banks", () => { + expectTypeOf(getBanks).parameters.toEqualTypeOf<[]>(); + expectTypeOf(getBanks).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-boleto-info/get-boleto-info.test.ts b/src/get-boleto-info/get-boleto-info.test.ts index d57a7b81..adf592b7 100644 --- a/src/get-boleto-info/get-boleto-info.test.ts +++ b/src/get-boleto-info/get-boleto-info.test.ts @@ -1,5 +1,9 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { getBoletoInfo } from "./get-boleto-info"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generateBoleto } from "../generate-boleto/generate-boleto"; +import { isValidBoleto } from "../is-valid-boleto/is-valid-boleto"; +import { type BoletoInfo, type GetBoletoInfoOptions, getBoletoInfo } from "./get-boleto-info"; const withFactor = { "0000": "00190000090114971860168524522114100000000102656", @@ -155,4 +159,74 @@ describe("getBoletoInfo", () => { ).toBe(false); }); }); + + describe("properties", () => { + test("should read the bank code and the amount of a generated bank slip", () => { + fc.assert( + fc.property(fc.date({ noInvalidDate: true }), (referenceDate) => { + const value = generateBoleto(); + const info = getBoletoInfo(value, { referenceDate }); + + expect(info?.bankCode).toBe(value.slice(0, 3)); + expect(info?.amount).toBe(Number(value.slice(37, 47))); + expect(info?.type).toBeUndefined(); + }), + ); + }); + + test("should describe a generated arrecadação bank slip", () => { + fc.assert( + fc.property(fc.constant("arrecadacao" as const), (type) => { + const info = getBoletoInfo(generateBoleto({ type })); + + expect(info?.type).toBe("arrecadacao"); + expect(info?.bankCode).toBe(""); + expect(info?.expirationDate).toBeNull(); + expect(info?.value).toBe((info?.amount ?? 0) / 100); + }), + ); + }); + + test("should return a value exactly when the bank slip is valid", () => { + fc.assert( + fc.property(fc.string(), (value) => { + expect(getBoletoInfo(value) !== undefined).toBe(isValidBoleto(value)); + }), + ); + }); + + test("should never throw and always return an object or undefined", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const info = getBoletoInfo(value as string); + + expect(info === undefined || typeof info === "object").toBe(true); + }), + ); + }); + }); +}); + +describe("getBoletoInfo types", () => { + test("should take a string, optional options, and return boleto info or undefined", () => { + expectTypeOf(getBoletoInfo).parameter(0).toEqualTypeOf(); + expectTypeOf(getBoletoInfo).parameter(1).toEqualTypeOf(); + expectTypeOf(getBoletoInfo).returns.toEqualTypeOf(); + }); + + test("should restrict referenceDate to a Date", () => { + expectTypeOf().toEqualTypeOf(); + }); + + test("should restrict the boleto info shape", () => { + expectTypeOf().toEqualTypeOf<{ + amount: number; + expirationDate: Date | null; + bankCode: string; + type?: "arrecadacao"; + segment?: number; + value?: number; + hasEffectiveValue?: boolean; + }>(); + }); }); diff --git a/src/get-cbo/get-cbo.test.ts b/src/get-cbo/get-cbo.test.ts index e2b964ff..ce8c3e95 100644 --- a/src/get-cbo/get-cbo.test.ts +++ b/src/get-cbo/get-cbo.test.ts @@ -1,5 +1,11 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { getCbo } from "./get-cbo"; +import * as fc from "fast-check"; + +import { CBO_TITLES } from "../_internals/constants/cbo"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { isValidCbo } from "../is-valid-cbo/is-valid-cbo"; +import { getCbo, type Cbo } from "./get-cbo"; describe("getCbo", () => { it("should return the occupation for a code without a mask", () => { @@ -57,4 +63,40 @@ describe("getCbo", () => { it("should return null for whitespace only", () => { expect(getCbo(" ")).toBeNull(); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CBO_TITLES)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(getCbo, anyGarbage); + }); + + test("should resolve every known code, as a string or a number, and agree with isValidCbo", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + expect(getCbo(code)).toEqual({ code, title: CBO_TITLES[code] }); + expect(getCbo(Number(code))).toEqual({ code, title: CBO_TITLES[code] }); + expect(isValidCbo(code)).toBe(true); + }), + ); + }); + + test("should resolve every known code with the hyphen mask", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + const masked = `${code.slice(0, 4)}-${code.slice(4)}`; + + expect(getCbo(masked)).toEqual({ code, title: CBO_TITLES[code] }); + }), + ); + }); + }); +}); + +describe("getCbo types", () => { + test("should take a string or number and return a Cbo or null", () => { + expectTypeOf(getCbo).parameter(0).toEqualTypeOf(); + expectTypeOf(getCbo).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ code: string; title: string }>(); + }); }); diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.test.ts b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts index 357d4886..d0b44485 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.test.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts @@ -1,6 +1,16 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "../_internals/test/runtime"; import { + afterEach, + beforeEach, + describe, + expect, + expectTypeOf, + it, + vi, +} from "../_internals/test/runtime"; +import { + type CepAddressInfo, GetCepInfoByAddressError, + type GetCepInfoByAddressOptions, GetCepInfoByAddressNotFoundError, GetCepInfoByAddressValidationError, getCepInfoByAddress, @@ -206,3 +216,21 @@ describe("getCepInfoByAddress", () => { expect(fetchMock).toHaveBeenCalledTimes(3); }); }); + +describe("getCepInfoByAddress types", () => { + it("should take the address options and resolve to a list of CepAddressInfo", () => { + expectTypeOf(getCepInfoByAddress).parameter(0).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + federalUnit: string; + city: string; + street: string; + }>(); + expectTypeOf(getCepInfoByAddress).returns.resolves.toEqualTypeOf(); + }); + + it("should expose error classes that extend the base error", () => { + expectTypeOf(new GetCepInfoByAddressNotFoundError("m")).toExtend(); + expectTypeOf(new GetCepInfoByAddressValidationError("m")).toExtend(); + expectTypeOf(new GetCepInfoByAddressError("m")).toExtend(); + }); +}); diff --git a/src/get-cfop/get-cfop.test.ts b/src/get-cfop/get-cfop.test.ts index 4e03cf1f..297b4b43 100644 --- a/src/get-cfop/get-cfop.test.ts +++ b/src/get-cfop/get-cfop.test.ts @@ -1,5 +1,11 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { getCfop } from "./get-cfop"; +import * as fc from "fast-check"; + +import { CFOP_TABLE } from "../_internals/constants/cfop"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { isValidCfop } from "../is-valid-cfop/is-valid-cfop"; +import { getCfop, type Cfop } from "./get-cfop"; describe("getCfop", () => { it("should return the CFOP entry for a known code as a string", () => { @@ -72,4 +78,32 @@ describe("getCfop", () => { // @ts-expect-error not a string or number expect(getCfop()).toBeNull(); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CFOP_TABLE)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(getCfop, anyGarbage); + }); + + test("should resolve every known code, as a string or a number, and agree with isValidCfop", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + const expected = { code, description: CFOP_TABLE[code] }; + + expect(getCfop(code)).toEqual(expected); + expect(getCfop(Number(code))).toEqual(expected); + expect(isValidCfop(code)).toBe(true); + }), + ); + }); + }); +}); + +describe("getCfop types", () => { + test("should take a string or number and return a Cfop or null", () => { + expectTypeOf(getCfop).parameter(0).toEqualTypeOf(); + expectTypeOf(getCfop).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ code: string; description: string }>(); + }); }); diff --git a/src/get-cities/get-cities.test.ts b/src/get-cities/get-cities.test.ts index 1f670817..be93e5f3 100644 --- a/src/get-cities/get-cities.test.ts +++ b/src/get-cities/get-cities.test.ts @@ -1,5 +1,10 @@ +import * as fc from "fast-check"; + import { DATA } from "../_internals/constants/cities"; -import { describe, expect, it } from "../_internals/test/runtime"; +import { type StateCode } from "../_internals/constants/states"; +import { anyGarbage, stateCodes } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { getStates } from "../get-states/get-states"; import { getCities } from "./get-cities"; @@ -97,4 +102,29 @@ describe("getCities", () => { expect(getCities()).not.toContain("Cidade Inexistente"); expect(getCities()).toHaveLength(first.length - 1); }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrows(getCities, anyGarbage); + }); + + test("should return, for every state, only cities that are also in the combined list", () => { + const allCities = new Set(getCities()); + + fc.assert( + fc.property(stateCodes, (stateCode) => { + for (const city of getCities(stateCode)) { + expect(allCities.has(city)).toBe(true); + } + }), + ); + }); + }); +}); + +describe("getCities types", () => { + test("should take an optional StateCode and return an array of strings", () => { + expectTypeOf(getCities).parameter(0).toEqualTypeOf(); + expectTypeOf(getCities).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-cnae/get-cnae.test.ts b/src/get-cnae/get-cnae.test.ts index 8dcaa390..2a987fed 100644 --- a/src/get-cnae/get-cnae.test.ts +++ b/src/get-cnae/get-cnae.test.ts @@ -1,5 +1,12 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { getCnae } from "./get-cnae"; +import * as fc from "fast-check"; + +import { CNAE_SUBCLASSES } from "../_internals/constants/cnae"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatCnae } from "../format-cnae/format-cnae"; +import { isValidCnae } from "../is-valid-cnae/is-valid-cnae"; +import { getCnae, type Cnae } from "./get-cnae"; describe("getCnae", () => { it("should return the CNAE entry for a known code as a string", () => { @@ -52,4 +59,32 @@ describe("getCnae", () => { // @ts-expect-error not a string or number expect(getCnae()).toBeNull(); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CNAE_SUBCLASSES)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(getCnae, anyGarbage); + }); + + test("should resolve every known code, as a string or a number, and agree with formatCnae and isValidCnae", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + const expected = { code: formatCnae(code), description: CNAE_SUBCLASSES[code] }; + + expect(getCnae(code)).toEqual(expected); + expect(getCnae(Number(code))).toEqual(expected); + expect(isValidCnae(code)).toBe(true); + }), + ); + }); + }); +}); + +describe("getCnae types", () => { + test("should take a string or number and return a Cnae or null", () => { + expectTypeOf(getCnae).parameter(0).toEqualTypeOf(); + expectTypeOf(getCnae).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ code: string; description: string }>(); + }); }); diff --git a/src/get-format-license-plate/get-format-license-plate.test.ts b/src/get-format-license-plate/get-format-license-plate.test.ts index e4000ffe..cf3f122b 100644 --- a/src/get-format-license-plate/get-format-license-plate.test.ts +++ b/src/get-format-license-plate/get-format-license-plate.test.ts @@ -1,5 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { getFormatLicensePlate } from "./get-format-license-plate"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { generateLicensePlate } from "../generate-license-plate/generate-license-plate"; +import { type LicensePlateFormat, getFormatLicensePlate } from "./get-format-license-plate"; describe("getFormatLicensePlate", () => { it("should identify supported formats", () => { @@ -18,4 +21,38 @@ describe("getFormatLicensePlate", () => { it("should return null when the value does not match any supported format", () => { expect(getFormatLicensePlate("invalid")).toBeNull(); }); + + describe("properties", () => { + test("should name the format of every generated plate", () => { + fc.assert( + fc.property(fc.constantFrom("LLLNNNN", "LLLNLNN"), (format) => { + const plate = generateLicensePlate(format); + + expect(getFormatLicensePlate(plate)).toBe(format); + expect(getFormatLicensePlate(plate.toLowerCase())).toBe(format); + }), + ); + }); + + test("should never throw and always return a known format or null", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const format = getFormatLicensePlate(value as string); + + expect(format === null || format === "LLLNNNN" || format === "LLLNLNN").toBe(true); + }), + ); + }); + }); +}); + +describe("getFormatLicensePlate types", () => { + test("should take a string and return a license plate format or null", () => { + expectTypeOf(getFormatLicensePlate).parameter(0).toEqualTypeOf(); + expectTypeOf(getFormatLicensePlate).returns.toEqualTypeOf(); + }); + + test("should restrict the format to the supported license plate formats", () => { + expectTypeOf().toEqualTypeOf<"LLLNNNN" | "LLLNLNN">(); + }); }); diff --git a/src/get-holidays/get-holidays.test.ts b/src/get-holidays/get-holidays.test.ts index 315e1450..c75be584 100644 --- a/src/get-holidays/get-holidays.test.ts +++ b/src/get-holidays/get-holidays.test.ts @@ -1,6 +1,15 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; +import { DATA as STATES, type StateCode } from "../_internals/constants/states"; +import { bench, describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { isBusinessDay } from "../is-business-day/is-business-day"; import { STATE_HOLIDAYS } from "./constants"; -import { getHolidays } from "./get-holidays"; +import { getHolidays, type GetHolidaysOptions, type Holiday } from "./get-holidays"; + +function getHolidaysFor(year: number, stateCode: StateCode | null): Holiday[] { + return stateCode === null ? getHolidays(year) : getHolidays({ year, stateCode }); +} describe("getHolidays", () => { test("should return fixed holidays for the given year", () => { @@ -431,4 +440,74 @@ describe("getHolidays", () => { } } }); + + describe("properties", () => { + const yearArbitrary = fc.integer({ min: HOLIDAYS_MIN_YEAR, max: HOLIDAYS_MAX_YEAR }); + const stateCodeArbitrary = fc.constantFrom(...STATES.map((state) => state.code)); + + test("should place every holiday within the requested year", () => { + fc.assert( + fc.property(yearArbitrary, fc.option(stateCodeArbitrary), (year, stateCode) => { + const holidays = getHolidaysFor(year, stateCode); + + for (const holiday of holidays) { + expect(holiday.date.getFullYear()).toBe(year); + } + }), + ); + }); + + test("should return holidays sorted by date, ascending", () => { + fc.assert( + fc.property(yearArbitrary, fc.option(stateCodeArbitrary), (year, stateCode) => { + const holidays = getHolidaysFor(year, stateCode); + const timestamps = holidays.map((holiday) => holiday.date.getTime()); + const sorted = [...timestamps].sort((a, b) => a - b); + + expect(timestamps).toEqual(sorted); + }), + ); + }); + + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(() => getHolidays(value as never)).not.toThrow(); + }), + ); + }); + }); +}); + +describe("getHolidays types", () => { + test("should accept a year and return an array of Holiday", () => { + expectTypeOf(getHolidays(2024)).toEqualTypeOf(); + }); + + test("should accept a GetHolidaysOptions and return an array of Holiday", () => { + expectTypeOf().toEqualTypeOf<{ year: number; stateCode?: StateCode }>(); + expectTypeOf(getHolidays({ year: 2024, stateCode: "SP" })).toEqualTypeOf(); + }); + + test("should shape Holiday as name, date and type", () => { + expectTypeOf().toEqualTypeOf<{ + name: string; + date: Date; + type: "national" | "state" | "optional" | "religious"; + }>(); + }); +}); + +describe("getHolidays benchmarks", () => { + bench("getHolidays, national", () => { + getHolidays({ year: 2026 }); + }); + + bench("getHolidays, with state", () => { + getHolidays({ year: 2026, stateCode: "SP" }); + }); + + bench("isBusinessDay", () => { + isBusinessDay(new Date(2026, 6, 9), { stateCode: "SP" }); + }); }); diff --git a/src/get-legal-nature/get-legal-nature.test.ts b/src/get-legal-nature/get-legal-nature.test.ts index 79d97d7a..ddfa6d6b 100644 --- a/src/get-legal-nature/get-legal-nature.test.ts +++ b/src/get-legal-nature/get-legal-nature.test.ts @@ -1,5 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { getLegalNature } from "./get-legal-nature"; +import * as fc from "fast-check"; + +import { anyValue, digitsUpTo } from "../_internals/test/arbitraries"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; +import { isValidLegalNature } from "../is-valid-legal-nature/is-valid-legal-nature"; +import { getLegalNature, type LegalNature } from "./get-legal-nature"; describe("getLegalNature", () => { it("should reject a code with letters attached, like isValidLegalNature does", () => { @@ -59,4 +64,50 @@ describe("getLegalNature", () => { // @ts-expect-error not a string or number expect(getLegalNature()).toBeNull(); }); + + describe("properties", () => { + const knownCode = fc.constantFrom(...Object.keys(LEGAL_NATURE)); + + test("should look every code of the table up, masked, plain or numeric", () => { + fc.assert( + fc.property(knownCode, (code) => { + const entry = { code, description: LEGAL_NATURE[code] }; + + expect(getLegalNature(code)).toEqual(entry); + expect(getLegalNature(`${code.slice(0, 3)}-${code.slice(3)}`)).toEqual(entry); + expect(getLegalNature(Number(code))).toEqual(entry); + }), + ); + }); + + test("should agree with isValidLegalNature on every digits only value", () => { + fc.assert( + fc.property(digitsUpTo(6), (value) => { + expect(getLegalNature(value) !== null).toBe(isValidLegalNature(value)); + }), + ); + }); + + test("should never throw and always return null or an entry of the table", () => { + fc.assert( + fc.property(anyValue, (value) => { + const result = getLegalNature(value as string); + + expect(result === null || LEGAL_NATURE[result.code] === result.description).toBe(true); + }), + ); + }); + }); +}); + +describe("getLegalNature types", () => { + test("should take a string or number value and return a legal nature entry or null", () => { + expectTypeOf(getLegalNature).parameter(0).toEqualTypeOf(); + expectTypeOf(getLegalNature).returns.toEqualTypeOf(); + }); + + test("should type the legal nature entry fields as strings", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/get-legal-natures/get-legal-natures.test.ts b/src/get-legal-natures/get-legal-natures.test.ts index 3c3b479c..7cf617ac 100644 --- a/src/get-legal-natures/get-legal-natures.test.ts +++ b/src/get-legal-natures/get-legal-natures.test.ts @@ -1,6 +1,12 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getLegalNature } from "../get-legal-nature/get-legal-nature"; +import { isValidLegalNature } from "../is-valid-legal-nature/is-valid-legal-nature"; import { getLegalNatures } from "./get-legal-natures"; +const knownCode = (): fc.Arbitrary => fc.constantFrom(...Object.keys(getLegalNatures())); + describe("getLegalNatures", () => { it("should return legal nature entries", () => { expect(getLegalNatures()["2062"]).toBe("Sociedade Empresária Limitada"); @@ -18,4 +24,40 @@ describe("getLegalNatures", () => { expect(getLegalNatures()["2062"]).toBe("Sociedade Empresária Limitada"); }); + + describe("properties", () => { + const anyKey = fc.string(); + + test("should expose only codes its own validator and lookup accept", () => { + fc.assert( + fc.property(knownCode(), (code) => { + const entry = { code, description: getLegalNatures()[code] }; + + expect(code).toMatch(/^\d{4}$/); + expect(isValidLegalNature(code)).toBe(true); + expect(getLegalNature(code)).toEqual(entry); + }), + ); + }); + + test("should return a fresh object that a caller cannot mutate", () => { + fc.assert( + fc.property(anyKey, fc.string(), (code, description) => { + const natures = getLegalNatures(); + const before = natures[code]; + + natures[code] = description; + + expect(getLegalNatures()[code]).toBe(before); + }), + ); + }); + }); +}); + +describe("getLegalNatures types", () => { + test("should take no parameters and return a record of strings", () => { + expectTypeOf(getLegalNatures).parameters.toEqualTypeOf<[]>(); + expectTypeOf(getLegalNatures).returns.toEqualTypeOf>(); + }); }); diff --git a/src/get-municipalities/get-municipalities.test.ts b/src/get-municipalities/get-municipalities.test.ts index b50d17d9..18f7af21 100644 --- a/src/get-municipalities/get-municipalities.test.ts +++ b/src/get-municipalities/get-municipalities.test.ts @@ -1,6 +1,10 @@ -import { DATA } from "../_internals/constants/cities"; +import * as fc from "fast-check"; + +import { DATA, type Municipality } from "../_internals/constants/cities"; import { type StateCode } from "../_internals/constants/states"; -import { describe, expect, it } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getCities } from "../get-cities/get-cities"; +import { getMunicipalityByCode } from "../get-municipality-by-code/get-municipality-by-code"; import { getStates } from "../get-states/get-states"; import { getMunicipalities } from "./get-municipalities"; @@ -103,4 +107,48 @@ describe("getMunicipalities", () => { }); } }); + + describe("properties", () => { + const stateCodeArbitrary = fc.constantFrom(...getStates().map((state) => state.code)); + + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(() => getMunicipalities(value as never)).not.toThrow(); + }), + ); + }); + + test("should list, for every state, the same names and order as getCities", () => { + fc.assert( + fc.property(stateCodeArbitrary, (stateCode) => { + const names = getMunicipalities(stateCode).map((municipality) => municipality.name); + + expect(names).toEqual(getCities(stateCode)); + }), + ); + }); + + test("should have every municipality resolve back to itself through getMunicipalityByCode", () => { + const municipalityArbitrary = fc.constantFrom(...getMunicipalities()); + + fc.assert( + fc.property(municipalityArbitrary, (municipality) => { + expect(getMunicipalityByCode(municipality.code)).toEqual(municipality); + }), + ); + }); + }); +}); + +describe("getMunicipalities types", () => { + test("should take an optional string and return an array of Municipality", () => { + expectTypeOf(getMunicipalities).parameter(0).toEqualTypeOf(); + expectTypeOf(getMunicipalities).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + code: string; + name: string; + stateCode: StateCode; + }>(); + }); }); diff --git a/src/get-municipality-by-code/get-municipality-by-code.test.ts b/src/get-municipality-by-code/get-municipality-by-code.test.ts index cad63aae..faa1a4e8 100644 --- a/src/get-municipality-by-code/get-municipality-by-code.test.ts +++ b/src/get-municipality-by-code/get-municipality-by-code.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { type Municipality } from "../_internals/constants/cities"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getMunicipalities } from "../get-municipalities/get-municipalities"; import { getMunicipalityByCode } from "./get-municipality-by-code"; describe("getMunicipalityByCode", () => { @@ -74,4 +78,32 @@ describe("getMunicipalityByCode", () => { stateCode: "SP", }); }); + + describe("properties", () => { + const municipalityArbitrary = fc.constantFrom(...getMunicipalities()); + + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(() => getMunicipalityByCode(value as never)).not.toThrow(); + }), + ); + }); + + test("should resolve every known municipality code, as a string or as a number", () => { + fc.assert( + fc.property(municipalityArbitrary, (municipality) => { + expect(getMunicipalityByCode(municipality.code)).toEqual(municipality); + expect(getMunicipalityByCode(Number(municipality.code))).toEqual(municipality); + }), + ); + }); + }); +}); + +describe("getMunicipalityByCode types", () => { + test("should take a string or number and return a Municipality or null", () => { + expectTypeOf(getMunicipalityByCode).parameter(0).toEqualTypeOf(); + expectTypeOf(getMunicipalityByCode).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-municipality/get-municipality.test.ts b/src/get-municipality/get-municipality.test.ts index 6c0c20a3..f53bc536 100644 --- a/src/get-municipality/get-municipality.test.ts +++ b/src/get-municipality/get-municipality.test.ts @@ -1,5 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { type GetMunicipalityByNameOptions, getMunicipality } from "./get-municipality"; +import { describe, expect, expectTypeOf, it } from "../_internals/test/runtime"; +import { + type GetMunicipalityByCodeOptions, + type GetMunicipalityByNameOptions, + type GetMunicipalityOptions, + getMunicipality, +} from "./get-municipality"; describe("getMunicipality", () => { it("should get municipality code by name", async () => { @@ -163,3 +168,20 @@ describe("getMunicipality", () => { }); }); }); + +describe("getMunicipality types", () => { + it("should take a code or a name plus uf and resolve to a pair, a name or null", () => { + expectTypeOf(getMunicipality).parameter(0).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + GetMunicipalityByCodeOptions | GetMunicipalityByNameOptions + >(); + expectTypeOf().toEqualTypeOf<{ code: string }>(); + expectTypeOf().toEqualTypeOf<{ + municipalityName: string; + uf: string; + }>(); + expectTypeOf(getMunicipality).returns.resolves.toEqualTypeOf< + [string, string] | string | null + >(); + }); +}); diff --git a/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts b/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts index fbe51334..c8dfd23e 100644 --- a/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts +++ b/src/get-state-by-ibge-code/get-state-by-ibge-code.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { DATA as STATES, type State } from "../_internals/constants/states"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { getStateByIbgeCode } from "./get-state-by-ibge-code"; describe("getStateByIbgeCode", () => { @@ -67,4 +72,35 @@ describe("getStateByIbgeCode", () => { it("should ignore non-digit characters around the code", () => { expect(getStateByIbgeCode(" 35 ")?.code).toBe("SP"); }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrows(getStateByIbgeCode, anyGarbage); + }); + + test("should resolve every known ibgeCode regardless of surrounding non-digit noise", () => { + const knownIbgeCodeArbitrary = fc.constantFrom(...STATES.map((state) => state.ibgeCode)); + const noiseArbitrary = fc + .array(fc.constantFrom(" ", "-", ".", "/", "R", "$", "a", "Z")) + .map((characters) => characters.join("")); + + fc.assert( + fc.property( + knownIbgeCodeArbitrary, + noiseArbitrary, + noiseArbitrary, + (ibgeCode, prefix, suffix) => { + expect(getStateByIbgeCode(`${prefix}${ibgeCode}${suffix}`)?.ibgeCode).toBe(ibgeCode); + }, + ), + ); + }); + }); +}); + +describe("getStateByIbgeCode types", () => { + test("should take a string or number and return a State or null", () => { + expectTypeOf(getStateByIbgeCode).parameter(0).toEqualTypeOf(); + expectTypeOf(getStateByIbgeCode).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-state-code-by-name/get-state-code-by-name.test.ts b/src/get-state-code-by-name/get-state-code-by-name.test.ts index 278137db..01c34c1d 100644 --- a/src/get-state-code-by-name/get-state-code-by-name.test.ts +++ b/src/get-state-code-by-name/get-state-code-by-name.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { DATA as STATES, type StateCode } from "../_internals/constants/states"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { getStateCodeByName } from "./get-state-code-by-name"; describe("getStateCodeByName", () => { @@ -62,4 +66,30 @@ describe("getStateCodeByName", () => { // @ts-expect-error: intentionally invalid input expect(getStateCodeByName(35)).toBeNull(); }); + + describe("properties", () => { + const stateArbitrary = fc.constantFrom(...STATES); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(getStateCodeByName, fc.anything()); + }); + + test("should resolve every known state name regardless of case, accents or padding", () => { + fc.assert( + fc.property(stateArbitrary, fc.boolean(), fc.boolean(), (state, upper, pad) => { + const cased = upper ? state.name.toUpperCase() : state.name.toLowerCase(); + const padded = pad ? ` ${cased} ` : cased; + + expect(getStateCodeByName(padded)).toBe(state.code); + }), + ); + }); + }); +}); + +describe("getStateCodeByName types", () => { + test("should take a string and return a StateCode or null", () => { + expectTypeOf(getStateCodeByName).parameter(0).toEqualTypeOf(); + expectTypeOf(getStateCodeByName).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-state-name-by-code/get-state-name-by-code.test.ts b/src/get-state-name-by-code/get-state-name-by-code.test.ts index c6d219dd..6bb39bf2 100644 --- a/src/get-state-name-by-code/get-state-name-by-code.test.ts +++ b/src/get-state-name-by-code/get-state-name-by-code.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { DATA as STATES, type StateName } from "../_internals/constants/states"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { getStateNameByCode } from "./get-state-name-by-code"; describe("getStateNameByCode", () => { @@ -49,4 +53,30 @@ describe("getStateNameByCode", () => { // @ts-expect-error: intentionally invalid input expect(getStateNameByCode(11)).toBeNull(); }); + + describe("properties", () => { + const stateArbitrary = fc.constantFrom(...STATES); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(getStateNameByCode, fc.anything()); + }); + + test("should resolve every known state code regardless of case or padding", () => { + fc.assert( + fc.property(stateArbitrary, fc.boolean(), fc.boolean(), (state, upper, pad) => { + const cased = upper ? state.code.toUpperCase() : state.code.toLowerCase(); + const padded = pad ? ` ${cased} ` : cased; + + expect(getStateNameByCode(padded)).toBe(state.name); + }), + ); + }); + }); +}); + +describe("getStateNameByCode types", () => { + test("should take a string and return a StateName or null", () => { + expectTypeOf(getStateNameByCode).parameter(0).toEqualTypeOf(); + expectTypeOf(getStateNameByCode).returns.toEqualTypeOf(); + }); }); diff --git a/src/get-states/get-states.test.ts b/src/get-states/get-states.test.ts index 9f8e98ad..fb84c79c 100644 --- a/src/get-states/get-states.test.ts +++ b/src/get-states/get-states.test.ts @@ -1,9 +1,16 @@ -import { DATA } from "../_internals/constants/states"; -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { DATA, type State, type StateCode, type StateName } from "../_internals/constants/states"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getStateByIbgeCode } from "../get-state-by-ibge-code/get-state-by-ibge-code"; +import { getStateCodeByName } from "../get-state-code-by-name/get-state-code-by-name"; +import { getStateNameByCode } from "../get-state-name-by-code/get-state-name-by-code"; import { getStates } from "./get-states"; const NUMBER_OF_BRAZILIAN_STATES = 27; +const stateArbitrary = (): fc.Arbitrary => fc.constantFrom(...getStates()); + describe("getStates", () => { it(`should return an array with ${DATA.length} states`, () => { expect(getStates().length).toBe(DATA.length); @@ -64,4 +71,37 @@ describe("getStates", () => { expect(second.at(0)?.name).not.toBe("X"); expect(second).toEqual(DATA.map((state) => Object.assign({}, state))); }); + + describe("properties", () => { + test("should have a code and name that are inverses of each other", () => { + fc.assert( + fc.property(stateArbitrary(), (state) => { + expect(getStateCodeByName(state.name)).toBe(state.code); + expect(getStateNameByCode(state.code)).toBe(state.name); + }), + ); + }); + + test("should have an ibgeCode that resolves back to the same state", () => { + fc.assert( + fc.property(stateArbitrary(), (state) => { + expect(getStateByIbgeCode(state.ibgeCode)).toEqual(state); + }), + ); + }); + }); +}); + +describe("getStates types", () => { + test("should take no arguments and return an array of State", () => { + expectTypeOf(getStates).parameter(0).toBeUndefined(); + expectTypeOf(getStates).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + readonly code: StateCode; + readonly name: StateName; + readonly regionCode: "N" | "NE" | "CO" | "SE" | "S"; + readonly regionName: "Norte" | "Nordeste" | "Centro-Oeste" | "Sudeste" | "Sul"; + readonly ibgeCode: number; + }>(); + }); }); diff --git a/src/get-timezone-by-state/get-timezone-by-state.test.ts b/src/get-timezone-by-state/get-timezone-by-state.test.ts index a5d4446e..7213c7d5 100644 --- a/src/get-timezone-by-state/get-timezone-by-state.test.ts +++ b/src/get-timezone-by-state/get-timezone-by-state.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { stateCodes } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { getTimezoneByState } from "./get-timezone-by-state"; describe("getTimezoneByState", () => { @@ -110,4 +114,29 @@ describe("getTimezoneByState", () => { expect(getTimezoneByState("toString")).toBeNull(); expect(getTimezoneByState("__proto__")).toBeNull(); }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrows(getTimezoneByState, fc.anything()); + }); + + test("should resolve every known state code regardless of case or padding", () => { + fc.assert( + fc.property(stateCodes, fc.boolean(), fc.boolean(), (stateCode, upper, pad) => { + const cased = upper ? stateCode.toUpperCase() : stateCode.toLowerCase(); + const padded = pad ? ` ${cased} ` : cased; + + expect(getTimezoneByState(padded)).toBe(getTimezoneByState(stateCode)); + expect(getTimezoneByState(padded)).not.toBeNull(); + }), + ); + }); + }); +}); + +describe("getTimezoneByState types", () => { + test("should take a string and return a string or null", () => { + expectTypeOf(getTimezoneByState).parameter(0).toEqualTypeOf(); + expectTypeOf(getTimezoneByState).returns.toEqualTypeOf(); + }); }); diff --git a/src/index.test.ts b/src/index.test.ts index 7dfa4e38..f67d90aa 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -61,6 +61,7 @@ import { type LicensePlateFormat, type Municipality, type NfeKey, + type NfeKeyModel, type NumberToWordsGender, type ParseCnpjOptions, type ParseCurrencyOptions, @@ -311,6 +312,7 @@ describe("Public API", () => { LicensePlateFormat: LicensePlateFormat; Municipality: Municipality; NfeKey: NfeKey; + NfeKeyModel: NfeKeyModel; NumberToWordsGender: NumberToWordsGender; ParseCnpjOptions: ParseCnpjOptions; ParseCurrencyOptions: ParseCurrencyOptions; diff --git a/src/is-business-day/is-business-day.test.ts b/src/is-business-day/is-business-day.test.ts index 44b50ac4..138174b2 100644 --- a/src/is-business-day/is-business-day.test.ts +++ b/src/is-business-day/is-business-day.test.ts @@ -1,5 +1,15 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { isBusinessDay } from "./is-business-day"; +import * as fc from "fast-check"; + +import { type StateCode } from "../_internals/constants/states"; +import { holidayYears, monthDays, monthIndexes, stateCodes } from "../_internals/test/arbitraries"; +import { expectNeverThrowsWithOptions } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getHolidays, type Holiday } from "../get-holidays/get-holidays"; +import { isBusinessDay, type IsBusinessDayOptions } from "./is-business-day"; + +function getHolidaysFor(year: number, stateCode: StateCode | null): Holiday[] { + return stateCode === null ? getHolidays(year) : getHolidays({ year, stateCode }); +} describe("isBusinessDay", () => { it("should return true for a plain weekday that is not a holiday (noon, DST-safe)", () => { @@ -100,4 +110,59 @@ describe("isBusinessDay", () => { expect(value.getTime()).toBe(original.getTime()); }); + + describe("properties", () => { + const stateAndOptionalArbitrary = fc.tuple(fc.option(stateCodes), fc.boolean()); + + test("should return false for every Saturday and Sunday", () => { + fc.assert( + fc.property(holidayYears, monthIndexes, monthDays, (year, month, day) => { + const date = new Date(year, month, day); + + if (date.getDay() === 0 || date.getDay() === 6) { + expect(isBusinessDay(date)).toBe(false); + } + }), + ); + }); + + test("should agree with getHolidays and the weekend rule", () => { + fc.assert( + fc.property( + holidayYears, + monthIndexes, + monthDays, + stateAndOptionalArbitrary, + (year, month, day, [stateCode, includeOptional]) => { + const date = new Date(year, month, day); + const holidays = getHolidaysFor(year, stateCode); + const isHolidayMatch = holidays.some( + (holiday) => + (includeOptional || holiday.type !== "optional") && + holiday.date.getMonth() === month && + holiday.date.getDate() === day, + ); + const isWeekend = date.getDay() === 0 || date.getDay() === 6; + const options = { stateCode: stateCode ?? undefined, includeOptional }; + + expect(isBusinessDay(date, options)).toBe(!isWeekend && !isHolidayMatch); + }, + ), + ); + }); + + test("should never throw, regardless of the input", () => { + expectNeverThrowsWithOptions(isBusinessDay, fc.anything(), fc.anything()); + }); + }); +}); + +describe("isBusinessDay types", () => { + test("should take a Date, options, and return a boolean", () => { + expectTypeOf(isBusinessDay).parameter(0).toEqualTypeOf(); + expectTypeOf(isBusinessDay).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(isBusinessDay).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-holiday/is-holiday.test.ts b/src/is-holiday/is-holiday.test.ts index 7e8f2152..f2c53ed4 100644 --- a/src/is-holiday/is-holiday.test.ts +++ b/src/is-holiday/is-holiday.test.ts @@ -1,5 +1,15 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { isHoliday } from "./is-holiday"; +import * as fc from "fast-check"; + +import { type StateCode } from "../_internals/constants/states"; +import { holidayYears, monthDays, monthIndexes, stateCodes } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getHolidays, type Holiday } from "../get-holidays/get-holidays"; +import { isHoliday, type IsHolidayOptions } from "./is-holiday"; + +function getHolidaysFor(year: number, stateCode: StateCode | null): Holiday[] { + return stateCode === null ? getHolidays(year) : getHolidays({ year, stateCode }); +} describe("isHoliday", () => { it("should return true for a national holiday built from local date components", () => { @@ -69,4 +79,39 @@ describe("isHoliday", () => { expect(isHoliday({ targetDate: new Date(2024, 11, 25) })).toBe(true); }); }); + + describe("properties", () => { + test("should agree with getHolidays for the same year and state", () => { + fc.assert( + fc.property( + holidayYears, + monthIndexes, + monthDays, + fc.option(stateCodes), + (year, month, day, stateCode) => { + const targetDate = new Date(year, month, day); + const holidays = getHolidaysFor(year, stateCode); + const expected = holidays.some( + (holiday) => holiday.date.getMonth() === month && holiday.date.getDate() === day, + ); + const options = stateCode === null ? { targetDate } : { targetDate, stateCode }; + + expect(isHoliday(options)).toBe(expected); + }, + ), + ); + }); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(isHoliday, fc.anything()); + }); + }); +}); + +describe("isHoliday types", () => { + test("should take an options object and return a boolean", () => { + expectTypeOf(isHoliday).parameter(0).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ targetDate: Date; stateCode?: StateCode }>(); + expectTypeOf(isHoliday).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-bank-account/is-valid-bank-account.test.ts b/src/is-valid-bank-account/is-valid-bank-account.test.ts index f417a1d6..6269b663 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.test.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.test.ts @@ -1,7 +1,9 @@ +import * as fc from "fast-check"; + import { BANKS } from "../_internals/constants/banks"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { bench, describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { COMPE_CODES } from "./constants"; -import { isValidBankAccount } from "./is-valid-bank-account"; +import { isValidBankAccount, type IsValidBankAccountOptions } from "./is-valid-bank-account"; const BANCO_DO_BRASIL_AGENCY_TOO_LONG_PARAMS = { bankCode: "001", @@ -10,6 +12,14 @@ const BANCO_DO_BRASIL_AGENCY_TOO_LONG_PARAMS = { digit: "5", }; +const LISTED_CODES = new Set( + Array.from({ length: COMPE_CODES.length / 3 }, (_, index) => + COMPE_CODES.slice(index * 3, index * 3 + 3), + ), +); + +const CHECK_CHARACTERS = [...Array.from({ length: 10 }, (_, digit) => String(digit)), "X", "P"]; + describe("isValidBankAccount", () => { describe("should return false", () => { test("when params is null", () => { @@ -1540,4 +1550,95 @@ describe("isValidBankAccount", () => { ).toBe(true); }); }); + + describe("properties", () => { + const rules = fc.constantFrom( + { bankCode: "001", accountLength: 8 }, + { bankCode: "033", accountLength: 8 }, + { bankCode: "041", accountLength: 9 }, + { bankCode: "104", accountLength: 11 }, + { bankCode: "237", accountLength: 7 }, + { bankCode: "260", accountLength: 7 }, + { bankCode: "341", accountLength: 5 }, + { bankCode: "399", accountLength: 6 }, + { bankCode: "745", accountLength: 10 }, + ); + + const agencies = fc.stringMatching(/^[0-9]{4}$/); + + const accounts = fc.stringMatching(/^[0-9]{13}$/); + + test("should accept one check digit per account of a bank with a published rule", () => { + fc.assert( + fc.property(rules, agencies, accounts, (rule, agency, pool) => { + const account = pool.slice(0, rule.accountLength); + const accepted = CHECK_CHARACTERS.filter((digit) => + isValidBankAccount({ bankCode: rule.bankCode, agency, account, digit }), + ); + + if (accepted.length === 1) return; + + expect(rule.bankCode).toBe("237"); + expect(accepted).toEqual(["0", "P"]); + }), + ); + }); + + test("should accept any single digit for a bank validated by structure only", () => { + const structureOnly = fc.constantFrom("077", "085", "197", "290", "336", "748", "756"); + + fc.assert( + fc.property(structureOnly, agencies, accounts, (bankCode, agency, pool) => { + const account = pool.slice(0, 6); + + expect(isValidBankAccount({ bankCode, agency, account, digit: "7" })).toBe(true); + }), + ); + }); + + test("should reject a bank that is not in the participants table", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{3}$/), agencies, (bankCode, agency) => { + fc.pre(!LISTED_CODES.has(bankCode)); + + expect(isValidBankAccount({ bankCode, agency, account: "00210169", digit: "6" })).toBe( + false, + ); + }), + ); + }); + + test("should never throw and always judge a bank account with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidBankAccount(value as never)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidBankAccount types", () => { + test("should take required bank account options and return a boolean", () => { + expectTypeOf(isValidBankAccount).parameter(0).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(isValidBankAccount).returns.toEqualTypeOf(); + }); +}); + +describe("isValidBankAccount benchmarks", () => { + bench("Banco do Brasil", () => { + isValidBankAccount({ bankCode: "001", agency: "1584", account: "00210169", digit: "6" }); + }); + + bench("Bradesco", () => { + isValidBankAccount({ bankCode: "237", agency: "1234", account: "0238069", digit: "2" }); + }); + + bench("unknown bank code", () => { + isValidBankAccount({ bankCode: "999", agency: "1234", account: "123456", digit: "0" }); + }); }); diff --git a/src/is-valid-boleto/is-valid-boleto.test.ts b/src/is-valid-boleto/is-valid-boleto.test.ts index 671041e8..fc5892d1 100644 --- a/src/is-valid-boleto/is-valid-boleto.test.ts +++ b/src/is-valid-boleto/is-valid-boleto.test.ts @@ -1,5 +1,8 @@ +import * as fc from "fast-check"; + import { BOLETO_LENGTH } from "../_internals/constants/boleto"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generateBoleto } from "../generate-boleto/generate-boleto"; import { isValidBoleto } from "./is-valid-boleto"; describe("isValidBoleto", () => { @@ -118,4 +121,54 @@ describe("isValidBoleto", () => { }); }); }); + + describe("properties", () => { + const types = ["bancario", "arrecadacao"] as const; + + const digitString = fc.string({ unit: fc.constantFrom("0", "1", "2", "7", "9") }); + + test("should accept every generated bank slip", () => { + fc.assert( + fc.property(fc.constantFrom(...types), (type) => { + expect(isValidBoleto(generateBoleto({ type }))).toBe(true); + }), + ); + }); + + test("should ignore the mask characters of a generated bank slip", () => { + fc.assert( + fc.property(fc.constantFrom(...types), (type) => { + const value = generateBoleto({ type }); + const masked = `${value.slice(0, 5)}. ${value.slice(5, 20)}-${value.slice(20)}`; + + expect(isValidBoleto(masked)).toBe(true); + }), + ); + }); + + test("should reject every digit string that has no bank slip length", () => { + fc.assert( + fc.property(digitString, (value) => { + fc.pre(value.length !== BOLETO_LENGTH); + + expect(isValidBoleto(value)).toBe(false); + }), + ); + }); + + test("should never throw and always judge a bank slip with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidBoleto(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidBoleto types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidBoleto).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidBoleto).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-caepf/is-valid-caepf.test.ts b/src/is-valid-caepf/is-valid-caepf.test.ts index e9d5ba77..887340c2 100644 --- a/src/is-valid-caepf/is-valid-caepf.test.ts +++ b/src/is-valid-caepf/is-valid-caepf.test.ts @@ -1,6 +1,13 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCaepf } from "./is-valid-caepf"; +const CHECK_DIGIT_PAIRS = Array.from({ length: 100 }, (_, pair) => String(pair).padStart(2, "0")); + +const findCaepf = (base: string): string => + CHECK_DIGIT_PAIRS.map((pair) => `${base}${pair}`).find((value) => isValidCaepf(value)) ?? ""; + describe("isValidCaepf", () => { describe("should return false", () => { test("when it is null", () => { @@ -81,4 +88,45 @@ describe("isValidCaepf", () => { expect(isValidCaepf(" 293 118 610 001 84 ")).toBe(true); }); }); + + describe("properties", () => { + const bases = fc.stringMatching(/^[0-9]{12}$/); + + test("should accept exactly one pair of check digits for any CPF base", () => { + fc.assert( + fc.property(bases, (base) => { + const accepted = CHECK_DIGIT_PAIRS.filter((pair) => isValidCaepf(`${base}${pair}`)); + + expect(accepted.length).toBe(1); + }), + ); + }); + + test("should read the same registration with and without the official mask", () => { + fc.assert( + fc.property(bases, (base) => { + const value = findCaepf(base); + const cpf = `${value.slice(0, 3)}.${value.slice(3, 6)}.${value.slice(6, 9)}`; + const masked = `${cpf}/${value.slice(9, 12)}-${value.slice(12)}`; + + expect(isValidCaepf(masked)).toBe(true); + }), + ); + }); + + test("should never throw and always judge a CAEPF number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidCaepf(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidCaepf types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCaepf).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCaepf).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cbo/is-valid-cbo.test.ts b/src/is-valid-cbo/is-valid-cbo.test.ts index 79055010..468d7c2a 100644 --- a/src/is-valid-cbo/is-valid-cbo.test.ts +++ b/src/is-valid-cbo/is-valid-cbo.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { CBO_TITLES } from "../_internals/constants/cbo"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidCbo } from "./is-valid-cbo"; describe("isValidCbo", () => { @@ -14,6 +19,12 @@ describe("isValidCbo", () => { expect(isValidCbo(212_405)).toBe(true); }); + it("should pad a number with leading zeros before looking it up", () => { + expect(isValidCbo(10_205)).toBe(true); + expect(isValidCbo("010205")).toBe(true); + expect(isValidCbo("10205")).toBe(false); + }); + it("should validate a CBO code with surrounding whitespace", () => { expect(isValidCbo(" 212405 ")).toBe(true); }); @@ -45,4 +56,30 @@ describe("isValidCbo", () => { it("should return false for a non numeric string", () => { expect(isValidCbo("abcdef")).toBe(false); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CBO_TITLES)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(isValidCbo, anyGarbage); + }); + + test("should validate every known code, with or without the hyphen mask", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + const masked = `${code.slice(0, 4)}-${code.slice(4)}`; + + expect(isValidCbo(code)).toBe(true); + expect(isValidCbo(masked)).toBe(true); + }), + ); + }); + }); +}); + +describe("isValidCbo types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCbo).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCbo).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cei/is-valid-cei.test.ts b/src/is-valid-cei/is-valid-cei.test.ts index cbbd4130..2850055a 100644 --- a/src/is-valid-cei/is-valid-cei.test.ts +++ b/src/is-valid-cei/is-valid-cei.test.ts @@ -1,6 +1,13 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCei } from "./is-valid-cei"; +const LAST_DIGITS = Array.from({ length: 10 }, (_, digit) => String(digit)); + +const findCei = (base: string): string => + LAST_DIGITS.map((digit) => `${base}${digit}`).find((value) => isValidCei(value)) ?? ""; + describe("isValidCei", () => { describe("should return false", () => { test("when it is null", () => { @@ -85,4 +92,55 @@ describe("isValidCei", () => { expect(isValidCei(" 11 583 00249 85 ")).toBe(true); }); }); + + describe("properties", () => { + const bases = fc.stringMatching(/^[0-9]{11}$/); + + test("should accept at most one check digit for any base", () => { + fc.assert( + fc.property(bases, (base) => { + const accepted = LAST_DIGITS.filter((digit) => isValidCei(`${base}${digit}`)); + + expect(accepted.length).toBeLessThanOrEqual(1); + }), + ); + }); + + test("should read the same number with and without the official mask", () => { + fc.assert( + fc.property(bases, (base) => { + const value = findCei(base); + + fc.pre(value !== ""); + + const masked = `${value.slice(0, 2)}.${value.slice(2, 5)}.${value.slice(5, 10)}/${value.slice(10)}`; + + expect(isValidCei(masked)).toBe(true); + }), + ); + }); + + test("should reject a number made of a single repeated digit", () => { + fc.assert( + fc.property(fc.constantFrom(...LAST_DIGITS), (digit) => { + expect(isValidCei(digit.repeat(12))).toBe(false); + }), + ); + }); + + test("should never throw and always judge a CEI number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidCei(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidCei types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCei).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCei).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cep/is-valid-cep.test.ts b/src/is-valid-cep/is-valid-cep.test.ts index 8e4696f2..c25e6535 100644 --- a/src/is-valid-cep/is-valid-cep.test.ts +++ b/src/is-valid-cep/is-valid-cep.test.ts @@ -1,4 +1,15 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import { + anyValue, + digits, + digitsOfOtherLength, + maskedValues, +} from "../_internals/test/arbitraries"; +import { + expectAccepted, + expectAlwaysReturnsType, + expectRejected, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCep } from "./is-valid-cep"; describe("isValidCep", () => { @@ -64,4 +75,25 @@ describe("isValidCep", () => { expect(isValidCep("01310.100")).toBe(true); }); }); + + describe("properties", () => { + test("should ignore dots, hyphens and spaces wherever they appear", () => { + expectAccepted(isValidCep, maskedValues(digits(8), [".", "-", " "], 2)); + }); + + test("should reject any digits only value that is not 8 digits long", () => { + expectRejected(isValidCep, digitsOfOtherLength(16, [8])); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidCep, "boolean", anyValue); + }); + }); +}); + +describe("isValidCep types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCep).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCep).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-certidao/is-valid-certidao.test.ts b/src/is-valid-certidao/is-valid-certidao.test.ts index e6bbadfc..050cff2d 100644 --- a/src/is-valid-certidao/is-valid-certidao.test.ts +++ b/src/is-valid-certidao/is-valid-certidao.test.ts @@ -1,5 +1,14 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { isValidCertidao } from "./is-valid-certidao"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { CERTIDAO_TYPES } from "../parse-certidao/constants"; +import { type CertidaoType } from "../parse-certidao/parse-certidao"; +import { isValidCertidao, type IsValidCertidaoOptions } from "./is-valid-certidao"; + +const CHECK_DIGIT_PAIRS = Array.from({ length: 100 }, (_, index) => String(index).padStart(2, "0")); + +const findCertidao = (base: string): string => + CHECK_DIGIT_PAIRS.map((pair) => `${base}${pair}`).find((value) => isValidCertidao(value)) ?? ""; describe("isValidCertidao", () => { describe("should return false", () => { @@ -159,4 +168,64 @@ describe("isValidCertidao", () => { ).toBe(false); }); }); + + describe("properties", () => { + const bases = fc.stringMatching(/^[0-9]{30}$/); + + const books = fc.tuple( + fc.stringMatching(/^[0-9]{14}$/), + fc.integer({ min: 1, max: 9 }), + fc.stringMatching(/^[0-9]{15}$/), + ); + + test("should accept exactly one pair of check digits for any base", () => { + fc.assert( + fc.property(bases, (base) => { + const accepted = CHECK_DIGIT_PAIRS.filter((pair) => isValidCertidao(`${base}${pair}`)); + + expect(accepted.length).toBe(1); + }), + ); + }); + + test("should read the same matrícula however it is punctuated", () => { + fc.assert( + fc.property(bases, fc.constantFrom(".", "-", "/", " "), (base, separator) => { + const value = findCertidao(base); + const masked = `${value.slice(0, 6)}${separator}${value.slice(6, 15)}${separator}${value.slice(15)}`; + + expect(isValidCertidao(masked)).toBe(true); + }), + ); + }); + + test("should honour the books it was told to accept", () => { + fc.assert( + fc.property(books, ([head, typeCode, tail]) => { + const value = findCertidao(`${head}${typeCode}${tail}`); + const type = CERTIDAO_TYPES[typeCode - 1]; + + expect(isValidCertidao(value, { accept: [type] })).toBe(true); + expect(isValidCertidao(value, { accept: [] })).toBe(false); + }), + ); + }); + + test("should never throw and always judge a matrícula with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidCertidao(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidCertidao types", () => { + test("should take a string or number, optional options, and return a boolean", () => { + expectTypeOf(isValidCertidao).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCertidao).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(isValidCertidao).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cfop/is-valid-cfop.test.ts b/src/is-valid-cfop/is-valid-cfop.test.ts index 6207f372..7591df70 100644 --- a/src/is-valid-cfop/is-valid-cfop.test.ts +++ b/src/is-valid-cfop/is-valid-cfop.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { CFOP_TABLE } from "../_internals/constants/cfop"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidCfop } from "./is-valid-cfop"; describe("isValidCfop", () => { @@ -54,4 +59,28 @@ describe("isValidCfop", () => { it("should return false for a non numeric string", () => { expect(isValidCfop("abcd")).toBe(false); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CFOP_TABLE)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(isValidCfop, anyGarbage); + }); + + test("should validate every known code, as a string or a number", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + expect(isValidCfop(code)).toBe(true); + expect(isValidCfop(Number(code))).toBe(true); + }), + ); + }); + }); +}); + +describe("isValidCfop types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCfop).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCfop).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cnae/is-valid-cnae.test.ts b/src/is-valid-cnae/is-valid-cnae.test.ts index aac9acb0..16926071 100644 --- a/src/is-valid-cnae/is-valid-cnae.test.ts +++ b/src/is-valid-cnae/is-valid-cnae.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { CNAE_SUBCLASSES } from "../_internals/constants/cnae"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { isValidCnae } from "./is-valid-cnae"; describe("isValidCnae", () => { @@ -14,6 +19,12 @@ describe("isValidCnae", () => { expect(isValidCnae(6_201_501)).toBe(true); }); + it("should pad a number with leading zeros before looking it up", () => { + expect(isValidCnae(111_301)).toBe(true); + expect(isValidCnae("0111301")).toBe(true); + expect(isValidCnae("111301")).toBe(false); + }); + it("should validate a CNAE code with surrounding whitespace", () => { expect(isValidCnae(" 6201501 ")).toBe(true); }); @@ -45,4 +56,30 @@ describe("isValidCnae", () => { it("should return false for a non numeric string", () => { expect(isValidCnae("abcdefg")).toBe(false); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...Object.keys(CNAE_SUBCLASSES)); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(isValidCnae, anyGarbage); + }); + + test("should validate every known code, with or without the mask", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + const masked = `${code.slice(0, 4)}-${code.slice(4, 5)}/${code.slice(5)}`; + + expect(isValidCnae(code)).toBe(true); + expect(isValidCnae(masked)).toBe(true); + }), + ); + }); + }); +}); + +describe("isValidCnae types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCnae).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCnae).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cnh/is-valid-cnh.test.ts b/src/is-valid-cnh/is-valid-cnh.test.ts index 0626ee32..d14e302b 100644 --- a/src/is-valid-cnh/is-valid-cnh.test.ts +++ b/src/is-valid-cnh/is-valid-cnh.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType, expectRejected } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { generateCnh } from "../generate-cnh/generate-cnh"; import { isValidCnh } from "./is-valid-cnh"; describe("isValidCnh", () => { @@ -31,4 +36,32 @@ describe("isValidCnh", () => { // @ts-expect-error: intentionally invalid input expect(isValidCnh()).toBe(false); }); + + describe("properties", () => { + test("should accept a generated CNH whatever mask characters surround its digits", () => { + fc.assert( + fc.property(maskSeparators([".", "-", "/", " "], 3, 3), (separators) => { + const cnh = generateCnh(); + const base = `${separators[0]}${cnh.slice(0, 9)}${separators[1]}`; + + expect(isValidCnh(`${base}${cnh.slice(9)}${separators[2]}`)).toBe(true); + }), + ); + }); + + test("should reject any digits only value that is not 11 digits long", () => { + expectRejected(isValidCnh, digitsOfOtherLength(22, [11])); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidCnh, "boolean", anyValue); + }); + }); +}); + +describe("isValidCnh types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidCnh).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCnh).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cno/is-valid-cno.test.ts b/src/is-valid-cno/is-valid-cno.test.ts index 29c59a54..0e73e043 100644 --- a/src/is-valid-cno/is-valid-cno.test.ts +++ b/src/is-valid-cno/is-valid-cno.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { isValidCei } from "../is-valid-cei/is-valid-cei"; import { isValidCno } from "./is-valid-cno"; describe("isValidCno", () => { @@ -80,4 +83,45 @@ describe("isValidCno", () => { expect(isValidCno(" 11 084 01680 62 ")).toBe(true); }); }); + + describe("properties", () => { + test("should agree with isValidCei, whose numbering the CNO kept", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,14}$/), (value) => { + expect(isValidCno(value)).toBe(isValidCei(value)); + }), + ); + }); + + test("should accept a registration written with its mask", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{11}$/), (base) => { + const candidates = Array.from({ length: 10 }, (_, digit) => `${base}${digit}`); + const accepted = candidates.filter((value) => isValidCno(value)); + + fc.pre(accepted.length === 1); + + const value = accepted[0]; + const masked = `${value.slice(0, 2)}.${value.slice(2, 5)}.${value.slice(5, 10)}/${value.slice(10)}`; + + expect(isValidCno(masked)).toBe(true); + }), + ); + }); + + test("should never throw and always judge a CNO number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidCno(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidCno types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCno).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCno).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cnpj/is-valid-cnpj.test.ts b/src/is-valid-cnpj/is-valid-cnpj.test.ts index 70746183..ec4809a2 100644 --- a/src/is-valid-cnpj/is-valid-cnpj.test.ts +++ b/src/is-valid-cnpj/is-valid-cnpj.test.ts @@ -1,8 +1,11 @@ +import * as fc from "fast-check"; + import { CNPJ_LENGTH } from "../_internals/constants/cnpj"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; +import { bench, describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { RESERVED_NUMBERS } from "./constants"; -import { isValidCnpj } from "./is-valid-cnpj"; +import { isValidCnpj, type IsValidCnpjOptions } from "./is-valid-cnpj"; describe("isValidCnpj", () => { describe("should return false", () => { @@ -139,4 +142,70 @@ describe("isValidCnpj", () => { expect(isValidCnpj(cnpj, { version })).toBe(true); } }); + + describe("properties", () => { + const masks = maskSeparators([".", "-", "/", " "], 4, 3); + const version = fc.constantFrom(1 as const, 2 as const); + + test("should accept a generated CNPJ written with any of the documented masks", () => { + fc.assert( + fc.property(version, masks, (currentVersion, separators) => { + const cnpj = generateCnpj(currentVersion); + const head = `${cnpj.slice(0, 2)}${separators[0]}${cnpj.slice(2, 5)}`; + const body = `${separators[1]}${cnpj.slice(5, 8)}${separators[2]}`; + const tail = `${cnpj.slice(8, 12)}${separators[3]}${cnpj.slice(12)}`; + const masked = `${head}${body}${tail}`; + + expect(isValidCnpj(masked, { version: currentVersion })).toBe(true); + expect(isValidCnpj(masked.toLowerCase(), { version: 2 })).toBe(true); + }), + ); + }); + + test(`should reject any digits only value that is not ${CNPJ_LENGTH} digits long`, () => { + const wrongLength = digitsOfOtherLength(28, [CNPJ_LENGTH]); + + fc.assert( + fc.property(wrongLength, version, (value, currentVersion) => { + expect(isValidCnpj(value, { version: currentVersion })).toBe(false); + }), + ); + }); + + test("should never throw and always return a boolean", () => { + fc.assert( + fc.property(anyValue, version, (value, currentVersion) => { + const result = isValidCnpj(value as string, { version: currentVersion }); + + expect(typeof result).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidCnpj types", () => { + test("should take a string and options and return a boolean", () => { + expectTypeOf(isValidCnpj).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCnpj).parameter(1).toEqualTypeOf(); + expectTypeOf(isValidCnpj).returns.toEqualTypeOf(); + }); + + test("should type the version option as an optional 1 or 2", () => { + expectTypeOf().toEqualTypeOf<1 | 2 | undefined>(); + }); +}); + +describe("isValidCnpj benchmarks", () => { + bench("valid numeric, masked", () => { + isValidCnpj("11.444.777/0001-61"); + }); + + bench("valid alphanumeric", () => { + isValidCnpj("12ABC34501DE35"); + }); + + bench("invalid check digit", () => { + isValidCnpj("11444777000162"); + }); }); diff --git a/src/is-valid-cns/is-valid-cns.test.ts b/src/is-valid-cns/is-valid-cns.test.ts index 085d1320..08004417 100644 --- a/src/is-valid-cns/is-valid-cns.test.ts +++ b/src/is-valid-cns/is-valid-cns.test.ts @@ -1,6 +1,22 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCns } from "./is-valid-cns"; +const CARD_DIGITS = Array.from({ length: 10 }, (_, digit) => String(digit)); + +const findCns = (base: string): string => { + for (const suffix of ["000", "001"]) { + const card = CARD_DIGITS.map((digit) => `${base}${suffix}${digit}`).find((value) => + isValidCns(value), + ); + + if (card !== undefined) return card; + } + + return ""; +}; + describe("isValidCns", () => { describe("should return false", () => { test("when it is null", () => { @@ -109,4 +125,60 @@ describe("isValidCns", () => { expect(isValidCns("712345678901236")).toBe(true); }); }); + + describe("properties", () => { + const bases = fc.stringMatching(/^[12][0-9]{10}$/); + + test("should accept exactly one check digit for a definitive card", () => { + fc.assert( + fc.property(bases, (base) => { + const card = findCns(base); + const others = CARD_DIGITS.filter((digit) => digit !== card.slice(14)); + + expect(card).not.toBe(""); + + for (const digit of others) { + expect(isValidCns(`${card.slice(0, 14)}${digit}`)).toBe(false); + } + }), + ); + }); + + test("should ignore the display groups of a card number", () => { + fc.assert( + fc.property(bases, (base) => { + const card = findCns(base); + const head = `${card.slice(0, 3)} ${card.slice(3, 7)}`; + const tail = `${card.slice(7, 11)} ${card.slice(11)}`; + + expect(isValidCns(`${head} ${tail}`)).toBe(true); + }), + ); + }); + + test("should reject any value that is not fifteen digits long", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,20}$/), (value) => { + fc.pre(value.length !== 15); + + expect(isValidCns(value)).toBe(false); + }), + ); + }); + + test("should never throw and always judge a health card with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidCns(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidCns types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCns).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCns).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cpf/is-valid-cpf.test.ts b/src/is-valid-cpf/is-valid-cpf.test.ts index 69a6e15d..13abe749 100644 --- a/src/is-valid-cpf/is-valid-cpf.test.ts +++ b/src/is-valid-cpf/is-valid-cpf.test.ts @@ -1,5 +1,9 @@ +import * as fc from "fast-check"; + import { CPF_LENGTH } from "../_internals/constants/cpf"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType, expectRejected } from "../_internals/test/properties"; +import { bench, describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { RESERVED_NUMBERS } from "./constants"; import { isValidCpf } from "./is-valid-cpf"; @@ -96,4 +100,50 @@ describe("isValidCpf", () => { } }); }); + + describe("properties", () => { + const masks = maskSeparators([".", "-", "/", " "], 3, 3); + const spaces = fc.string({ unit: fc.constantFrom(" ", "\t", "\n"), maxLength: 2 }); + + test("should accept a generated CPF written with any of the documented masks", () => { + fc.assert( + fc.property(masks, spaces, spaces, (separators, before, after) => { + const cpf = generateCpf(); + const body = `${cpf.slice(0, 3)}${separators[0]}${cpf.slice(3, 6)}${separators[1]}${cpf.slice(6, 9)}${separators[2]}${cpf.slice(9)}`; + + expect(isValidCpf(`${before}${body}${after}`)).toBe(isValidCpf(cpf)); + expect(isValidCpf(`${before}${body}${after}`)).toBe(true); + }), + ); + }); + + test(`should reject any digits only value that is not ${CPF_LENGTH} digits long`, () => { + expectRejected(isValidCpf, digitsOfOtherLength(22, [CPF_LENGTH])); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidCpf, "boolean", anyValue); + }); + }); +}); + +describe("isValidCpf types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidCpf).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCpf).returns.toEqualTypeOf(); + }); +}); + +describe("isValidCpf benchmarks", () => { + bench("valid, digits only", () => { + isValidCpf("15503516030"); + }); + + bench("valid, masked", () => { + isValidCpf("155.035.160-30"); + }); + + bench("invalid check digit", () => { + isValidCpf("15503516031"); + }); }); diff --git a/src/is-valid-credit-card/is-valid-credit-card.test.ts b/src/is-valid-credit-card/is-valid-credit-card.test.ts index f0989a7d..72dc7c85 100644 --- a/src/is-valid-credit-card/is-valid-credit-card.test.ts +++ b/src/is-valid-credit-card/is-valid-credit-card.test.ts @@ -1,6 +1,10 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidCreditCard } from "./is-valid-credit-card"; +const LUHN_DIGITS = Array.from({ length: 10 }, (_, digit) => String(digit)); + describe("isValidCreditCard", () => { describe("should return true", () => { test("for the Visa test number 4111111111111111", () => { @@ -96,4 +100,55 @@ describe("isValidCreditCard", () => { expect(isValidCreditCard([])).toBe(false); }); }); + + describe("properties", () => { + test("should accept exactly one Luhn check digit for any base", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{11,18}$/), (base) => { + const accepted = LUHN_DIGITS.filter((digit) => isValidCreditCard(`${base}${digit}`)); + + expect(accepted.length).toBe(1); + }), + ); + }); + + test("should ignore the spaces and hyphens between the digits", () => { + fc.assert( + fc.property( + fc.stringMatching(/^[0-9]{12,19}$/), + fc.constantFrom(" ", "-"), + (card, separator) => { + const masked = card.replaceAll(/(\d{4})(?=\d)/g, `$1${separator}`); + + expect(isValidCreditCard(masked)).toBe(isValidCreditCard(card)); + }, + ), + ); + }); + + test("should reject any card number outside the ISO 7812 digit range", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,25}$/), (value) => { + fc.pre(value.length < 12 || value.length > 19); + + expect(isValidCreditCard(value)).toBe(false); + }), + ); + }); + + test("should never throw and always judge a card number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidCreditCard(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidCreditCard types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCreditCard).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCreditCard).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-csosn/is-valid-csosn.test.ts b/src/is-valid-csosn/is-valid-csosn.test.ts index 61585d58..5a620d23 100644 --- a/src/is-valid-csosn/is-valid-csosn.test.ts +++ b/src/is-valid-csosn/is-valid-csosn.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { CSOSN_CODES } from "./constants"; import { isValidCsosn } from "./is-valid-csosn"; @@ -43,4 +47,28 @@ describe("isValidCsosn", () => { it("should return false for a non numeric string", () => { expect(isValidCsosn("abc")).toBe(false); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...CSOSN_CODES); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(isValidCsosn, anyGarbage); + }); + + test("should validate every known code, as a string or a number", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + expect(isValidCsosn(code)).toBe(true); + expect(isValidCsosn(Number(code))).toBe(true); + }), + ); + }); + }); +}); + +describe("isValidCsosn types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidCsosn).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCsosn).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-cst/is-valid-cst.test.ts b/src/is-valid-cst/is-valid-cst.test.ts index 6288cd92..d6620c4e 100644 --- a/src/is-valid-cst/is-valid-cst.test.ts +++ b/src/is-valid-cst/is-valid-cst.test.ts @@ -1,5 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { isValidCst } from "./is-valid-cst"; +import * as fc from "fast-check"; + +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrowsWithOptions } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { ICMS_CST_CODES, IPI_CST_CODES, PIS_COFINS_CST_CODES } from "./constants"; +import { isValidCst, type IsValidCstOptions } from "./is-valid-cst"; describe("isValidCst", () => { describe("icms", () => { @@ -127,4 +132,55 @@ describe("isValidCst", () => { it("should sanitize whitespace and mask characters", () => { expect(isValidCst(" 1-10 ", { tax: "icms" })).toBe(true); }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrowsWithOptions(isValidCst, anyGarbage, anyGarbage); + }); + + test("should validate every origin digit 0-8 combined with a known icms code", () => { + fc.assert( + fc.property( + fc.integer({ min: 0, max: 8 }), + fc.constantFrom(...ICMS_CST_CODES), + (origin, code) => { + expect(isValidCst(`${origin}${code}`, { tax: "icms" })).toBe(true); + }, + ), + ); + }); + + test("should reject origin digit 9 for icms, regardless of the code", () => { + fc.assert( + fc.property(fc.constantFrom(...ICMS_CST_CODES), (code) => { + expect(isValidCst(`9${code}`, { tax: "icms" })).toBe(false); + }), + ); + }); + + test("should validate every known ipi, pis and cofins code", () => { + fc.assert( + fc.property(fc.constantFrom(...IPI_CST_CODES), (code) => { + expect(isValidCst(code, { tax: "ipi" })).toBe(true); + }), + ); + fc.assert( + fc.property(fc.constantFrom(...PIS_COFINS_CST_CODES), (code) => { + expect(isValidCst(code, { tax: "pis" })).toBe(true); + expect(isValidCst(code, { tax: "cofins" })).toBe(true); + }), + ); + }); + }); +}); + +describe("isValidCst types", () => { + test("should take a string or number, options, and return a boolean", () => { + expectTypeOf(isValidCst).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidCst).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + "icms" | "ipi" | "pis" | "cofins" | undefined + >(); + expectTypeOf(isValidCst).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-email/is-valid-email.test.ts b/src/is-valid-email/is-valid-email.test.ts index 2ccbec55..6b8af69e 100644 --- a/src/is-valid-email/is-valid-email.test.ts +++ b/src/is-valid-email/is-valid-email.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidEmail } from "./is-valid-email"; describe("isValidEmail", () => { @@ -62,4 +64,51 @@ describe("isValidEmail", () => { expect(isValidEmail("user+tag@example.co.uk")).toBe(true); }); }); + + describe("properties", () => { + const addresses = fc.stringMatching( + /^[a-z0-9][a-z0-9_+-]{0,15}@[a-z0-9][a-z0-9-]{0,10}\.[a-z]{2,6}$/, + ); + + test("should accept a well-formed address", () => { + fc.assert( + fc.property(addresses, (value) => { + expect(isValidEmail(value)).toBe(true); + }), + ); + }); + + test("should not care about the case of the address", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + expect(isValidEmail(value.toUpperCase())).toBe(isValidEmail(value)); + }), + ); + }); + + test("should reject any value without an at sign", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + fc.pre(!value.includes("@")); + + expect(isValidEmail(value)).toBe(false); + }), + ); + }); + + test("should never throw and always judge an address with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidEmail(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidEmail types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidEmail).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidEmail).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-iban/is-valid-iban.test.ts b/src/is-valid-iban/is-valid-iban.test.ts index d9a0bec6..620e8717 100644 --- a/src/is-valid-iban/is-valid-iban.test.ts +++ b/src/is-valid-iban/is-valid-iban.test.ts @@ -1,6 +1,13 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidIban } from "./is-valid-iban"; +const CHECK_DIGITS = Array.from({ length: 97 }, (_, index) => String(index).padStart(2, "0")); + +const findIban = (body: string): string => + CHECK_DIGITS.map((pair) => `BR${pair}${body}`).find((iban) => isValidIban(iban)) ?? ""; + describe("isValidIban", () => { describe("should return true", () => { test("for a known valid IBAN (iban.com Brazil example)", () => { @@ -97,4 +104,57 @@ describe("isValidIban", () => { expect(isValidIban([])).toBe(false); }); }); + + describe("properties", () => { + const bodies = fc.stringMatching(/^[0-9]{23}[CP][A-Z0-9]$/); + + test("should accept exactly one pair of check digits for any account", () => { + fc.assert( + fc.property(bodies, (body) => { + const accepted = CHECK_DIGITS.filter((pair) => isValidIban(`BR${pair}${body}`)); + + expect(accepted.length).toBe(1); + }), + ); + }); + + test("should ignore the grouping spaces and the case of an IBAN", () => { + fc.assert( + fc.property(bodies, (body) => { + const iban = findIban(body); + const grouped = iban.replaceAll(/(.{4})(?=.)/g, "$1 "); + + expect(isValidIban(grouped)).toBe(true); + expect(isValidIban(grouped.toLowerCase())).toBe(true); + }), + ); + }); + + test("should reject an IBAN of any other country", () => { + fc.assert( + fc.property(bodies, fc.stringMatching(/^[A-Z]{2}$/), (body, countryCode) => { + fc.pre(countryCode !== "BR"); + + const iban = findIban(body); + + expect(isValidIban(`${countryCode}${iban.slice(2)}`)).toBe(false); + }), + ); + }); + + test("should never throw and always judge an IBAN with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidIban(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidIban types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidIban).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidIban).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-ie/is-valid-ie.test.ts b/src/is-valid-ie/is-valid-ie.test.ts index 2c6bdaae..f0598be5 100644 --- a/src/is-valid-ie/is-valid-ie.test.ts +++ b/src/is-valid-ie/is-valid-ie.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { DATA as STATES, type StateCode } from "../_internals/constants/states"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidIe } from "./is-valid-ie"; describe("isValidIe", () => { @@ -796,4 +799,49 @@ describe("isValidIe", () => { expect(isValidIe("RJ", "625X45372")).toBe(true); }); }); + + describe("properties", () => { + const stateCodeArbitrary = fc.constantFrom(...STATES.map((state) => state.code)); + + test("should never throw and always return a boolean, for any state code and any input", () => { + fc.assert( + fc.property(fc.anything(), fc.anything(), (stateCode, ie) => { + let result: unknown; + + expect(() => { + result = isValidIe(stateCode as never, ie as never); + }).not.toThrow(); + expect(typeof result).toBe("boolean"); + }), + ); + }); + + test("should never throw and always return a boolean, for every known state code and arbitrary text", () => { + fc.assert( + fc.property(stateCodeArbitrary, fc.string({ unit: "grapheme" }), (stateCode, ie) => { + expect(typeof isValidIe(stateCode, ie)).toBe("boolean"); + }), + ); + }); + + test("should return false for a state code that does not exist", () => { + const unknownStateCodeArbitrary = fc + .string() + .filter((code) => !STATES.some((state) => state.code === code.toUpperCase())); + + fc.assert( + fc.property(unknownStateCodeArbitrary, fc.string(), (stateCode, ie) => { + expect(isValidIe(stateCode as never, ie)).toBe(false); + }), + ); + }); + }); +}); + +describe("isValidIe types", () => { + test("should take a StateCode and a string and return a boolean", () => { + expectTypeOf(isValidIe).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidIe).parameter(1).toEqualTypeOf(); + expectTypeOf(isValidIe).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-landline-phone/is-valid-landline-phone.test.ts b/src/is-valid-landline-phone/is-valid-landline-phone.test.ts index c0ecdc18..8923ec38 100644 --- a/src/is-valid-landline-phone/is-valid-landline-phone.test.ts +++ b/src/is-valid-landline-phone/is-valid-landline-phone.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generatePhone } from "../generate-phone/generate-phone"; import { isValidLandlinePhone } from "./is-valid-landline-phone"; describe("isValidLandlinePhone", () => { @@ -57,4 +60,42 @@ describe("isValidLandlinePhone", () => { expect(isValidLandlinePhone("(11) 6265-4321")).toBe(true); expect(isValidLandlinePhone("1172654321")).toBe(false); }); + + describe("properties", () => { + test("should accept every generated landline number, masked or not", () => { + fc.assert( + fc.property(fc.constant("landline" as const), (type) => { + const phone = generatePhone(type); + const masked = `(${phone.slice(0, 2)}) ${phone.slice(2, 6)}-${phone.slice(6)}`; + + expect(isValidLandlinePhone(phone)).toBe(true); + expect(isValidLandlinePhone(masked)).toBe(true); + expect(isValidLandlinePhone(`+55 ${masked}`)).toBe(true); + }), + ); + }); + + test("should reject every generated mobile number", () => { + fc.assert( + fc.property(fc.constant("mobile" as const), (type) => { + expect(isValidLandlinePhone(generatePhone(type))).toBe(false); + }), + ); + }); + + test("should never throw and always judge a landline number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidLandlinePhone(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidLandlinePhone types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidLandlinePhone).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidLandlinePhone).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-legal-nature/is-valid-legal-nature.test.ts b/src/is-valid-legal-nature/is-valid-legal-nature.test.ts index 240093ba..1dc38922 100644 --- a/src/is-valid-legal-nature/is-valid-legal-nature.test.ts +++ b/src/is-valid-legal-nature/is-valid-legal-nature.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { anyValue, maskedValues } from "../_internals/test/arbitraries"; +import { expectAccepted, expectAlwaysReturnsType } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { LEGAL_NATURE } from "./constants"; import { isValidLegalNature } from "./is-valid-legal-nature"; describe("isValidLegalNature", () => { @@ -47,4 +52,33 @@ describe("isValidLegalNature", () => { expect(isValidLegalNature("toString")).toBe(false); expect(isValidLegalNature("__proto__")).toBe(false); }); + + describe("properties", () => { + const knownCode = fc.constantFrom(...Object.keys(LEGAL_NATURE)); + + test("should accept every code of the table however its mask is spread", () => { + expectAccepted(isValidLegalNature, maskedValues(knownCode, [".", "-", " "], 2)); + }); + + test("should reject a known code as soon as a character that is not a mask is added", () => { + const extra = fc.stringMatching(/^[0-9A-Za-z]{1,3}$/); + + fc.assert( + fc.property(knownCode, extra, (code, suffix) => { + expect(isValidLegalNature(`${code}${suffix}`)).toBe(false); + }), + ); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidLegalNature, "boolean", anyValue); + }); + }); +}); + +describe("isValidLegalNature types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidLegalNature).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidLegalNature).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-license-plate/is-valid-license-plate.test.ts b/src/is-valid-license-plate/is-valid-license-plate.test.ts index 95f0d921..c729fa08 100644 --- a/src/is-valid-license-plate/is-valid-license-plate.test.ts +++ b/src/is-valid-license-plate/is-valid-license-plate.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { generateLicensePlate } from "../generate-license-plate/generate-license-plate"; +import { getFormatLicensePlate } from "../get-format-license-plate/get-format-license-plate"; import { isValidLicensePlate } from "./is-valid-license-plate"; describe("isValidLicensePlate", () => { @@ -73,4 +77,51 @@ describe("isValidLicensePlate", () => { expect(isValidLicensePlate("ABC-1D23")).toBe(true); }); }); + + describe("properties", () => { + const formats = ["LLLNNNN", "LLLNLNN"] as const; + + test("should accept every generated plate of both formats", () => { + fc.assert( + fc.property(fc.constantFrom(...formats), (format) => { + expect(isValidLicensePlate(generateLicensePlate(format))).toBe(true); + }), + ); + }); + + test("should ignore the separator and the case of a generated plate", () => { + fc.assert( + fc.property(fc.constantFrom(...formats), (format) => { + const plate = generateLicensePlate(format); + const masked = `${plate.slice(0, 3)}-${plate.slice(3)}`; + + expect(isValidLicensePlate(masked.toLowerCase())).toBe(true); + expect(isValidLicensePlate(` ${plate.slice(0, 3)} ${plate.slice(3)} `)).toBe(true); + }), + ); + }); + + test("should agree with getFormatLicensePlate on every value", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + expect(isValidLicensePlate(value)).toBe(getFormatLicensePlate(value) !== null); + }), + ); + }); + + test("should never throw and always judge a plate with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidLicensePlate(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidLicensePlate types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidLicensePlate).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidLicensePlate).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts b/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts index 7d2dd0f2..02c09361 100644 --- a/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts +++ b/src/is-valid-mobile-phone/is-valid-mobile-phone.test.ts @@ -1,5 +1,12 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { isValidMobilePhone } from "./is-valid-mobile-phone"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generatePhone } from "../generate-phone/generate-phone"; +import { + type IsValidMobilePhoneOptions, + type PhoneVersion, + isValidMobilePhone, +} from "./is-valid-mobile-phone"; describe("isValidMobilePhone", () => { describe("should return false", () => { @@ -60,4 +67,59 @@ describe("isValidMobilePhone", () => { expect(isValidMobilePhone("+55 55 98765-4321")).toBe(true); }); }); + + describe("properties", () => { + test("should accept every generated mobile number under both rules", () => { + fc.assert( + fc.property(fc.constantFrom(1, 2), (version) => { + const phone = generatePhone("mobile"); + + expect(isValidMobilePhone(phone, { version })).toBe(true); + }), + ); + }); + + test("should ignore the mask and the country code of a generated mobile number", () => { + fc.assert( + fc.property(fc.constant("mobile" as const), (type) => { + const phone = generatePhone(type); + const masked = `(${phone.slice(0, 2)}) ${phone.slice(2, 7)}-${phone.slice(7)}`; + + expect(isValidMobilePhone(masked)).toBe(true); + expect(isValidMobilePhone(`+55 ${masked}`)).toBe(true); + }), + ); + }); + + test("should reject every generated landline number", () => { + fc.assert( + fc.property(fc.constant("landline" as const), (type) => { + expect(isValidMobilePhone(generatePhone(type))).toBe(false); + }), + ); + }); + + test("should never throw and always judge a mobile number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidMobilePhone(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidMobilePhone types", () => { + test("should take a string, optional options, and return a boolean", () => { + expectTypeOf(isValidMobilePhone).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidMobilePhone) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf(isValidMobilePhone).returns.toEqualTypeOf(); + }); + + test("should restrict version to the supported numbering rules", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<1 | 2>(); + }); }); diff --git a/src/is-valid-ncm/is-valid-ncm.test.ts b/src/is-valid-ncm/is-valid-ncm.test.ts index 1ccaa17e..89960d4f 100644 --- a/src/is-valid-ncm/is-valid-ncm.test.ts +++ b/src/is-valid-ncm/is-valid-ncm.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { NCM_CODES } from "./constants"; import { isValidNcm } from "./is-valid-ncm"; describe("isValidNcm", () => { @@ -54,4 +59,41 @@ describe("isValidNcm", () => { it("should return false for a non numeric string", () => { expect(isValidNcm("abcdefgh")).toBe(false); }); + + describe("properties", () => { + const codeArbitrary = fc.constantFrom(...NCM_CODES); + const nonZeroLeadingCodeArbitrary = fc.constantFrom( + ...NCM_CODES.filter((code) => !code.startsWith("0")), + ); + + test("should never throw, regardless of the input", () => { + expectNeverThrows(isValidNcm, anyGarbage); + }); + + test("should validate every known code, with or without the NNNN.NN.NN mask", () => { + fc.assert( + fc.property(codeArbitrary, (code) => { + const masked = `${code.slice(0, 4)}.${code.slice(4, 6)}.${code.slice(6)}`; + + expect(isValidNcm(code)).toBe(true); + expect(isValidNcm(masked)).toBe(true); + }), + ); + }); + + test("should validate every known code without a leading zero when given as a number", () => { + fc.assert( + fc.property(nonZeroLeadingCodeArbitrary, (code) => { + expect(isValidNcm(Number(code))).toBe(true); + }), + ); + }); + }); +}); + +describe("isValidNcm types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidNcm).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidNcm).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts index c8389f53..77fa1edf 100644 --- a/src/is-valid-nfe-key/is-valid-nfe-key.test.ts +++ b/src/is-valid-nfe-key/is-valid-nfe-key.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidNfeKey } from "./is-valid-nfe-key"; const VALID_A = "35120859597245000190550000000095831710040056"; @@ -7,6 +9,10 @@ const VALID_C = "35170358716523000119550010000000301000000300"; const VALID_D = "43160472202112000136550000000010571048440722"; const INVALID_TYPE = "42100484684182000157550010000000020108042108"; +const NFE_CHECK_DIGITS = Array.from({ length: 10 }, (_, digit) => String(digit)); + +const NFE_KEY = "35170458716523000119550010000000121000123458"; + describe("isValidNfeKey", () => { describe("should return true", () => { test("for a real NF-e access key without a mask, the br-validate-dfe-access-key README/tests example (SP)", () => { @@ -165,4 +171,54 @@ describe("isValidNfeKey", () => { }); } }); + + describe("properties", () => { + test("should accept at most one check digit for any 43 digit base", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{43}$/), (base) => { + const accepted = NFE_CHECK_DIGITS.filter((digit) => isValidNfeKey(`${base}${digit}`)); + + expect(accepted.length).toBeLessThanOrEqual(1); + }), + ); + }); + + test("should ignore whitespace anywhere between the digits", () => { + fc.assert( + fc.property(fc.integer({ min: 1, max: 43 }), (index) => { + const masked = `${NFE_KEY.slice(0, index)} ${NFE_KEY.slice(index)}`; + + expect(isValidNfeKey(masked)).toBe(true); + expect(isValidNfeKey(`NFe${masked}`)).toBe(true); + }), + ); + }); + + test("should reject a key whose state code belongs to no state", () => { + fc.assert( + fc.property( + fc.stringMatching(/^[0-9]{42}$/), + fc.constantFrom("00", "01", "89", "99"), + (rest, uf) => { + expect(isValidNfeKey(`${uf}${rest}`)).toBe(false); + }, + ), + ); + }); + + test("should never throw and always judge an access key with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidNfeKey(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidNfeKey types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidNfeKey).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidNfeKey).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-passport/is-valid-passport.test.ts b/src/is-valid-passport/is-valid-passport.test.ts index 77579352..fdfd3909 100644 --- a/src/is-valid-passport/is-valid-passport.test.ts +++ b/src/is-valid-passport/is-valid-passport.test.ts @@ -1,4 +1,12 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { anyValue, maskedValues } from "../_internals/test/arbitraries"; +import { + expectAccepted, + expectAlwaysReturnsType, + expectRejected, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidPassport } from "./is-valid-passport"; describe("isValidPassport", () => { @@ -46,4 +54,31 @@ describe("isValidPassport", () => { expect(isValidPassport("AB.123.456")).toBe(true); }); }); + + describe("properties", () => { + test("should ignore case and every non alphanumeric character", () => { + const passport = fc.stringMatching(/^[A-Za-z]{2}[0-9]{6}$/); + + expectAccepted(isValidPassport, maskedValues(passport, [".", "-", "/", " ", "_"], 2)); + }); + + test("should reject any alphanumeric value that is not 2 letters and 6 digits", () => { + const notAPassport = fc + .stringMatching(/^[0-9A-Z]{0,12}$/) + .filter((value) => !/^[A-Z]{2}[0-9]{6}$/.test(value)); + + expectRejected(isValidPassport, notAPassport); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidPassport, "boolean", anyValue); + }); + }); +}); + +describe("isValidPassport types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidPassport).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidPassport).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-phone/is-valid-phone.test.ts b/src/is-valid-phone/is-valid-phone.test.ts index 85365736..ad60c118 100644 --- a/src/is-valid-phone/is-valid-phone.test.ts +++ b/src/is-valid-phone/is-valid-phone.test.ts @@ -1,5 +1,13 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { isValidPhone } from "./is-valid-phone"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generatePhone } from "../generate-phone/generate-phone"; +import { + type IsValidPhoneOptions, + type PhoneType, + type PhoneVersion, + isValidPhone, +} from "./is-valid-phone"; describe("isValidPhone", () => { describe("service numbers written with a country code", () => { @@ -90,4 +98,64 @@ describe("isValidPhone", () => { expect(isValidPhone("08001234567", { accept: ["mobile", "landline", "service"] })).toBe(true); }); }); + + describe("properties", () => { + const geographic = ["mobile", "landline"] as const; + + test("should accept every generated geographic number, masked or not", () => { + fc.assert( + fc.property(fc.constantFrom(...geographic), (type) => { + const phone = generatePhone(type); + const masked = `(${phone.slice(0, 2)}) ${phone.slice(2, -4)}-${phone.slice(-4)}`; + + expect(isValidPhone(phone)).toBe(true); + expect(isValidPhone(masked)).toBe(true); + expect(isValidPhone(`+55 ${masked}`)).toBe(true); + expect(isValidPhone(`0055${phone}`)).toBe(true); + }), + ); + }); + + test("should only accept a generated service number when asked to", () => { + fc.assert( + fc.property(fc.constant("service" as const), (type) => { + const phone = generatePhone(type); + + expect(isValidPhone(phone)).toBe(false); + expect(isValidPhone(phone, { accept: ["service"] })).toBe(true); + }), + ); + }); + + test("should accept nothing when no kind of number is accepted", () => { + fc.assert( + fc.property(fc.constantFrom("mobile", "landline", "service"), (type) => { + expect(isValidPhone(generatePhone(type), { accept: [] })).toBe(false); + }), + ); + }); + + test("should never throw and always judge a phone number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidPhone(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidPhone types", () => { + test("should take a string, optional options, and return a boolean", () => { + expectTypeOf(isValidPhone).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidPhone).parameter(1).toEqualTypeOf(); + expectTypeOf(isValidPhone).returns.toEqualTypeOf(); + }); + + test("should restrict version and accept to the documented values", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<1 | 2>(); + expectTypeOf().toEqualTypeOf<"mobile" | "landline" | "service">(); + }); }); diff --git a/src/is-valid-pis/is-valid-pis.test.ts b/src/is-valid-pis/is-valid-pis.test.ts index a9a5a771..7df1bdd6 100644 --- a/src/is-valid-pis/is-valid-pis.test.ts +++ b/src/is-valid-pis/is-valid-pis.test.ts @@ -1,5 +1,10 @@ +import * as fc from "fast-check"; + import { PIS_LENGTH } from "../_internals/constants/pis"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType, expectRejected } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generatePis } from "../generate-pis/generate-pis"; import { RESERVED_NUMBERS } from "./constants"; import { isValidPis } from "./is-valid-pis"; @@ -91,4 +96,35 @@ describe("isValidPis", () => { expect(isValidPis("120.7041.469-0")).toBe(true); }); }); + + describe("properties", () => { + test("should accept a generated PIS written with any of the accepted mask characters", () => { + const masks = maskSeparators([".", "-", "/", " ", "(", ")", ",", "*"], 4, 3); + + fc.assert( + fc.property(masks, (separators) => { + const pis = generatePis(); + const head = `${separators[0]}${pis.slice(0, 3)}${separators[1]}`; + const tail = `${pis.slice(3, 8)}${separators[2]}${pis.slice(8)}`; + + expect(isValidPis(`${head}${tail}${separators[3]}`)).toBe(true); + }), + ); + }); + + test(`should reject any digits only value that is not ${PIS_LENGTH} digits long`, () => { + expectRejected(isValidPis, digitsOfOtherLength(22, [PIS_LENGTH])); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidPis, "boolean", anyValue); + }); + }); +}); + +describe("isValidPis types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidPis).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidPis).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-pix-key/is-valid-pix-key.test.ts b/src/is-valid-pix-key/is-valid-pix-key.test.ts index 81decd34..c8fa5512 100644 --- a/src/is-valid-pix-key/is-valid-pix-key.test.ts +++ b/src/is-valid-pix-key/is-valid-pix-key.test.ts @@ -1,7 +1,11 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { generateCpf } from "../generate-cpf/generate-cpf"; -import { isValidPixKey } from "./is-valid-pix-key"; +import { generatePhone } from "../generate-phone/generate-phone"; +import { type PixKeyType, parsePixKey } from "../parse-pix-key/parse-pix-key"; +import { type IsValidPixKeyOptions, isValidPixKey } from "./is-valid-pix-key"; describe("isValidPixKey", () => { describe("should return false", () => { @@ -95,4 +99,59 @@ describe("isValidPixKey", () => { expect(isValidPixKey("123.456.789-09", null)).toBe(true); }); }); + + describe("properties", () => { + const emails = fc.stringMatching(/^[a-z0-9]{1,10}@[a-z0-9]{1,10}\.com$/); + + test("should accept every kind of key the DICT defines", () => { + fc.assert( + fc.property(emails, fc.uuid(), (email, evp) => { + const phone = `+55${generatePhone("mobile")}`; + + for (const key of [generateCpf(), generateCnpj(), email, evp, phone]) { + expect(isValidPixKey(key)).toBe(true); + } + }), + ); + }); + + test("should honour the kinds of key it was told to accept", () => { + fc.assert( + fc.property(emails, fc.uuid(), (email, evp) => { + expect(isValidPixKey(email, { accept: ["email"] })).toBe(true); + expect(isValidPixKey(email, { accept: ["evp", "cpf"] })).toBe(false); + expect(isValidPixKey(evp, { accept: ["evp"] })).toBe(true); + expect(isValidPixKey(evp, { accept: [] })).toBe(false); + }), + ); + }); + + test("should agree with parsePixKey on every value", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + expect(isValidPixKey(value)).toBe(parsePixKey(value) !== null); + }), + ); + }); + + test("should never throw and always judge a Pix key with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidPixKey(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidPixKey types", () => { + test("should take a string, optional options, and return a boolean", () => { + expectTypeOf(isValidPixKey).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidPixKey).parameter(1).toEqualTypeOf(); + expectTypeOf(isValidPixKey).returns.toEqualTypeOf(); + }); + + test("should restrict accept to an array of Pix key types", () => { + expectTypeOf().toEqualTypeOf(); + }); }); diff --git a/src/is-valid-pix-payload/is-valid-pix-payload.test.ts b/src/is-valid-pix-payload/is-valid-pix-payload.test.ts index 0c4d3a59..020da73c 100644 --- a/src/is-valid-pix-payload/is-valid-pix-payload.test.ts +++ b/src/is-valid-pix-payload/is-valid-pix-payload.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generateCpf } from "../generate-cpf/generate-cpf"; +import { generatePixPayload } from "../generate-pix-payload/generate-pix-payload"; import { isValidPixPayload } from "./is-valid-pix-payload"; const BACEN_STATIC = @@ -187,4 +191,52 @@ describe("isValidPixPayload", () => { expect(isValidPixPayload("pix copia e cola")).toBe(false); }); }); + + describe("properties", () => { + const names = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{0,24}$/); + + const cities = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{0,14}$/); + + test("should accept every generated payload", () => { + fc.assert( + fc.property(names, cities, (merchantName, merchantCity) => { + const key = generateCpf(); + const payload = generatePixPayload({ key, merchantName, merchantCity }); + + expect(isValidPixPayload(payload ?? "")).toBe(true); + expect(isValidPixPayload(` ${payload}\n`)).toBe(true); + }), + ); + }); + + test("should reject a payload whose text was changed", () => { + fc.assert( + fc.property(names, fc.nat(), (merchantName, offset) => { + const key = generateCpf(); + const payload = generatePixPayload({ key, merchantName, merchantCity: "BRASILIA" }); + const text = payload ?? ""; + const index = offset % (text.length - 4); + const replacement = text.charAt(index) === "0" ? "1" : "0"; + const changed = `${text.slice(0, index)}${replacement}${text.slice(index + 1)}`; + + expect(isValidPixPayload(changed)).toBe(false); + }), + ); + }); + + test("should never throw and always judge a BR Code with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidPixPayload(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidPixPayload types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidPixPayload).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidPixPayload).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts index 5fd6f876..8252fbc9 100644 --- a/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts +++ b/src/is-valid-processo-juridico/is-valid-processo-juridico.test.ts @@ -1,5 +1,10 @@ +import * as fc from "fast-check"; + import { PROCESSO_JURIDICO_LENGTH } from "../_internals/constants/processo-juridico"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType, expectRejected } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generateProcessoJuridico } from "../generate-processo-juridico/generate-processo-juridico"; import { isValidProcessoJuridico } from "./is-valid-processo-juridico"; describe("isValidProcessoJuridico", () => { @@ -44,4 +49,35 @@ describe("isValidProcessoJuridico", () => { expect(isValidProcessoJuridico("0002080-25.2012.515.0049")).toBe(true); }); }); + + describe("properties", () => { + test("should accept a generated number whatever mask separates its fields", () => { + fc.assert( + fc.property(maskSeparators([".", "-", "/", " "], 5, 3), (separators) => { + const value = generateProcessoJuridico() as string; + const head = `${value.slice(0, 7)}${separators[0]}${value.slice(7, 9)}`; + const body = `${separators[1]}${value.slice(9, 13)}${separators[2]}`; + const court = `${value.slice(13, 14)}${separators[3]}${value.slice(14, 16)}`; + const tail = `${separators[4]}${value.slice(16)}`; + + expect(isValidProcessoJuridico(`${head}${body}${court}${tail}`)).toBe(true); + }), + ); + }); + + test(`should reject any digits only value that is not ${PROCESSO_JURIDICO_LENGTH} long`, () => { + expectRejected(isValidProcessoJuridico, digitsOfOtherLength(30, [PROCESSO_JURIDICO_LENGTH])); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidProcessoJuridico, "boolean", anyValue); + }); + }); +}); + +describe("isValidProcessoJuridico types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidProcessoJuridico).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidProcessoJuridico).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts index f3d80fa5..08b167f0 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.test.ts @@ -1,5 +1,14 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { isValidRegistroProfissional } from "./is-valid-registro-profissional"; +import * as fc from "fast-check"; + +import { DATA, type StateCode } from "../_internals/constants/states"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { type RegistroProfissionalCouncil } from "./constants"; +import { + isValidRegistroProfissional, + type IsValidRegistroProfissionalOptions, +} from "./is-valid-registro-profissional"; + +const STATE_CODES = DATA.map((state) => state.code); describe("isValidRegistroProfissional", () => { describe("should return false", () => { @@ -86,4 +95,77 @@ describe("isValidRegistroProfissional", () => { expect(isValidRegistroProfissional("RJ-654321/T-9", { council: "CRC" })).toBe(true); }); }); + + describe("properties", () => { + const states = fc.constantFrom(...STATE_CODES); + + const numbers = fc.integer({ min: 1000, max: 999_999 }); + + test("should accept a well-formed number for every council", () => { + fc.assert( + fc.property(states, numbers, (stateCode, number) => { + for (const council of ["OAB", "CRM", "CRO"] as const) { + expect(isValidRegistroProfissional(`${number}/${stateCode}`, { council })).toBe(true); + expect( + isValidRegistroProfissional(`${number}-${stateCode}`, { council, stateCode }), + ).toBe(true); + } + + expect(isValidRegistroProfissional(`06/${number}`, { council: "CRP" })).toBe(true); + expect( + isValidRegistroProfissional(`${stateCode}-${number}/O-3`, { council: "CRC" }), + ).toBe(true); + }), + ); + }); + + test("should reject a registration whose UF is not the expected one", () => { + fc.assert( + fc.property(states, states, numbers, (stateCode, other, number) => { + fc.pre(stateCode !== other); + + const value = `${number}/${other}`; + + expect(isValidRegistroProfissional(value, { council: "OAB", stateCode })).toBe(false); + }), + ); + }); + + test("should reject a number that carries no UF at all", () => { + fc.assert( + fc.property(numbers, (number) => { + expect(isValidRegistroProfissional(`${number}`, { council: "CRM" })).toBe(false); + }), + ); + }); + + test("should never throw and always judge a registration with a boolean", () => { + fc.assert( + fc.property(fc.anything(), fc.anything(), (value, options) => { + const result = isValidRegistroProfissional(value as string, options as never); + + expect(typeof result).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidRegistroProfissional types", () => { + test("should take a string, required options, and return a boolean", () => { + expectTypeOf(isValidRegistroProfissional).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidRegistroProfissional) + .parameter(1) + .toEqualTypeOf(); + expectTypeOf< + IsValidRegistroProfissionalOptions["council"] + >().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + StateCode | undefined + >(); + expectTypeOf().toEqualTypeOf< + "OAB" | "CRM" | "CRO" | "CRP" | "CRC" + >(); + expectTypeOf(isValidRegistroProfissional).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-renavam/is-valid-renavam.test.ts b/src/is-valid-renavam/is-valid-renavam.test.ts index 3d41fb25..89bf6471 100644 --- a/src/is-valid-renavam/is-valid-renavam.test.ts +++ b/src/is-valid-renavam/is-valid-renavam.test.ts @@ -1,6 +1,10 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidRenavam } from "./is-valid-renavam"; +const RENAVAM_DIGITS = Array.from({ length: 10 }, (_, digit) => String(digit)); + describe("isValidRenavam", () => { describe("should return false", () => { test("when it is an empty string", () => { @@ -88,4 +92,52 @@ describe("isValidRenavam", () => { expect(isValidRenavam("00000000060")).toBe(true); }); }); + + describe("properties", () => { + test("should accept exactly one check digit for any base", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{10}$/), (base) => { + const accepted = RENAVAM_DIGITS.filter((digit) => isValidRenavam(`${base}${digit}`)); + + expect(accepted.length).toBe(1); + }), + ); + }); + + test("should read a nine digit registration as its zero padded form", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{9}$/), (value) => { + const masked = `${value.slice(0, 4)}.${value.slice(4)}`; + + expect(isValidRenavam(`00${value}`)).toBe(isValidRenavam(value)); + expect(isValidRenavam(masked)).toBe(isValidRenavam(value)); + }), + ); + }); + + test("should reject any value whose length is neither nine nor eleven", () => { + fc.assert( + fc.property(fc.stringMatching(/^[0-9]{0,16}$/), (value) => { + fc.pre(value.length !== 9 && value.length !== 11); + + expect(isValidRenavam(value)).toBe(false); + }), + ); + }); + + test("should never throw and always judge a RENAVAM with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidRenavam(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidRenavam types", () => { + test("should take a string or number and return a boolean", () => { + expectTypeOf(isValidRenavam).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidRenavam).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-service-phone/is-valid-service-phone.test.ts b/src/is-valid-service-phone/is-valid-service-phone.test.ts index 1d3bb507..70610ebf 100644 --- a/src/is-valid-service-phone/is-valid-service-phone.test.ts +++ b/src/is-valid-service-phone/is-valid-service-phone.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { SERVICE_PHONE_UTILITY_CODES } from "../_internals/constants/service-phone"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { generatePhone } from "../generate-phone/generate-phone"; import { isValidServicePhone } from "./is-valid-service-phone"; describe("isValidServicePhone", () => { @@ -102,4 +106,48 @@ describe("isValidServicePhone", () => { expect(isValidServicePhone("199")).toBe(true); }); }); + + describe("properties", () => { + test("should accept every generated service number", () => { + fc.assert( + fc.property(fc.constant("service" as const), (type) => { + const phone = generatePhone(type); + + expect(isValidServicePhone(phone)).toBe(true); + expect([8, 11].includes(phone.length)).toBe(true); + }), + ); + }); + + test("should accept every utility code Anatel designated", () => { + fc.assert( + fc.property(fc.constantFrom(...SERVICE_PHONE_UTILITY_CODES), (code) => { + expect(isValidServicePhone(code)).toBe(true); + }), + ); + }); + + test("should reject every generated geographic number", () => { + fc.assert( + fc.property(fc.constantFrom("mobile", "landline"), (type) => { + expect(isValidServicePhone(generatePhone(type))).toBe(false); + }), + ); + }); + + test("should never throw and always judge a service number with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidServicePhone(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidServicePhone types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidServicePhone).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidServicePhone).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-vin/is-valid-vin.test.ts b/src/is-valid-vin/is-valid-vin.test.ts index d84f6774..bfc119a2 100644 --- a/src/is-valid-vin/is-valid-vin.test.ts +++ b/src/is-valid-vin/is-valid-vin.test.ts @@ -1,6 +1,12 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { isValidVin } from "./is-valid-vin"; +const VIN_CHECK_CHARACTERS = Array.from({ length: 11 }, (_, index) => + index === 10 ? "X" : String(index), +); + describe("isValidVin", () => { describe("should return true", () => { test("for a known valid VIN with a numeric check digit", () => { @@ -105,4 +111,61 @@ describe("isValidVin", () => { expect(isValidVin([])).toBe(false); }); }); + + describe("properties", () => { + const bodies = fc.stringMatching(/^[0-9A-HJ-NPR-Z]{16}$/); + + const vins = fc.stringMatching(/^[0-9A-HJ-NPR-Z]{17}$/); + + test("should accept exactly one check character for any body", () => { + fc.assert( + fc.property(bodies, (body) => { + const candidates = VIN_CHECK_CHARACTERS.map( + (character) => `${body.slice(0, 8)}${character}${body.slice(8)}`, + ); + const accepted = candidates.filter((candidate) => isValidVin(candidate)); + + expect(accepted.length).toBe(1); + }), + ); + }); + + test("should ignore the case and the surrounding whitespace", () => { + fc.assert( + fc.property(vins, (vin) => { + expect(isValidVin(` ${vin.toLowerCase()} `)).toBe(isValidVin(vin)); + }), + ); + }); + + test("should reject a chassis containing the excluded letters", () => { + fc.assert( + fc.property( + vins, + fc.integer({ min: 0, max: 16 }), + fc.constantFrom("I", "O", "Q"), + (vin, index, letter) => { + expect(isValidVin(`${vin.slice(0, index)}${letter}${vin.slice(index + 1)}`)).toBe( + false, + ); + }, + ), + ); + }); + + test("should never throw and always judge a chassis with a boolean", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof isValidVin(value as string)).toBe("boolean"); + }), + ); + }); + }); +}); + +describe("isValidVin types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidVin).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidVin).returns.toEqualTypeOf(); + }); }); diff --git a/src/is-valid-voter-id/is-valid-voter-id.test.ts b/src/is-valid-voter-id/is-valid-voter-id.test.ts index bdba32ca..6e71778d 100644 --- a/src/is-valid-voter-id/is-valid-voter-id.test.ts +++ b/src/is-valid-voter-id/is-valid-voter-id.test.ts @@ -1,4 +1,8 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { anyValue, digitsOfOtherLength, maskSeparators } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType, expectRejected } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { generateVoterId } from "../generate-voter-id/generate-voter-id"; import { isValidVoterId } from "./is-valid-voter-id"; @@ -87,4 +91,32 @@ describe("isValidVoterId", () => { expect(isValidVoterId("000000010191")).toBe(true); expect(isValidVoterId("000000002801")).toBe(true); }); + + describe("properties", () => { + test("should accept a generated voter id whatever mask surrounds its digits", () => { + fc.assert( + fc.property(maskSeparators([".", "-", "/", " "], 3, 3), (separators) => { + const voterId = generateVoterId(); + const head = `${separators[0]}${voterId.slice(0, 8)}${separators[1]}`; + + expect(isValidVoterId(`${head}${voterId.slice(8)}${separators[2]}`)).toBe(true); + }), + ); + }); + + test("should reject any digits only value that is neither 12 nor 13 digits long", () => { + expectRejected(isValidVoterId, digitsOfOtherLength(26, [12, 13])); + }); + + test("should never throw and always return a boolean", () => { + expectAlwaysReturnsType(isValidVoterId, "boolean", anyValue); + }); + }); +}); + +describe("isValidVoterId types", () => { + test("should take a string and return a boolean", () => { + expectTypeOf(isValidVoterId).parameter(0).toEqualTypeOf(); + expectTypeOf(isValidVoterId).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-boleto/parse-boleto.test.ts b/src/parse-boleto/parse-boleto.test.ts index 4897059e..c016fcd8 100644 --- a/src/parse-boleto/parse-boleto.test.ts +++ b/src/parse-boleto/parse-boleto.test.ts @@ -1,4 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { bench, describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { generateBoleto } from "../generate-boleto/generate-boleto"; +import { getBoletoInfo } from "../get-boleto-info/get-boleto-info"; +import { isValidBoleto } from "../is-valid-boleto/is-valid-boleto"; import { parseBoleto } from "./parse-boleto"; describe("parseBoleto", () => { @@ -44,4 +49,70 @@ describe("parseBoleto", () => { ); }); }); + + describe("properties", () => { + test("should only ever return digits, and never more than a bank slip has", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const parsed = parseBoleto(value); + + expect(/^\d*$/.test(parsed)).toBe(true); + expect(parsed.length).toBeLessThanOrEqual(48); + }), + ); + }); + + test("should be idempotent over the bank slip digits", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const parsed = parseBoleto(value); + + expect(parseBoleto(parsed)).toBe(parsed); + }), + ); + }); + + test("should give back the digits of a masked generated bank slip", () => { + fc.assert( + fc.property(fc.constantFrom("bancario", "arrecadacao"), (type) => { + const value = generateBoleto({ type }); + const masked = `${value.slice(0, 5)}. ${value.slice(5, 20)}-${value.slice(20)}`; + + expect(parseBoleto(masked)).toBe(value); + }), + ); + }); + + test("should never throw and always return the bank slip digits as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof parseBoleto(text)).toBe("string"); + expect(typeof parseBoleto(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("parseBoleto benchmarks", () => { + const LINE = "23793.38128 60007.827136 95000.063305 9 84350000026035"; + + bench("parseBoleto", () => { + parseBoleto(LINE); + }); + + bench("isValidBoleto", () => { + isValidBoleto(LINE); + }); + + bench("getBoletoInfo", () => { + getBoletoInfo(LINE); + }); +}); + +describe("parseBoleto types", () => { + test("should take a string or number and return a string", () => { + expectTypeOf(parseBoleto).parameter(0).toEqualTypeOf(); + expectTypeOf(parseBoleto).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-cep/parse-cep.test.ts b/src/parse-cep/parse-cep.test.ts index c71b150b..98d14fb7 100644 --- a/src/parse-cep/parse-cep.test.ts +++ b/src/parse-cep/parse-cep.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parseCep } from "./parse-cep"; describe("parseCep", () => { @@ -18,4 +24,25 @@ describe("parseCep", () => { // @ts-expect-error not a string or number expect(parseCep(null)).toBe(""); }); + + describe("properties", () => { + test("should return at most the digits of a CEP", () => { + expectMatchesPattern(parseCep, /^\d{0,8}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCep, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCep, "string", anyValue); + }); + }); +}); + +describe("parseCep types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCep).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCep).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-certidao/parse-certidao.test.ts b/src/parse-certidao/parse-certidao.test.ts index 99a5ac17..26dee308 100644 --- a/src/parse-certidao/parse-certidao.test.ts +++ b/src/parse-certidao/parse-certidao.test.ts @@ -1,5 +1,18 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { parseCertidao } from "./parse-certidao"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { CERTIDAO_TYPES } from "./constants"; +import { parseCertidao, type Certidao, type CertidaoType } from "./parse-certidao"; + +const findMatricula = (base: string): string => { + for (let pair = 0; pair < 100; pair++) { + const value = `${base}${String(pair).padStart(2, "0")}`; + + if (parseCertidao(value) !== null) return value; + } + + return ""; +}; describe("parseCertidao", () => { describe("should return null", () => { @@ -108,4 +121,79 @@ describe("parseCertidao", () => { }); }); }); + + describe("properties", () => { + const parts = fc.tuple( + fc.stringMatching(/^[0-9]{6}$/), + fc.stringMatching(/^[0-9]{2}$/), + fc.stringMatching(/^[0-9]{2}$/), + fc.integer({ min: 1000, max: 9999 }), + fc.integer({ min: 1, max: 9 }), + fc.stringMatching(/^[0-9]{5}$/), + fc.stringMatching(/^[0-9]{3}$/), + fc.stringMatching(/^[0-9]{7}$/), + ); + + test("should give back every field of a valid matrícula", () => { + fc.assert( + fc.property(parts, (fields) => { + const [registryCns, acervo, service, year, typeCode, book, page, term] = fields; + const registry = `${registryCns}${acervo}${service}${year}${typeCode}`; + const value = findMatricula(`${registry}${book}${page}${term}`); + const parsed = parseCertidao(value); + + expect(parsed?.registryCns).toBe(registryCns); + expect(parsed?.acervo).toBe(acervo); + expect(parsed?.service).toBe(service); + expect(parsed?.year).toBe(year); + expect(parsed?.typeCode).toBe(typeCode); + expect(parsed?.book).toBe(book); + expect(parsed?.page).toBe(page); + expect(parsed?.term).toBe(term); + expect(parsed?.checkDigits).toBe(value.slice(30)); + expect(parsed?.type).toBe(CERTIDAO_TYPES[typeCode - 1]); + }), + ); + }); + + test("should never throw and always return a matrícula or null", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const parsed = parseCertidao(value as string); + + expect(parsed === null || typeof parsed.registryCns === "string").toBe(true); + }), + ); + }); + }); +}); + +describe("parseCertidao types", () => { + test("should take a string or number and return a Certidao or null", () => { + expectTypeOf(parseCertidao).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCertidao).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + registryCns: string; + acervo: string; + service: string; + year: number; + type: CertidaoType; + typeCode: number; + book: string; + page: string; + term: string; + checkDigits: string; + }>(); + expectTypeOf().toEqualTypeOf< + | "birth" + | "marriage" + | "religious-marriage" + | "death" + | "stillbirth" + | "banns" + | "other" + | "emancipation" + | "interdiction" + >(); + }); }); diff --git a/src/parse-cnh/parse-cnh.test.ts b/src/parse-cnh/parse-cnh.test.ts index 9132adc2..010f1ee9 100644 --- a/src/parse-cnh/parse-cnh.test.ts +++ b/src/parse-cnh/parse-cnh.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parseCnh } from "./parse-cnh"; describe("parseCnh", () => { @@ -18,4 +24,25 @@ describe("parseCnh", () => { // @ts-expect-error not a string or number expect(parseCnh(null)).toBe(""); }); + + describe("properties", () => { + test("should return at most the digits of a CNH", () => { + expectMatchesPattern(parseCnh, /^\d{0,11}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCnh, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCnh, "string", anyValue); + }); + }); +}); + +describe("parseCnh types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCnh).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCnh).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-cnpj/parse-cnpj.test.ts b/src/parse-cnpj/parse-cnpj.test.ts index f6e445b9..2f4b1830 100644 --- a/src/parse-cnpj/parse-cnpj.test.ts +++ b/src/parse-cnpj/parse-cnpj.test.ts @@ -1,5 +1,9 @@ -import { describe, expect, it } from "../_internals/test/runtime"; -import { parseCnpj } from "./parse-cnpj"; +import * as fc from "fast-check"; + +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { expectAlwaysReturnsType } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { parseCnpj, type ParseCnpjOptions } from "./parse-cnpj"; describe("parseCnpj", () => { it("should remove CNPJ mask characters", () => { @@ -26,4 +30,43 @@ describe("parseCnpj", () => { // @ts-expect-error not a string or number expect(parseCnpj(null)).toBe(""); }); + + describe("properties", () => { + test("should return at most the characters of a CNPJ, for both versions", () => { + fc.assert( + fc.property(anyText, (value) => { + expect(parseCnpj(value)).toMatch(/^\d{0,14}$/); + expect(parseCnpj(value, { version: 2 })).toMatch(/^[0-9A-Z]{0,14}$/); + }), + ); + }); + + test("should be idempotent for both versions", () => { + fc.assert( + fc.property(anyText, (value) => { + const numeric = parseCnpj(value); + const alphanumeric = parseCnpj(value, { version: 2 }); + + expect(parseCnpj(numeric)).toBe(numeric); + expect(parseCnpj(alphanumeric, { version: 2 })).toBe(alphanumeric); + }), + ); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCnpj, "string", anyValue); + }); + }); +}); + +describe("parseCnpj types", () => { + test("should take a string or number value and options and return a string", () => { + expectTypeOf(parseCnpj).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCnpj).parameter(1).toEqualTypeOf(); + expectTypeOf(parseCnpj).returns.toEqualTypeOf(); + }); + + test("should type the version option as an optional 1 or 2", () => { + expectTypeOf().toEqualTypeOf<1 | 2 | undefined>(); + }); }); diff --git a/src/parse-cpf/parse-cpf.test.ts b/src/parse-cpf/parse-cpf.test.ts index 8d43a5b1..ec858e94 100644 --- a/src/parse-cpf/parse-cpf.test.ts +++ b/src/parse-cpf/parse-cpf.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parseCpf } from "./parse-cpf"; describe("parseCpf", () => { @@ -18,4 +24,25 @@ describe("parseCpf", () => { // @ts-expect-error not a string or number expect(parseCpf(null)).toBe(""); }); + + describe("properties", () => { + test("should return at most the digits of a CPF", () => { + expectMatchesPattern(parseCpf, /^\d{0,11}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseCpf, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseCpf, "string", anyValue); + }); + }); +}); + +describe("parseCpf types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseCpf).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCpf).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-currency/parse-currency.test.ts b/src/parse-currency/parse-currency.test.ts index fb99af25..7b0bbf42 100644 --- a/src/parse-currency/parse-currency.test.ts +++ b/src/parse-currency/parse-currency.test.ts @@ -1,6 +1,10 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { twoDecimalAmounts } from "../_internals/test/arbitraries"; +import { expectRoundTrip } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { formatCurrency } from "../format-currency/format-currency"; -import { parseCurrency } from "./parse-currency"; +import { parseCurrency, type ParseCurrencyOptions } from "./parse-currency"; describe("parseCurrency", () => { describe("should return correct values", () => { @@ -121,4 +125,29 @@ describe("parseCurrency", () => { ); }); }); + + describe("properties", () => { + test("should never throw and always return a finite number, regardless of the input", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const result = parseCurrency(value as never); + + expect(Number.isFinite(result)).toBe(true); + }), + ); + }); + + test("should round-trip with formatCurrency for any value with 2 decimals", () => { + expectRoundTrip(formatCurrency, parseCurrency, twoDecimalAmounts); + }); + }); +}); + +describe("parseCurrency types", () => { + test("should take a string, options, and return a number", () => { + expectTypeOf(parseCurrency).parameter(0).toEqualTypeOf(); + expectTypeOf(parseCurrency).parameter(1).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(parseCurrency).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-iban/parse-iban.test.ts b/src/parse-iban/parse-iban.test.ts index fde05391..f209de8d 100644 --- a/src/parse-iban/parse-iban.test.ts +++ b/src/parse-iban/parse-iban.test.ts @@ -1,7 +1,19 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { formatIban } from "../format-iban/format-iban"; import { isValidIban } from "../is-valid-iban/is-valid-iban"; -import { parseIban } from "./parse-iban"; +import { parseIban, type Iban } from "./parse-iban"; + +const findBrazilianIban = (body: string): string => { + for (let pair = 0; pair < 97; pair++) { + const candidate = `BR${String(pair).padStart(2, "0")}${body}`; + + if (isValidIban(candidate)) return candidate; + } + + return ""; +}; describe("parseIban", () => { describe("should return the parsed iban", () => { @@ -128,4 +140,55 @@ describe("parseIban", () => { }); } }); + + describe("properties", () => { + const bodies = fc.stringMatching(/^[0-9]{23}[CP][A-Z0-9]$/); + + test("should split an IBAN into fields that spell it back", () => { + fc.assert( + fc.property(bodies, (body) => { + const iban = findBrazilianIban(body); + const parsed = parseIban(formatIban(iban)); + const account = `${parsed?.bankIspb}${parsed?.branch}${parsed?.account}`; + const owner = `${parsed?.accountType}${parsed?.owner}`; + + expect(`${parsed?.countryCode}${parsed?.checkDigits}${account}${owner}`).toBe(iban); + }), + ); + }); + + test("should return a value exactly when the IBAN is valid", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + expect(parseIban(value) !== null).toBe(isValidIban(value)); + }), + ); + }); + + test("should never throw and always return an IBAN or null", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const parsed = parseIban(value as string); + + expect(parsed === null || parsed.countryCode === "BR").toBe(true); + }), + ); + }); + }); +}); + +describe("parseIban types", () => { + test("should take a string and return an Iban or null", () => { + expectTypeOf(parseIban).parameter(0).toEqualTypeOf(); + expectTypeOf(parseIban).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + countryCode: "BR"; + checkDigits: string; + bankIspb: string; + branch: string; + account: string; + accountType: "C" | "P"; + owner: string; + }>(); + }); }); diff --git a/src/parse-legal-nature/parse-legal-nature.test.ts b/src/parse-legal-nature/parse-legal-nature.test.ts index 60e8ff62..dfbc0dd6 100644 --- a/src/parse-legal-nature/parse-legal-nature.test.ts +++ b/src/parse-legal-nature/parse-legal-nature.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parseLegalNature } from "./parse-legal-nature"; describe("parseLegalNature", () => { @@ -14,4 +20,25 @@ describe("parseLegalNature", () => { // @ts-expect-error not a string or number expect(parseLegalNature(null)).toBe(""); }); + + describe("properties", () => { + test("should return at most the digits of a legal nature code", () => { + expectMatchesPattern(parseLegalNature, /^\d{0,4}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseLegalNature, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseLegalNature, "string", anyValue); + }); + }); +}); + +describe("parseLegalNature types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseLegalNature).parameter(0).toEqualTypeOf(); + expectTypeOf(parseLegalNature).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-license-plate/parse-license-plate.test.ts b/src/parse-license-plate/parse-license-plate.test.ts index b29eadee..6d58be54 100644 --- a/src/parse-license-plate/parse-license-plate.test.ts +++ b/src/parse-license-plate/parse-license-plate.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parseLicensePlate } from "./parse-license-plate"; describe("parseLicensePlate", () => { @@ -16,4 +18,39 @@ describe("parseLicensePlate", () => { // @ts-expect-error not a string expect(parseLicensePlate(null)).toBe(""); }); + + describe("properties", () => { + test("should only ever return up to seven uppercase alphanumerics", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + expect(/^[A-Z0-9]{0,7}$/.test(parseLicensePlate(value))).toBe(true); + }), + ); + }); + + test("should be idempotent over the plate characters", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const parsed = parseLicensePlate(value); + + expect(parseLicensePlate(parsed)).toBe(parsed); + }), + ); + }); + + test("should never throw and always return the plate characters as a string", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(typeof parseLicensePlate(value as string)).toBe("string"); + }), + ); + }); + }); +}); + +describe("parseLicensePlate types", () => { + test("should take a string and return a string", () => { + expectTypeOf(parseLicensePlate).parameter(0).toEqualTypeOf(); + expectTypeOf(parseLicensePlate).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-nfe-key/parse-nfe-key.test.ts b/src/parse-nfe-key/parse-nfe-key.test.ts index ec106b0a..8c77d204 100644 --- a/src/parse-nfe-key/parse-nfe-key.test.ts +++ b/src/parse-nfe-key/parse-nfe-key.test.ts @@ -1,10 +1,19 @@ -import { describe, expect, test } from "../_internals/test/runtime"; -import { parseNfeKey } from "./parse-nfe-key"; +import * as fc from "fast-check"; + +import { IBGE_UF_CODES } from "../_internals/constants/ibge-uf-codes"; +import { type StateCode } from "../_internals/constants/states"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { parseNfeKey, type NfeKey, type NfeKeyModel } from "./parse-nfe-key"; const KEY_SP = "35170458716523000119550010000000121000123458"; const KEY_RS = "43160472202112000136550000000010571048440722"; const KEY_CPF_PADDED = "35170400040364478829550010000000121000123457"; +const CHECK_DIGITS = Array.from({ length: 10 }, (_, digit) => String(digit)); + +const buildNfeKey = (base: string): string => + CHECK_DIGITS.map((digit) => `${base}${digit}`).find((key) => parseNfeKey(key) !== null) ?? ""; + describe("parseNfeKey", () => { describe("should return null", () => { test("when it is null", () => { @@ -92,4 +101,71 @@ describe("parseNfeKey", () => { expect(parseNfeKey("35170458716523000119650010000000121000123450")?.model).toBe("65"); }); }); + + describe("properties", () => { + const parts = fc.tuple( + fc.constantFrom(...Object.keys(IBGE_UF_CODES)), + fc.stringMatching(/^[0-9]{2}$/), + fc.integer({ min: 1, max: 12 }), + fc.stringMatching(/^[0-9]{14}$/), + fc.constantFrom("55", "57", "58", "65"), + fc.stringMatching(/^[0-9]{3}$/), + fc.integer({ min: 1, max: 999_999_999 }), + fc.integer({ min: 1, max: 9 }), + fc.stringMatching(/^[0-9]{8}$/), + ); + + test("should give back every field of a well-formed access key", () => { + fc.assert( + fc.property(parts, (fields) => { + const [uf, year, month, taxId, model, series, number, emissionType, code] = fields; + const issuer = `${uf}${year}${String(month).padStart(2, "0")}${taxId}`; + const document = `${model}${series}${String(number).padStart(9, "0")}`; + const key = buildNfeKey(`${issuer}${document}${emissionType}${code}`); + const parsed = parseNfeKey(key); + + expect(parsed?.state).toBe(IBGE_UF_CODES[uf]); + expect(parsed?.year).toBe(2000 + Number(year)); + expect(parsed?.month).toBe(month); + expect(parsed?.taxId).toBe(taxId); + expect(parsed?.model).toBe(model); + expect(parsed?.series).toBe(Number(series)); + expect(parsed?.number).toBe(number); + expect(parsed?.emissionType).toBe(emissionType); + expect(parsed?.code).toBe(code); + expect(parsed?.checkDigit).toBe(Number(key.charAt(43))); + }), + ); + }); + + test("should never throw and always return an access key or null", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const parsed = parseNfeKey(value as string); + + expect(parsed === null || typeof parsed.taxId === "string").toBe(true); + }), + ); + }); + }); +}); + +describe("parseNfeKey types", () => { + test("should take a string and return an NfeKey or null", () => { + expectTypeOf(parseNfeKey).parameter(0).toEqualTypeOf(); + expectTypeOf(parseNfeKey).returns.toEqualTypeOf(); + expectTypeOf().toEqualTypeOf<{ + state: StateCode; + year: number; + month: number; + taxId: string; + model: NfeKeyModel; + series: number; + number: number; + emissionType: number; + code: string; + checkDigit: number; + }>(); + expectTypeOf().toEqualTypeOf<"55" | "57" | "58" | "65">(); + }); }); diff --git a/src/parse-passport/parse-passport.test.ts b/src/parse-passport/parse-passport.test.ts index 0af65c8d..4e44a03f 100644 --- a/src/parse-passport/parse-passport.test.ts +++ b/src/parse-passport/parse-passport.test.ts @@ -1,4 +1,11 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import { anyText, anyValue, asciiAlphanumericText } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectCaseInsensitive, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { parsePassport } from "./parse-passport"; describe("parsePassport", () => { @@ -32,4 +39,29 @@ describe("parsePassport", () => { expect(parsePassport(null)).toBe(""); }); }); + + describe("properties", () => { + test("should always return at most 8 uppercase alphanumeric characters", () => { + expectMatchesPattern(parsePassport, /^[0-9A-Z]{0,8}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parsePassport, anyText); + }); + + test("should ignore the case of an ascii alphanumeric value", () => { + expectCaseInsensitive(parsePassport, asciiAlphanumericText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parsePassport, "string", anyValue); + }); + }); +}); + +describe("parsePassport types", () => { + test("should take a string and return a string", () => { + expectTypeOf(parsePassport).parameter(0).toEqualTypeOf(); + expectTypeOf(parsePassport).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-phone/parse-phone.test.ts b/src/parse-phone/parse-phone.test.ts index 296c07b5..11c42266 100644 --- a/src/parse-phone/parse-phone.test.ts +++ b/src/parse-phone/parse-phone.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { generatePhone } from "../generate-phone/generate-phone"; import { parsePhone } from "./parse-phone"; describe("parsePhone", () => { @@ -50,4 +53,55 @@ describe("parsePhone", () => { it("should accept numbers", () => { expect(parsePhone(11_988_887_777)).toBe("11988887777"); }); + + describe("properties", () => { + test("should only ever return at most eleven digits", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const parsed = parsePhone(value); + + expect(/^\d*$/.test(parsed)).toBe(true); + expect(parsed.length).toBeLessThanOrEqual(11); + }), + ); + }); + + test("should be idempotent over the phone digits", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), (value) => { + const parsed = parsePhone(value); + + expect(parsePhone(parsed)).toBe(parsed); + }), + ); + }); + + test("should drop the country code of a generated number however it is written", () => { + fc.assert( + fc.property(fc.constantFrom("mobile", "landline"), (type) => { + const phone = generatePhone(type); + + expect(parsePhone(phone)).toBe(phone); + expect(parsePhone(`+55 ${phone}`)).toBe(phone); + expect(parsePhone(`0055${phone}`)).toBe(phone); + }), + ); + }); + + test("should never throw and always return the phone digits as a string", () => { + fc.assert( + fc.property(fc.string({ unit: "grapheme" }), fc.integer(), (text, number) => { + expect(typeof parsePhone(text)).toBe("string"); + expect(typeof parsePhone(number)).toBe("string"); + }), + ); + }); + }); +}); + +describe("parsePhone types", () => { + test("should take a string or number and return a string", () => { + expectTypeOf(parsePhone).parameter(0).toEqualTypeOf(); + expectTypeOf(parsePhone).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-pis/parse-pis.test.ts b/src/parse-pis/parse-pis.test.ts index bd76e846..b2f3bab6 100644 --- a/src/parse-pis/parse-pis.test.ts +++ b/src/parse-pis/parse-pis.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parsePis } from "./parse-pis"; describe("parsePis", () => { @@ -18,4 +24,25 @@ describe("parsePis", () => { // @ts-expect-error not a string or number expect(parsePis(null)).toBe(""); }); + + describe("properties", () => { + test("should return at most the digits of a PIS", () => { + expectMatchesPattern(parsePis, /^\d{0,11}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parsePis, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parsePis, "string", anyValue); + }); + }); +}); + +describe("parsePis types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parsePis).parameter(0).toEqualTypeOf(); + expectTypeOf(parsePis).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-pix-key/parse-pix-key.test.ts b/src/parse-pix-key/parse-pix-key.test.ts index 661e62b4..2c4cc298 100644 --- a/src/parse-pix-key/parse-pix-key.test.ts +++ b/src/parse-pix-key/parse-pix-key.test.ts @@ -1,11 +1,30 @@ -import { describe, expect, test } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; +import { formatCnpj } from "../format-cnpj/format-cnpj"; import { generateCnpj } from "../generate-cnpj/generate-cnpj"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { generatePhone } from "../generate-phone/generate-phone"; -import { parsePixKey } from "./parse-pix-key"; +import { type PixKey, type PixKeyType, parsePixKey } from "./parse-pix-key"; const AMBIGUOUS = "51998259765"; +const PIX_KEY_KINDS = ["cpf", "cnpj", "email", "evp", "phone"] as const; + +const maskCnpj = (cnpj: string): string => { + const head = `${cnpj.slice(0, 2)}.${cnpj.slice(2, 5)}.${cnpj.slice(5, 8)}`; + + return `${head}/${cnpj.slice(8, 12)}-${cnpj.slice(12)}`; +}; + +const buildPixKey = (kind: (typeof PIX_KEY_KINDS)[number], email: string, evp: string): string => { + if (kind === "cpf") return generateCpf(); + if (kind === "cnpj") return maskCnpj(generateCnpj()); + if (kind === "phone") return `+55${generatePhone("mobile")}`; + + return kind === "email" ? email : evp; +}; + describe("parsePixKey", () => { describe("should return null", () => { test("when it is an empty or blank string", () => { @@ -255,8 +274,70 @@ describe("parsePixKey", () => { for (let index = 0; index < 200; index++) { const cnpj = generateCnpj(); - expect(parsePixKey(cnpj)).toEqual({ type: "cnpj", value: cnpj }); + expect(parsePixKey(formatCnpj(cnpj))).toEqual({ type: "cnpj", value: cnpj }); } }); }); + + describe("properties", () => { + const emails = fc.stringMatching(/^[a-z0-9]{1,10}@[a-z0-9]{1,10}\.com$/); + + const keys = fc.tuple(fc.constantFrom(...PIX_KEY_KINDS), emails, fc.uuid()); + + test("should recognize every kind of key the DICT defines", () => { + fc.assert( + fc.property(keys, ([kind, email, evp]) => { + const parsed = parsePixKey(buildPixKey(kind, email, evp)); + + expect(parsed?.type).toBe(kind); + }), + ); + }); + + test("should return a canonical value that parses back to itself", () => { + fc.assert( + fc.property(keys, ([kind, email, evp]) => { + const parsed = parsePixKey(buildPixKey(kind, email, evp)); + const again = parsePixKey(parsed?.value ?? ""); + + expect(again?.type).toBe(parsed?.type); + expect(again?.value).toBe(parsed?.value); + }), + ); + }); + + test("should normalize the case and the spacing of a key", () => { + fc.assert( + fc.property(keys, ([kind, email, evp]) => { + const key = buildPixKey(kind, email, evp); + const parsed = parsePixKey(key); + const shouted = parsePixKey(` ${key.toUpperCase()} `); + + expect(shouted?.value).toBe(parsed?.value); + }), + ); + }); + + test("should never throw and always return a Pix key or null", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const parsed = parsePixKey(value as string); + + expect(parsed === null || typeof parsed.value === "string").toBe(true); + }), + ); + }); + }); +}); + +describe("parsePixKey types", () => { + test("should take a string and return a Pix key or null", () => { + expectTypeOf(parsePixKey).parameter(0).toEqualTypeOf(); + expectTypeOf(parsePixKey).returns.toEqualTypeOf(); + }); + + test("should restrict the Pix key shape and its type", () => { + expectTypeOf().toEqualTypeOf<{ type: PixKeyType; value: string }>(); + expectTypeOf().toEqualTypeOf<"cpf" | "cnpj" | "email" | "phone" | "evp">(); + }); }); diff --git a/src/parse-pix-payload/parse-pix-payload.test.ts b/src/parse-pix-payload/parse-pix-payload.test.ts index 42a6e8e3..f5db93c1 100644 --- a/src/parse-pix-payload/parse-pix-payload.test.ts +++ b/src/parse-pix-payload/parse-pix-payload.test.ts @@ -1,8 +1,10 @@ +import * as fc from "fast-check"; + import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; -import { describe, expect, test } from "../_internals/test/runtime"; +import { describe, expect, expectTypeOf, test } from "../_internals/test/runtime"; import { generateCpf } from "../generate-cpf/generate-cpf"; import { generatePixPayload } from "../generate-pix-payload/generate-pix-payload"; -import { parsePixPayload } from "./parse-pix-payload"; +import { type PixPayload, type PixPointOfInitiation, parsePixPayload } from "./parse-pix-payload"; const BACEN_STATIC = "00020126580014br.gov.bcb.pix0136123e4567-e12b-12d1-a456-4266554400005204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***63041D3D"; @@ -386,4 +388,66 @@ describe("parsePixPayload", () => { } }); }); + + describe("properties", () => { + const names = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{0,24}$/); + + test("should read back every key a generated payload can carry", () => { + fc.assert( + fc.property(names, fc.uuid(), (merchantName, key) => { + const payload = generatePixPayload({ key, merchantName, merchantCity: "BRASILIA" }); + const parsed = parsePixPayload(payload ?? ""); + + expect(parsed?.merchantName).toBe(merchantName); + expect(parsed?.merchantCity).toBe("BRASILIA"); + expect(parsed?.key).toBe(key); + }), + ); + }); + + test("should return null when the CRC does not match the payload", () => { + fc.assert( + fc.property(names, fc.integer({ min: 0, max: 3 }), (merchantName, index) => { + const key = generateCpf(); + const payload = generatePixPayload({ key, merchantName, merchantCity: "BRASILIA" }); + const crc = (payload ?? "").slice(-4); + const replacement = crc.charAt(index) === "0" ? "1" : "0"; + const broken = `${(payload ?? "").slice(0, -4)}${crc.slice(0, index)}${replacement}${crc.slice(index + 1)}`; + + expect(parsePixPayload(broken)).toBeNull(); + }), + ); + }); + + test("should never throw and always return a BR Code or null", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + const parsed = parsePixPayload(value as string); + + expect(parsed === null || typeof parsed.merchantName === "string").toBe(true); + }), + ); + }); + }); +}); + +describe("parsePixPayload types", () => { + test("should take a string and return a Pix payload or null", () => { + expectTypeOf(parsePixPayload).parameter(0).toEqualTypeOf(); + expectTypeOf(parsePixPayload).returns.toEqualTypeOf(); + }); + + test("should restrict the Pix payload shape and its point of initiation", () => { + expectTypeOf().toEqualTypeOf<{ + key?: string; + url?: string; + description?: string; + merchantName: string; + merchantCity: string; + amount?: number; + txid?: string; + pointOfInitiation?: PixPointOfInitiation; + }>(); + expectTypeOf().toEqualTypeOf<"static" | "dynamic">(); + }); }); diff --git a/src/parse-processo-juridico/parse-processo-juridico.test.ts b/src/parse-processo-juridico/parse-processo-juridico.test.ts index fece2282..91aab9b3 100644 --- a/src/parse-processo-juridico/parse-processo-juridico.test.ts +++ b/src/parse-processo-juridico/parse-processo-juridico.test.ts @@ -1,4 +1,10 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyText, anyValue } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { parseProcessoJuridico } from "./parse-processo-juridico"; describe("parseProcessoJuridico", () => { @@ -22,4 +28,25 @@ describe("parseProcessoJuridico", () => { // @ts-expect-error not a string or number expect(parseProcessoJuridico(null)).toBe(""); }); + + describe("properties", () => { + test("should return at most the digits of a processo juridico", () => { + expectMatchesPattern(parseProcessoJuridico, /^\d{0,20}$/, anyText); + }); + + test("should be idempotent", () => { + expectIdempotent(parseProcessoJuridico, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseProcessoJuridico, "string", anyValue); + }); + }); +}); + +describe("parseProcessoJuridico types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseProcessoJuridico).parameter(0).toEqualTypeOf(); + expectTypeOf(parseProcessoJuridico).returns.toEqualTypeOf(); + }); }); diff --git a/src/parse-voter-id/parse-voter-id.test.ts b/src/parse-voter-id/parse-voter-id.test.ts index 4c213ff8..638b41d9 100644 --- a/src/parse-voter-id/parse-voter-id.test.ts +++ b/src/parse-voter-id/parse-voter-id.test.ts @@ -1,4 +1,12 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import { anyText, anyValue, digits } from "../_internals/test/arbitraries"; +import { + expectAlwaysReturnsType, + expectIdempotent, + expectMatchesPattern, + expectRoundTrip, +} from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { formatVoterId } from "../format-voter-id/format-voter-id"; import { parseVoterId } from "./parse-voter-id"; describe("parseVoterId", () => { @@ -32,4 +40,29 @@ describe("parseVoterId", () => { it("should ignore digits after the 12-digit length when the 9th/10th digits are not SP/MG, even with extra digits", () => { expect(parseVoterId("12345678905999")).toBe("123456789059"); }); + + describe("properties", () => { + test("should return at most the digits of the longest voter id", () => { + expectMatchesPattern(parseVoterId, /^\d{0,13}$/, anyText); + }); + + test("should undo the formatting of a 12 digit voter id", () => { + expectRoundTrip(formatVoterId, parseVoterId, digits(12)); + }); + + test("should be idempotent", () => { + expectIdempotent(parseVoterId, anyText); + }); + + test("should never throw and always return a string", () => { + expectAlwaysReturnsType(parseVoterId, "string", anyValue); + }); + }); +}); + +describe("parseVoterId types", () => { + test("should take a string or number value and return a string", () => { + expectTypeOf(parseVoterId).parameter(0).toEqualTypeOf(); + expectTypeOf(parseVoterId).returns.toEqualTypeOf(); + }); }); diff --git a/src/remove-accents/remove-accents.test.ts b/src/remove-accents/remove-accents.test.ts index e937cfbe..66d7e00b 100644 --- a/src/remove-accents/remove-accents.test.ts +++ b/src/remove-accents/remove-accents.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it } from "../_internals/test/runtime"; +import * as fc from "fast-check"; + +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; import { removeAccents } from "./remove-accents"; describe("removeAccents", () => { @@ -57,4 +59,57 @@ describe("removeAccents", () => { // @ts-expect-error: intentionally invalid input expect(removeAccents(123)).toBe(""); }); + + describe("properties", () => { + const accentedWordArbitrary = fc.constantFrom( + "São Paulo", + "Piauí", + "Ceará", + "Açaí", + "Você", + "à", + "Bahia", + "coração", + "não", + "então", + "único", + "Maranhão", + "Goiânia", + "café com pão", + ); + const accentedTextArbitrary = fc.array(accentedWordArbitrary).map((words) => words.join(" ")); + + test("should never throw, regardless of the input", () => { + fc.assert( + fc.property(fc.anything(), (value) => { + expect(() => removeAccents(value as never)).not.toThrow(); + }), + ); + }); + + test("should be idempotent", () => { + fc.assert( + fc.property(accentedTextArbitrary, (value) => { + const once = removeAccents(value); + + expect(removeAccents(once)).toBe(once); + }), + ); + }); + + test("should never return a string longer than the input", () => { + fc.assert( + fc.property(accentedTextArbitrary, (value) => { + expect(removeAccents(value).length).toBeLessThanOrEqual(value.length); + }), + ); + }); + }); +}); + +describe("removeAccents types", () => { + test("should take a string and return a string", () => { + expectTypeOf(removeAccents).parameter(0).toEqualTypeOf(); + expectTypeOf(removeAccents).returns.toEqualTypeOf(); + }); }); diff --git a/vite.config.ts b/vite.config.ts index 39097a4d..61ca8067 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -414,7 +414,7 @@ export default defineConfig({ }, }, { - files: ["**/*.test.ts", "**/*.test-d.ts", "**/*.bench.ts"], + files: ["**/*.test.ts", "src/_internals/test/**"], rules: { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unsafe-type-assertion": "off", @@ -448,6 +448,10 @@ export default defineConfig({ }, test: { exclude: ["**/node_modules/**", "**/dist/**", "**/.stryker-tmp/**", "**/reports/**"], + benchmark: { + include: ["src/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/dist/**", "**/.stryker-tmp/**", "**/reports/**"], + }, browser: { provider: webdriverio(), connectTimeout: 120_000,