Skip to content

Commit

Permalink
feat(tests): Basic test coverage for sourceNodes integration.
Browse files Browse the repository at this point in the history
Signed-off-by: Jesse Stuart <hi@jessestuart.com>
  • Loading branch information
jessestuart committed Apr 17, 2019
1 parent 467ce51 commit 85ed2d3
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 119 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
rules: {
semi: ['error', 'never'],
'@typescript-eslint/camelcase': ['off'],
'@typescript-eslint/explicit-function-return-type': ['off'],
'@typescript-eslint/indent': ['error', 2],
'@typescript-eslint/member-delimiter-style': [
'error',
Expand Down
9 changes: 9 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
coverage:
status:
project:
default:
threshold: 30%
patch:
default:
target: 50%
only_pulls: true
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '(/__tests__/.*\\.([tj]sx?)|(\\.|/)(test|spec))\\.([tj]sx?)$',
transform: { '^.+\\.tsx?$': 'ts-jest' },
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"author": "Jesse Stuart <hi@jessestuart.com>",
"dependencies": {
"@babel/runtime": "7.4.3",
"aws-sdk": "2.437.0",
"axios": "0.18.0",
"aws-sdk": "2.438.0",
"bluebird": "3.5.4",
"exif-parser": "0.1.12",
"lodash": "4.17.11",
Expand Down Expand Up @@ -34,7 +33,7 @@
"eslint-config-standard": "12.0.0",
"eslint-friendly-formatter": "4.0.1",
"eslint-loader": "2.1.2",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-import": "2.17.2",
"eslint-plugin-node": "8.0.1",
"eslint-plugin-prettier": "3.0.1",
"eslint-plugin-promise": "4.1.1",
Expand All @@ -44,9 +43,12 @@
"jest": "24.7.1",
"jest-junit": "6.3.0",
"json-stringify-pretty-compact": "2.0.0",
"mitm": "1.7.0",
"nodemon": "1.18.11",
"prettier": "1.17.0",
"prettier-eslint": "8.8.2",
"redux": "4.0.1",
"redux-mock-store": "1.5.3",
"semantic-release": "15.13.3",
"ts-jest": "24.0.2",
"typescript": "3.4.3"
Expand Down
59 changes: 59 additions & 0 deletions src/__tests__/fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"IsTruncated": false,
"Contents": [
{
"Key": "2017-12-25/",
"LastModified": "2018-02-15T09:55:23.000Z",
"ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
"Size": 0,
"StorageClass": "STANDARD"
},
{
"Key": "2017-12-25/_DSC7589.jpg",
"LastModified": "2018-02-15T09:55:25.000Z",
"ETag": "\"8ed1dc689c7baeba0cf0196037ac70c0\"",
"Size": 900211,
"StorageClass": "STANDARD"
},
{
"Key": "2019-04-10/",
"LastModified": "2019-04-11T02:54:23.000Z",
"ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
"Size": 0,
"StorageClass": "STANDARD"
},
{
"Key": "2019-04-10/DSC02621.jpg",
"LastModified": "2019-04-11T02:59:06.000Z",
"ETag": "\"921da2f4adcf76e0d02719bb2696cc58\"",
"Size": 15285486,
"StorageClass": "STANDARD"
},
{
"Key": "2019-04-10/DSC02943.jpg",
"LastModified": "2019-04-11T02:54:39.000Z",
"ETag": "\"833816655f9709cb1b2b8ac9505a3c65\"",
"Size": 7275401,
"StorageClass": "STANDARD"
},
{
"Key": "2019-04-10/DSC02943.jpg",
"LastModified": "2019-04-11T02:54:39.000Z",
"ETag": "\"833816659f9709cb1b2b8ac9505a3c65\"",
"Size": 7275401,
"StorageClass": "STANDARD"
},
{
"Key": "2019-04-10/DSC02943.jpg",
"LastModified": "2019-04-11T02:54:39.000Z",
"ETag": "\"833816660f9709cb1b2b8ac9505a3c65\"",
"Size": 7275401,
"StorageClass": "STANDARD"
}
],
"Name": "js-photos-dev",
"Prefix": "",
"MaxKeys": 1000,
"CommonPrefixes": [],
"KeyCount": 5
}
70 changes: 70 additions & 0 deletions src/__tests__/source-nodes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import sourceFilesystem from 'gatsby-source-filesystem'

import Mitm from 'mitm'
import configureMockStore from 'redux-mock-store'

import { sourceNodes } from '../source-nodes'
import fixtures from './fixtures.json'

const mockStore = configureMockStore()

// Mock out Gatby's source-filesystem API.
sourceFilesystem.createRemoteFileNode = jest.fn().mockImplementation(node => {
console.log({ node })
})

// Mock out `aws-sdk` module to prevent unnecessary calls to S3 during testing.
jest.mock('aws-sdk', () => ({
S3: class {
public listObjectsV2() {
return {
promise: () => fixtures,
}
}
},
}))

describe('source S3ImageAsset nodes', () => {
let args
let nodes = {}

beforeEach(() => {
spyOn(sourceFilesystem, 'createRemoteFileNode')
args = {
actions: {
createNode: jest.fn(node => (nodes[node.id] = node)),
},
cache: {
get: jest.fn(),
set: jest.fn(),
},
createContentDigest: jest.fn(),
createNodeId: jest.fn().mockImplementation(node => {
console.log('create node ID', { node })
return node
}),
store: mockStore,
}
Mitm().on('request', () => {
throw new Error('Network requests forbidden in offline mode.')
})
})

test('sourceNodes', async () => {
// NB: pulls from fixtures defined above, not S3 API.
const entityNodes = await sourceNodes(args, { bucketName: 'fake-bucket' })
// `createRemoteFileNode` called once for each of the five images in fixtures.
expect(sourceFilesystem.createRemoteFileNode).toHaveBeenCalledTimes(5)
// 5 images + 2 directories = 7 nodes
expect(entityNodes).toHaveLength(7)
})

test('createS3ImageAssetNode', async () => {
// NB: pulls from fixtures defined above, not S3 API.
const entityNodes = await sourceNodes(args, { bucketName: 'fake-bucket' })
// `createRemoteFileNode` called once for each of the five images in fixtures.
expect(sourceFilesystem.createRemoteFileNode).toHaveBeenCalledTimes(5)
// 5 images + 2 directories = 7 nodes
expect(entityNodes).toHaveLength(7)
})
})
2 changes: 1 addition & 1 deletion src/utils.spec.ts → src/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { constructS3UrlForAsset, isImage } from './utils'
import { constructS3UrlForAsset, isImage } from '../utils'

describe('utils', () => {
test('isImage', () => {
Expand Down
127 changes: 54 additions & 73 deletions src/source-nodes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { S3 } from 'aws-sdk'
import { createRemoteFileNode } from 'gatsby-source-filesystem'
import AWS from 'aws-sdk'
import _ from 'lodash'
import mime from 'mime-types'

Expand All @@ -8,12 +8,12 @@ import { constructS3UrlForAsset, isImage } from './utils'
// =================
// AWS config setup.
// =================
const S3Instance: S3 = new S3({ apiVersion: '2006-03-01' })
const S3Instance = new AWS.S3({ apiVersion: '2006-03-01' })

// =========================
// Plugin-specific constants.
// =========================
const S3SourceGatsbyNodeType = 'S3ImageAsset'
export const S3SourceGatsbyNodeType = 'S3ImageAsset'

// =================
// Type definitions.
Expand All @@ -24,9 +24,9 @@ export interface SourceS3Options {
// overridden to e.g., support CDN's (such as CloudFront),
// or any other S3-compliant API (such as DigitalOcean
// Spaces.)
domain: string
domain?: string
// Defaults to HTTPS.
protocol: string
protocol?: string
}

export const sourceNodes = async (
Expand All @@ -35,98 +35,78 @@ export const sourceNodes = async (
): Promise<any> => {
const { createNode } = actions

const listObjectsResponse: S3.ListObjectsV2Output = await S3Instance.listObjectsV2(
const listObjectsResponse: AWS.S3.ListObjectsV2Output = await S3Instance.listObjectsV2(
{ Bucket: bucketName }
).promise()

const s3Entities: S3.ObjectList | undefined = _.get(
const s3Entities: AWS.S3.ObjectList | undefined = _.get(
listObjectsResponse,
'Contents'
)
if (!s3Entities) {
return Promise.resolve([])
return Promise.resolve()
}

return await Promise.all(
_.compact(
s3Entities.map(
async (entity: S3.Object): Promise<void> => {
if (!isImage(entity)) {
return
}
s3Entities.map(async (entity: AWS.S3.Object) => {
if (!isImage(entity)) {
return
}

const url: string | undefined = constructS3UrlForAsset({
bucketName,
domain,
key: entity.Key,
protocol,
})
if (!url) {
return
}
const url: string | undefined = constructS3UrlForAsset({
bucketName,
domain,
key: entity.Key,
protocol,
})
if (!url) {
return
}

try {
const fileNode = await createS3RemoteFileNode({
cache,
createNode,
createNodeId,
url,
store,
})
if (!fileNode) {
return
}

return await createS3ImageAssetNode({
createNode,
createNodeId,
entity,
fileNode,
url,
})
} catch (err) {
Promise.reject(err)
}
try {
const fileNode = await createRemoteFileNode({
cache,
createNode,
createNodeId,
store,
url,
})
if (!fileNode) {
return
}
)
)
)
}

const createS3RemoteFileNode = async ({
cache,
createNode,
createNodeId,
url,
store,
}): Promise<any> => {
try {
return await createRemoteFileNode({
cache,
createNode,
createNodeId,
store,
url,
return await createS3ImageAssetNode({
createNode,
createNodeId,
entity,
fileNode,
url,
})
} catch (err) {
Promise.reject(err)
}
})
} catch (err) {
return Promise.reject(err)
}
)
}

const createS3ImageAssetNode = async ({
export const createS3ImageAssetNode = async ({
createNode,
createNodeId,
entity,
fileNode,
url,
}: {
createNode: Function
createNodeId: Function
entity: S3.Object
createNodeId: (any) => string
entity: AWS.S3.Object
fileNode: { absolutePath: string; id: string }
url: string
}): Promise<void> => {
const { Key, ETag } = entity
}): Promise<any> => {
if (!fileNode) {
return Promise.reject()
}

const { ETag, Key } = entity
// TODO: Use the `mime-types` lib to populate this dynamically.
// const ContentType = 'image/jpeg'
const mediaType = mime.lookup(entity.Key)
Expand All @@ -136,9 +116,10 @@ const createS3ImageAssetNode = async ({
// @see https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html
const objectHash: string = ETag!.replace(/"/g, '')
const fileNodeId: string = _.get(fileNode, 'id')
await createNode({
const absolutePath: string = _.get(fileNode, 'absolutePath')
return await createNode({
...entity,
absolutePath: fileNode.absolutePath,
absolutePath: absolutePath,
children: [fileNodeId],
ETag: objectHash,
id: createNodeId(objectHash),
Expand Down
Loading

0 comments on commit 85ed2d3

Please sign in to comment.