Просмотр исходного кода

Merge branch 'reactify-admin/CustomizePage' into create-function-settings-frontside

itizawa 6 лет назад
Родитель
Сommit
af57bdd59e

+ 2 - 1
CHANGES.md

@@ -2,7 +2,8 @@
 
 ## 3.5.19-RC
 
-* 
+* Improvement: Drop unnecessary MongoDB collection indexes
+* Improvement: Organize MongoDB collection indexes uniqueness
 
 ## 3.5.18
 

+ 10 - 8
config/jest.config.js

@@ -1,6 +1,15 @@
 // For a detailed explanation regarding each configuration property, visit:
 // https://jestjs.io/docs/en/configuration.html
 
+const MODULE_NAME_MAPPING = {
+  '@root/(.+)': '<rootDir>/$1',
+  '@commons/(.+)': '<rootDir>/src/lib/$1',
+  '@server/(.+)': '<rootDir>/src/server/$1',
+  '@alias/logger': '<rootDir>/src/lib/service/logger',
+  // -- doesn't work with unknown error -- 2019.06.19 Yuki Takei
+  // debug: '<rootDir>/src/lib/service/logger/alias-for-debug',
+};
+
 module.exports = {
   // Indicates whether each individual test should be reported during the run
   verbose: true,
@@ -19,14 +28,7 @@ module.exports = {
       // Automatically clear mock calls and instances between every test
       clearMocks: true,
       // A map from regular expressions to module names that allow to stub out resources with a single module
-      moduleNameMapper: {
-        '@root/(.+)': '<rootDir>/$1',
-        '@commons/(.+)': '<rootDir>/src/lib/$1',
-        '@server/(.+)': '<rootDir>/src/server/$1',
-        '@alias/logger': '<rootDir>/src/lib/service/logger',
-        // -- doesn't work with unknown error -- 2019.06.19 Yuki Takei
-        // debug: '<rootDir>/src/lib/service/logger/alias-for-debug',
-      },
+      moduleNameMapper: MODULE_NAME_MAPPING,
     },
     // {
     //   displayName: 'client',

+ 5 - 5
src/client/js/components/Admin/Common/AdminUpdateButton.jsx → src/client/js/components/Admin/Common/AdminUpdateButtonRow.jsx

@@ -2,15 +2,15 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-class AdminUpdateButton extends React.PureComponent {
+class AdminUpdateButtonRow extends React.PureComponent {
 
   render() {
     const { t } = this.props;
 
     return (
-      <div className="form-group my-3">
+      <div className="row my-3">
         <div className="col-xs-offset-4 col-xs-5">
-          <div className="btn btn-primary" onClick={this.props.onClick}>{ t('Update') }</div>
+          <button type="button" className="btn btn-primary" onClick={this.props.onClick}>{ t('Update') }</button>
         </div>
       </div>
     );
@@ -18,10 +18,10 @@ class AdminUpdateButton extends React.PureComponent {
 
 }
 
-AdminUpdateButton.propTypes = {
+AdminUpdateButtonRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
   onClick: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(AdminUpdateButton);
+export default withTranslation()(AdminUpdateButtonRow);

+ 2 - 2
src/client/js/components/Admin/Customize/Customize.jsx

@@ -17,10 +17,10 @@ class Customize extends React.Component {
 
     return (
       <Fragment>
-        <div className="row my-3">
+        <div className="my-3">
           <CustomizeLayoutSetting />
         </div>
-        <div className="row my-3">
+        <div className="my-3">
           <CustomizeBehaviorSetting />
         </div>
         <div className="row my-3">

+ 2 - 2
src/client/js/components/Admin/Customize/CustomizeBehaviorOption.jsx

@@ -7,7 +7,7 @@ class CustomizeBehaviorOption extends React.PureComponent {
   render() {
 
     return (
-      <div className="col-xs-6">
+      <React.Fragment>
         <h4>
           <div className="radio radio-primary">
             <input type="radio" id={`radioBehavior${this.props.behaviorType}`} checked={this.props.isSelected} onChange={this.props.onSelected} />
@@ -19,7 +19,7 @@ class CustomizeBehaviorOption extends React.PureComponent {
         </h4>
         {/* render layout description */}
         {this.props.children}
-      </div>
+      </React.Fragment>
     );
   }
 

+ 40 - 34
src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx

@@ -11,7 +11,7 @@ import AppContainer from '../../../services/AppContainer';
 
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import CustomizeBehaviorOption from './CustomizeBehaviorOption';
-import AdminUpdateButton from '../Common/AdminUpdateButton';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:importer');
 
@@ -42,39 +42,45 @@ class CustomizeBehaviorSetting extends React.Component {
     return (
       <React.Fragment>
         <h2>{t('customize_page.Behavior')}</h2>
-        <CustomizeBehaviorOption
-          behaviorType="growi"
-          isSelected={adminCustomizeContainer.state.currentBehavior === 'growi'}
-          onSelected={() => adminCustomizeContainer.switchBehaviorType('growi')}
-          labelHtml='GROWI Simplified Behavior <small class="text-success">(Recommended)</small>'
-        >
-          {/* TODO i18n */}
-          <ul>
-            <li>Both of <code>/page</code> and <code>/page/</code> shows the same page</li>
-            <li><code>/nonexistent_page</code> shows editing form</li>
-            <li>All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b></li>
-          </ul>
-        </CustomizeBehaviorOption>
-
-        <CustomizeBehaviorOption
-          behaviorType="crowi-plus"
-          isSelected={adminCustomizeContainer.state.currentBehavior === 'crowi-plus'}
-          onSelected={() => adminCustomizeContainer.switchBehaviorType('crowi-plus')}
-          labelHtml="Crowi Classic Behavior"
-        >
-          {/* TODO i18n */}
-          <ul>
-            <li><code>/page</code> shows the page</li>
-            <li><code>/page/</code> shows the list of sub pages</li>
-            <ul>
-              <li>If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown</li>
-            </ul>
-            <li><code>/nonexistent_page</code> shows editing form</li>
-            <li><code>/nonexistent_page/</code> the list of sub pages</li>
-          </ul>
-        </CustomizeBehaviorOption>
-
-        <AdminUpdateButton onClick={this.onClickSubmit} />
+        <div className="row">
+          <div className="col-xs-6">
+            <CustomizeBehaviorOption
+              behaviorType="growi"
+              isSelected={adminCustomizeContainer.state.currentBehavior === 'growi'}
+              onSelected={() => adminCustomizeContainer.switchBehaviorType('growi')}
+              labelHtml='GROWI Simplified Behavior <small class="text-success">(Recommended)</small>'
+            >
+              {/* TODO i18n */}
+              <ul>
+                <li>Both of <code>/page</code> and <code>/page/</code> shows the same page</li>
+                <li><code>/nonexistent_page</code> shows editing form</li>
+                <li>All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b></li>
+              </ul>
+            </CustomizeBehaviorOption>
+          </div>
+
+          <div className="col-xs-6">
+            <CustomizeBehaviorOption
+              behaviorType="crowi-plus"
+              isSelected={adminCustomizeContainer.state.currentBehavior === 'crowi-plus'}
+              onSelected={() => adminCustomizeContainer.switchBehaviorType('crowi-plus')}
+              labelHtml="Crowi Classic Behavior"
+            >
+              {/* TODO i18n */}
+              <ul>
+                <li><code>/page</code> shows the page</li>
+                <li><code>/page/</code> shows the list of sub pages</li>
+                <ul>
+                  <li>If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown</li>
+                </ul>
+                <li><code>/nonexistent_page</code> shows editing form</li>
+                <li><code>/nonexistent_page/</code> the list of sub pages</li>
+              </ul>
+            </CustomizeBehaviorOption>
+          </div>
+        </div>
+
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} />
       </React.Fragment>
     );
   }

+ 2 - 2
src/client/js/components/Admin/Customize/CustomizeLayoutOption.jsx

@@ -8,7 +8,7 @@ class CustomizeLayoutOption extends React.Component {
     const { layoutType } = this.props;
 
     return (
-      <div className="col-sm-4">
+      <React.Fragment>
         <h4>
           <div className="radio radio-primary">
             <input type="radio" id={`radio-layout-${layoutType}`} checked={this.props.isSelected} onChange={this.props.onSelected} />
@@ -23,7 +23,7 @@ class CustomizeLayoutOption extends React.Component {
         </a>
         {/* render layout description */}
         {this.props.children}
-      </div>
+      </React.Fragment>
     );
   }
 

+ 50 - 44
src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx

@@ -14,52 +14,58 @@ class CustomizeLayoutOptions extends React.Component {
     const { adminCustomizeContainer } = this.props;
 
     return (
-      <React.Fragment>
-        <CustomizeLayoutOption
-          layoutType="crowi-plus"
-          isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
-          onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
-          labelHtml={'GROWI Enhanced Layout <small class="text-success">(Recommended)</small>'}
-        >
-          {/* TODO i18n */}
-          <h4>Simple and Clear</h4>
-          <ul>
-            <li>Full screen layout and thin margins/paddings</li>
-            <li>Show and post comments at the bottom of the page</li>
-            <li>Affix Table-of-contents</li>
-          </ul>
-        </CustomizeLayoutOption>
+      <div className="row">
+        <div className="col-sm-4">
+          <CustomizeLayoutOption
+            layoutType="crowi-plus"
+            isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
+            onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
+            labelHtml={'GROWI Enhanced Layout <small class="text-success">(Recommended)</small>'}
+          >
+            {/* TODO i18n */}
+            <h4>Simple and Clear</h4>
+            <ul>
+              <li>Full screen layout and thin margins/paddings</li>
+              <li>Show and post comments at the bottom of the page</li>
+              <li>Affix Table-of-contents</li>
+            </ul>
+          </CustomizeLayoutOption>
+        </div>
 
-        <CustomizeLayoutOption
-          layoutType="kibela"
-          isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
-          onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
-          labelHtml="Kibela Like Layout"
-        >
-          {/* TODO i18n */}
-          <h4>Easy Viewing Structure</h4>
-          <ul>
-            <li>Center aligned contents</li>
-            <li>Show and post comments at the bottom of the page</li>
-            <li>Affix Table-of-contents</li>
-          </ul>
-        </CustomizeLayoutOption>
+        <div className="col-sm-4">
+          <CustomizeLayoutOption
+            layoutType="kibela"
+            isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
+            onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
+            labelHtml="Kibela Like Layout"
+          >
+            {/* TODO i18n */}
+            <h4>Easy Viewing Structure</h4>
+            <ul>
+              <li>Center aligned contents</li>
+              <li>Show and post comments at the bottom of the page</li>
+              <li>Affix Table-of-contents</li>
+            </ul>
+          </CustomizeLayoutOption>
+        </div>
 
-        <CustomizeLayoutOption
-          layoutType="classic"
-          isSelected={adminCustomizeContainer.state.currentLayout === 'crowi'}
-          onSelected={() => adminCustomizeContainer.switchLayoutType('crowi')}
-          labelHtml="Crowi Classic Layout"
-        >
-          {/* TODO i18n */}
-          <h4>Separated Functions</h4>
-          <ul>
-            <li>Collapsible Sidebar</li>
-            <li>Show and post comments in Sidebar</li>
-            <li>Collapsible Table-of-contents</li>
-          </ul>
-        </CustomizeLayoutOption>
-      </React.Fragment>
+        <div className="col-sm-4">
+          <CustomizeLayoutOption
+            layoutType="classic"
+            isSelected={adminCustomizeContainer.state.currentLayout === 'crowi'}
+            onSelected={() => adminCustomizeContainer.switchLayoutType('crowi')}
+            labelHtml="Crowi Classic Layout"
+          >
+            {/* TODO i18n */}
+            <h4>Separated Functions</h4>
+            <ul>
+              <li>Collapsible Sidebar</li>
+              <li>Show and post comments in Sidebar</li>
+              <li>Collapsible Table-of-contents</li>
+            </ul>
+          </CustomizeLayoutOption>
+        </div>
+      </div>
     );
   }
 

+ 2 - 2
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -12,7 +12,7 @@ import AppContainer from '../../../services/AppContainer';
 import CustomizeLayoutOptions from './CustomizeLayoutOptions';
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AdminUpdateButton from '../Common/AdminUpdateButton';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:importer');
 
@@ -59,7 +59,7 @@ class CustomizeLayoutSetting extends React.Component {
         <h2>{ t('customize_page.Theme') }</h2>
         {this.renderDevAlert()}
         <CustomizeThemeOptions />
-        <AdminUpdateButton onClick={this.onClickSubmit} />
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} />
       </React.Fragment>
     );
   }

+ 28 - 0
src/migrations/20191102223900-drop-configs-indices.js

@@ -0,0 +1,28 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:drop-configs-indices');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+async function dropIndexIfExists(collection, indexName) {
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const collection = db.collection('configs');
+    await dropIndexIfExists(collection, 'ns_1');
+    await dropIndexIfExists(collection, 'key_1');
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 29 - 0
src/migrations/20191102223901-drop-pages-indices.js

@@ -0,0 +1,29 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:drop-pages-indices');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+async function dropIndexIfExists(collection, indexName) {
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const collection = db.collection('pages');
+    await dropIndexIfExists(collection, 'lastUpdateUser_1');
+    await dropIndexIfExists(collection, 'liker_1');
+    await dropIndexIfExists(collection, 'seenUsers_1');
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 3 - 1
src/server/models/attachment.js

@@ -6,6 +6,7 @@ const logger = require('@alias/logger')('growi:models:attachment');
 const path = require('path');
 
 const mongoose = require('mongoose');
+const uniqueValidator = require('mongoose-unique-validator');
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
@@ -21,12 +22,13 @@ module.exports = function(crowi) {
     page: { type: ObjectId, ref: 'Page', index: true },
     creator: { type: ObjectId, ref: 'User', index: true },
     filePath: { type: String }, // DEPRECATED: remains for backward compatibility for v3.3.x or below
-    fileName: { type: String, required: true },
+    fileName: { type: String, required: true, unique: true },
     originalName: { type: String },
     fileFormat: { type: String, required: true },
     fileSize: { type: Number, default: 0 },
     createdAt: { type: Date, default: Date.now },
   });
+  attachmentSchema.plugin(uniqueValidator);
 
   attachmentSchema.virtual('filePathProxied').get(function() {
     return `/attachment/${this._id}`;

+ 7 - 4
src/server/models/bookmark.js

@@ -1,10 +1,12 @@
-// disable no-return-await for model functions
 /* eslint-disable no-return-await */
 
+const debug = require('debug')('growi:models:bookmark');
+const mongoose = require('mongoose');
+const uniqueValidator = require('mongoose-unique-validator');
+
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
 module.exports = function(crowi) {
-  const debug = require('debug')('growi:models:bookmark');
-  const mongoose = require('mongoose');
-  const ObjectId = mongoose.Schema.Types.ObjectId;
   const bookmarkEvent = crowi.event('bookmark');
 
   let bookmarkSchema = null;
@@ -16,6 +18,7 @@ module.exports = function(crowi) {
     createdAt: { type: Date, default: Date.now },
   });
   bookmarkSchema.index({ page: 1, user: 1 }, { unique: true });
+  bookmarkSchema.plugin(uniqueValidator);
 
   bookmarkSchema.statics.countByPageId = async function(pageId) {
     return await this.count({ page: pageId });

+ 8 - 7
src/server/models/config.js

@@ -1,21 +1,22 @@
-// disable no-return-await for model functions
-/* eslint-disable no-return-await */
-
-/* eslint-disable no-use-before-define */
+const mongoose = require('mongoose');
+const uniqueValidator = require('mongoose-unique-validator');
 
 module.exports = function(crowi) {
-  const mongoose = require('mongoose');
 
   const configSchema = new mongoose.Schema({
-    ns: { type: String, required: true, index: true },
-    key: { type: String, required: true, index: true },
+    ns: { type: String, required: true },
+    key: { type: String, required: true },
     value: { type: String, required: true },
   });
+  // define unique compound index
+  configSchema.index({ ns: 1, key: 1 }, { unique: true });
+  configSchema.plugin(uniqueValidator);
 
   /**
    * default values when GROWI is cleanly installed
    */
   function getConfigsForInstalling() {
+    // eslint-disable-next-line no-use-before-define
     const config = getDefaultCrowiConfigs();
 
     // overwrite

+ 5 - 0
src/server/models/page-tag-relation.js

@@ -5,6 +5,7 @@ const flatMap = require('array.prototype.flatmap');
 
 const mongoose = require('mongoose');
 const mongoosePaginate = require('mongoose-paginate-v2');
+const uniqueValidator = require('mongoose-unique-validator');
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
@@ -17,6 +18,7 @@ const schema = new mongoose.Schema({
     type: ObjectId,
     ref: 'Page',
     required: true,
+    index: true,
   },
   relatedTag: {
     type: ObjectId,
@@ -24,7 +26,10 @@ const schema = new mongoose.Schema({
     required: true,
   },
 });
+// define unique compound index
+schema.index({ page: 1, user: 1 }, { unique: true });
 schema.plugin(mongoosePaginate);
+schema.plugin(uniqueValidator);
 
 /**
  * PageTagRelation Class

+ 3 - 3
src/server/models/page.js

@@ -39,9 +39,9 @@ const pageSchema = new mongoose.Schema({
   grantedUsers: [{ type: ObjectId, ref: 'User' }],
   grantedGroup: { type: ObjectId, ref: 'UserGroup', index: true },
   creator: { type: ObjectId, ref: 'User', index: true },
-  lastUpdateUser: { type: ObjectId, ref: 'User', index: true },
-  liker: [{ type: ObjectId, ref: 'User', index: true }],
-  seenUsers: [{ type: ObjectId, ref: 'User', index: true }],
+  lastUpdateUser: { type: ObjectId, ref: 'User' },
+  liker: [{ type: ObjectId, ref: 'User' }],
+  seenUsers: [{ type: ObjectId, ref: 'User' }],
   commentCount: { type: Number, default: 0 },
   extended: {
     type: String,

+ 2 - 0
src/server/models/tag.js

@@ -3,6 +3,7 @@
 
 const mongoose = require('mongoose');
 const mongoosePaginate = require('mongoose-paginate-v2');
+const uniqueValidator = require('mongoose-unique-validator');
 
 /*
  * define schema
@@ -15,6 +16,7 @@ const schema = new mongoose.Schema({
   },
 });
 schema.plugin(mongoosePaginate);
+schema.plugin(uniqueValidator);
 
 /**
  * Tag Class

+ 3 - 0
src/server/models/user-group-relation.js

@@ -1,6 +1,7 @@
 const debug = require('debug')('growi:models:userGroupRelation');
 const mongoose = require('mongoose');
 const mongoosePaginate = require('mongoose-paginate-v2');
+const uniqueValidator = require('mongoose-unique-validator');
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
@@ -14,6 +15,8 @@ const schema = new mongoose.Schema({
   createdAt: { type: Date, default: Date.now, required: true },
 });
 schema.plugin(mongoosePaginate);
+schema.plugin(uniqueValidator);
+
 
 /**
  * UserGroupRelation Class

+ 3 - 3
src/server/models/user.js

@@ -44,14 +44,14 @@ module.exports = function(crowi) {
     name: { type: String },
     username: { type: String, required: true, unique: true },
     email: { type: String, unique: true, sparse: true },
-    // === The official settings
+    // === Crowi settings
     // username: { type: String, index: true },
     // email: { type: String, required: true, index: true },
     // === crowi-plus (>= 2.1.0, <2.3.0) settings
     // email: { type: String, required: true, unique: true },
-    introduction: { type: String },
+    introduction: String,
     password: String,
-    apiToken: String,
+    apiToken: { type: String, index: true },
     lang: {
       type: String,
       // eslint-disable-next-line no-eval

+ 7 - 4
src/server/routes/comment.js

@@ -121,10 +121,13 @@ module.exports = function(crowi, app) {
     }
 
     // update page
-    const page = await Page.findOneAndUpdate({ _id: pageId }, {
-      lastUpdateUser: req.user,
-      updatedAt: new Date(),
-    });
+    const page = await Page.findOneAndUpdate(
+      { _id: pageId },
+      {
+        lastUpdateUser: req.user,
+        updatedAt: new Date(),
+      },
+    );
 
     res.json(ApiResponse.success({ comment: createdComment }));
 

+ 7 - 10
src/test/crowi/crowi.test.js

@@ -1,26 +1,23 @@
-const helpers = require('@commons/util/helpers');
+const { getInstance } = require('../setup-crowi');
 
 describe('Test for Crowi application context', () => {
 
-  const Crowi = require('@server/crowi');
-
   describe('construction', () => {
-    test('initialize crowi context', () => {
-      const crowi = new Crowi(helpers.root());
-      expect(crowi).toBeInstanceOf(Crowi);
+    test('initialize crowi context', async() => {
+      const crowi = await getInstance();
       expect(crowi.version).toBe(require('@root/package.json').version);
       expect(typeof crowi.env).toBe('object');
     });
 
-    test('config getter, setter', () => {
-      const crowi = new Crowi(helpers.root());
+    test('config getter, setter', async() => {
+      const crowi = await getInstance();
       expect(crowi.getConfig()).toEqual({});
       crowi.setConfig({ test: 1 });
       expect(crowi.getConfig()).toEqual({ test: 1 });
     });
 
-    test('model getter, setter', () => {
-      const crowi = new Crowi(helpers.root());
+    test('model getter, setter', async() => {
+      const crowi = await getInstance();
       // set
       crowi.model('hoge', { fuga: 1 });
       expect(crowi.model('hoge')).toEqual({ fuga: 1 });

+ 12 - 0
src/test/global-setup.js

@@ -1,3 +1,5 @@
+require('module-alias/register');
+
 // check env
 if (process.env.NODE_ENV !== 'test') {
   throw new Error('\'process.env.NODE_ENV\' must be \'test\'');
@@ -7,8 +9,18 @@ const mongoose = require('mongoose');
 
 const { getMongoUri } = require('../lib/util/mongoose-utils');
 
+const { getInstance } = require('./setup-crowi');
+
 module.exports = async() => {
   await mongoose.connect(getMongoUri(), { useNewUrlParser: true });
+
+  // drop database
   await mongoose.connection.dropDatabase();
+
+  // init DB
+  const crowi = await getInstance();
+  const appService = crowi.appService;
+  await appService.initDB();
+
   await mongoose.disconnect();
 };

+ 0 - 4
src/test/setup-crowi.js

@@ -8,10 +8,6 @@ async function createInstance() {
   const crowi = new Crowi(helpers.root());
   await crowi.initForTest();
 
-  // init DB
-  const appService = crowi.appService;
-  await appService.initDB();
-
   return crowi;
 }
 

+ 10 - 5
src/test/util/slack.test.js

@@ -1,10 +1,15 @@
-const helpers = require('@commons/util/helpers');
-
-const Crowi = require('@server/crowi');
+const { getInstance } = require('../setup-crowi');
 
 describe('Slack Util', () => {
-  const crowi = new Crowi(helpers.root());
-  const slack = require(`${crowi.libDir}/util/slack`)(crowi);
+
+  let crowi;
+  let slack;
+
+  beforeEach(async(done) => {
+    crowi = await getInstance();
+    slack = require(`${crowi.libDir}/util/slack`)(crowi);
+    done();
+  });
 
   test('post comment method exists', () => {
     expect(slack.postComment).toBeInstanceOf(Function);