From f6701e97cd4f343587c09b606e33ea59cb0e8a71 Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Fri, 12 Sep 2025 10:32:46 +0200 Subject: [PATCH] Split and update tests --- test/all.test.ts | 211 ------------------------------------------- test/env.test.ts | 57 ++++++++++++ test/parsing.test.ts | 184 +++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 211 deletions(-) delete mode 100644 test/all.test.ts create mode 100644 test/env.test.ts create mode 100644 test/parsing.test.ts diff --git a/test/all.test.ts b/test/all.test.ts deleted file mode 100644 index f19cb95..0000000 --- a/test/all.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import {expect, test, describe, beforeEach, afterEach} from 'bun:test' -import {yup} from '../src/index' -import {type FileResult, fileSync} from 'tmp' -import * as fs from 'node:fs/promises' - -import * as parsing from '../src/parsing' -import {parse} from '../src' - -beforeEach(() => process.env = {}) - -async function testing(value: string | undefined, scheme: yup.AnySchema, should_be_successful: boolean, data?: unknown) { - const key = 'DB_PORT' - if (value !== undefined) process.env[key] = value - - const res = await parsing.generic(scheme, key) - - if (should_be_successful) expect(res).toEqual({ok: true, data}) - else expect(res.ok).not.toBeTrue() -} - -// Most important function : read_object -describe('Object', () => { - test('basic', async () => { - const scheme = yup.object({}) - const res = await parsing.object(scheme) - expect(res).toEqual({ok: true, data: {}}) - }) - test('One level', async () => { - process.env = { - NAME: 'test', - PORT: '12', - TS: '2025-06-08 23:51:12' - } - const scheme = yup.object({name: yup.string(), port: yup.number(), ts: yup.date()}) - const res = await parsing.object(scheme) - expect(res).toEqual({ok: true, data: {name: 'test', port: 12, ts: new Date('2025-06-08 23:51:12')}}) - }) - test('Two levels', async () => { - process.env = { - DB_PORT: '12', - DB_HOST: 'localhost', - THING_A: 'Hello!', - THING_B: '212', - NAME: 'Pascal', - AGE: '27', - } - const expected = { - name: 'Pascal', - age: 27, - db: { - port: 12, - host: 'localhost' - }, - thing: { - a: 'Hello!', - b: 212, - } - } - - const scheme = yup.object({ - name: yup.string().required(), - age: yup.number().required(), - db: yup.object({ - port: yup.number().required(), - host: yup.string().required() - }).required(), - thing: yup.object({ - a: yup.string().required(), - b: yup.number().required(), - }).required() - }) - - const res = await parsing.object(scheme) - expect(res).toEqual({ok: true, data: expected}) - - const res2 = await parse(scheme) - expect(res.ok).toBeTrue() - expect(res2.data).toEqual(expected) - }) - test('Three levels', async () => { - process.env = { - DB_HOST: 'localhost', - THING_A: 'Hello!', - AGE: '27', - THING_THANG_A: '2025-06-06 23:56:12' - } - const expected = { - age: 27, - db: { - host: 'localhost' - }, - thing: { - a: 'Hello!', - thang: { - a: new Date('2025-06-06 23:56:12') - } - } - } - - const scheme = yup.object({ - age: yup.number(), - db: yup.object({ - host: yup.string() - }), - thing: yup.object({ - a: yup.string(), - thang: yup.object({ - a: yup.date() - }) - }) - }) - - const res = await parsing.object(scheme) - expect(res).toEqual({ok: true, data: expected}) - - const res2 = await parse(scheme) - expect(res2.data).toEqual(expected) - }) -}) - -// Read any from file -describe('from_file', () => { - let file: FileResult | undefined = undefined; - afterEach(() => { - file?.removeCallback() - file = undefined - }) - - async function testing(value: string | undefined, scheme: yup.AnySchema, should_be_successful: boolean, data?: unknown) { - file = fileSync() - - const name = file.name - if (value !== undefined) { - await fs.writeFile(file.name, value) - } else { - file.removeCallback() - file = undefined - } - - const key = 'DB_PORT' - process.env[key + '__FILE'] = name - - const res = await parsing.generic(scheme, key) - - if (should_be_successful) expect(res).toEqual({ok: true, data}) - else expect(res.ok).not.toBeTrue() - } - - test('basic string', () => testing('Coucou', yup.string(), true, 'Coucou')) - test('basic number', () => testing('12', yup.number(), true, 12)) - test('file does not exist', () => testing(undefined, yup.string(), false)) - test('Last empty line is trimmed', () => testing('Coucou\n\r\n ', yup.string(), true, 'Coucou')) -}) - -// Read generic types -describe('Integer', () => { - test('basic', () => testing('12', yup.number(), true, 12)) - test('not present', () => testing(undefined, yup.number().required(), false)) - test('optional', () => testing(undefined, yup.number(), true)) - test('default', () => testing(undefined, yup.number().default(21), true, 21)) - test('string', () => testing('coucou', yup.number(), false)) - test('min-max OK', () => testing('12', yup.number().min(11).max(13), true, 12)) - test('min-max KO', () => testing('10', yup.number().min(11).max(13), false, 10)) - test('min-max KO 2', () => testing('10', yup.number().min(11).max(13), false, 14)) -}) -describe('String', () => { - test('basic', () => testing('coucou', yup.string(), true, 'coucou')) - test('not present', () => testing(undefined, yup.string().required(), false)) - test('optional', () => testing(undefined, yup.string(), true, undefined)) - test('default', () => testing(undefined, yup.string().default('yeet'), true, 'yeet')) - test('Length min-max', () => testing('coucou', yup.string().max(7), true, 'coucou')) - test('KO: Length min-max', () => testing('coucou', yup.string().max(5), false)) -}) -describe('boolean', () => { - test('basic true', () => testing('true', yup.boolean(), true, true)) - test('basic 1', () => testing('1', yup.boolean(), true, true)) - test('basic 0', () => testing('0', yup.boolean(), true, false)) - test('not present', () => testing(undefined, yup.boolean().required(), false)) - test('optional', () => testing(undefined, yup.boolean(), true)) - test('default', () => testing(undefined, yup.boolean().default(false), true, false)) -}) - -// Read specific types -describe('Date', () => { - async function testing(value: string | undefined, scheme: yup.DateSchema, should_be_successful: boolean, data?: unknown) { - const key = 'DB_PORT' - if (value !== undefined) process.env[key] = value - - const res = await parsing.generic(scheme, key) - - if (should_be_successful) { - expect(res).toEqual({ok: true, data}) - } else { - expect(res).toEqual({ok: false}) - } - } - - test('basic', () => testing('2025-06-08 22:40:17', yup.date(), true, new Date('2025-06-08 22:40:17'))) - test('optional', () => testing(undefined, yup.date(), true)) -}) -describe('Array', () => { - test('basic', () => testing('12,13,14', yup.array(yup.number()), true, [12,13,14])) - /*test('not present', () => testing(undefined, yup.number().required(), false)) - test('optional', () => testing(undefined, yup.number(), true)) - test('default', () => testing(undefined, yup.number().default(21), true, 21)) - test('string', () => testing('coucou', yup.number(), false)) - test('min-max OK', () => testing('12', yup.number().min(11).max(13), true, 12)) - test('min-max KO', () => testing('10', yup.number().min(11).max(13), false, 10)) - test('min-max KO 2', () => testing('10', yup.number().min(11).max(13), false, 14))*/ -}) -describe.todo('Tuple') diff --git a/test/env.test.ts b/test/env.test.ts new file mode 100644 index 0000000..c59ae9a --- /dev/null +++ b/test/env.test.ts @@ -0,0 +1,57 @@ +import {test, describe, afterEach, beforeEach, expect} from 'bun:test' +import {type FileResult, fileSync, dirSync, type DirResult} from 'tmp' +import * as fs from 'node:fs/promises' + +import * as env from '../src/env' + +let file: FileResult +beforeEach(() => (file = fileSync())) +afterEach(() => file.removeCallback()) + +describe('File IO', () => { + test('Can read file content', async () => { + const message = 'coucou' + await fs.writeFile(file.name, message) + const res = await env.get_file_content(file.name) + expect(res).toEqual({ok: true, data: message}) + }) + test("Fails safe if file doesn't exist", async () => { + const res = await env.get_file_content('tralalero tralala') + expect(res.ok).not.toBeTrue() + }) + test('Reading a file trims whitespaces', async () => { + const message = 'coucou\r\n\n\r\n' + await fs.writeFile(file.name, message) + const res = await env.from_file(file.name) + expect(res).toEqual({ok: true, data: 'coucou'}) + }) + test.todo('Fails safe if no access to read') +}) +describe('Folder', () => { + let folder: DirResult + beforeEach(() => (folder = dirSync())) + afterEach(() => folder.removeCallback()) + test('Fails safe if path points to a folder', async () => { + const res = await env.get_file_content(folder.name) + expect(res.ok).not.toBeTrue() + }) +}) +describe('read_env', () => { + test('Can get value from env', async () => { + const data = 'coucou' + const key = 'DB_PORT' + process.env[key] = data + + const res = await env.read_env(key) + expect(res).toEqual({ok: true, data}) + }) + test('Can get value from file', async () => { + const data = 'coucou' + const key = 'DB_PORT' + process.env[`${key}__FILE`] = file.name + await fs.writeFile(file.name, data) + + const res = await env.read_env(key) + expect(res).toEqual({ok: true, data}) + }) +}) diff --git a/test/parsing.test.ts b/test/parsing.test.ts new file mode 100644 index 0000000..e7a3e54 --- /dev/null +++ b/test/parsing.test.ts @@ -0,0 +1,184 @@ +import {expect, test, describe, beforeEach} from 'bun:test' + +import {yup} from '../src/index' +import * as parsing from '../src/parsing' +import {Ok} from '../src/helpers' + +beforeEach(() => (process.env = {})) + +describe('Object', () => { + test('basic', async () => { + const scheme = yup.object({}) + const res = await parsing.object(scheme) + expect(res).toEqual({ok: true, data: {}}) + }) + + describe('empty', () => { + test('Optional', async () => { + const res = await parsing.object(yup.object({thing: yup.number()})) + // @ts-expect-error empty is undefined, duh ?? + expect(res).toEqual({ok: true, data: {}}) + }) + test('Required missing', async () => { + const res = await parsing.object(yup.object({thing: yup.number().required()})) + expect(res.ok).not.toBeTrue() + }) + test('default', async () => { + const res = await parsing.object(yup.object({thing: yup.number().default(42)})) + expect(res).toEqual({ok: true, data: {thing: 42}}) + }) + }) + + describe('Nested objects', () => { + test('One level', async () => { + process.env = { + NAME: 'test', + PORT: '12', + TS: '2025-06-08 23:51:12' + } + const scheme = yup.object({name: yup.string(), port: yup.number(), ts: yup.date()}) + const res = await parsing.object(scheme) + expect(res).toEqual({ok: true, data: {name: 'test', port: 12, ts: new Date('2025-06-08 23:51:12')}}) + }) + test('Two levels', async () => { + process.env = { + DB_PORT: '12', + DB_HOST: 'localhost', + THING_A: 'Hello!', + THING_B: '212', + NAME: 'Pascal', + AGE: '27' + } + const expected = { + name: 'Pascal', + age: 27, + db: { + port: 12, + host: 'localhost' + }, + thing: { + a: 'Hello!', + b: 212 + } + } + + const scheme = yup.object({ + name: yup.string().required(), + age: yup.number().required(), + db: yup + .object({ + port: yup.number().required(), + host: yup.string().required() + }) + .required(), + thing: yup + .object({ + a: yup.string().required(), + b: yup.number().required() + }) + .required() + }) + + const res = await parsing.object(scheme) + expect(res).toEqual({ok: true, data: expected}) + }) + test('Three levels', async () => { + process.env = { + DB_HOST: 'localhost', + THING_A: 'Hello!', + AGE: '27', + THING_THANG_A: '2025-06-06 23:56:12' + } + const expected = { + age: 27, + db: { + host: 'localhost' + }, + thing: { + a: 'Hello!', + thang: { + a: new Date('2025-06-06 23:56:12') + } + } + } + + const scheme = yup.object({ + age: yup.number(), + db: yup.object({ + host: yup.string() + }), + thing: yup.object({ + a: yup.string(), + thang: yup.object({ + a: yup.date() + }) + }) + }) + + const res = await parsing.object(scheme) + expect(res).toEqual({ok: true, data: expected}) + }) + }) + + describe('Specific parsing', () => { + async function testing(env_value: string, scheme: yup.AnySchema, should_be_successful: boolean, thing?: T) { + process.env['THING'] = env_value + const my_scheme = yup.object({thing: scheme}) + + const res: Ok<{thing: T}> = await parsing.object(my_scheme) + // @ts-expect-error annoying typing issue, but it works, so, yk + if (should_be_successful) expect(res).toEqual({ok: true, data: {thing}}) + else expect(res.ok).not.toBeTrue() + } + + describe('array', () => { + test('basic', () => testing('1,2,3', yup.array(yup.number()), true, [1, 2, 3])) + test('basic with whitespaces', () => testing(' 1 , 2 , 3 ', yup.array(yup.number()), true, [1, 2, 3])) + test('Empty', () => testing('', yup.array(yup.number()), true, [])) + test('Empty fails with minimum length', () => testing('', yup.array(yup.number()).min(3), false)) + test('With minimum elements', () => testing('1,2,3', yup.array(yup.number()).min(3), true, [1, 2, 3])) + }) + + describe('tuple', () => { + test('basic', () => testing('1,2,3', yup.tuple([yup.number(), yup.number(), yup.number()]), true, [1, 2, 3])) + test('Missing value', () => testing('1,2', yup.tuple([yup.number(), yup.number(), yup.number()]), false)) + test('Too much values', () => testing('1,2,3,4', yup.tuple([yup.number(), yup.number(), yup.number()]), false)) + }) + }) +}) + +describe('generic', () => { + async function testing( + value: string | string[], + scheme: yup.AnySchema, + should_be_successful: boolean, + data?: unknown + ) { + const res = await parsing.generic(value, scheme) + if (should_be_successful) expect(res).toEqual({ok: true, data}) + else expect(res.ok).not.toBeTrue() + } + + describe('Integer', () => { + test('basic', () => testing('12', yup.number(), true, 12)) + test('string', () => testing('coucou', yup.number(), false)) + test('min-max OK', () => testing('12', yup.number().min(11).max(13), true, 12)) + test('min-max KO', () => testing('10', yup.number().min(11).max(13), false, 10)) + test('min-max KO 2', () => testing('10', yup.number().min(11).max(13), false, 14)) + }) + describe('String', () => { + test('basic', () => testing('coucou', yup.string(), true, 'coucou')) + test('Length min-max', () => testing('coucou', yup.string().max(7), true, 'coucou')) + test('KO: Length min-max', () => testing('coucou', yup.string().max(5), false)) + }) + describe('boolean', () => { + test('basic true', () => testing('true', yup.boolean(), true, true)) + test('basic false', () => testing('false', yup.boolean(), true, false)) + test('basic 1', () => testing('1', yup.boolean(), true, true)) + test('basic 0', () => testing('0', yup.boolean(), true, false)) + test('junk', () => testing('yeet', yup.boolean(), false)) + }) + describe('Date', () => { + test('basic', () => testing('2025-06-08 22:40:17', yup.date(), true, new Date('2025-06-08 22:40:17'))) + }) +})