Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backup Cognito user pool and copy it to S3 #2

Merged
merged 18 commits into from
May 3, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"plugins": [["babel-plugin-dotenv", {
"replacedModuleName": "babel-dotenv"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to make babel and dotenv play nicely together.

}]],
"presets": [
"@babel/preset-env"
],
"env": {
"test": {
"plugins": ["@babel/plugin-transform-runtime"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to use async/await in test suite.

}
}
}
72 changes: 72 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#

defaults: &defaults
docker:
- image: circleci/node:10.15.3
working_directory: ~/sinopia_user_backup

version: 2
jobs:
build:
<<: *defaults
steps:
- checkout
- setup_remote_docker
- restore_cache:
keys:
- dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- dependencies-
- run:
name: Install dependencies
command: npm install
- save_cache:
paths:
- node_modules
key: dependencies-{{ checksum "package.json" }}
- run:
name: Run linter
command: npm run lint
- run:
name: Setup Code Climate test-reporter
command: |
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
- run:
name: Run unit tests and send coverage results
command: |
./cc-test-reporter before-build
npm run ci
./cc-test-reporter after-build --exit-code $?
register_image:
<<: *defaults
steps:
- checkout
- setup_remote_docker
- restore_cache:
key: dependencies-{{ checksum "package.json" }}
- attach_workspace:
at: .
- run:
name: Build & push docker image
# NOTE: the env variables holding docker credentials are stored in the CircleCI dashboard
command: |
docker build -t ld4p/sinopia_user_backup:latest .
echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
docker push ld4p/sinopia_user_backup:latest
workflows:
version: 2
build:
jobs:
- build
- register_image:
requires:
- build
filters:
branches:
only:
- master
22 changes: 22 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: "2"

exclude_patterns:
- "__tests__/"
- "__mocks__/"
- 'eslintrc.js'
- 'jest.config.js'
- '**/node_modules/'

plugins:
eslint:
enabled: true
config:
config: .eslintrc.js
channel: "eslint-5"
duplication:
enabled: true
config:
languages:
- javascript:
nodesecurity:
enabled: true
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.dockerignore
.env
.git
.node-version
Dockerfile
__mocks__
__tests__
node_modules
npm-debug.log
40 changes: 40 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module.exports = {
plugins: [
"import",
"jest",
"security"
],
extends: [
"eslint:recommended",
"plugin:node/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:security/recommended",
"plugin:jest/recommended"
],
env: {
"es6": true,
"jest": true,
"node": true
},
parser: "babel-eslint",
parserOptions: {
ecmaVersion: 2019,
sourceType: "module"
},
overrides: [
{
"files": ["**/*.js"],
"rules": {
// See https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/es-syntax.md
// rule supposedly matches ECMA version with node
// we get: "Import and export declarations are not supported yet"
"node/no-unsupported-features/es-syntax": "off",
// Avoiding: "warning Found fs.readFileSync with non literal argument ..."
"security/detect-non-literal-fs-filename": "off",
// this is a CLI tool; we DO want to send output to console
"no-console": "off",
}
}
]
}
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM circleci/node:10.15

WORKDIR /home/circleci

COPY . .

RUN npm install

CMD ["bin/backup"]
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
[![CircleCI](https://circleci.com/gh/LD4P/sinopia_user_backup.svg?style=svg)](https://circleci.com/gh/LD4P/sinopia_user_backup)
[![Code Climate](https://codeclimate.com/github/LD4P/sinopia_user_backup/badges/gpa.svg)](https://codeclimate.com/github/LD4P/sinopia_user_backup)
[![Code Climate Test Coverage](https://codeclimate.com/github/LD4P/sinopia_user_backup/badges/coverage.svg)](https://codeclimate.com/github/LD4P/sinopia_user_backup/coverage)

# sinopia_user_backup
A Node component for backing up the Cognito user pool for Sinopia

A Node application that backs up Cognito user pools for the Sinopia project.

## Testing

### Run the linter

```shell
$ npm run lint
```

### Run unit tests

```shell
$ npm test
```

## Build and push image

The CircleCI build is configured to perform these steps automatically on any successful build on the `master` branch. If you need to manually build and push an image, you can do this:

```shell
$ docker build -t ld4p/sinopia_user_backup:latest .
$ docker push ld4p/sinopia_user_backup:latest
```
18 changes: 18 additions & 0 deletions __mocks__/cognito-fake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default class CognitoFake {
listUsers() {
return {
promise: async () => {
mjgiarlo marked this conversation as resolved.
Show resolved Hide resolved
return new Promise(resolve => {
return resolve(
{
Users: [
{ Username: 'user1' },
{ Username: 'user2' }
]
}
)
})
}
}
}
}
33 changes: 33 additions & 0 deletions __mocks__/cognito-pagination-fake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export default class CognitoPaginationFake {
constructor() {
this.calledAlready = false
mjgiarlo marked this conversation as resolved.
Show resolved Hide resolved
}

listUsers() {
return {
promise: async () => {
return new Promise(resolve => {
if (this.calledAlready) {
return resolve(
{
Users: [
{ Username: 'user2' }
]
}
)
} else {
this.calledAlready= true
return resolve(
{
Users: [
{ Username: 'user1' }
],
PaginationToken: 'token'
}
)
}
})
}
}
}
}
5 changes: 5 additions & 0 deletions __mocks__/s3-error-fake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class S3ErrorFake {
putObject(_, callback) {
callback('uh oh')
}
}
5 changes: 5 additions & 0 deletions __mocks__/s3-success-fake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class S3SuccessFake {
putObject(_, callback) {
callback()
}
}
66 changes: 66 additions & 0 deletions __tests__/CopyUsers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import AWS from 'aws-sdk'
import CopyUsers from '../src/CopyUsers'
import S3ErrorFake from '../__mocks__/s3-error-fake'
import S3SuccessFake from '../__mocks__/s3-success-fake'

describe('CopyUsers', () => {
const userList = [
{ Username: 'user1' },
{ Username: 'user2' },
]
const copier = new CopyUsers(userList)

describe('constructor()', () => {
it('sets userListString', () => {
expect(copier.userListString).toEqual("[{\"Username\":\"user1\"},{\"Username\":\"user2\"}]")
})
it('sets s3', () => {
expect(copier.s3).toBeInstanceOf(AWS.S3)
})
it('sets objectKey', () => {
// Matches: {ISO DATE}/{AWS_REGION}_{USER_POOL_ID}.json
expect(copier.objectKey).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\/my-region-1_uSeRPoOlId\.json/)
})
})

describe('copy()', () => {
const logSpy = jest.spyOn(console, 'log')
const errorSpy = jest.spyOn(console, 'error')

describe('when successful', () => {
beforeAll(() => {
copier.s3 = new S3SuccessFake()
})

it('calls putObject() on this.s3 and logs a non-error message', () => {
const s3Spy = jest.spyOn(copier.s3, 'putObject')
copier.copy()
expect(s3Spy).toHaveBeenCalledWith({
Body: "[{\"Username\":\"user1\"},{\"Username\":\"user2\"}]",
Bucket: 'my-bucket-name',
Key: expect.any(String)
mjgiarlo marked this conversation as resolved.
Show resolved Hide resolved
}, expect.any(Function))
expect(logSpy).toHaveBeenCalledWith('Users backed up to S3')
expect(errorSpy).not.toHaveBeenCalled()
})
})

describe('when it errors out', () => {
beforeAll(() => {
copier.s3 = new S3ErrorFake()
})

it('calls putObject() on this.s3 and logs an error', () => {
const s3Spy = jest.spyOn(copier.s3, 'putObject')
copier.copy()
expect(s3Spy).toHaveBeenCalledWith({
Body: "[{\"Username\":\"user1\"},{\"Username\":\"user2\"}]",
Bucket: 'my-bucket-name',
Key: expect.any(String)
}, expect.any(Function))
expect(logSpy).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalledWith('error copying backup to S3: uh oh')
})
})
})
})
51 changes: 51 additions & 0 deletions __tests__/ExportUsers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import AWS from 'aws-sdk'
import ExportUsers from '../src/ExportUsers'
import CognitoFake from '../__mocks__/cognito-fake'
import CognitoPaginationFake from '../__mocks__/cognito-pagination-fake'

describe('ExportUsers', () => {
const exporter = new ExportUsers()

describe('constructor()', () => {
it('sets cognito', () => {
expect(exporter.cognito).toBeInstanceOf(AWS.CognitoIdentityServiceProvider)
})
})

describe('export()', () => {
const logSpy = jest.spyOn(console, 'log')

describe('without pagination', () => {
beforeAll(() => {
exporter.cognito = new CognitoFake()
})

it('logs a message', async () => {
await exporter.export()
expect(logSpy).toHaveBeenCalledWith('Users exported from Cognito')
})

it('returns a list of users', async () => {
const userList = await exporter.export()
expect(userList).toEqual([{"Username":"user1"},{"Username":"user2"}])
})
})

describe('with pagination', () => {
// NOTE: Using beforeEach instead of beforeAll because the pagination fake stores the state of whether listUsers() has been called already
beforeEach(() => {
exporter.cognito = new CognitoPaginationFake()
})

it('logs a message', async () => {
await exporter.export()
expect(logSpy).toHaveBeenCalledWith('Users exported from Cognito')
})

it('returns a list of users', async () => {
const userList = await exporter.export()
expect(userList).toEqual([{"Username":"user1"},{"Username":"user2"}])
})
})
})
})
Loading