Skip to content

Commit

Permalink
Comprehensive testing
Browse files Browse the repository at this point in the history
  • Loading branch information
chriscarrollsmith committed Sep 26, 2023
1 parent 15a7a7d commit 73b69c6
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 112 deletions.
28 changes: 13 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,24 @@ jobs:

steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4

- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v3
with:
node-version: 20
cache: npm

- name: Install Dependencies
id: npm-ci
run: npm ci

- name: Check Format
id: npm-format-check
run: npm run format:check

- name: Lint
id: npm-lint
run: npm run lint

- name: Test
id: npm-ci-test
run: npm run ci-test

test-action:
Expand All @@ -46,18 +40,22 @@ jobs:

steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4

- name: Test Local Action
id: test-action
- name: Test Local Action with JSON
uses: ./
with:
feed_url: "https://modelingmarkets.substack.com/feed"
file_path: "temp.json"

- name: Save and Validate JSON structure
run: |
cat <<EOL > temp.json
${{ steps.test-action.outputs.feed }}
EOL
node -e "const fs = require('fs'); const data = fs.readFileSync('temp.json', 'utf8'); const obj = JSON.parse(data); if (!obj || !obj.rss || !obj.rss.$) { console.error('Invalid object'); process.exit(1); }"
- name: Validate JSON
run: node -e "const fs = require('fs'); const data = fs.readFileSync('temp.json', 'utf8'); const obj = JSON.parse(data); if (!obj || !obj.rss || !obj.rss.$) { console.error('Invalid object'); process.exit(1); }"

- name: Test Local Action with XML
uses: ./
with:
feed_url: "https://modelingmarkets.substack.com/feed"
file_path: "temp.xml"

- name: Validate XML
run: node -e "const fs = require('fs'); const data = fs.readFileSync('temp.xml', 'utf8'); if (!data.includes('<rss') || !data.includes('</rss>')) { console.error('Invalid XML'); process.exit(1); }"
167 changes: 118 additions & 49 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ const { fetchRssFeed } = require('../src/index')
jest.mock('isomorphic-fetch')
jest.mock('fs', () => ({
accessSync: jest.fn(),
existsSync: jest.fn(),
mkdirSync: jest.fn(),
writeFileSync: jest.fn(),
constants: { W_OK: 'some_value' },
promises: {
access: jest.fn()
}
}))

jest.mock('xml2js')
jest.mock('@actions/core')

Expand All @@ -29,17 +30,71 @@ describe('fetchRssFeed', () => {
expect(core.setFailed).toHaveBeenCalledWith('Feed URL is not provided')
})

it('should set failure if fetch fails', async () => {
it('should set failure if no file path is provided', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = '' // Setting file path to empty
await fetchRssFeed()
expect(core.setFailed).toHaveBeenCalledWith('File path is not provided')
})

// Test case for unsupported file extension
it('should set failure if unsupported file extension is provided', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = './feed.txt' // Unsupported extension

await fetchRssFeed()

expect(core.setFailed).toHaveBeenCalledWith(
'File extension must be .json or .xml'
)
})

it('should set failure if fetch returns a non-ok response', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = './feed.json'
fetch.mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found'
})

await fetchRssFeed()

expect(core.setFailed).toHaveBeenCalledWith(
'Failed to fetch RSS feed (404 Not Found)'
)
})

it('should set failure if fetch fails due to other error', async () => {
expect.assertions(1)
process.env.INPUT_FEED_URL = 'http://example.com/feed'
fetch.mockRejectedValue(new Error('Fetch failed'))
process.env.INPUT_FILE_PATH = './feed.json'
fetch.mockRejectedValue(new Error('Network error'))

await fetchRssFeed()

expect(core.setFailed).toHaveBeenCalledWith('Fetch failed')
expect(core.setFailed).toHaveBeenCalledWith(
'Failed to fetch RSS feed due to network error or invalid URL'
)
})

it('should save the feed to a file if file path is provided', async () => {
it('should set failure if RSS parsing fails', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = './feed.json'
fetch.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('<rss></rss>')
})
parseStringPromise.mockRejectedValue(new Error('Invalid XML'))

await fetchRssFeed()

expect(core.setFailed).toHaveBeenCalledWith(
'Failed to parse RSS feed. The feed might not be valid XML.'
)
})

it('should save the feed to a JSON file if .json file path is provided', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = './feed.json'
fetch.mockResolvedValue({
Expand All @@ -55,71 +110,85 @@ describe('fetchRssFeed', () => {
'./feed.json',
JSON.stringify({ rss: {} }, null, 2)
)
expect(core.setOutput).toHaveBeenCalledWith(
'feed',
JSON.stringify({ rss: {} }, null, 2)
)
})

it('should not save the feed to a file if no file path is provided', async () => {
// Test case for .xml extension
it('should save the feed to an XML file if .xml extension is provided', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = ''
process.env.INPUT_FILE_PATH = './feed.xml'
const mockXmlData = '<rss></rss>'
fetch.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('<rss></rss>')
text: jest.fn().mockResolvedValue(mockXmlData)
})
parseStringPromise.mockResolvedValue({ rss: {} })

await fetchRssFeed()

expect(fs.writeFileSync).not.toHaveBeenCalled()
expect(core.setOutput).toHaveBeenCalledWith(
'feed',
expect(fs.writeFileSync).toHaveBeenCalledWith('./feed.xml', mockXmlData)
})

it('should remove lastBuildDate if specified', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = './feed.json'
process.env.INPUT_REMOVE_LAST_BUILD_DATE = 'true'
fetch.mockResolvedValue({
ok: true,
text: jest
.fn()
.mockResolvedValue(
'<rss><lastBuildDate>some date</lastBuildDate></rss>'
)
})
parseStringPromise.mockImplementation(async xmlString => {
if (xmlString.includes('<lastBuildDate>')) {
return { rss: { lastBuildDate: 'some date' } }
} else {
return { rss: {} }
}
})

await fetchRssFeed()

expect(fs.writeFileSync).toHaveBeenCalledWith(
'./feed.json',
JSON.stringify({ rss: {} }, null, 2)
)
})
})

// Verify that mocks work correctly
describe('fetch mock', () => {
it('should reject when mockRejectedValue is used', async () => {
// Set up the mock to reject
fetch.mockRejectedValue(new Error('Fetch failed'))
it('should handle directory creation if directory does not exist', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = './someDir/feed.json'
fs.existsSync.mockReturnValue(false)

// Expect that calling fetch will reject with the specified error
await expect(fetch('some_url')).rejects.toThrow('Fetch failed')
})
})
// Mock fetch for this test
fetch.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('<rss></rss>')
})

describe('fs mock', () => {
it('should call accessSync without errors', () => {
fs.accessSync.mockReturnValue(true)
expect(() => fs.accessSync('some_path', fs.constants.W_OK)).not.toThrow()
})
await fetchRssFeed()

it('should call writeFileSync without errors', () => {
fs.writeFileSync.mockReturnValue(true)
expect(() => fs.writeFileSync('some_path', 'some_data')).not.toThrow()
expect(fs.mkdirSync).toHaveBeenCalledWith('./someDir', { recursive: true })
})
})

describe('xml2js mock', () => {
it('should resolve parseStringPromise', async () => {
parseStringPromise.mockResolvedValue({ rss: {} })
await expect(parseStringPromise('<rss></rss>')).resolves.toEqual({
rss: {}
it('should handle file write permission errors gracefully', async () => {
process.env.INPUT_FEED_URL = 'http://example.com/feed'
process.env.INPUT_FILE_PATH = './feed.json'
fs.writeFileSync.mockImplementation(() => {
throw new Error('Permission denied')
})
})
})

describe('@actions/core mock', () => {
it('should call setOutput without errors', () => {
core.setOutput.mockReturnValue(true)
expect(() => core.setOutput('key', 'value')).not.toThrow()
})
// Mock fetch for this test
fetch.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('<rss></rss>')
})

await fetchRssFeed()

it('should call setFailed without errors', () => {
core.setFailed.mockReturnValue(true)
expect(() => core.setFailed('Some error')).not.toThrow()
expect(core.setFailed).toHaveBeenCalledWith(
'Failed to write the file due to permissions or other file system error: Permission denied'
)
})
})
12 changes: 6 additions & 6 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
name: 'RSS Feed Fetch Action'
description: 'Fetches an RSS feed and optionally saves it to a file'
description: 'Fetches an RSS feed and saves it to a file'
author: 'Christopher C. Smith <[email protected]>'

inputs:
feed_url:
description: 'The URL of the RSS feed to fetch'
required: true
file_path:
description: 'The relative file path to save the fetched RSS feed'
description: 'The relative file path at which to save the fetched RSS feed'
required: true
remove_last_build_date:
description: 'Remove the lastBuildDate tag from the fetched RSS feed for easier diffing'
required: false

outputs:
feed:
description: 'The fetched RSS feed in JSON format'
default: false

runs:
using: node20
Expand Down
2 changes: 1 addition & 1 deletion badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 73b69c6

Please sign in to comment.