3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-29

第3章 Userサービス

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

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

reireias/microservice-sample-user

チュートリアル全体

構成

microservice-tutorial01.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という名前で作成しておきましょう。

# プロジェクト初期化(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
.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: {}
}
.prettierrc
{
  "semi": false,
  "singleQuote": true
}
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"
    }
  },
...
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, () => {})
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
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
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
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)
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)
}
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)
  })
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
}
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' })
})
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サービス

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?