serverless × node.js × DynamoDB チートシート〜serverless.yml 定義例、DynamoDB Client 使用例、テスト記述例 〜

こんにちは最近、 serverless ✕ DynamoDB ✕ node.js の組み合わせでのサービス開発を
自分の中のメイン方式にするべく習得中です。

2週間くらい経つと学んだことを忘れてしまうので、
重宝した参考記事と書いた設定/コードを備忘のため、チートシートとしてまとめます。

# 本記事で書くこと

- ①serverless.yml でのDynamoDBテーブル定義例
- ②node.jsのAWS SDK「DynamoDB Client」の使用例
- ③node.js ローカル環境でのテスト記述例

# 前提: 題材としたシステムについて

家族とシェアしている Google Photos の写真をLINE Botで検索、
懐かしの思い出の写真を自動投稿　する、ほのぼの系Botを作っています。

Room/Album/Photoの3つのテーブルを例に説明します。

## テーブル：「Room」 BOTとの会話部屋情報を管理

- PrimaryKey は `roomId`
- SortKey はなしです。

```room.json
  {
  	"roomId": "room1",
  	"albumKeyword": "keyword1",
  	"roomType": "user",
  	"users": [{
  		"userId": "user1",
  		"googleRefreshToken": "token1"
  	}]
  }
```


## テーブル：「Album」 写真アルバム情報を管理

- PrimaryKey は `roomId`
- SortKey は　`albumId`

```album.json
{
	"roomId": "room1",
	"albumId": "album1",
	"albumTitle": "AlbumName1 keyword1",
	"numPhotos": 3,
	"status": "WAITING_UPDATE"
}
```

## テーブル：「Photo」 写真情報を管理

- PrimaryKey は　`roomId`
- SortKey　が　`albumIKey` (albumId　と　photoId　の複合キー)
 - albumIDをキーに写真検索したかったので。アルバム削除時に写真も削除したいため。
- LocalSecondaryIndexに　`date` を指定
 - 日付で写真検索したかったので。

```photo.json
{
    "roomId": "room1",
    "albumIdKey": "album1-photo1",
    "date": "20170201",
    "timestamp": "1485955237000",
    "type": "image/jpeg",
    "url": "https://lh3.googleusercontent.com/xxx/xxx/xxx/a1-p1.JPG"
}
```


# ①serverless.yml でのDynamoDBテーブル定義例

## 参考記事

以下の記事にお世話になりました。

- [AWS+Serverless+DynamoDB+APIGateway+Lambdaでアプリ用サーバーAPIを簡単？に作成する](https://qiita.com/takaoka_hide/items/2ce95c22778353ab1ece)
- [Serverless アプリケーションをローカルで開発する](https://qiita.com/noralife/items/e36621ddd0e5b8ff4447)
- [gistに置かれていたserverless.ymlの定義例](https://gist.github.com/DavidWells/c7df5df9c3e5039ee8c7c888aece2dd5)

## 書いた設定ファイル

```serverless.yml
# ...略...
custom:
  defaultStage: dev
  tableNameRoom: '${self:service}-Room-${self:provider.stage}'
  tableNameAlbum: '${self:service}-Album-${self:provider.stage}'
  tableNamePhoto: '${self:service}-Photo-${self:provider.stage}'
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migrate: true
      seed: true
    seed:
      development:
        sources:
          - table: ${self:custom.tableNameRoom}
            sources: [./migrations/rooms.json]
          - table: ${self:custom.tableNameAlbum}
            sources: [./migrations/albums.json]
          - table: ${self:custom.tableNamePhoto}
            sources: [./migrations/photos.json]

resources:
  Resources:
    RoomTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: ${self:custom.tableNameRoom}
        AttributeDefinitions:
          - AttributeName: roomId
            AttributeType: S
        KeySchema:
          - AttributeName: roomId
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
    AlbumTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: ${self:custom.tableNameAlbum}
        AttributeDefinitions:
          - AttributeName: roomId
            AttributeType: S
          - AttributeName: albumId
            AttributeType: S
        KeySchema:
          - AttributeName: roomId
            KeyType: HASH
          - AttributeName: albumId
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
    PhotoTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: ${self:custom.tableNamePhoto}
        AttributeDefinitions:
          - AttributeName: roomId
            AttributeType: S
          - AttributeName: albumIdKey
            AttributeType: S
          - AttributeName: date
            AttributeType: S
        KeySchema:
          - AttributeName: roomId
            KeyType: HASH
          - AttributeName: albumIdKey
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        LocalSecondaryIndexes:
          - IndexName: dateIndex
            KeySchema:
              - AttributeName: roomId
                KeyType: HASH
              - AttributeName: date
                KeyType: RANGE
            Projection:
              ProjectionType: ALL
```

# ②node.jsのAWS SDK「DynamoDB Client」の使い方

## 参考記事

- [【詳解】JavascriptでDynamoDBを操作する](https://qiita.com/Fujimon_fn/items/66be7b807a8329496899)
  - ものすごく具体的かつわかりやすくまとまっており、大変参考になりました。
- [AWS公式リファレンス - Class: AWS.DynamoDB.DocumentClient](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html)

## 書いたコード (table毎のDynamoDB操作をモジュール化)

```dynamodb.js
'use strict';

const AWS = require('aws-sdk');


class DynamoDBClient {

    constructor(options) {
        this.docClient = new AWS.DynamoDB.DocumentClient(options);
        this.tableNameRoom = options.tableNameRoom;
        this.tableNameAlbum = options.tableNameAlbum;
        this.tableNamePhoto = options.tableNamePhoto;
    }


    async scanRoom() {
        const params = {
            TableName: this.tableNameRoom
        };
        return this.docClient.scan(params).promise();
    }


    async getRoomItem(roomId) {
        const params = {
            TableName: this.tableNameRoom,
            Key: {
                roomId: roomId
            }
        };
        return this.docClient.get(params).promise();
    }


    async getAlbums(roomId) {
        const params = {
            TableName: this.tableNameAlbum,
            KeyConditionExpression: '#pk = :pk_value',
            ExpressionAttributeNames: {
                '#pk': 'roomId',
            },
            ExpressionAttributeValues: {
                ':pk_value': roomId,
            }
        };
        return this.docClient.query(params).promise();
    }


    async getAlbumItem(roomId, albumId) {
        const params = {
            TableName: this.tableNameAlbum,
            Key: {
                roomId: roomId,
                albumId: albumId
            }
        };
        return this.docClient.get(params).promise();
    }


    async getPhotosByAlbumId(roomId, albumId) {
        const params = {
            TableName: this.tableNamePhoto,
            KeyConditionExpression: '#pk = :pk_value AND begins_with(#sk, :sk_value)',
            ExpressionAttributeNames: {
                '#pk': 'roomId',
                '#sk': 'albumIdKey'
            },
            ExpressionAttributeValues: {
                ':pk_value': roomId,
                ':sk_value': albumId
            }
        };
        return this.docClient.query(params).promise();
    }


    async getPhotosByDate(roomId, dateStr) {
        const params = {
            TableName: this.tableNamePhoto,
            IndexName: 'dateIndex',
            KeyConditionExpression: '#pk = :pk_value AND begins_with(#sk, :sk_value)',
            ExpressionAttributeNames: {
                '#pk': 'roomId',
                '#sk': 'date'
            },
            ExpressionAttributeValues: {
                ':pk_value': roomId,
                ':sk_value': dateStr
            }
        };
        return this.docClient.query(params).promise();
    };


    async putRoomItem(room) {
        const params = {
            TableName: this.tableNameRoom,
            Item: room
        };
        return this.docClient.put(params).promise();
    }


    async updateRoomItemUsers(roomId, users) {
        const params = {
            TableName: this.tableNameRoom,
            Key: {
                roomId: roomId
            },
            ExpressionAttributeNames: {
                '#users': 'users'
            },
            ExpressionAttributeValues: {
                ':newUsers': users
            },
            UpdateExpression: 'SET #users = :newUsers'
        };
        return this.docClient.update(params).promise();
    }


    async putAlbumItem(album) {
        const params = {
            TableName: this.tableNameAlbum,
            Item: album
        };
        return this.docClient.put(params).promise();
    }


    async putPhotoItem(photo) {
        const params = {
            TableName: this.tableNamePhoto,
            Item: photo
        };
        return this.docClient.put(params).promise();
    }


    async deleteRoomItem(roomId) {
        const params = {
            TableName: this.tableNameRoom,
            Key: {
                roomId: roomId
            },
        };
        return this.docClient.delete(params).promise();
    }


    async deleteAlbumItem(roomId, albumId) {
        const params = {
            TableName: this.tableNameAlbum,
            Key: {
                roomId: roomId,
                albumId: albumId
            }
        };
        return this.docClient.delete(params).promise();
    }


    async deletePhotoItem(roomId, albumIdKey) {
        const params = {
            TableName: this.tableNamePhoto,
            Key: {
                roomId: roomId,
                albumIdKey: albumIdKey
            }
        };
        return this.docClient.delete(params).promise();
    }
}

module.exports = DynamoDBClient;
```

# ③node.js ローカル環境でのテストの書き方

## 参考記事

- [AWS Lambdaのユニットテストのベストプラクティス(Node.js)](https://qiita.com/horike37/items/034f731f1b40c5a28e74)
- [Node.jsでmochaやproxyquireを使った単体テストをやってみたメモ](https://qiita.com/toshihirock/items/67e51303318d5a6030cd)

## 書いたコード (Dynamo.jsに対するテストコード)

```dynamodb-test.js
'use strict';

const _ = require('underscore');
const chai = require('chai');
const assert = chai.assert;

const TABLE_NAME_ROOM = 'YOUR_SERVICE_NAME-Room-dev';
const TABLE_NAME_ALBUM = 'YOUR_SERVICE_NAME-Album-dev';
const TABLE_NAME_PHOTO = 'YOUR_SERVICE_NAME-Photo-dev';

const DynamoClient = require('./../../lib/dynamodb');
const client = new DynamoClient({
    region: 'localhost',
    endpoint: 'http://localhost:8000',
    tableNameRoom: TABLE_NAME_ROOM,
    tableNameAlbum: TABLE_NAME_ALBUM,
    tableNamePhoto: TABLE_NAME_PHOTO
});


describe('check DynamoDB Client using local DynamoDB service', () => {


    it('should get all room items from local DynamoDB by scanRoom', async() => {
        const result = await client.scanRoom();
        assert.equal(result.Count, 2);

        const rooms = require('./../../migrations/rooms.json');
        const roomIds = rooms.map(room => room.roomId);
        const albumKeywords = rooms.map(room => room.albumKeyword);
        for (const item of result.Items) {
            assert.include(roomIds, item.roomId);
            assert.include(albumKeywords, item.albumKeyword);
        }
    });


    it('should get a room item from local DynamoDB by getRoomItem', async() => {
        const result1 = await client.getRoomItem('room1');
        assert.equal(result1.Item.albumKeyword, 'keyword1');
        assert.equal(result1.Item.roomType, 'user');
        assert.lengthOf(result1.Item.users, 1);

        const result2 = await client.getRoomItem('room2');
        assert.equal(result2.Item.albumKeyword, 'keyword2');
        assert.equal(result2.Item.roomType, 'room');
        assert.lengthOf(result2.Item.users, 3);

        // Pattern of No result.
        const result3 = await client.getRoomItem('room3');
        assert.deepStrictEqual(result3, {});
    });


    it('should get album items associated roomId from local DynamoDB by getAlbums', async() => {
        const result1 = await client.getAlbums('room1');
        assert.equal(result1.Count, 2);
        const albumIds = result1.Items.map(item => item.albumId);
        assert.include(albumIds, 'album1');
        assert.include(albumIds, 'album2');

        const result2 = await client.getAlbums('room2');
        assert.equal(result2.Count, 1);
        assert.equal(result2.Items[0].albumId, 'album2');

        // Pattern of No result.
        const result3 = await client.getAlbums('room3');
        assert.equal(result3.Count, 0);
    });


    it('should get a album item from local DynamoDB by getAlbumItem', async() => {
        const result1 = await client.getAlbumItem('room1', 'album1');
        assert.equal(result1.Item.numPhotos, 3);
        assert.equal(result1.Item.albumTitle, 'AlbumName1 keyword1');
        assert.equal(result1.Item.status, 'WAITING_UPDATE');

        const result2 = await client.getAlbumItem('room1', 'album2');
        assert.equal(result2.Item.numPhotos, 2);
        assert.equal(result2.Item.albumTitle, 'AlbumName2 keyword2');
        assert.equal(result2.Item.status, 'UPDATED');

        const result3 = await client.getAlbumItem('room2', 'album2');
        assert.equal(result3.Item.roomId, 'room2');
        assert.equal(result3.Item.albumId, 'album2');

        // Pattern of No result.
        const result4 = await client.getAlbumItem('room2', 'album1');
        assert.deepStrictEqual(result4, {});
    });

    it('should get photos from local DynamoDB by getPhotosByAlbumId', async() => {
        const result1 = await client.getPhotosByAlbumId('room1', 'album1');
        assert.equal(result1.Count, 3);
        const result1AlbumIdKeys = result1.Items.map(item => item.albumIdKey);
        assert.include(result1AlbumIdKeys, 'album1-photo1');
        assert.include(result1AlbumIdKeys, 'album1-photo2');
        assert.include(result1AlbumIdKeys, 'album1-photo3');

        const result2 = await client.getPhotosByAlbumId('room1', 'album2');
        assert.equal(result2.Count, 2);
        const result2AlbumIdKeys = result2.Items.map(item => item.albumIdKey);
        assert.include(result2AlbumIdKeys, 'album2-photo1');
        assert.include(result2AlbumIdKeys, 'album2-photo2');

        const result3 = await client.getPhotosByAlbumId('room2', 'album2');
        assert.equal(result3.Count, 1);
        assert.equal(result3.Items[0].albumIdKey, 'album2-photo1');
        assert.equal(result3.Items[0].url, 'https://lh3.googleusercontent.com/xxx/xxx/xxx/a2-p1.JPG');

        // Pattern of No result.
        const result4 = await client.getPhotosByAlbumId('room1', 'album3');
        assert.equal(result4.Count, 0);
    });


    it('should get photos from local DynamoDB by getPhotosByDate', async() => {
        const result1 = await client.getPhotosByDate('room1', '20170201');
        assert.equal(result1.Count, 1);
        assert.equal(result1.Items[0].albumIdKey, 'album1-photo1');

        const result2 = await client.getPhotosByDate('room1', '20170205');
        assert.equal(result2.Count, 3);
        const albumIdKeys = result2.Items.map(item => item.albumIdKey);
        assert.include(albumIdKeys, 'album1-photo3');
        assert.include(albumIdKeys, 'album2-photo1');
        assert.include(albumIdKeys, 'album2-photo2');

        const result3 = await client.getPhotosByDate('room2', '20170205');
        assert.equal(result3.Count, 1);
        assert.equal(result3.Items[0].albumIdKey, 'album2-photo1');

        // Pattern of No result.
        const result4 = await client.getPhotosByDate('room1', '20170202');
        assert.equal(result4.Count, 0);
    });


    it('should put a room item to local DynamoDB by putRoomItem', async() => {
        // new item
        const data = {
            roomId: 'newRoom1',
            albumKeyword: 'newKeyword1',
            roomType: 'room',
            users: [
                {userId: 'user1', googleRefreshToken: 'token1'}
            ]
        };
        // refresh the table
        await client.deleteRoomItem(data.roomId);

        // confirm empty result before put the item
        const emptyResult = await client.getRoomItem(data.roomId);
        assert.deepStrictEqual(emptyResult, {});

        // put the new item
        await client.putRoomItem(data);
        const afterPutResult = await client.getRoomItem(data.roomId);
        assert.deepStrictEqual(afterPutResult.Item, data);

        // update the item of users
        const newUsers = data.users;
        newUsers.push({userId: 'new-user1'});
        await client.updateRoomItemUsers(data.roomId, newUsers);
        const afterUpdateResult2 = await client.getRoomItem(data.roomId);
        assert.deepStrictEqual(afterUpdateResult2.Item.users, newUsers);

        // delete the item for re-test
        await client.deleteRoomItem(data.roomId);
    });


    it('should put a room items to local DynamoDB by putAlbumItem', async() => {
        // new item
        const dataRoomId = 'newRoom1';
        const dataAlbumId = 'newAlbum1';
        const data = {
            roomId: dataRoomId,
            albumId: dataAlbumId,
            albumTitle: 'AlbumName1 newKeyword1',
            numPhotos: 1,
            status: 'WAITING_UPDATE'
        };

        // refresh the table
        await client.deleteAlbumItem(dataRoomId, dataAlbumId);

        // confirm empty result before put the item
        const emptyResult = await client.getAlbumItem(dataRoomId, dataAlbumId);
        assert.deepStrictEqual(emptyResult, {});

        // put the new item
        await client.putAlbumItem(data);
        const afterPutResult = await client.getAlbumItem(dataRoomId, dataAlbumId);
        assert.deepStrictEqual(afterPutResult.Item, data);

        // update the item
        data.status = 'UPDATED';
        await client.putAlbumItem(data);
        const afterUpdateResult = await client.getAlbumItem(dataRoomId, dataAlbumId);
        assert.deepStrictEqual(afterUpdateResult.Item, data);

        // delete the item for re-test
        await client.deleteAlbumItem(dataRoomId, dataAlbumId);
    });


    it('should put a photo items to local DynamoDB by putPhotoItem', async() => {
        // new item
        const dataRoomId = 'newRoom1';
        const dataAlbumId = 'newAlbum1';
        const dataPhotoId = 'newPhoto1';
        const dataAlbumIdKey = `${dataAlbumId}-${dataPhotoId}`;

        const data = {
            roomId: dataRoomId,
            albumIdKey: dataAlbumIdKey,
            date: '20170203',
            timestamp: '1486076126000',
            type: 'image/jpeg',
            url: `https://lh3.googleusercontent.com/xxx/xxx/xxx/${dataAlbumIdKey}.JPG`
        };

        // refresh the table
        await client.deletePhotoItem(dataRoomId, dataAlbumIdKey);

        // confirm empty result before put the item
        const emptyResult = await client.getPhotosByAlbumId(dataRoomId, dataAlbumIdKey);
        assert.equal(emptyResult.Count, 0);

        // put the new item
        await client.putPhotoItem(data);
        const afterPutResult = await client.getPhotosByAlbumId(dataRoomId, dataAlbumIdKey);
        assert.equal(afterPutResult.Count, 1);
        assert.deepStrictEqual(afterPutResult.Items[0], data);

        // delete the item for re-test
        await client.deletePhotoItem(dataRoomId, dataAlbumIdKey);
    });

});
```

## その他テストコード　（SQS)

### 実行コード (SQS)

```sqs.js
'use strict';

const _ = require('underscore');
const AWS = require('aws-sdk');
const dateUtil = require('./date-util.js');


const MAX_NUMBER_OF_RECEIVE_MESSAGES = 10;
const RECEIVE_VISIBILITY_TIMEOUT = 10;


const newMessageTemplate = (roomId, photo) => {
    return {
        messageType: 'UPDATE',
        roomId: roomId,
        albumIdKey: `${photo.album_id}-${photo.id}`,
        date: dateUtil.getDay(parseInt(photo.timestamp)),
        timestamp: photo.timestamp,
        type: photo.content.type,
        url: photo.content.src
    };
};


const deleteMessageTemplate = (roomId, photo) => {
    return {
        messageType: 'DELETE',
        roomId: roomId,
        albumIdKey: photo.albumIdKey,
    };
};


const completeMessageTemplate = (roomId, albumId) => {
    return {
        messageType: 'COMPLETE',
        roomId: roomId,
        albumId: albumId,
    };
};


class SQSClient {

    constructor(options) {
        this.sqs = new AWS.SQS(options);
        this.defaultParams = {
            QueueUrl: options.QueueUrl
        };
    }

    // PRIVATE METHOD
    enqueue(message) {
        const params = _.extend({MessageBody: JSON.stringify(message)}, this.defaultParams);
        return this.sqs.sendMessage(params).promise();
    }


    enqueueNewMessage(roomId, photo) {
        return this.enqueue(newMessageTemplate(roomId, photo));
    }


    enqueueDeleteMessage(roomId, photo) {
        return this.enqueue(deleteMessageTemplate(roomId, photo));
    }


    enqueueCompleteMessage(roomId, albumId) {
        return this.enqueue(completeMessageTemplate(roomId, albumId));
    }


    // PRIVATE METHOD
    async receiveMessageHandler(message, handler) {
        await handler(message);
        const params = _.extend({ReceiptHandle: message.ReceiptHandle}, this.defaultParams);
        return this.sqs.deleteMessage(params).promise();
    }


    async receiveMessages(handler) {
        const params = _.extend({
            MaxNumberOfMessages: MAX_NUMBER_OF_RECEIVE_MESSAGES,
            VisibilityTimeout: RECEIVE_VISIBILITY_TIMEOUT
        }, this.defaultParams);
        const response = await this.sqs.receiveMessage(params).promise();
        if (!response.Messages) return;
        return Promise.all(response.Messages.map(message => this.receiveMessageHandler(message, handler)));
    }

}

module.exports = SQSClient;
```

### テストコード (SQS)

```sqs-test.js
'use strict';

const _ = require('underscore');
const chai = require('chai');
const assert = chai.assert;
const proxyquire = require('proxyquire');


// DUMMY RESPONSE of AWS.SQS.sendMessage
const dummyResponseSendMessage = {
    ResponseMetadata: {RequestId: 'e41cc172-b12c-51b1-851f-9218a2a3ec24'},
    MD5OfMessageBody: 'e3c27b4365ce1fb9f86d32e64102a304',
    MessageId: '2a669cd3-983d-44d0-9761-d60fd3ba8fa5'
};


// DUMMY RESPONSE of AWS.SQS.receiveMessage
const dummyResponseReceiveMessage = {
    ResponseMetadata: {RequestId: '74f00447-e65c-5b57-aab5-572df5c2a200'},
    Messages: [
        {
            MessageId: '2a669cd3-983d-44d0-9761-d60fd3ba8fa5',
            ReceiptHandle: 'AQEBQLUDuMah/rsKknwzxP9gEOv+sc8I9dC57v2QcgNZ/N0qSHlcyLw+W7H5gSF+4FtJ6KAv1w1wp7/Aoe48/vPGRDoORkS4JOMzJEqC2Ql+jh8SmKfpxAw8hNQmOEcLJpDKp14i3thXyKT0Ad9uQHmg7T06zDbl0I9LqIja9D52wqWXKElEPruL5ZVArxT46QmXk0u6bsPQePHXCculvba0Gy7MW0xEXMy5yIWXDZL5CYmLZjQ5pQvuyVohp0L0kFw/152eKemu3vPENnGYB4kS3rC8heu6/QMn3+ODqreQHcO7C7w3ciiSvACUKLnePeJJwY3XUXKBMnf+OnuJRUrXOnTtpsuShqALYgGOoR/sYBWN/aqLJsT0syFhIMByPLXXm9X5St4m/WshwmkgqFZdD+kPAQTVdpIXm3xmenDjjKU=',
            MD5OfBody: 'e3c27b4365ce1fb9f86d32e64102a304',
            Body: '{"messageType":"DELETE","roomId":"room-1","albumKey":"album-1-photo-1"}'
        },
        {
            MessageId: 'fe1b2e28-9200-48b2-b2ff-5a82be53c359',
            ReceiptHandle: 'AQEBwakNhLL9MPSsSml32FtjNUrG6aQeVozaWSRQYeH/pUT/+FG2L6DovCVwC+vMps6OROoDwLWIVTXaMMK0prqpJtm+NzLXaN+qvch9+juaEeHfcvsuI+xU4NRylgfd6fb5tsDKT0t2ThRLB8Z01+8GbsHjIX6BEs3nfR/p+4upt3j0KTWJKZVfh88/G/DaofLtHGNnlq3A2dDyFkXZ4/JjIQf5C/ZFzBqzUtFxX6Lx/AMLMDXFazbd6uI2kVmUsyeS4WIxx7vDYH7C2iZxUAH36B6QvfSqtWTzVNVzIKSHZxMpbXrWzp6CkJWuf53XNJ50JZf0bKIkHum3Tq0S5gsWnZQ6Sub7JHRkdU2OWzHar7Ebdwhws8aKBzQ6ENgT981HYLwd95M0yyP64Z55l06GtTBkSaGIZZekvN15hpqiw8g=',
            MD5OfBody: 'd822bfa70dba0a6029f3a156479f7bf8',
            Body: '{"messageType":"COMPLETE","roomId":"room-1","albumId":"album-1"}'
        }]
};


// DUMMY RESPONSE of AWS.SQS.deleteMessage
const dummyResponseDeleteMessage = {
    ResponseMetadata: {RequestId: 'c3824af3-cf44-5dc7-bc7f-1999dcda3f3d'}
};

// AWS.SQS Stub for proxyquire
const awsStub = {
    SQS: class {
        constructor(options) {
        }

        sendMessage(params) {
            return {
                promise: async() => {
                    return dummyResponseSendMessage
                }
            };
        }

        receiveMessage(params) {
            return {
                promise: async() => {
                    return dummyResponseReceiveMessage
                }
            };
        }

        deleteMessage(params) {
            return {
                promise: async() => {
                    return dummyResponseDeleteMessage
                }
            };
        }
    }
};


const SQS = proxyquire('./../../lib/sqs', {
    'aws-sdk': awsStub
});
const sqsClient = new SQS({QueueUrl: 'http://hogehoge.com'});


describe('check SQS Client using proxyquire', () => {
        const roomId = 'room-1';
        const photo = {
            timestamp: '1525680256000',
            id: 'photo-1',
            album_id: 'album-1',
            content: {
                type: 'photo-type',
                url: 'photo-url'
            }
        };

        it('should call enqueueNewMessage', async() => {
            const res = await sqsClient.enqueueNewMessage(roomId, photo);
            assert.deepEqual(dummyResponseSendMessage, res);
        });

        it('should call enqueueDeleteMessage', async() => {
            const res = await sqsClient.enqueueDeleteMessage(roomId, photo);
            assert.deepEqual(dummyResponseSendMessage, res);
        });

        it('should call enqueueCompleteMessage', async() => {
            const res = await sqsClient.enqueueCompleteMessage(roomId, photo.album_id);
            assert.deepEqual(dummyResponseSendMessage, res);
        });

        it('should call receiveMessages', async() => {
            const messageHandler = message => {
                assert.include(['2a669cd3-983d-44d0-9761-d60fd3ba8fa5', 'fe1b2e28-9200-48b2-b2ff-5a82be53c359'], message.MessageId);
            };
            const res = await sqsClient.receiveMessages(messageHandler);
            assert.deepEqual(res, [dummyResponseDeleteMessage, dummyResponseDeleteMessage]);
        });
    }
);
```