From b585fed8fa4ff05566013a554fa124cc829175d0 Mon Sep 17 00:00:00 2001 From: Ryan Waskiewicz Date: Fri, 11 Apr 2025 07:19:10 -0500 Subject: [PATCH] add tests for finding tag from releases add tests for updated functionality to break when we find a release. the logic has been extracted into its own function, to make testing simpler by avoiding over mocking/stubbing of network calls that would create or update a release. the tests that were added use jest's describe/it blocks, but use node's assert function to align with other tests. there isn't any prior art for mocking function calls in the codebase, so for now we use simple promises in "mock" objects that adhere to the Releaser interface --- __tests__/github.test.ts | 256 ++++++++++++++++++++++++++++++++++++++- src/github.ts | 40 ++++-- src/main.ts | 8 +- 3 files changed, 290 insertions(+), 14 deletions(-) diff --git a/__tests__/github.test.ts b/__tests__/github.test.ts index 6202c0b..19ab4ae 100644 --- a/__tests__/github.test.ts +++ b/__tests__/github.test.ts @@ -1,6 +1,11 @@ import * as assert from "assert"; -import { text } from "stream/consumers"; -import { mimeOrDefault, asset } from "../src/github"; +import { + mimeOrDefault, + asset, + Releaser, + Release, + findTagFromReleases, +} from "../src/github"; describe("github", () => { describe("mimeOrDefault", () => { @@ -20,4 +25,251 @@ describe("github", () => { assert.equal(size, 10); }); }); + + describe("findTagFromReleases", () => { + const owner = "owner"; + const repo = "repo"; + + const mockRelease: Release = { + id: 1, + upload_url: `https://api.github.com/repos/${owner}/${repo}/releases/1/assets`, + html_url: `https://github.com/${owner}/${repo}/releases/tag/v1.0.0`, + tag_name: "v1.0.0", + name: "Test Release", + body: "Test body", + target_commitish: "main", + draft: false, + prerelease: false, + assets: [], + } as const; + + const mockReleaser: Releaser = { + getReleaseByTag: () => Promise.reject("Not implemented"), + createRelease: () => Promise.reject("Not implemented"), + updateRelease: () => Promise.reject("Not implemented"), + allReleases: async function* () { + yield { data: [mockRelease] }; + }, + } as const; + + describe("when the tag_name is not an empty string", () => { + const targetTag = "v1.0.0"; + + it("finds a matching release in first batch of results", async () => { + const targetRelease = { + ...mockRelease, + owner, + repo, + tag_name: targetTag, + }; + const otherRelease = { + ...mockRelease, + owner, + repo, + tag_name: "v1.0.1", + }; + + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [targetRelease] }; + yield { data: [otherRelease] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + targetTag, + ); + + assert.deepStrictEqual(result, targetRelease); + }); + + it("finds a matching release in second batch of results", async () => { + const targetRelease = { + ...mockRelease, + owner, + repo, + tag_name: targetTag, + }; + const otherRelease = { + ...mockRelease, + owner, + repo, + tag_name: "v1.0.1", + }; + + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [otherRelease] }; + yield { data: [targetRelease] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + targetTag, + ); + assert.deepStrictEqual(result, targetRelease); + }); + + it("returns undefined when a release is not found in any batch", async () => { + const otherRelease = { + ...mockRelease, + owner, + repo, + tag_name: "v1.0.1", + }; + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [otherRelease] }; + yield { data: [otherRelease] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + targetTag, + ); + + assert.strictEqual(result, undefined); + }); + + it("returns undefined when no releases are returned", async () => { + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + targetTag, + ); + + assert.strictEqual(result, undefined); + }); + }); + + describe("when the tag_name is an empty string", () => { + const emptyTag = ""; + + it("finds a matching release in first batch of results", async () => { + const targetRelease = { + ...mockRelease, + owner, + repo, + tag_name: emptyTag, + }; + const otherRelease = { + ...mockRelease, + owner, + repo, + tag_name: "v1.0.1", + }; + + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [targetRelease] }; + yield { data: [otherRelease] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + emptyTag, + ); + + assert.deepStrictEqual(result, targetRelease); + }); + + it("finds a matching release in second batch of results", async () => { + const targetRelease = { + ...mockRelease, + owner, + repo, + tag_name: emptyTag, + }; + const otherRelease = { + ...mockRelease, + owner, + repo, + tag_name: "v1.0.1", + }; + + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [otherRelease] }; + yield { data: [targetRelease] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + emptyTag, + ); + assert.deepStrictEqual(result, targetRelease); + }); + + it("returns undefined when a release is not found in any batch", async () => { + const otherRelease = { + ...mockRelease, + owner, + repo, + tag_name: "v1.0.1", + }; + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [otherRelease] }; + yield { data: [otherRelease] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + emptyTag, + ); + + assert.strictEqual(result, undefined); + }); + + it("returns undefined when no releases are returned", async () => { + const releaser = { + ...mockReleaser, + allReleases: async function* () { + yield { data: [] }; + }, + }; + + const result = await findTagFromReleases( + releaser, + owner, + repo, + emptyTag, + ); + + assert.strictEqual(result, undefined); + }); + }); + }); }); diff --git a/src/github.ts b/src/github.ts index 7f8015b..35af897 100644 --- a/src/github.ts +++ b/src/github.ts @@ -229,16 +229,7 @@ export const release = async ( // so we must find one in the list of all releases let _release: Release | undefined = undefined; if (config.input_draft) { - for await (const response of releaser.allReleases({ - owner, - repo, - })) { - _release = response.data.find((release) => release.tag_name === tag); - // detect if we found a release - note that a draft release tag may be an empty string - if (typeof _release !== "undefined") { - break; - } - } + _release = await findTagFromReleases(releaser, owner, repo, tag); } else { _release = ( await releaser.getReleaseByTag({ @@ -342,6 +333,35 @@ export const release = async ( } }; +/** + * Finds a release by tag name from all a repository's releases. + * + * @param releaser - The GitHub API wrapper for release operations + * @param owner - The owner of the repository + * @param repo - The name of the repository + * @param tag - The tag name to search for + * @returns The release with the given tag name, or undefined if no release with that tag name is found + */ +export async function findTagFromReleases( + releaser: Releaser, + owner: string, + repo: string, + tag: string, +) { + let _release: Release | undefined; + for await (const response of releaser.allReleases({ + owner, + repo, + })) { + _release = response.data.find((release) => release.tag_name === tag); + // detect if we found a release - note that a draft release tag may be an empty string + if (typeof _release !== "undefined") { + break; + } + } + return _release; +} + async function createRelease( tag: string, config: Config, diff --git a/src/main.ts b/src/main.ts index 438be84..4d37e9a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,9 +67,13 @@ async function run() { const files = paths(config.input_files); if (files.length == 0) { if (config.input_fail_on_unmatched_files) { - throw new Error(`⚠️ ${config.input_files} does not include a valid file.`); + throw new Error( + `⚠️ ${config.input_files} does not include a valid file.`, + ); } else { - console.warn(`🤔 ${config.input_files} does not include a valid file.`); + console.warn( + `🤔 ${config.input_files} does not include a valid file.`, + ); } } const currentAssets = rel.assets;