From 8fb424d147010d928151b050c51d8506615c41d5 Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Thu, 12 Dec 2024 16:53:12 +0100 Subject: [PATCH 1/3] feat: add tests for logger --- check-required-env/check-required-env.test.ts | 36 ++++----- deno.jsonc | 3 +- deno.lock | 75 ++++++++++++++++++- logger/logger-context.test.ts | 61 +++++++++++++++ logger/logger-format.test.ts | 33 ++++++++ logger/logger.ts | 20 +++-- 6 files changed, 194 insertions(+), 34 deletions(-) create mode 100644 logger/logger-context.test.ts create mode 100644 logger/logger-format.test.ts diff --git a/check-required-env/check-required-env.test.ts b/check-required-env/check-required-env.test.ts index f408b94..0197076 100644 --- a/check-required-env/check-required-env.test.ts +++ b/check-required-env/check-required-env.test.ts @@ -1,45 +1,39 @@ import process from 'node:process' import { test } from '@cross/test' -import { assertEquals, assertExists } from '@std/assert' +import { assertExists } from '@std/assert' +import sinon from 'sinon' import { checkRequiredEnv } from './check-required-env.ts' test('checkRequiredEnv - returns when env variable exists', () => { // Setup - const testVarName = 'TEST_ENV_VAR' - process.env[testVarName] = 'test-value' + const envStub = sinon.stub(process, 'env').value({ + TEST_ENV_VAR: 'test-value', + }) // Test - checkRequiredEnv(testVarName) + checkRequiredEnv('TEST_ENV_VAR') // Verify - assertExists(process.env[testVarName]) + assertExists(process.env.TEST_ENV_VAR) // Cleanup - delete process.env[testVarName] + envStub.restore() }) test('checkRequiredEnv - exits when env variable is missing', () => { // Setup - const testVarName = 'MISSING_ENV_VAR' - const originalExit = process.exit - let exitCalled = false - let exitCode: number | undefined - - // Mock process.exit - process.exit = ((code?: number) => { - exitCalled = true - exitCode = code - // Don't actually exit - }) as typeof process.exit + const envStub = sinon.stub(process, 'env').value({}) + const exitStub = sinon.stub(process, 'exit') // Test - checkRequiredEnv(testVarName) + checkRequiredEnv('MISSING_ENV_VAR') // Verify - assertEquals(exitCalled, true) - assertEquals(exitCode, 1) + sinon.assert.calledOnce(exitStub) + sinon.assert.calledWith(exitStub, 1) // Cleanup - process.exit = originalExit + exitStub.restore() + envStub.restore() }) diff --git a/deno.jsonc b/deno.jsonc index b5749c9..67c1906 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -35,6 +35,7 @@ "@biomejs/biome": "npm:@biomejs/biome@^1.9.4", "@types/node": "npm:@types/node@^22.10.2", "@cross/test": "jsr:@cross/test@^0.0.10", - "@std/assert": "jsr:@std/assert@^1.0.9" + "@std/assert": "jsr:@std/assert@^1.0.9", + "sinon": "npm:sinon@^17.0.1" } } diff --git a/deno.lock b/deno.lock index a4368c0..f3828d9 100644 --- a/deno.lock +++ b/deno.lock @@ -11,6 +11,7 @@ "npm:@types/node@*": "22.5.4", "npm:@types/node@^22.10.2": "22.10.2", "npm:luxon@^3.5.0": "3.5.0", + "npm:sinon@^17.0.1": "17.0.2", "npm:winston@^3.17.0": "3.17.0" }, "jsr": { @@ -88,6 +89,29 @@ "kuler" ] }, + "@sinonjs/commons@3.0.1": { + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": [ + "type-detect@4.0.8" + ] + }, + "@sinonjs/fake-timers@11.3.1": { + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dependencies": [ + "@sinonjs/commons" + ] + }, + "@sinonjs/samsam@8.0.2": { + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dependencies": [ + "@sinonjs/commons", + "lodash.get", + "type-detect@4.1.0" + ] + }, + "@sinonjs/text-encoding@0.7.3": { + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==" + }, "@types/node@22.10.2": { "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dependencies": [ @@ -136,6 +160,9 @@ "text-hex" ] }, + "diff@5.2.0": { + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==" + }, "enabled@2.0.0": { "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, @@ -145,6 +172,9 @@ "fn.name@1.1.0": { "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, @@ -154,9 +184,15 @@ "is-stream@2.0.1": { "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, + "just-extend@6.2.0": { + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" + }, "kuler@2.0.0": { "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "lodash.get@4.4.2": { + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "logform@2.7.0": { "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "dependencies": [ @@ -174,12 +210,25 @@ "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "nise@5.1.9": { + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dependencies": [ + "@sinonjs/commons", + "@sinonjs/fake-timers", + "@sinonjs/text-encoding", + "just-extend", + "path-to-regexp" + ] + }, "one-time@1.0.0": { "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", "dependencies": [ "fn.name" ] }, + "path-to-regexp@6.3.0": { + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + }, "readable-stream@3.6.2": { "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": [ @@ -200,6 +249,17 @@ "is-arrayish" ] }, + "sinon@17.0.2": { + "integrity": "sha512-uihLiaB9FhzesElPDFZA7hDcNABzsVHwr3YfmM9sBllVwab3l0ltGlRV1XhpNfIacNDLGD1QRZNLs5nU5+hTuA==", + "dependencies": [ + "@sinonjs/commons", + "@sinonjs/fake-timers", + "@sinonjs/samsam", + "diff", + "nise", + "supports-color" + ] + }, "stack-trace@0.0.10": { "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" }, @@ -209,12 +269,24 @@ "safe-buffer" ] }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": [ + "has-flag" + ] + }, "text-hex@1.0.0": { "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, "triple-beam@1.4.1": { "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" }, + "type-detect@4.0.8": { + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-detect@4.1.0": { + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==" + }, "undici-types@6.19.8": { "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, @@ -311,7 +383,8 @@ "jsr:@cross/test@^0.0.10", "jsr:@std/assert@^1.0.9", "npm:@biomejs/biome@^1.9.4", - "npm:@types/node@^22.10.2" + "npm:@types/node@^22.10.2", + "npm:sinon@^17.0.1" ], "members": { "check-required-env": { diff --git a/logger/logger-context.test.ts b/logger/logger-context.test.ts new file mode 100644 index 0000000..cf01ae1 --- /dev/null +++ b/logger/logger-context.test.ts @@ -0,0 +1,61 @@ +import process from 'node:process' +import { test } from '@cross/test' +import { assertEquals, assertExists } from '@std/assert' +import sinon from 'sinon' + +import logger from './logger.ts' + +test('logger - includes global context in log events', () => { + // Setup + const envStub = sinon.stub(process, 'env').value({ + K_REVISION: 'test-revision', + SERVICE_NAME: 'test-service', + STAGE: 'test', + npm_package_version: '1.0.0', + }) + + const writeStub = sinon.stub(process.stdout, 'write') + let loggedOutput = '' + writeStub.callsFake((str: string) => { + loggedOutput = str + return true + }) + + // Test + logger.info('test message', { + source: 'test-source', + data: { test: 'data' }, + }) + + // Verify + const loggedData = JSON.parse(loggedOutput) + assertEquals(loggedData.host, 'test-revision') + assertEquals(loggedData.serviceName, 'test-service') + assertEquals(loggedData.stage, 'test') + assertEquals(loggedData.version, '1.0.0') + assertExists(loggedData.runtime) + + // Cleanup + writeStub.restore() + envStub.restore() +}) + +test('logger - sets debug level correctly', () => { + // Test & Verify + assertEquals(logger.level, process.env.STAGE === 'dev' ? 'debug' : 'info') +}) + +test('logger - has correct syslog levels', () => { + const expectedLevels = { + emerg: 0, + alert: 1, + crit: 2, + error: 3, + warning: 4, + notice: 5, + info: 6, + debug: 7, + } + + assertEquals(logger.levels, expectedLevels) +}) diff --git a/logger/logger-format.test.ts b/logger/logger-format.test.ts new file mode 100644 index 0000000..1829675 --- /dev/null +++ b/logger/logger-format.test.ts @@ -0,0 +1,33 @@ +import process from 'node:process' +import { test } from '@cross/test' +import { assertEquals, assertExists } from '@std/assert' +import sinon from 'sinon' + +import logger from './logger.ts' + +test('logger - formats errors correctly', () => { + // Setup + const testError = new Error('test error') + const writeStub = sinon.stub(process.stdout, 'write') + let loggedOutput = '' + writeStub.callsFake((str: string) => { + loggedOutput = str + return true + }) + + // Test + logger.error('error occurred', { + source: 'test-source', + error: testError, + }) + + // Verify + const loggedData = JSON.parse(loggedOutput) + assertExists(loggedData.error.message) + assertExists(loggedData.error.stack) + assertEquals(loggedData.error.message, 'test error') + assertExists(loggedData.runtime) + + // Cleanup + writeStub.restore() +}) diff --git a/logger/logger.ts b/logger/logger.ts index 0532969..743cb5c 100644 --- a/logger/logger.ts +++ b/logger/logger.ts @@ -46,16 +46,14 @@ const convertGlobals = format((event) => { }) // Set up format based on environment -let formatConfig: Logform.Format = format.combine(convertError(), convertGlobals(), format.json()) -if (process.env.IS_LOCAL === 'true') { - formatConfig = format.combine( - convertError(), - convertGlobals(), - format.timestamp(), - format.json({ space: 4 }), - format.colorize({ all: true }), - ) -} +const formatConfig: Logform.Format = format.combine(convertError(), convertGlobals(), format.json()) +const formatConfigLocal: Logform.Format = format.combine( + convertError(), + convertGlobals(), + format.timestamp(), + format.json({ space: 4 }), + format.colorize({ all: true }), +) /** * Use the exported logger to log messages. @@ -97,7 +95,7 @@ export const logger: Logger = createLogger({ level: process.env.STAGE === 'dev' ? 'debug' : 'info', levels: config.syslog.levels, exitOnError: false, - format: formatConfig, + format: process.env.IS_LOCAL === 'true' ? formatConfigLocal : formatConfig, transports: [new transports.Console()], }) From 1937ab640d8767275ff8e77ab91d590b9178872b Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Thu, 12 Dec 2024 16:55:30 +0100 Subject: [PATCH 2/3] fix: npm dependencies --- .github/workflows/test.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f84694..c000c51 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,10 @@ on: permissions: contents: read +env: + JSR_DEPENDENCIES: @cross/test @std/assert @frytg/logger + NPM_DEPENDENCIES: luxon sinon + jobs: test-on-deno-and-lint: runs-on: ${{ matrix.os }} @@ -83,10 +87,10 @@ jobs: run: bun --version - name: Install JSR dependencies - run: bunx jsr add @cross/test @std/assert @frytg/logger + run: bunx jsr add ${{ env.JSR_DEPENDENCIES }} - name: Install NPM dependencies - run: bun add luxon + run: bun add ${{ env.NPM_DEPENDENCIES }} - name: Test run: bun test @@ -121,10 +125,10 @@ jobs: run: node --version - name: Install JSR dependencies - run: npx jsr add @cross/test @std/assert @frytg/logger + run: npx jsr add ${{ env.JSR_DEPENDENCIES }} - name: Install NPM dependencies - run: npm install luxon + run: npm install ${{ env.NPM_DEPENDENCIES }} - name: Set up package.json run: 'echo ''{ "type": "module" }'' > package.json' From df3f85c4a6ae51ac45c8cbf8edfed2206294b634 Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Thu, 12 Dec 2024 16:55:56 +0100 Subject: [PATCH 3/3] fix: npm dependencies --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c000c51..5102a59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,8 @@ permissions: contents: read env: - JSR_DEPENDENCIES: @cross/test @std/assert @frytg/logger - NPM_DEPENDENCIES: luxon sinon + JSR_DEPENDENCIES: "@cross/test @std/assert @frytg/logger" + NPM_DEPENDENCIES: "luxon sinon" jobs: test-on-deno-and-lint: