Skip to content

Commit

Permalink
Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nahuelhds committed Jan 13, 2024
1 parent 121ca29 commit 0985779
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 71 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ need to perform some initial setup steps before you can develop your action.
1. :building_construction: Package the TypeScript for distribution

```bash
npm fetchEntries bundle
npm runAction bundle
```

1. :white_check_mark: Run the tests
Expand Down Expand Up @@ -84,19 +84,19 @@ inputs, and outputs for your action.
## Update the Action Code

The [`src/`](./src/) directory is the heart of your action! This contains the
source code that will be fetchEntries when your action is invoked. You can replace the
source code that will be runAction when your action is invoked. You can replace the
contents of this directory with your own code.

There are a few things to keep in mind when writing your action code:

- Most GitHub Actions toolkit and CI/CD operations are processed asynchronously.
In `main.ts`, you will see that the action is fetchEntries in an `async` function.
In `main.ts`, you will see that the action is runAction in an `async` function.

```javascript
import * as core from '@actions/core'
//...

async function fetchEntries() {
async function runAction() {
try {
//...
} catch (error) {
Expand All @@ -121,14 +121,14 @@ So, what are you waiting for? Go ahead and start customizing your action!
1. Format, test, and build the action

```bash
npm fetchEntries all
npm runAction all
```

> [!WARNING]
>
> This step is important! It will fetchEntries [`ncc`](https://github.com/vercel/ncc)
> This step is important! It will runAction [`ncc`](https://github.com/vercel/ncc)
> to build the final JavaScript action code with all dependencies included.
> If you do not fetchEntries this step, your action will not work correctly when it is
> If you do not runAction this step, your action will not work correctly when it is
> used in a workflow. This step also includes the `--license` option for
> `ncc`, which will create a license file for all of the production node
> modules used in your project.
Expand Down Expand Up @@ -175,7 +175,7 @@ steps:

- name: Print Output
id: output
fetchEntries: echo "${{ steps.test-action.outputs.time }}"
runAction: echo "${{ steps.test-action.outputs.time }}"
```
For example workflow runs, check out the
Expand Down Expand Up @@ -206,7 +206,7 @@ steps:

- name: Print Output
id: output
fetchEntries: echo "${{ steps.test-action.outputs.time }}"
runAction: echo "${{ steps.test-action.outputs.time }}"
```
## Publishing a new release
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"jest": {
"preset": "ts-jest",
"verbose": true,
"resetMocks": true,
"testEnvironment": "node",
"moduleFileExtensions": [
"js",
Expand Down
2 changes: 1 addition & 1 deletion src/extractors/extractFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FeedWithoutEntriesError } from "./errors";
import { feedExtractor } from "./helpers";
import { FeedData, FeedEntry } from "./types/feed-extractor";

type FeedWithEntries = FeedData & { entries: FeedEntry[] };
export type FeedWithEntries = FeedData & { entries: FeedEntry[] };

export async function extractFeed(feedUrl: URL) {
const extract = await feedExtractor();
Expand Down
4 changes: 2 additions & 2 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

import * as main from "./main";

jest.spyOn(main, "fetchEntries");
jest.spyOn(main, "runAction");

describe("index", () => {
it("calls run when imported", async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./index");

expect(main.fetchEntries).toHaveBeenCalled();
expect(main.runAction).toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
* The entrypoint for the action.
*/

import { fetchEntries } from "./main";
import { runAction } from "./main";

void fetchEntries();
void runAction();
6 changes: 4 additions & 2 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createLogger, format, transports } from "winston";
import { createLogger, format, Logger, transports } from "winston";

export const logger = createLogger({
const logger: Logger = createLogger({
level: "info",
format: format.combine(
format.timestamp({
Expand All @@ -17,3 +17,5 @@ export const logger = createLogger({
}),
],
});

export default logger;
59 changes: 49 additions & 10 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,57 @@
* Unit tests for the action's entrypoint, src/index.ts
*/

import { fetchEntries } from "./main";
import requireActual = jest.requireActual;
import { setFailed } from "@actions/core";

jest.mock("@extractus/feed-extractor", () => jest.fn());
import { runAction } from "./main";
import { processFeed } from "./processors/processFeed";
import { InvalidUrlError, UnknownError } from "./utils/errors";
import { getInputFeedUrls } from "./utils/io";

jest.mock("./utils/utils", () => ({
...requireActual("./utils"),
getInputFeedUrls: jest.fn(() => ["https://www.google.com"]),
}));
jest.mock("@actions/core");
jest.mock("./processors/processFeed");
jest.mock("./utils/io");

describe("fetchEntries", () => {
it("calls run when imported", async () => {
await fetchEntries();
describe("runAction", () => {
const setFailedMock = setFailed as jest.MockedFunction<typeof setFailed>;
const getInputFeedUrlsMock = getInputFeedUrls as jest.MockedFunction<
typeof getInputFeedUrls
>;
const processFeedMock = processFeed as jest.MockedFunction<
typeof processFeed
>;

it("returns error if something unexpected happens", async () => {
const thrownError = new Error();
const expectedError = new UnknownError(thrownError);
getInputFeedUrlsMock.mockImplementation(() => {
throw thrownError;
});

await runAction();

expect(setFailedMock).toHaveBeenCalledWith(expectedError.message);
});

it("returns error if the feed urls throw", async () => {
const expectedError = new InvalidUrlError("asd", new Error());
getInputFeedUrlsMock.mockImplementation(() => {
throw expectedError;
});

await runAction();

expect(setFailedMock).toHaveBeenCalledWith(expectedError.message);
});

it("starts processing the feed if everything is ok", async () => {
const expectedUrl = new URL("https://www.google.com");
const expectedUrl2 = new URL("https://www.google.com.uy");
getInputFeedUrlsMock.mockReturnValue([expectedUrl, expectedUrl2]);

await runAction();

expect(processFeedMock).toHaveBeenCalledWith(expectedUrl);
expect(processFeedMock).toHaveBeenCalledWith(expectedUrl2);
});
});
48 changes: 6 additions & 42 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,18 @@
import { setFailed } from "@actions/core";
import { CustomError } from "ts-custom-error";

import { extractArticle } from "./extractors/extractArticle";
import { extractFeed } from "./extractors/extractFeed";
import { FeedEntry } from "./extractors/types/feed-extractor";
import { logger } from "./logger";
import { processFeed } from "./processors/processFeed";
import { UnknownError } from "./utils/errors";
import { storeFile } from "./utils/fs";
import { buildFilename, getInputFeedUrls, getOutputDir } from "./utils/io";
import { getInputFeedUrls } from "./utils/io";

export async function fetchEntries() {
export async function runAction() {
try {
const feedUrls = getInputFeedUrls();

feedUrls.map(async (feedUrl: URL) => {
try {
const feedData = await extractFeed(feedUrl);
void Promise.all(feedData.entries.map(processEntry));
} catch (err) {
if (err instanceof CustomError) {
logger.warn(err.message);
return;
}

setFailed(new UnknownError(err).message);
return;
}
});
} catch (error) {
setFailed(new UnknownError(error).message);
return;
}
}

async function processEntry(feedEntry: FeedEntry) {
const outputDir = getOutputDir();
try {
const article = await extractArticle(feedEntry);
const filename = buildFilename(article.url);
const destinationFile = `${outputDir}/${filename}.json`;
const fileContents = JSON.stringify(article, null, 2);
storeFile(destinationFile, fileContents);
logger.info(
`New article stored: "%s". Path: "%s"`,
article.url,
destinationFile,
);
feedUrls.forEach((feedUrl) => processFeed(feedUrl));
} catch (err) {
// Even if it's a custom error, we want to return error for the process here
if (err instanceof CustomError) {
logger.warn(err.message);
setFailed(err.message);
return;
}

Expand Down
33 changes: 33 additions & 0 deletions src/processors/processEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { setFailed } from "@actions/core";
import { CustomError } from "ts-custom-error";

import { extractArticle } from "../extractors/extractArticle";
import { FeedEntry } from "../extractors/types/feed-extractor";
import logger from "../logger";
import { UnknownError } from "../utils/errors";
import { storeFile } from "../utils/fs";
import { buildFilename, getOutputDir } from "../utils/io";

export async function processEntry(feedEntry: FeedEntry) {
const outputDir = getOutputDir();
try {
const article = await extractArticle(feedEntry);
const filename = buildFilename(article.url);
const destinationFile = `${outputDir}/${filename}.json`;
const fileContents = JSON.stringify(article, null, 2);
storeFile(destinationFile, fileContents);
logger.info(
`New article stored: "%s". Path: "%s"`,
article.url,
destinationFile,
);
} catch (err) {
if (err instanceof CustomError) {
logger.warn(err.message);
return;
}

setFailed(new UnknownError(err).message);
return;
}
}
61 changes: 61 additions & 0 deletions src/processors/processFeed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { setFailed } from "@actions/core";

import { FeedWithoutEntriesError } from "../extractors/errors";
import { extractFeed, FeedWithEntries } from "../extractors/extractFeed";
import { FeedEntry } from "../extractors/types/feed-extractor";
import logger from "../logger";
import { UnknownError } from "../utils/errors";
import { processEntry } from "./processEntry";
import { processFeed } from "./processFeed";

jest.mock("@actions/core");
jest.mock("../extractors/extractFeed");
jest.mock("../logger");
jest.mock("./processEntry");

describe("processor", () => {
describe("processFeed", () => {
logger.warn = jest.fn();
const setFailedMock = setFailed as jest.MockedFunction<typeof setFailed>;
const extractFeedMock = extractFeed as jest.MockedFunction<
typeof extractFeed
>;
const processEntryMock = processEntry as jest.MockedFunction<
typeof processEntry
>;

it("returns error if something unexpected happens", async () => {
const url = new URL("https://www.google.com");
const thrownError = new Error("Unpredictable error");
const expectedError = new UnknownError(thrownError);

extractFeedMock.mockRejectedValue(thrownError);

await processFeed(url);
expect(logger.warn).not.toHaveBeenCalled();
expect(setFailedMock).toHaveBeenCalledWith(expectedError.message);
});

it("warns the error if something happens", async () => {
const url = new URL("https://www.google.com");
const expectedError = new FeedWithoutEntriesError(url, {});
extractFeedMock.mockRejectedValue(expectedError);

await processFeed(url);
expect(logger.warn).toHaveBeenCalledWith(expectedError.message);
expect(setFailedMock).not.toHaveBeenCalled();
});

it("process the entries", async () => {
const entry = { id: "test-entry" } as FeedEntry;
const feedData = { entries: [entry] } as FeedWithEntries;
extractFeedMock.mockResolvedValue(feedData);

await processFeed(new URL("https://www.google.com"));

expect(processEntryMock).toHaveBeenCalledWith(entry);
expect(logger.warn).not.toHaveBeenCalledWith();
expect(setFailedMock).not.toHaveBeenCalled();
});
});
});
22 changes: 22 additions & 0 deletions src/processors/processFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { setFailed } from "@actions/core";
import { CustomError } from "ts-custom-error";

import { extractFeed } from "../extractors/extractFeed";
import logger from "../logger";
import { UnknownError } from "../utils/errors";
import { processEntry } from "./processEntry";

export async function processFeed(feedUrl: URL) {
try {
const feedData = await extractFeed(feedUrl);
void Promise.all(feedData.entries.map((entry) => processEntry(entry)));
} catch (err) {
if (err instanceof CustomError) {
logger.warn(err.message);
return;
}

setFailed(new UnknownError(err).message);
return;
}
}
4 changes: 1 addition & 3 deletions src/utils/io.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ describe("io", () => {
beforeEach(() => {
process.env = { ...originalEnv };
});
afterEach(() => {
jest.resetModules();
});

describe("getInputFeedUrls", () => {
it("throws NoUrlsGivenError if INPUT_FEED_URL is an empty string", () => {
process.env.INPUT_FEED_URL = "";
Expand Down

0 comments on commit 0985779

Please sign in to comment.