KubernetesとNode.jsでマイクロサービスを作成する 3/6 Userサービス

# 第3章 Userサービス

本章ではNode.jsを利用してUserの管理とフォロー関係の管理を行うUserサービスを作成します。

なお、第2章と同様に、Kubernetesだけ触りたいという方は以下の完成済みのリポジトリをforkすることで、この章実装をスキップすることができます。

[reireias/microservice-sample-user](https://github.com/reireias/microservice-sample-user)

## チュートリアル全体
- [第1章 概要](https://qiita.com/reireias/items/6a78d6b8df781a76f94f)
- [第2章 Tweetサービス](https://qiita.com/reireias/items/82cfbb285130caf3e091)
- [第3章 Userサービス](https://qiita.com/reireias/items/27e21896b701360bc152)
- [第4章 Webサービス](https://qiita.com/reireias/items/204c080cd5ec1bf432fd)
- [第5章 Dockerを使ったサービス構築](https://qiita.com/reireias/items/efefe718d74ed8447931)
- [第6章 Kubernetes with minikube](https://qiita.com/reireias/items/084888abb3a757c0c5e2)

## 構成
![microservice-tutorial01.png](https://qiita-image-store.s3.amazonaws.com/0/214526/36201231-6030-9ac3-03ad-aaa99619eed2.png)

## システム構成
Userサービスのシステム構成は以下のようにします。
第2章のTweetサービスと同じ構成となっています。

| 要素                 | 採用技術 |
|----------------------|----------|
| 言語                 | Node.js  |
| フレームワーク       | express  |
| DB                   | MongoDB  |
| DB用ライブラリ       | mongoose |
| テストフレームワーク | ava      |

## REST API
Userサービスに必要なAPIについて設計していきます。

Userサービスに関係ありそうな操作は以下になります。

- ユーザー情報の取得
- ユーザー情報の作成(ログイン時)
- ユーザー情報の更新(ログイン時)
- ユーザー一覧の取得
- フォローしているユーザー一覧の取得
- フォロー
- フォロー解除

リソースは"ユーザー"と"フォロー"の2種類が必要そうです。
"ユーザー"と"フォロー"に対するCRUDを実装することにしましょう。
また、ログイン時のユーザーの作成と更新を1リクエストで実施できるAPIも提供することにしましょう。

上記より、REST APIを作成していきます。

| method | path                | description                 |
|--------|---------------------|-----------------------------|
| GET    | /users              | ユーザー一覧取得            |
| POST   | /users              | ユーザー作成                |
| POST   | /users/loginUser    | ログインユーザーの作成/更新 |
| GET    | /users/{id}         | ユーザー取得                |
| DELETE | /users/{id}         | ユーザー削除                |
| GET    | /users/{id}/follows | フォロー一覧取得            |
| POST   | /users/{id}/follows | フォロー                    |
| DELETE | /users/{id}/follows | アンフォロー                |

## DBスキーマ
続いてDBの保存するデータについて設計します。

REST API設計の際に、"ユーザー"リソースと"フォロー"リソースが登場しました。
MongoDBはjsonオブジェクト形式でレコードを保存できるため、1ユーザーに関するユーザー情報とフォロー情報を1レコードで保存することも可能です。
ですが、無限に増える項目はcollectionとして設計した方がよいため、ユーザーとフォローをそれぞれcollectionとして定義します。

ユーザーが持つ情報としては以下になります。

- 名前
- アバター画像のURL

フォローリソースが持つ情報は以下になります。

- フォローしているユーザーのID
- フォローされているユーザーのID

これらをMongoDBのスキーマに落とし込むと、それぞれ以下のようになります。

```
user document
{
  _id:       ObjectId,
  name:      String,
  avatorUrl: String
}

follow document
{
  _id:      ObjectId,
  userId:   ObjectId,
  followId: ObjectId
}
```

`ObjectId`型はMongoDBが生成するID型を表しています。

## 実装
では、実際にUserサービスを実装していきますが、本章の実装は第2章の実装とほぼ同じ内容なので、大部分を割愛し、最終的なコードのみ記載します。

リポジトリは`microservice-sample-user`という名前で作成しておきましょう。

```sh
# プロジェクト初期化(microservice-sample-userディレクトリ内)
yarn init
# 依存モジュール追加(devDependencies)
yarn add -D eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node prettier eslint-config-prettier eslint-plugin-prettier husky nodemon ava supertest@^3.4.2 mongodb-memory-server
# 依存モジュール追加
yarn add express morgan body-parser mongodb mongoose swagger-jsdoc
```

ディレクトリ構成

```
microservice-sample-user
├── LICENSE
├── app.js
├── controllers
│   ├── index.js
│   └── v1
│       ├── follows.js
│       └── users.js
├── models
│   ├── follow.js
│   └── user.js
├── package.json
├── scripts
│   └── initialize.js
├── swagger
│   ├── components.yml
│   ├── swagger.yml
│   └── swaggerDef.js
├── test
│   ├── follows.js
│   ├── helpers
│   │   └── generator.js
│   └── users.js
└── yarn.lock
```

各ファイルの実装

```:.gitignore
node_modules
yarn-error.log
```

```js:.eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {
    ecmaVersion: 2018
  },
  extends: [
    'standard',
    'plugin:prettier/recommended'
  ],
  plugins: [
    'prettier'
  ],
  // add your custom rules here
  rules: {}
}
```

```json:.prettierrc
{
  "semi": false,
  "singleQuote": true
}
```

```json:package.json
...
  "scripts": {
    "start": "node app.js",
    "dev": "NODE_ENV=development nodemon ./app.js",
    "lint": "eslint --ext .js --ignore-path .gitignore .",
    "swagger": "swagger-jsdoc -o ./swagger/swagger.yml -d ./swagger/swaggerDef.js ./controllers/**/*.js ./swagger/components.yml",
    "test": "ava",
    "watch": "ava --watch"
  },
  "husky": {
    "hooks": {
      "pre-commit": "yarn lint"
    }
  },
...
```

```js:app.js
const express = require('express')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const mongoose = require('mongoose')
const app = express()
app.use(morgan('short'))

// database
const dbUrl = process.env.MONGODB_URL || 'mongodb://localhost:27017/user'
const options = { useNewUrlParser: true }
if (process.env.MONGODB_ADMIN_NAME) {
  options.user = process.env.MONGODB_ADMIN_NAME
  options.pass = process.env.MONGODB_ADMIN_PASS
  options.auth = { authSource: 'admin' }
}
mongoose.connect(dbUrl, options)

// server
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use(require('./controllers'))

app.listen(process.env.PORT || 3000, () => {})
```

```js:controllers/index.js
const express = require('express')
const users = require('./v1/users.js')
const follows = require('./v1/follows.js')

const router = express.Router()

// swagger
if (process.env.NODE_ENV === 'development') {
  const swaggerJSDoc = require('swagger-jsdoc')
  const options = {
    swaggerDefinition: require('../swagger/swaggerDef.js'),
    apis: [
      './controllers/v1/users.js',
      './controllers/v1/follows.js',
      './swagger/components.yml'
    ]
  }
  const swaggerSpec = swaggerJSDoc(options)
  // CROS
  router.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*')
    res.header(
      'Access-Control-Allow-Methods',
      'GET, POST, DELETE, PUT, PATCH, OPTIONS'
    )
    res.header(
      'Access-Control-Allow-Headers',
      'Origin, X-Requested-With, Content-Type, Accept'
    )
    next()
  })
  router.get('/v1/swagger.json', (req, res) => {
    res.setHeader('Content-Type', 'application/json')
    res.send(swaggerSpec)
  })
}

router.use('/v1/users', users)
router.use('/v1/users/:id/follows', follows)

module.exports = router
```

```js:controllers/v1/users.js
const express = require('express')
const model = require('../../models/user.js')
const User = model.User
const router = express.Router()

/**
 * @swagger
 *
 * /users:
 *   get:
 *     description: Returns a list of users.
 *     tags:
 *       - users
 *     responses:
 *       '200':
 *         description: A JSON array of users.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Users'
 */
router.get('/', (req, res, next) => {
  ;(async () => {
    if (req.query.name) {
      const users = await User.find(
        { name: req.query.name },
        {},
        { sort: { name: 1 } }
      ).exec()
      res.status(200).json(users)
    } else {
      const users = await User.find().exec()
      res.status(200).json(users)
    }
  })().catch(next)
})

/**
 * @swagger
 *
 * /users/{id}:
 *   get:
 *     description: Find user by ID.
 *     tags:
 *       - users
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: User ID.
 *         schema:
 *           type: 'string'
 *         example: '000000000000000000000000'
 *     responses:
 *       '200':
 *         description: A JSON object of user.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/User'
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 *       '404':
 *         $ref: '#/components/responses/NotFound'
 */
router.get('/:id', (req, res, next) => {
  ;(async () => {
    try {
      const user = await User.findById(req.params.id).exec()
      if (user) {
        res.status(200).json(user)
      } else {
        res.status(404).json({ error: 'NotFound' })
      }
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

/**
 * @swagger
 *
 * /users:
 *   post:
 *     description: Create a user.
 *     tags:
 *       - users
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               name:
 *                 type: string
 *                 example: 'alice'
 *               avatarUrl:
 *                 type: string
 *                 example: 'https://1.bp.blogspot.com/-LFh4mfdjPSQ/VCIiwe10YhI/AAAAAAAAme0/J5m8xVexqqM/s800/animal_neko.png'
 *             required:
 *               - name
 *               - avatarUrl
 *     responses:
 *       '200':
 *         description: Created user.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/User'
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 */
router.post('/', (req, res, next) => {
  ;(async () => {
    try {
      const record = new User({
        name: req.body.name
      })
      const savedRecord = await record.save()
      res.status(200).json(savedRecord)
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

/**
 * @swagger
 *
 * /users/{id}:
 *   put:
 *     description: Update a user or create a user if user not exist.
 *     tags:
 *       - users
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: User ID.
 *         schema:
 *           type: 'string'
 *         example: '000000000000000000000000'
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               name:
 *                 type: string
 *                 example: 'alice'
 *               avatarUrl:
 *                 type: string
 *                 example: 'https://1.bp.blogspot.com/-LFh4mfdjPSQ/VCIiwe10YhI/AAAAAAAAme0/J5m8xVexqqM/s800/animal_neko.png'
 *             required:
 *               - name
 *               - avatarUrl
 *     responses:
 *       '200':
 *         description: Updated or created user.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/User'
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 *       '404':
 *         $ref: '#/components/responses/NotFound'
 */
router.put('/:id', (req, res, next) => {
  ;(async () => {
    try {
      // idにloginUserを指定された場合は、ログインユーザー情報の登録or更新を行う
      if (req.params.id === 'loginUser') {
        const record = await User.findOneAndUpdate(
          { name: req.body.name },
          {
            name: req.body.name,
            avatarUrl: req.body.avatarUrl
          },
          {
            new: true,
            upsert: true
          }
        ).exec()
        res.status(200).json(record)
      } else {
        const record = await User.findByIdAndUpdate(req.params.id, req.body, {
          new: true
        }).exec()
        if (record) {
          res.status(200).json(record)
        } else {
          res.status(404).json({ error: 'NotFound' })
        }
      }
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

/**
 * @swagger
 *
 * /users/{id}:
 *   delete:
 *     description: Delete a user.
 *     tags:
 *       - users
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: User ID.
 *         schema:
 *           type: 'string'
 *         example: '000000000000000000000000'
 *     responses:
 *       '200':
 *         description: Empty body.
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 *       '404':
 *         $ref: '#/components/responses/NotFound'
 */
router.delete('/:id', (req, res, next) => {
  ;(async () => {
    try {
      const removedRecord = await User.findByIdAndDelete(req.params.id).exec()
      if (removedRecord) {
        res.status(200).json({})
      } else {
        res.status(404).json({ error: 'NotFound' })
      }
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

module.exports = router
```

```js:controllers/v1/follows.js
const express = require('express')
const model = require('../../models/follow.js')
const User = require('../../models/user.js').User
const Follow = model.Follow
const router = express.Router({ mergeParams: true })

/**
 * @swagger
 *
 * /users/{id}/follows:
 *   get:
 *     description: Returns follow user list.
 *     tags:
 *       - follows
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: User ID.
 *         schema:
 *           type: 'string'
 *         example: '000000000000000000000000'
 *     responses:
 *       '200':
 *         description: A JSON array of users.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Users'
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 */
router.get('/', (req, res, next) => {
  ;(async () => {
    if (!model.validateObjectId(req.params.id)) {
      res.status(400).json({ error: 'BadRequest' })
      return
    }
    const records = await Follow.find({ userId: req.params.id }).exec()
    const followIds = records.map(follow => follow.followId)
    const follows = await User.find({ _id: { $in: followIds } }).exec()
    res.status(200).json(follows)
  })().catch(next)
})

/**
 * @swagger
 *
 * /users/{id}/follows:
 *   post:
 *     description: Follow a user.
 *     tags:
 *       - follows
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: User ID.
 *         schema:
 *           type: 'string'
 *         example: '000000000000000000000000'
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               followId:
 *                 type: string
 *                 example: '000000000000000000000001'
 *             required:
 *               - followId
 *     responses:
 *       '200':
 *         description: Empty body.
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 */
router.post('/', (req, res, next) => {
  ;(async () => {
    const follow = new Follow({
      userId: req.params.id,
      followId: req.body.followId
    })
    const error = follow.validateSync()
    if (error) {
      res.status(400).json({ error: 'BadRequest' })
      return
    }
    await follow.save()
    res.status(200).json({})
  })().catch(next)
})

/**
 * @swagger
 *
 * /users/{id}/follows:
 *   delete:
 *     description: Unfollow a user.
 *     tags:
 *       - follows
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: User ID.
 *         schema:
 *           type: 'string'
 *         example: '000000000000000000000000'
 *       - name: followId
 *         in: query
 *         required: true
 *         description: Unfollow target user ID.
 *         schema:
 *           type: 'string'
 *         example: '000000000000000000000001'
 *     responses:
 *       '200':
 *         description: Empty body.
 *       '404':
 *         $ref: '#/components/responses/NotFound'
 */
router.delete('/', (req, res, next) => {
  ;(async () => {
    const result = await Follow.findOneAndDelete({
      userId: req.params.id,
      followId: req.query.followId
    }).exec()
    if (result) {
      res.status(200).json({})
    } else {
      res.status(404).json({ error: 'NotFound' })
    }
  })().catch(next)
})

module.exports = router
```

```js:models/user.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const UserSchema = new Schema(
  {
    name: { type: String, required: true, unique: true },
    avatarUrl: { type: String }
  },
  {
    versionKey: false
  }
)

exports.User = mongoose.model('User', UserSchema)
```

```js:models/follow.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ObjectId = mongoose.Types.ObjectId

const FollowSchema = new Schema(
  {
    userId: { type: ObjectId, required: true },
    followId: { type: ObjectId, required: true }
  },
  {
    versionKey: false
  }
)
FollowSchema.index({ userId: 1, followId: 1 }, { unique: true })

exports.Follow = mongoose.model('Follow', FollowSchema)

exports.validateObjectId = idString => {
  return ObjectId.isValid(idString)
}
```

```js:scripts/initialize.js
const mongoose = require('mongoose')
const User = require('../models/user.js').User
const Follow = require('../models/follow.js').Follow

const dbUrl = process.env.MONGODB_URL || 'mongodb://localhost:27017/user'
const options = { useNewUrlParser: true, useCreateIndex: true }
if (process.env.MONGODB_ADMIN_NAME) {
  options.user = process.env.MONGODB_ADMIN_NAME
  options.pass = process.env.MONGODB_ADMIN_PASS
  options.auth = { authSource: 'admin' }
}

const ObjectId = mongoose.Types.ObjectId
const user1Id = new ObjectId('000000000000000000000000')
const user2Id = new ObjectId('000000000000000000000001')
const user3Id = new ObjectId('000000000000000000000002')
const users = [
  {
    _id: user1Id,
    name: 'alice',
    avatarUrl:
      'https://1.bp.blogspot.com/-LFh4mfdjPSQ/VCIiwe10YhI/AAAAAAAAme0/J5m8xVexqqM/s800/animal_neko.png'
  },
  {
    _id: user2Id,
    name: 'bob',
    avatarUrl:
      'https://4.bp.blogspot.com/-CtY5GzX0imo/VCIixcXx6PI/AAAAAAAAmfY/AzH9OmbuHZQ/s800/animal_penguin.png'
  },
  {
    _id: user3Id,
    name: 'carol',
    avatarUrl:
      'https://3.bp.blogspot.com/-n0PpkJL1BxE/VCIitXhWwpI/AAAAAAAAmfE/xLraJLXXrgk/s800/animal_hamster.png'
  }
]
const follows = [
  {
    userId: user1Id,
    followId: user2Id
  },
  {
    userId: user1Id,
    followId: user3Id
  },
  {
    userId: user2Id,
    followId: user1Id
  }
]

const initialize = async () => {
  mongoose.connect(dbUrl, options)

  await User.deleteMany().exec()
  await Follow.deleteMany().exec()

  await User.insertMany(users)
  await Follow.insertMany(follows)
  mongoose.disconnect()
}

initialize()
  .then(() => {
    // eslint-disable-next-line no-console
    console.log('finish.')
  })
  .catch(error => {
    console.error(error)
  })
```

```js:test/helpers/generator.js
const mongoose = require('mongoose')

const User = require('../../models/user.js').User
const Follow = require('../../models/follow.js').Follow
const ObjectId = mongoose.Types.ObjectId

const user1Id = new ObjectId('000000000000000000000000')
const user2Id = new ObjectId('000000000000000000000001')
const user3Id = new ObjectId('000000000000000000000002')

const users = [
  {
    _id: user1Id,
    name: 'alice',
    avatarUrl: 'https://example.com/dummy1'
  },
  {
    _id: user2Id,
    name: 'bob',
    avatarUrl: 'https://example.com/dummy2'
  },
  {
    _id: user3Id,
    name: 'carol',
    avatarUrl: 'https://example.com/dummy3'
  }
]
const follows = [
  {
    userId: user1Id,
    followId: user2Id
  },
  {
    userId: user1Id,
    followId: user3Id
  },
  {
    userId: user2Id,
    followId: user1Id
  }
]

module.exports = {
  createData: async () => {
    await User.insertMany(users)
    await Follow.insertMany(follows)
  },

  deleteData: async () => {
    await User.deleteMany().exec()
    await Follow.deleteMany().exec()
  },
  users: users
}
```

```js:test/users.js
const test = require('ava')
const supertest = require('supertest')
const mongoose = require('mongoose')
const express = require('express')
const bodyParser = require('body-parser')
const { MongoMemoryServer } = require('mongodb-memory-server')

console.error = () => {}
const generator = require('./helpers/generator.js')
const router = require('../controllers/v1/users.js')
const model = require('../models/user.js')
const User = model.User
const users = generator.users

const mongod = new MongoMemoryServer()
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use('/users', router)

test.before(async () => {
  const uri = await mongod.getConnectionString()
  mongoose.connect(uri, { useNewUrlParser: true })
})

test.beforeEach(async () => {
  await generator.createData()
})

test.afterEach.always(async () => {
  await generator.deleteData()
})

// GET /users
test.serial('get users', async t => {
  const res = await supertest(app).get('/users')
  t.is(res.status, 200)
  t.is(res.body.length, 3)
})

// GET /users/:id
test.serial('get user', async t => {
  const userId = users[0]._id.toString()
  const res = await supertest(app).get(`/users/${userId}`)
  t.is(res.status, 200)
  t.is(res.body._id, userId)
  t.is(res.body.name, users[0].name)
})

test.serial('get user not found', async t => {
  const res = await supertest(app).get(
    `/users/${new mongoose.Types.ObjectId()}`
  )
  t.is(res.status, 404)
  t.deepEqual(res.body, { error: 'NotFound' })
})

test.serial('get user id is invalid', async t => {
  const res = await supertest(app).get('/users/invalid')
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

// POST /users
test.serial('create user', async t => {
  const name = 'dave'
  const res = await supertest(app)
    .post('/users')
    .send({ name: name })
  t.is(res.status, 200)
  t.true('_id' in res.body)
  t.is(res.body.name, name)
})

test.serial('create user name is empty', async t => {
  const res = await supertest(app)
    .post('/users')
    .send({})
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

// PUT /users/:id
test.serial('update user', async t => {
  const name = 'new name'
  const res = await supertest(app)
    .put(`/users/${users[0]._id}`)
    .send({ name: name })
  t.is(res.status, 200)
  t.is(res.body.name, name)
})

test.serial('put user not found', async t => {
  const res = await supertest(app).put(
    `/users/${new mongoose.Types.ObjectId()}`
  )
  t.is(res.status, 404)
  t.deepEqual(res.body, { error: 'NotFound' })
})

test.serial('put user id is invalid', async t => {
  const res = await supertest(app).put('/users/invalid')
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

// DELETE /users/:id
test.serial('delete user', async t => {
  const res = await supertest(app).delete(`/users/${users[0]._id}`)
  t.is(res.status, 200)
  const actual = await User.find()
  t.is(actual.length, 2)
})

test.serial('delete user not found', async t => {
  const res = await supertest(app).delete(
    `/users/${new mongoose.Types.ObjectId()}`
  )
  t.is(res.status, 404)
  t.deepEqual(res.body, { error: 'NotFound' })
})

test.serial('delete user id is invalid', async t => {
  const res = await supertest(app).delete('/users/invalid')
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})
```

```js:test/follows.js
const test = require('ava')
const supertest = require('supertest')
const mongoose = require('mongoose')
const express = require('express')
const bodyParser = require('body-parser')
const { MongoMemoryServer } = require('mongodb-memory-server')

console.error = () => {}
const generator = require('./helpers/generator.js')
const router = require('../controllers/v1/follows.js')
const model = require('../models/follow.js')
const Follow = model.Follow

const user1Id = generator.users[0]._id.toString()
const user2Id = generator.users[1]._id.toString()
const user3Id = generator.users[2]._id.toString()

const mongod = new MongoMemoryServer()
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use('/users/:id/follows', router)

test.before(async () => {
  const uri = await mongod.getConnectionString()
  mongoose.connect(uri, { useNewUrlParser: true, useCreateIndex: true })
})

test.beforeEach(async () => {
  await generator.createData()
})

test.afterEach.always(async () => {
  await generator.deleteData()
})

// GET /users/:id/follows
test.serial('get follows', async t => {
  const res = await supertest(app).get(`/users/${user1Id}/follows`)
  t.is(res.status, 200)
  t.is(res.body.length, 2)
})

test.serial('get follows not found', async t => {
  const res = await supertest(app).get(
    `/users/999999999999999999999999/follows`
  )
  t.is(res.status, 200)
  t.is(res.body.length, 0)
})

test.serial('get follows invalid id', async t => {
  const res = await supertest(app).get(`/users/invalid/follows`)
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

// POST /users/:id/follows
test.serial('create follow', async t => {
  const res = await supertest(app)
    .post(`/users/${user2Id}/follows`)
    .send({ followId: user3Id })
  t.is(res.status, 200)
  const follows = await Follow.find({ userId: user2Id }).exec()
  t.is(follows.length, 2)
})

test.serial('create follow no followId', async t => {
  const res = await supertest(app)
    .post(`/users/${user2Id}/follows`)
    .send({})
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

// DELETE /users/:id/follows
test.serial('delete follow', async t => {
  const res = await supertest(app)
    .delete(`/users/${user1Id}/follows`)
    .query({ followId: user2Id })
  t.is(res.status, 200)
  const follows = await Follow.find({ userId: user1Id }).exec()
  t.is(follows.length, 1)
})

test.serial('delete follow not found', async t => {
  const res = await supertest(app)
    .delete(`/users/${user3Id}/follows`)
    .query({ followId: user2Id })
  t.is(res.status, 404)
  t.deepEqual(res.body, { error: 'NotFound' })
})
```

## 第3章まとめ
第3章では第2章同様にNode.js + MongoDBを利用してUserサービスを構築しました。

次の章では第2章のTweetサービス、第3章のUserサービスを利用してWebUIを提供するWebサービスを構築していきます。

次章: [第4章 Webサービス](https://qiita.com/reireias/items/204c080cd5ec1bf432fd)