From 8421b3116e5798acb47d2ac281c3eb11c0f35c45 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 12:40:59 -0800 Subject: [PATCH 01/20] test: WIP adding tests [#1] --- index.test.ts | 122 +++++ index.ts | 13 +- package-lock.json | 1232 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 7 +- 4 files changed, 1359 insertions(+), 15 deletions(-) create mode 100644 index.test.ts diff --git a/index.test.ts b/index.test.ts new file mode 100644 index 0000000..18dbb98 --- /dev/null +++ b/index.test.ts @@ -0,0 +1,122 @@ +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' +import createFetchMock from 'vitest-fetch-mock' +import { + handlePayload, + isSongfishPayload, + loginBluesky, + type SongfishWebhookPayload, +} from './index' + +const fetchMocker = createFetchMock(vi) +fetchMocker.enableMocks() + +describe('isSongfishPayload', () => { + test('is a function', () => { + expect(typeof isSongfishPayload).toBe('function') + }) + describe('with non-string body', () => { + test('is false', () => { + expect(isSongfishPayload()).toBe(false) + expect(isSongfishPayload(9)).toBe(false) + expect(isSongfishPayload('foo')).toBe(false) + }) + }) + describe('with an event object containing "body" property with value of a stringified JSON object', () => { + const fakeSongfishPayload:SongfishWebhookPayload = { + body: JSON.stringify({show_id: 'foo'}) + } + test('retuns true', () => { + expect(isSongfishPayload(fakeSongfishPayload)).toBe(true) + }) + }) +}) + +describe('handlePayload', () => { + test('is a function', () => { + expect(typeof handlePayload).toBe('function') + }) + describe('with malformed payload', () => { + test('throws', async () => { + await expect(() => handlePayload({body: '{foo'})).rejects.toThrow(/JSON/) + }) + }) + describe('with valid payload', () => { + let payload + beforeEach(() => { + const data = { + show_id: 'quxxxx' + } + payload = { + body: JSON.stringify(data) + } + }) + describe.only('with invalid login', () => { + beforeEach(() => { + vi.mock('./index', async (importOriginal) => { + const originalImplementation = await importOriginal() + console.log('tryna mock the index import...', originalImplementation) + return { + ...originalImplementation, + loginBluesky: function mockedLoginBluesky() { + console.log('mocked loginBluesky implementation......') + throw new Error('mock: loginBluesky failure') + }, + } + }); + }) + test.skip('(SKIPPED: old implementation) resolves with a message', async () => { + await expect(handlePayload(payload)).resolves.toMatch(/foooooo/) + }) + test('throws', async () => { + await expect(() => handlePayload(payload)).rejects.toThrow() + }) + }) + describe('with valid login', () => { + beforeEach(() => { + vi.mock('loginBluesky', { + getAuthorFeed: 'foo' //vi.fn({authorFeedData:'this is mocked author feed data'}) + }) + }) + describe(`with invalid payload`, () => { + beforeEach(() => { + fetchMocker.mockIf(/\bkglw\.net\b.+\blatest\.json$/, () => 'mocked Songfish payload is malformed') + }) + test('returns a helpful message', async () => { + await expect(handlePayload({body:JSON.stringify({show_id:123})})).resolves.toBe( + 'payload show_id does not match latest show' + ) + }) + }) + describe(`when payload's show_id does _not_ match fetched JSON's data[0].show_id`, () => { + beforeEach(() => { + fetchMocker.mockIf(/\bkglw\.net\b.+\blatest\.json$/, () => ({data: [ + {show_id: 666, songname: 'Most Recent Song Name'}, + {foo: 'bar'}, + {foo: 'baz'}, + {foo: 'qux'}, + ]})) + }) + test('returns a helpful message', async () => { + await expect(handlePayload({body:JSON.stringify({show_id:123})})).resolves.toBe( + 'payload show_id does not match latest show' + ) + }) + }) + describe(`when payload's show_id matches fetched JSON's data[0].show_id`, () => { + beforeEach(() => { + fetchMocker.mockIf(/kglw/, () => ({data: [ + {show_id: 123, songname: 'Most Recent Song Name'}, + {foo: 'bar'}, + {foo: 'baz'}, + {foo: 'qux'}, + ]})) + }) + test('does something useful', async () => { + await expect(handlePayload({body:JSON.stringify({show_id:123})})).resolves.toBe( + 'whattttt' + ) + }) + }) + }) + }) +}) diff --git a/index.ts b/index.ts index 719bdea..ecf2265 100644 --- a/index.ts +++ b/index.ts @@ -8,7 +8,7 @@ type SongfishWebhookPayload = { body:{show_id:number} } -async function loginBluesky():Promise { +export async function loginBluesky():Promise { const agent = new BskyAgent({ service: 'https://bsky.social', }) @@ -19,23 +19,24 @@ async function loginBluesky():Promise { return agent } -function isSongfishPayload(event:any):event is SongfishWebhookPayload { - // note that testing this via commandline will mean that the event is a string payload, whereas on Lambda it is a true object +export function isSongfishPayload(event:any):event is SongfishWebhookPayload { return typeof event?.body === 'string' && event.body.includes('"show_id"') + // TODO could further verify that the song_id appears to be an int... } -async function handlePayload(event:SongfishWebhookPayload):Promise { +export async function handlePayload(event:SongfishWebhookPayload):Promise { let payloadBody try { payloadBody = JSON.parse(event.body) } catch (err) { - console.log('error parsing event body', err) + // console.log('error parsing event body', err) throw err } let bsky + console.log('about to call loginBluesky...', loginBluesky) try { bsky = await loginBluesky() - console.log('logged in successfully!') + // console.log('logged in successfully!') } catch (err) { console.log('login error', err) throw err diff --git a/package-lock.json b/package-lock.json index ea887cc..74f636a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,9 @@ "esbuild": "^0.24.0", "funpack": "^1.3.2", "tsx": "^4.19.2", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "vitest": "^2.1.5", + "vitest-fetch-mock": "^0.4.2" }, "engines": { "node": ">=20.0.0" @@ -460,6 +462,252 @@ "node": ">=18" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.3.tgz", + "integrity": "sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.3.tgz", + "integrity": "sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.3.tgz", + "integrity": "sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.3.tgz", + "integrity": "sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.3.tgz", + "integrity": "sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.3.tgz", + "integrity": "sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.3.tgz", + "integrity": "sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.3.tgz", + "integrity": "sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.3.tgz", + "integrity": "sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.3.tgz", + "integrity": "sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.3.tgz", + "integrity": "sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.3.tgz", + "integrity": "sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.3.tgz", + "integrity": "sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.3.tgz", + "integrity": "sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.3.tgz", + "integrity": "sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.3.tgz", + "integrity": "sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.3.tgz", + "integrity": "sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.3.tgz", + "integrity": "sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -469,6 +717,112 @@ "undici-types": "~6.19.8" } }, + "node_modules/@vitest/expect": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz", + "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.5", + "@vitest/utils": "2.1.5", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz", + "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", + "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz", + "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.5", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz", + "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.5", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz", + "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz", + "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.5", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -538,6 +892,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -629,6 +992,40 @@ "node": "*" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -690,6 +1087,32 @@ "node": ">= 10" } }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -710,6 +1133,12 @@ "once": "^1.4.0" } }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, "node_modules/esbuild": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", @@ -1069,12 +1498,30 @@ "node": ">=12" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1346,6 +1793,21 @@ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.13", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.13.tgz", + "integrity": "sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1358,11 +1820,35 @@ "node": "*" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1390,6 +1876,55 @@ "node": ">=0.10.0" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1457,6 +1992,43 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rollup": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.3.tgz", + "integrity": "sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.27.3", + "@rollup/rollup-android-arm64": "4.27.3", + "@rollup/rollup-darwin-arm64": "4.27.3", + "@rollup/rollup-darwin-x64": "4.27.3", + "@rollup/rollup-freebsd-arm64": "4.27.3", + "@rollup/rollup-freebsd-x64": "4.27.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.27.3", + "@rollup/rollup-linux-arm-musleabihf": "4.27.3", + "@rollup/rollup-linux-arm64-gnu": "4.27.3", + "@rollup/rollup-linux-arm64-musl": "4.27.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.27.3", + "@rollup/rollup-linux-riscv64-gnu": "4.27.3", + "@rollup/rollup-linux-s390x-gnu": "4.27.3", + "@rollup/rollup-linux-x64-gnu": "4.27.3", + "@rollup/rollup-linux-x64-musl": "4.27.3", + "@rollup/rollup-win32-arm64-msvc": "4.27.3", + "@rollup/rollup-win32-ia32-msvc": "4.27.3", + "@rollup/rollup-win32-x64-msvc": "4.27.3", + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1477,6 +2049,33 @@ } ] }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1502,6 +2101,45 @@ "node": ">=6" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tlds": { "version": "1.255.0", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz", @@ -1985,6 +2623,586 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz", + "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz", + "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.5", + "@vitest/mocker": "2.1.5", + "@vitest/pretty-format": "^2.1.5", + "@vitest/runner": "2.1.5", + "@vitest/snapshot": "2.1.5", + "@vitest/spy": "2.1.5", + "@vitest/utils": "2.1.5", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.5", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.5", + "@vitest/ui": "2.1.5", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest-fetch-mock": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.2.tgz", + "integrity": "sha512-MuN/TCAvvUs9sLMdOPKqdXEUOD0E5cNW/LN7Tro3KkrLBsvUaH7iQWcznNUU4ml+GqX6ZbNguDmFQ2tliKqhCg==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "vitest": ">=2.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 4d7fcd5..f4828b3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "funpack", "postbuild": "echo okay now copy the index.js file, but not the package.json, and put it up into Lambda", "prebuild": "echo Building...", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest --run", + "test:watch": "vitest --watch" }, "engines": { "node": ">=20.0.0" @@ -35,7 +36,9 @@ "esbuild": "^0.24.0", "funpack": "^1.3.2", "tsx": "^4.19.2", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "vitest": "^2.1.5", + "vitest-fetch-mock": "^0.4.2" }, "funpack": { "settings": {}, From 270e84f615987b9a64199e8e58a39b1d3b4b8e7e Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 13:32:08 -0800 Subject: [PATCH 02/20] ops: run CI tests on GitHub Actions [#1] --- .github/workflows/main.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..804114b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,19 @@ +name: test suite +on: [push] +jobs: + + test: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + - name: setup node v20 + uses: actions/setup-node@v4 + with: + node-version: 20.18.1 # match .tool-versions file + - name: install + steps: + - run: npm ci + - name: tests + steps: + - run: npm run test From 8450bd58bb8daa7c35288f0c589583a81b4a9fef Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 13:35:52 -0800 Subject: [PATCH 03/20] fix: I can yaml [#1] --- .github/workflows/main.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 804114b..34ac042 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,6 @@ jobs: with: node-version: 20.18.1 # match .tool-versions file - name: install - steps: - - run: npm ci + run: npm ci - name: tests - steps: - - run: npm run test + run: npm run test From e816b4795a0982bf817b41586564d0dc6a029f67 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 13:36:47 -0800 Subject: [PATCH 04/20] test: remove `only`, skip a WIP test... [#1] --- index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.test.ts b/index.test.ts index 18dbb98..e3e2c2d 100644 --- a/index.test.ts +++ b/index.test.ts @@ -40,7 +40,7 @@ describe('handlePayload', () => { await expect(() => handlePayload({body: '{foo'})).rejects.toThrow(/JSON/) }) }) - describe('with valid payload', () => { + describe.skip('with valid payload', () => { let payload beforeEach(() => { const data = { @@ -50,7 +50,7 @@ describe('handlePayload', () => { body: JSON.stringify(data) } }) - describe.only('with invalid login', () => { + describe('with invalid login', () => { beforeEach(() => { vi.mock('./index', async (importOriginal) => { const originalImplementation = await importOriginal() From 79274c418896da3f0a680fe1401427a84ae2014c Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 13:37:22 -0800 Subject: [PATCH 05/20] ops: set version of NodeJS --- .tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..e8fc3f8 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 20.18.1 From b19666de9a7f0f6d483fa6cd9f6ac65689b8bd1d Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 14:49:06 -0800 Subject: [PATCH 06/20] test: extract function so it can be mocked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 😒 vitest [#1] --- bluesky.ts | 13 ++++++++++ index.test.ts | 71 +++++++++++++++++++++++++++------------------------ index.ts | 20 ++++----------- 3 files changed, 56 insertions(+), 48 deletions(-) create mode 100644 bluesky.ts diff --git a/bluesky.ts b/bluesky.ts new file mode 100644 index 0000000..157a7b5 --- /dev/null +++ b/bluesky.ts @@ -0,0 +1,13 @@ +import * as process from 'process' + +export async function login():Promise { + console.log('... this is THE REAL login ...') + const agent = new BskyAgent({ + service: 'https://bsky.social', + }) + await agent.login({ + identifier: process.env.BLUESKY_USERNAME!, + password: process.env.BLUESKY_PASSWORD!, + }) + return agent +} diff --git a/index.test.ts b/index.test.ts index e3e2c2d..6e6cf61 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,14 +1,32 @@ import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import createFetchMock from 'vitest-fetch-mock' +import {login} from './bluesky' import { handlePayload, isSongfishPayload, - loginBluesky, type SongfishWebhookPayload, } from './index' +console.log('...tryna mock login().....') +vi.mock('./bluesky', async (importOriginal) => ({ + ...(await importOriginal()), + login: function mockedLoginBluesky() { + console.log('!! mocked loginBluesky implementation......') + throw new Error('mock: loginBluesky failure') + }, +} + )); + const fetchMocker = createFetchMock(vi) fetchMocker.enableMocks() +console.log('mocks: fetch mock enabled') +// function mockJson(urlMatcher, jsonPayload) { +// console.log('mocks: fetch mock if', urlMatcher) +// fetchMocker.mockIf(urlMatcher, () => ({ +// body: JSON.stringify(jsonPayload), +// contentType: 'application/json', +// })) +// } describe('isSongfishPayload', () => { test('is a function', () => { @@ -40,11 +58,11 @@ describe('handlePayload', () => { await expect(() => handlePayload({body: '{foo'})).rejects.toThrow(/JSON/) }) }) - describe.skip('with valid payload', () => { + describe('with valid payload', () => { let payload beforeEach(() => { const data = { - show_id: 'quxxxx' + show_id: 123 } payload = { body: JSON.stringify(data) @@ -52,52 +70,38 @@ describe('handlePayload', () => { }) describe('with invalid login', () => { beforeEach(() => { - vi.mock('./index', async (importOriginal) => { - const originalImplementation = await importOriginal() - console.log('tryna mock the index import...', originalImplementation) - return { - ...originalImplementation, - loginBluesky: function mockedLoginBluesky() { - console.log('mocked loginBluesky implementation......') - throw new Error('mock: loginBluesky failure') - }, - } - }); - }) - test.skip('(SKIPPED: old implementation) resolves with a message', async () => { - await expect(handlePayload(payload)).resolves.toMatch(/foooooo/) + // TODO }) test('throws', async () => { - await expect(() => handlePayload(payload)).rejects.toThrow() + console.log('okay now here comes the expect()...') + await expect(() => handlePayload(payload)).rejects.toThrow('mock: loginBluesky failure') }) }) - describe('with valid login', () => { + describe.todo('with valid login', () => { beforeEach(() => { - vi.mock('loginBluesky', { - getAuthorFeed: 'foo' //vi.fn({authorFeedData:'this is mocked author feed data'}) - }) + // TODO }) - describe(`with invalid payload`, () => { + describe.skip(`with malformed Latest.json`, () => { beforeEach(() => { - fetchMocker.mockIf(/\bkglw\.net\b.+\blatest\.json$/, () => 'mocked Songfish payload is malformed') + fetchMocker.mockIf(/\bkglw\.net\b.+\blatest\.json$/, () => 'this mocked Songfish response is malformed JSON') }) test('returns a helpful message', async () => { - await expect(handlePayload({body:JSON.stringify({show_id:123})})).resolves.toBe( + await expect(handlePayload(payload)).resolves.toBe( 'payload show_id does not match latest show' ) }) }) describe(`when payload's show_id does _not_ match fetched JSON's data[0].show_id`, () => { beforeEach(() => { - fetchMocker.mockIf(/\bkglw\.net\b.+\blatest\.json$/, () => ({data: [ - {show_id: 666, songname: 'Most Recent Song Name'}, - {foo: 'bar'}, - {foo: 'baz'}, - {foo: 'qux'}, - ]})) + // mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ + // {show_id: 666, songname: 'Most Recent Song Name'}, + // {foo: 'bar'}, + // {foo: 'baz'}, + // {foo: 'qux'}, + // ]}) }) test('returns a helpful message', async () => { - await expect(handlePayload({body:JSON.stringify({show_id:123})})).resolves.toBe( + await expect(handlePayload(payload)).resolves.toBe( 'payload show_id does not match latest show' ) }) @@ -112,11 +116,12 @@ describe('handlePayload', () => { ]})) }) test('does something useful', async () => { - await expect(handlePayload({body:JSON.stringify({show_id:123})})).resolves.toBe( + await expect(handlePayload(payload)).resolves.toBe( 'whattttt' ) }) }) }) }) + describe.todo('with fixture payload') }) diff --git a/index.ts b/index.ts index ecf2265..0286b6e 100644 --- a/index.ts +++ b/index.ts @@ -1,24 +1,14 @@ import {BskyAgent} from '@atproto/api' import * as dotenv from 'dotenv' import * as process from 'process' +import {login} from './bluesky' dotenv.config() // read env var declarations from a `.env` file -type SongfishWebhookPayload = { +export type SongfishWebhookPayload = { body:{show_id:number} } -export async function loginBluesky():Promise { - const agent = new BskyAgent({ - service: 'https://bsky.social', - }) - await agent.login({ - identifier: process.env.BLUESKY_USERNAME!, - password: process.env.BLUESKY_PASSWORD!, - }) - return agent -} - export function isSongfishPayload(event:any):event is SongfishWebhookPayload { return typeof event?.body === 'string' && event.body.includes('"show_id"') // TODO could further verify that the song_id appears to be an int... @@ -33,10 +23,10 @@ export async function handlePayload(event:SongfishWebhookPayload):Promise Date: Wed, 20 Nov 2024 14:49:51 -0800 Subject: [PATCH 07/20] ops: specify version of NodeJS that asdf supports --- .github/workflows/main.yml | 2 +- .tool-versions | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 34ac042..2d7d1fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: - name: setup node v20 uses: actions/setup-node@v4 with: - node-version: 20.18.1 # match .tool-versions file + node-version: 20.18.0 # match .tool-versions file - name: install run: npm ci - name: tests diff --git a/.tool-versions b/.tool-versions index e8fc3f8..f31e6b0 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 20.18.1 +nodejs 20.18.0 diff --git a/package.json b/package.json index f4828b3..fde00cf 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test:watch": "vitest --watch" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.18.0" }, "keywords": [ "AWS Lambda", From 1eaa34e24cee5326011e3813a0e7a3c9c9c3cbd8 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 18:00:04 -0800 Subject: [PATCH 08/20] test: finish test of happy path [#1] --- README.md | 3 +- bluesky.ts | 1 - index.test.ts | 90 ++++++++++++++++++++++++++------------------------- index.ts | 21 ++++++------ 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 3428a1b..ba6cc1d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ Brought to you by the [KGLW.net] Tech & Live-Coverage Teams! ## Docs -* [NPM package: `@proto/api`](https://github.com/bluesky-social/atproto/blob/main/packages/api/README.md) +* Bluesky agent API: [`@proto/api` NPM package](https://github.com/bluesky-social/atproto/blob/main/packages/api/README.md) +* test suite: [Vitest](https://vitest.dev/api/vi.html) ## Ops diff --git a/bluesky.ts b/bluesky.ts index 157a7b5..057146c 100644 --- a/bluesky.ts +++ b/bluesky.ts @@ -1,7 +1,6 @@ import * as process from 'process' export async function login():Promise { - console.log('... this is THE REAL login ...') const agent = new BskyAgent({ service: 'https://bsky.social', }) diff --git a/index.test.ts b/index.test.ts index 6e6cf61..c28da3d 100644 --- a/index.test.ts +++ b/index.test.ts @@ -7,26 +7,17 @@ import { type SongfishWebhookPayload, } from './index' -console.log('...tryna mock login().....') -vi.mock('./bluesky', async (importOriginal) => ({ - ...(await importOriginal()), - login: function mockedLoginBluesky() { - console.log('!! mocked loginBluesky implementation......') - throw new Error('mock: loginBluesky failure') - }, -} - )); +vi.mock('./bluesky') // use e.g.: vi.mocked(login).mockResolvedValue({}) const fetchMocker = createFetchMock(vi) fetchMocker.enableMocks() -console.log('mocks: fetch mock enabled') -// function mockJson(urlMatcher, jsonPayload) { -// console.log('mocks: fetch mock if', urlMatcher) -// fetchMocker.mockIf(urlMatcher, () => ({ -// body: JSON.stringify(jsonPayload), -// contentType: 'application/json', -// })) -// } + +function mockJson(urlMatcher, jsonPayload) { + fetchMocker.mockIf(urlMatcher, () => ({ + body: JSON.stringify(jsonPayload), + contentType: 'application/json', + })) +} describe('isSongfishPayload', () => { test('is a function', () => { @@ -55,7 +46,7 @@ describe('handlePayload', () => { }) describe('with malformed payload', () => { test('throws', async () => { - await expect(() => handlePayload({body: '{foo'})).rejects.toThrow(/JSON/) + await expect(() => handlePayload([])).rejects.toThrow('not valid JSON') }) }) describe('with valid payload', () => { @@ -70,55 +61,66 @@ describe('handlePayload', () => { }) describe('with invalid login', () => { beforeEach(() => { - // TODO + vi.mocked(login).mockRejectedValue('mocked bluesky.login()') }) test('throws', async () => { - console.log('okay now here comes the expect()...') - await expect(() => handlePayload(payload)).rejects.toThrow('mock: loginBluesky failure') + await expect(() => handlePayload(payload)).rejects.toThrow('mocked bluesky.login()') }) }) - describe.todo('with valid login', () => { + describe('with valid login and prior post does not match latest song title', () => { + let mockedLoginReturnValue = { + getAuthorFeed: vi.fn().mockReturnValueOnce({data: {feed: [{post: {record: {text: 'Prior Post'}}}] }}), + } beforeEach(() => { - // TODO + vi.mocked(login).mockResolvedValue(mockedLoginReturnValue) + }) + afterEach(() => { + vi.mocked(login).mockReset() }) - describe.skip(`with malformed Latest.json`, () => { + describe(`with malformed Latest.json`, () => { beforeEach(() => { fetchMocker.mockIf(/\bkglw\.net\b.+\blatest\.json$/, () => 'this mocked Songfish response is malformed JSON') }) test('returns a helpful message', async () => { - await expect(handlePayload(payload)).resolves.toBe( - 'payload show_id does not match latest show' - ) + await expect(handlePayload(payload)).rejects.toThrow('not valid JSON') }) }) - describe(`when payload's show_id does _not_ match fetched JSON's data[0].show_id`, () => { + describe(`when payload's show_id does _not_ match fetched JSON's data[-1].show_id`, () => { + let mockedPost beforeEach(() => { - // mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ - // {show_id: 666, songname: 'Most Recent Song Name'}, - // {foo: 'bar'}, - // {foo: 'baz'}, - // {foo: 'qux'}, - // ]}) + mockedPost = vi.fn() + vi.mocked(login).mockResolvedValue({ + ...mockedLoginReturnValue, + post: mockedPost, + }) + mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ + {show_id: 666, songname: 'Most Recent Song Name'}, + ]}) }) test('returns a helpful message', async () => { await expect(handlePayload(payload)).resolves.toBe( 'payload show_id does not match latest show' ) + expect(mockedPost).not.toHaveBeenCalled() }) }) - describe(`when payload's show_id matches fetched JSON's data[0].show_id`, () => { + describe(`when payload's show_id matches fetched JSON's data[-1].show_id`, () => { + let mockedPost beforeEach(() => { - fetchMocker.mockIf(/kglw/, () => ({data: [ + mockedPost = vi.fn() + vi.mocked(login).mockResolvedValue({ + ...mockedLoginReturnValue, + post: mockedPost, + }) + mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ + {show_id: 789, songname: 'A Different Show and Song'}, + {show_id: 456, songname: 'Yet Another Different Show and Song'}, {show_id: 123, songname: 'Most Recent Song Name'}, - {foo: 'bar'}, - {foo: 'baz'}, - {foo: 'qux'}, - ]})) + ]}) }) - test('does something useful', async () => { - await expect(handlePayload(payload)).resolves.toBe( - 'whattttt' - ) + test('posts the song title', async () => { + await handlePayload(payload) + expect(mockedPost).toHaveBeenCalledWith({text: 'Most Recent Song Name'}) }) }) }) diff --git a/index.ts b/index.ts index 0286b6e..c77405c 100644 --- a/index.ts +++ b/index.ts @@ -5,8 +5,15 @@ import {login} from './bluesky' dotenv.config() // read env var declarations from a `.env` file -export type SongfishWebhookPayload = { - body:{show_id:number} +export type SongfishWebhookPayload = { // TODO split this up into LambdaEvent and WebhookPayload + body: { + show_id: number + } +} + +export type BlueskyAgent = { + getAuthorFeed: Function + post: Function } export function isSongfishPayload(event:any):event is SongfishWebhookPayload { @@ -19,14 +26,12 @@ export async function handlePayload(event:SongfishWebhookPayload):Promise => { - console.log('handler!!', event) if (!isSongfishPayload(event)) return { statusCode: 400, body: `unexpected payload... typeof event: ${typeof event}`, } - console.log('tryna handle it...') try { return { statusCode: 200, From 064a4066364491c8f6ec7cb6a233be1e23802a48 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 18:04:55 -0800 Subject: [PATCH 09/20] test: mocked function has obviously-mocked return value [#1] --- index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.test.ts b/index.test.ts index c28da3d..5de57f5 100644 --- a/index.test.ts +++ b/index.test.ts @@ -107,7 +107,7 @@ describe('handlePayload', () => { describe(`when payload's show_id matches fetched JSON's data[-1].show_id`, () => { let mockedPost beforeEach(() => { - mockedPost = vi.fn() + mockedPost = vi.fn().mockReturnValueOnce({mocked: true}) vi.mocked(login).mockResolvedValue({ ...mockedLoginReturnValue, post: mockedPost, From bf3d486b71d8aab215cd50f30c8048ec597443a2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 18:13:12 -0800 Subject: [PATCH 10/20] docs: changelog --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c2f36ba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [Unreleased] + +### Added + +* test suite (addresses #1) +* this CHANGELOG file 🤘 + + +## [v1.0.0] + +### Added + +* basic functionality + + +[Unreleased]: https://github.com/kglw-dot-net/bot-bluesky-live/compare/v1.0.0...HEAD +[v1.0.0]: https://github.com/kglw-dot-net/bot-bluesky-live/releases/tag/v1.0.0 From e26f3ff058da970e91a9857bcfe9de98b45d83eb Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 18:13:17 -0800 Subject: [PATCH 11/20] ops: bump patch-version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fde00cf..1691302 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kglw-dot-net/bot-bluesky-live", - "version": "1.0.0", + "version": "1.0.1", "description": "post a message on Bluesky whenever King Gizzard starts a new song", "exports": "./dist/index.mjs", "type": "module", From 17644ab51a1b062d927a61536e82ebe2b80861b0 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 18:14:41 -0800 Subject: [PATCH 12/20] docs: update versions in changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f36ba..9a5888d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] -### Added +* ... + + +## [v1.0.1] * test suite (addresses #1) * this CHANGELOG file 🤘 @@ -20,5 +23,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), * basic functionality -[Unreleased]: https://github.com/kglw-dot-net/bot-bluesky-live/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/kglw-dot-net/bot-bluesky-live/compare/v1.0.1...HEAD +[v1.0.1]: https://github.com/kglw-dot-net/bot-bluesky-live/releases/tag/v1.0.1 [v1.0.0]: https://github.com/kglw-dot-net/bot-bluesky-live/releases/tag/v1.0.0 From fc88f2e7103bc9c5eb83e12a808e76c59fefec2f Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 18:15:40 -0800 Subject: [PATCH 13/20] docs: manual link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5888d..fdcafdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [v1.0.1] -* test suite (addresses #1) +* test suite (addresses [issue #1](https://github.com/kglw-dot-net/bot-bluesky-live/issues/1)) * this CHANGELOG file 🤘 From 790d60bb513bb2b54c8bc1b4de43d4bccbeb58bf Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 19:03:07 -0800 Subject: [PATCH 14/20] test: use fixture of full Lambda payload [#1] --- index.test.ts | 33 +++++++++++++++++++++++++++++++-- test-fixture.json | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 test-fixture.json diff --git a/index.test.ts b/index.test.ts index 5de57f5..dfffe32 100644 --- a/index.test.ts +++ b/index.test.ts @@ -6,6 +6,7 @@ import { isSongfishPayload, type SongfishWebhookPayload, } from './index' +import testFixture from './test-fixture.json' vi.mock('./bluesky') // use e.g.: vi.mocked(login).mockResolvedValue({}) @@ -68,7 +69,7 @@ describe('handlePayload', () => { }) }) describe('with valid login and prior post does not match latest song title', () => { - let mockedLoginReturnValue = { + const mockedLoginReturnValue = { getAuthorFeed: vi.fn().mockReturnValueOnce({data: {feed: [{post: {record: {text: 'Prior Post'}}}] }}), } beforeEach(() => { @@ -125,5 +126,33 @@ describe('handlePayload', () => { }) }) }) - describe.todo('with fixture payload') + describe('with fixture payload matching latest show_id', () => { + const testWithFixture = test.extend({ + event: async ({}, use) => { + await use(testFixture.event) + } + }) + const mockedLoginReturnValue = { + getAuthorFeed: vi.fn().mockReturnValueOnce({data: {feed: [{post: {record: {text: 'Prior Post'}}}] }}), + } + let mockedPost + beforeEach(() => { + mockedPost = vi.fn().mockReturnValueOnce({mocked: true}) + vi.mocked(login).mockResolvedValue({ + ...mockedLoginReturnValue, + post: mockedPost, + }) + mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ + // the id 1699404057 is defined in the fixture file + {show_id: 1699404057, songname: 'Name of Song From Show #1699404057'}, + ]}) + }) + afterEach(() => { + vi.mocked(login).mockReset() + }) + testWithFixture('does not throw', async ({event}) => { + await expect(handlePayload(event)).resolves.not.to.throw() + expect(mockedPost).toHaveBeenCalledWith({text: 'Name of Song From Show #1699404057'}) + }) + }) }) diff --git a/test-fixture.json b/test-fixture.json new file mode 100644 index 0000000..b171a9c --- /dev/null +++ b/test-fixture.json @@ -0,0 +1,40 @@ +{ + "event": { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256", + "content-length": "22", + "x-amzn-tls-version": "TLSv1.3", + "x-amzn-trace-id": "Root=1-673ab04e-002e78a15109e2ae29abb650", + "x-forwarded-proto": "https", + "host": "lambda-domain-prefix-identifier.lambda-url.us-west-1.on.aws", + "x-forwarded-port": "443", + "content-type": "application/json", + "x-forwarded-for": "149.28.253.24", + "accept": "*/*" + }, + "requestContext": { + "accountId": "anonymous", + "apiId": "lambda-domain-prefix-identifier", + "domainName": "lambda-domain-prefix-identifier.lambda-url.us-west-1.on.aws", + "domainPrefix": "lambda-domain-prefix-identifier", + "http": { + "method": "POST", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "149.28.253.24", + "userAgent": null + }, + "requestId": "36366359-454e-4f2a-87cf-53e666d930fe", + "routeKey": "$default", + "stage": "$default", + "time": "18/Nov/2024:03:11:10 +0000", + "timeEpoch": 1731899470068 + }, + "body": "{\"show_id\":1699404057}", + "isBase64Encoded": false + } +} From afac2302506fc189e7cefa57c16203f784120843 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 19:07:40 -0800 Subject: [PATCH 15/20] test: check when previous post matches the current song name [#1] --- index.test.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/index.test.ts b/index.test.ts index dfffe32..362c3a6 100644 --- a/index.test.ts +++ b/index.test.ts @@ -125,6 +125,34 @@ describe('handlePayload', () => { }) }) }) + describe('with valid login and prior post _does_ match latest song title', () => { + const mockedLoginReturnValue = { + getAuthorFeed: vi.fn().mockReturnValueOnce({data: {feed: [{post: {record: {text: 'Song Title'}}}] }}), + } + beforeEach(() => { + vi.mocked(login).mockResolvedValue(mockedLoginReturnValue) + }) + afterEach(() => { + vi.mocked(login).mockReset() + }) + describe(`when payload's show_id matches fetched JSON's data[-1].show_id`, () => { + let mockedPost + beforeEach(() => { + mockedPost = vi.fn().mockReturnValueOnce({mocked: true}) + vi.mocked(login).mockResolvedValue({ + ...mockedLoginReturnValue, + post: mockedPost, + }) + mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ + {show_id: 123, songname: 'Song Title'}, + ]}) + }) + test('does _not_ post the song title', async () => { + await handlePayload(payload) + expect(mockedPost).not.toHaveBeenCalled() + }) + }) + }) }) describe('with fixture payload matching latest show_id', () => { const testWithFixture = test.extend({ From df4add977ae5ec46db3270c41ea0ec4ef822af75 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 19:34:53 -0800 Subject: [PATCH 16/20] fix: need this import there... --- bluesky.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bluesky.ts b/bluesky.ts index 057146c..71b9bbf 100644 --- a/bluesky.ts +++ b/bluesky.ts @@ -1,4 +1,5 @@ import * as process from 'process' +import {BskyAgent} from '@atproto/api' export async function login():Promise { const agent = new BskyAgent({ From 847c2c5009d9de75894aa8fb8393e71d767bf959 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 19:35:50 -0800 Subject: [PATCH 17/20] types: be more specific with Lambda data vs Songfish data [#1] --- index.test.ts | 29 ++++++++++++++++------------- index.ts | 18 +++++++++--------- tsconfig.json | 6 +++--- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/index.test.ts b/index.test.ts index 362c3a6..db599ab 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,14 +1,16 @@ import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import createFetchMock from 'vitest-fetch-mock' -import {login} from './bluesky' +import {BskyAgent} from '@atproto/api' +import {login} from './bluesky.js' import { handlePayload, isSongfishPayload, + type LambdaEvent, type SongfishWebhookPayload, -} from './index' +} from './index.js' import testFixture from './test-fixture.json' -vi.mock('./bluesky') // use e.g.: vi.mocked(login).mockResolvedValue({}) +vi.mock('./bluesky.js') // use e.g.: vi.mocked(login).mockResolvedValue({}) const fetchMocker = createFetchMock(vi) fetchMocker.enableMocks() @@ -26,13 +28,13 @@ describe('isSongfishPayload', () => { }) describe('with non-string body', () => { test('is false', () => { - expect(isSongfishPayload()).toBe(false) + expect(isSongfishPayload(undefined)).toBe(false) expect(isSongfishPayload(9)).toBe(false) expect(isSongfishPayload('foo')).toBe(false) }) }) describe('with an event object containing "body" property with value of a stringified JSON object', () => { - const fakeSongfishPayload:SongfishWebhookPayload = { + const fakeSongfishPayload:LambdaEvent = { body: JSON.stringify({show_id: 'foo'}) } test('retuns true', () => { @@ -47,6 +49,7 @@ describe('handlePayload', () => { }) describe('with malformed payload', () => { test('throws', async () => { + // @ts-expect-error test passing invalid string argument await expect(() => handlePayload([])).rejects.toThrow('not valid JSON') }) }) @@ -73,7 +76,7 @@ describe('handlePayload', () => { getAuthorFeed: vi.fn().mockReturnValueOnce({data: {feed: [{post: {record: {text: 'Prior Post'}}}] }}), } beforeEach(() => { - vi.mocked(login).mockResolvedValue(mockedLoginReturnValue) + vi.mocked(login).mockResolvedValue(mockedLoginReturnValue as unknown as BskyAgent) }) afterEach(() => { vi.mocked(login).mockReset() @@ -93,7 +96,7 @@ describe('handlePayload', () => { vi.mocked(login).mockResolvedValue({ ...mockedLoginReturnValue, post: mockedPost, - }) + } as unknown as BskyAgent) mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ {show_id: 666, songname: 'Most Recent Song Name'}, ]}) @@ -112,7 +115,7 @@ describe('handlePayload', () => { vi.mocked(login).mockResolvedValue({ ...mockedLoginReturnValue, post: mockedPost, - }) + } as unknown as BskyAgent) mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ {show_id: 789, songname: 'A Different Show and Song'}, {show_id: 456, songname: 'Yet Another Different Show and Song'}, @@ -130,7 +133,7 @@ describe('handlePayload', () => { getAuthorFeed: vi.fn().mockReturnValueOnce({data: {feed: [{post: {record: {text: 'Song Title'}}}] }}), } beforeEach(() => { - vi.mocked(login).mockResolvedValue(mockedLoginReturnValue) + vi.mocked(login).mockResolvedValue(mockedLoginReturnValue as unknown as BskyAgent) }) afterEach(() => { vi.mocked(login).mockReset() @@ -142,7 +145,7 @@ describe('handlePayload', () => { vi.mocked(login).mockResolvedValue({ ...mockedLoginReturnValue, post: mockedPost, - }) + } as unknown as BskyAgent) mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ {show_id: 123, songname: 'Song Title'}, ]}) @@ -169,7 +172,7 @@ describe('handlePayload', () => { vi.mocked(login).mockResolvedValue({ ...mockedLoginReturnValue, post: mockedPost, - }) + } as unknown as BskyAgent) mockJson(/\bkglw\.net\b.+\blatest\.json$/, {data: [ // the id 1699404057 is defined in the fixture file {show_id: 1699404057, songname: 'Name of Song From Show #1699404057'}, @@ -178,8 +181,8 @@ describe('handlePayload', () => { afterEach(() => { vi.mocked(login).mockReset() }) - testWithFixture('does not throw', async ({event}) => { - await expect(handlePayload(event)).resolves.not.to.throw() + testWithFixture('does not throw', async ({event}:{event:LambdaEvent}) => { + await expect(handlePayload(event)).resolves.not.toThrow() expect(mockedPost).toHaveBeenCalledWith({text: 'Name of Song From Show #1699404057'}) }) }) diff --git a/index.ts b/index.ts index c77405c..7c79a94 100644 --- a/index.ts +++ b/index.ts @@ -1,14 +1,14 @@ -import {BskyAgent} from '@atproto/api' import * as dotenv from 'dotenv' import * as process from 'process' -import {login} from './bluesky' +import {login} from './bluesky.js' dotenv.config() // read env var declarations from a `.env` file -export type SongfishWebhookPayload = { // TODO split this up into LambdaEvent and WebhookPayload - body: { - show_id: number - } +export type LambdaEvent = { + body: string // ... which is JSON that parses into a type T +} +export type SongfishWebhookPayload = { + show_id: number } export type BlueskyAgent = { @@ -16,13 +16,13 @@ export type BlueskyAgent = { post: Function } -export function isSongfishPayload(event:any):event is SongfishWebhookPayload { +export function isSongfishPayload(event:any):event is LambdaEvent { return typeof event?.body === 'string' && event.body.includes('"show_id"') // TODO could further verify that the song_id appears to be an int... } -export async function handlePayload(event:SongfishWebhookPayload):Promise { - let payloadBody +export async function handlePayload(event:LambdaEvent):Promise { + let payloadBody:SongfishWebhookPayload try { payloadBody = JSON.parse(event.body) } catch (err) { diff --git a/tsconfig.json b/tsconfig.json index adfbc6a..3a746a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,7 +40,7 @@ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ @@ -82,8 +82,8 @@ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strict": false, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ From 9e21144c3206de93d1217287592d61ba30dbdcd4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 19:36:17 -0800 Subject: [PATCH 18/20] ops: run `lint` on CI [#1] --- .github/workflows/main.yml | 15 ++++++++++++++- package.json | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2d7d1fa..be09def 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,22 @@ name: test suite on: [push] +image: ubuntu-latest jobs: + lint: + steps: + - name: checkout + uses: actions/checkout@v4 + - name: setup node v20 + uses: actions/setup-node@v4 + with: + node-version: 20.18.0 # match .tool-versions file + - name: install + run: npm ci + - name: tests + run: npm run lint + test: - runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 diff --git a/package.json b/package.json index 1691302..19657b0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "type": "module", "scripts": { "build": "funpack", + "lint": "tsc", "postbuild": "echo okay now copy the index.js file, but not the package.json, and put it up into Lambda", + "postlint": "echo Lint succeeded", "prebuild": "echo Building...", "test": "vitest --run", "test:watch": "vitest --watch" From f5112e81b88f822034589cc55b666b782fead1bd Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 Nov 2024 20:05:19 -0800 Subject: [PATCH 19/20] ops: restore `runs-on` keys --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be09def..60d2432 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,9 @@ name: test suite on: [push] -image: ubuntu-latest jobs: lint: + runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 @@ -17,6 +17,7 @@ jobs: run: npm run lint test: + runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 From dbc90a626092bcf53f5b8990ae9b621db4ca6d39 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 21 Nov 2024 12:03:35 -0800 Subject: [PATCH 20/20] test: reword test names; more specific assertions thanks @InblEric ! [#1] --- index.test.ts | 41 ++++++++++++++++++++++++++++++----------- index.ts | 2 +- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/index.test.ts b/index.test.ts index db599ab..670298d 100644 --- a/index.test.ts +++ b/index.test.ts @@ -26,19 +26,38 @@ describe('isSongfishPayload', () => { test('is a function', () => { expect(typeof isSongfishPayload).toBe('function') }) - describe('with non-string body', () => { + describe('with non-object payload', () => { test('is false', () => { expect(isSongfishPayload(undefined)).toBe(false) expect(isSongfishPayload(9)).toBe(false) expect(isSongfishPayload('foo')).toBe(false) }) }) - describe('with an event object containing "body" property with value of a stringified JSON object', () => { - const fakeSongfishPayload:LambdaEvent = { - body: JSON.stringify({show_id: 'foo'}) - } - test('retuns true', () => { - expect(isSongfishPayload(fakeSongfishPayload)).toBe(true) + describe('with object payload', () => { + describe('when object does not have `body`', () => { + test('is false', () => { + expect(isSongfishPayload({foo:'bar'})).toBe(false) + }) + }) + describe('when object does have `body`', () => { + describe('but it is not stringified JSON', () => { + test('is false', () => { + expect(isSongfishPayload({body:[]})).toBe(false) + }) + }) + describe('set to stringified JSON', () => { + describe('when stringified JSON is _not_ an object with a `show_id` key', () => { + test('is false', () => { + expect(isSongfishPayload({body:'[1, 2, 3]'})).toBe(false) + expect(isSongfishPayload({body:'{}'})).toBe(false) + }) + }) + describe('when stringified JSON _is_ an object with a `show_id` key', () => { + test('is true', () => { + expect(isSongfishPayload({body:'{"show_id": 999}'})).toBe(true) + }) + }) + }) }) }) }) @@ -48,7 +67,7 @@ describe('handlePayload', () => { expect(typeof handlePayload).toBe('function') }) describe('with malformed payload', () => { - test('throws', async () => { + test('throws with the error message from parsing the payload', async () => { // @ts-expect-error test passing invalid string argument await expect(() => handlePayload([])).rejects.toThrow('not valid JSON') }) @@ -65,10 +84,10 @@ describe('handlePayload', () => { }) describe('with invalid login', () => { beforeEach(() => { - vi.mocked(login).mockRejectedValue('mocked bluesky.login()') + vi.mocked(login).mockRejectedValue('mocked login failure') }) - test('throws', async () => { - await expect(() => handlePayload(payload)).rejects.toThrow('mocked bluesky.login()') + test('throws with the error message from logging in', async () => { + await expect(() => handlePayload(payload)).rejects.toThrow('mocked login failure') }) }) describe('with valid login and prior post does not match latest song title', () => { diff --git a/index.ts b/index.ts index 7c79a94..eda7ead 100644 --- a/index.ts +++ b/index.ts @@ -50,7 +50,7 @@ export async function handlePayload(event:LambdaEvent):P } let lastPost try { - const feed = await bsky.getAuthorFeed({actor: process.env.BLUESKY_USERNAME}) + const feed = await bsky.getAuthorFeed({actor: process.env.BLUESKY_USERNAME}) // TODO extract this into bluesky file as well lastPost = feed.data.feed[0] } catch (err) { console.log('error fetching most recent post...', err)