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

Merge branch 'master' into imprv/migrate-like-states-to-swr

Shun Miyazawa 4 лет назад
Родитель
Сommit
e5e9b63fd9
73 измененных файлов с 1823 добавлено и 329 удалено
  1. 2 0
      .devcontainer/Dockerfile
  2. 44 0
      .github/workflows/ci-app-prod.yml
  3. 37 106
      .github/workflows/ci-app.yml
  4. 22 12
      .github/workflows/ci-slackbot-proxy.yml
  5. 272 0
      .github/workflows/reusable-app-prod.yml
  6. 91 0
      .github/workflows/reusable-app-reg-suit.yml
  7. 20 1
      CHANGELOG.md
  8. 53 0
      bin/github-actions/generate-cypress-spec-arg.js
  9. 1 1
      lerna.json
  10. 10 2
      package.json
  11. 5 0
      packages/app/.gitignore
  12. 7 0
      packages/app/config/ci/.env.local.for-auto-install
  13. 17 0
      packages/app/cypress.json
  14. 2 2
      packages/app/docker/README.md
  15. 7 12
      packages/app/jest.config.js
  16. 11 7
      packages/app/package.json
  17. 25 0
      packages/app/regconfig.json
  18. 7 7
      packages/app/src/client/nologin.jsx
  19. 14 4
      packages/app/src/components/InstallerForm.jsx
  20. 3 3
      packages/app/src/components/LoginForm.jsx
  21. 7 0
      packages/app/src/interfaces/lang.ts
  22. 2 0
      packages/app/src/interfaces/user.ts
  23. 31 40
      packages/app/src/server/crowi/index.js
  24. 1 1
      packages/app/src/server/routes/index.js
  25. 14 58
      packages/app/src/server/routes/installer.js
  26. 7 18
      packages/app/src/server/service/app.ts
  27. 30 0
      packages/app/src/server/service/config-loader.ts
  28. 118 0
      packages/app/src/server/service/installer.ts
  29. 1 1
      packages/app/src/server/views/installer.html
  30. 0 18
      packages/app/src/test/integration/setup-crowi.js
  31. 8 0
      packages/app/test/cypress/.eslintrc.js
  32. 6 0
      packages/app/test/cypress/fixtures/user-admin.json
  33. 58 0
      packages/app/test/cypress/integration/1-install/install.spec.ts
  34. 102 0
      packages/app/test/cypress/integration/2-advanced-examples/misc.spec.ts
  35. 59 0
      packages/app/test/cypress/integration/2-advanced-examples/viewport.spec.ts
  36. 33 0
      packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts
  37. 22 0
      packages/app/test/cypress/plugins/index.ts
  38. 39 0
      packages/app/test/cypress/support/commands.ts
  39. 30 0
      packages/app/test/cypress/support/index.ts
  40. 13 0
      packages/app/test/cypress/tsconfig.json
  41. 0 0
      packages/app/test/integration/crowi/crowi.test.js
  42. 0 0
      packages/app/test/integration/global-setup.js
  43. 0 0
      packages/app/test/integration/global-teardown.js
  44. 0 0
      packages/app/test/integration/middlewares/access-token-parser.test.js
  45. 0 0
      packages/app/test/integration/middlewares/login-required.test.js
  46. 1 1
      packages/app/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts
  47. 0 0
      packages/app/test/integration/models/config.test.js
  48. 0 0
      packages/app/test/integration/models/page.test.js
  49. 0 0
      packages/app/test/integration/models/share-link.test.js
  50. 0 0
      packages/app/test/integration/models/update-post.test.js
  51. 0 0
      packages/app/test/integration/models/user.test.js
  52. 0 0
      packages/app/test/integration/service/acl.test.js
  53. 0 0
      packages/app/test/integration/service/config-manager.test.js
  54. 0 0
      packages/app/test/integration/service/page.test.js
  55. 0 0
      packages/app/test/integration/service/passport.test.js
  56. 0 0
      packages/app/test/integration/service/search/search-service.test.js
  57. 39 0
      packages/app/test/integration/setup-crowi.js
  58. 0 0
      packages/app/test/integration/setup.js
  59. 0 0
      packages/app/test/integration/utils/slack-legacy.test.js
  60. 6 0
      packages/app/test/tsconfig.json
  61. 0 0
      packages/app/test/unit/middlewares/safe-redirect.test.js
  62. 0 0
      packages/app/test/unit/migrate-mongo-config.test.js
  63. 0 0
      packages/app/test/unit/utils/to-array-from-csv.test.js
  64. 0 1
      packages/app/tsconfig.build.server.json
  65. 1 1
      packages/codemirror-textlint/package.json
  66. 1 1
      packages/core/package.json
  67. 1 1
      packages/plugin-attachment-refs/package.json
  68. 1 1
      packages/plugin-lsx/package.json
  69. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  70. 2 1
      packages/slack/package.json
  71. 2 2
      packages/slackbot-proxy/package.json
  72. 1 1
      packages/ui/package.json
  73. 536 25
      yarn.lock

+ 2 - 0
.devcontainer/Dockerfile

@@ -32,6 +32,8 @@ RUN chown -R $USER_UID:$USER_GID /home/$USERNAME /workspace;
 ENV DEBIAN_FRONTEND=noninteractive
 RUN apt-get update \
    && apt-get -y install --no-install-recommends git-lfs \
+      # for Cypress
+      libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb fonts-noto-cjk \
 
    # Clean up
    && apt-get autoremove -y \

+ 44 - 0
.github/workflows/ci-app-prod.yml

@@ -0,0 +1,44 @@
+name: Node CI for app production
+
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    types: [opened, reopened, synchronize, closed]
+
+jobs:
+
+  test-prod-node12:
+    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    with:
+      node-version: 12.x
+      skip-cypress: true
+    secrets:
+      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  test-prod-node14:
+    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    with:
+      node-version: 14.x
+      cypress-report-artifact-name: Cypress report
+    secrets:
+      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  run-reg-suit-node14:
+    needs: [test-prod-node14]
+
+    uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
+
+    if: always()
+
+    with:
+      node-version: 14.x
+      cypress-report-artifact-name: Cypress report
+    secrets:
+      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
+      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 37 - 106
.github/workflows/ci.yml → .github/workflows/ci-app.yml

@@ -1,4 +1,4 @@
-name: Node CI for growi
+name: Node CI for app development
 
 on:
   push:
@@ -26,14 +26,19 @@ jobs:
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+
     - name: lerna bootstrap
       run: |
-        npx lerna bootstrap
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
+        npx lerna bootstrap -- --frozen-lockfile
 
     - name: lerna run lint for plugins
       run: |
@@ -53,6 +58,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
+
   test:
     runs-on: ubuntu-latest
 
@@ -79,14 +85,19 @@ jobs:
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+
     - name: lerna bootstrap
       run: |
-        npx lerna bootstrap
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
+        npx lerna bootstrap -- --frozen-lockfile
 
     - name: yarn test
       working-directory: ./packages/app
@@ -118,6 +129,7 @@ jobs:
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
+
   launch-dev:
     runs-on: ubuntu-latest
 
@@ -140,14 +152,19 @@ jobs:
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+
     - name: lerna bootstrap
       run: |
-        npx lerna bootstrap
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
+        npx lerna bootstrap -- --frozen-lockfile
 
     - name: yarn dev:ci
       working-directory: ./packages/app
@@ -162,93 +179,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*Node CI for growi - build-dev (${{ matrix.node-version }})*'
-        channel: '#ci'
-        isCompactMode: true
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}
-
-
-  launch-prod:
-    runs-on: ubuntu-latest
-
-    strategy:
-      matrix:
-        node-version: [12.x, 14.x]
-
-    services:
-      mongodb:
-        image: mongo:4.4
-        ports:
-        - 27017/tcp
-      mongodb36:
-        image: mongo:3.6
-        ports:
-        - 27017/tcp
-
-    steps:
-    - uses: actions/checkout@v2
-
-    - uses: actions/setup-node@v2
-      with:
-        node-version: ${{ matrix.node-version }}
-        cache: 'yarn'
-        cache-dependency-path: '**/yarn.lock'
-
-    - name: Remove unnecessary packages
-      run: |
-        rm -rf packages/slackbot-proxy
-    - name: lerna bootstrap
-      run: |
-        npx lerna bootstrap
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
-    - name: Build
-      run: |
-        yarn lerna run build
-      env:
-        ANALYZE_BUNDLE_SIZE: ${{ matrix.node-version == '14.x' }}
-    - name: lerna bootstrap --production
-      run: |
-        npx lerna bootstrap -- --production
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --production --depth=0
-    - name: Get DB name
-      id: getdbname
-      run: |
-        echo ::set-output name=suffix::$(echo '${{ matrix.node-version }}' | sed s/\\.//)
-    - name: yarn server:ci
-      working-directory: ./packages/app
-      run: |
-        cp config/ci/.env.local.for-ci .env.production.local
-        yarn server:ci
-      env:
-        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
-    - name: yarn server:ci with MongoDB 3.6
-      working-directory: ./packages/app
-      run: |
-        cp config/ci/.env.local.for-ci .env.production.local
-        yarn server:ci
-      env:
-        MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
-
-    - name: Upload report as artifact
-      uses: actions/upload-artifact@v2
-      with:
-        name: Bundle Analyzing Report
-        path: packages/app/report/bundle-analyzer.html
-
-    - name: Slack Notification
-      uses: weseek/ghaction-slack-notification@master
-      if: failure()
-      with:
-        type: ${{ job.status }}
-        job_name: '*Node CI for growi - build-prod (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - launch-dev (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 22 - 12
.github/workflows/ci-slackbot-proxy.yml

@@ -26,14 +26,19 @@ jobs:
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+
     - name: lerna bootstrap
       run: |
-        npx lerna bootstrap
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
+        npx lerna bootstrap -- --frozen-lockfile
 
     - name: yarn lint
       run: |
@@ -79,14 +84,19 @@ jobs:
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+
     - name: lerna bootstrap
       run: |
-        npx lerna bootstrap
-    - name: Print dependencies
-      run: |
-        echo -n "node " && node -v
-        echo -n "npm " && npm -v
-        yarn list --depth=0
+        npx lerna bootstrap -- --frozen-lockfile
 
     - name: yarn dev:ci
       working-directory: ./packages/slackbot-proxy

+ 272 - 0
.github/workflows/reusable-app-prod.yml

@@ -0,0 +1,272 @@
+name: Reusable build app workflow for production
+
+on:
+  workflow_call:
+    inputs:
+      node-version:
+        required: true
+        type: string
+      skip-cypress:
+        type: boolean
+      cypress-report-artifact-name:
+        type: string
+    secrets:
+      SLACK_WEBHOOK_URL:
+        required: true
+
+jobs:
+
+  build-prod:
+    runs-on: ubuntu-latest
+
+    outputs:
+      PROD_FILES: ${{ steps.archive-prod-files.outputs.file }}
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - uses: actions/setup-node@v2
+      with:
+        node-version: ${{ inputs.node-version }}
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
+
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-
+
+    - name: lerna bootstrap
+      run: |
+        npx lerna bootstrap -- --frozen-lockfile
+
+    - name: Remove unnecessary packages
+      run: |
+        rm -rf packages/slackbot-proxy
+
+    - name: Build
+      run: |
+        yarn lerna run build
+      env:
+        ANALYZE_BUNDLE_SIZE: 1
+
+    - name: Archive production files
+      id: archive-prod-files
+      run: |
+        tar -cf production.tar packages/**/dist packages/app/public
+        echo ::set-output name=file::production.tar
+
+    - name: Upload production files as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Production Files
+        path: ${{ steps.archive-prod-files.outputs.file }}
+
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Bundle Analyzing Report
+        path: packages/app/report/bundle-analyzer.html
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi - build-prod (${{ inputs.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+
+  launch-prod:
+    needs: [build-prod]
+    runs-on: ubuntu-latest
+
+    services:
+      mongodb:
+        image: mongo:4.4
+        ports:
+        - 27017/tcp
+      mongodb36:
+        image: mongo:3.6
+        ports:
+        - 27017/tcp
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - uses: actions/setup-node@v2
+      with:
+        node-version: ${{ inputs.node-version }}
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
+
+    - name: Get Date
+      id: get-date
+      run: |
+        echo "::set-output name=dateYmdHM::$(/bin/date -u "+%Y%m%d%H%M")"
+        echo "::set-output name=dateYm::$(/bin/date -u "+%Y%m")"
+
+    - name: Cache/Restore node_modules (not reused)
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ steps.get-date.outputs.dateYmdHM }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+          node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-
+          node_modules-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ steps.get-date.outputs.dateYm }}
+          node_modules-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-
+
+    - name: Remove unnecessary packages
+      run: |
+        rm -rf packages/slackbot-proxy
+
+    - name: lerna bootstrap --production
+      run: |
+        npx lerna bootstrap -- --production
+
+    - name: Download production files artifact
+      uses: actions/download-artifact@v2
+      with:
+        name: Production Files
+
+    - name: Extract procution files artifact
+      run: |
+        tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
+
+    - name: yarn server:ci
+      working-directory: ./packages/app
+      run: |
+        cp config/ci/.env.local.for-ci .env.production.local
+        yarn server:ci
+      env:
+        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi
+    - name: yarn server:ci with MongoDB 3.6
+      working-directory: ./packages/app
+      run: |
+        cp config/ci/.env.local.for-ci .env.production.local
+        yarn server:ci
+      env:
+        MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi - build-prod (${{ inputs.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+
+  run-cypress:
+    needs: [build-prod]
+
+    if: ${{ !inputs.skip-cypress }}
+
+    runs-on: ubuntu-latest
+    container: cypress/base:14.18.1
+
+    strategy:
+      fail-fast: false
+      matrix:
+        # List string expressions that is comma separated ids of tests in "test/cypress/integration"
+        spec-group: ['1', '2']
+
+    services:
+      mongodb:
+        image: mongo:4.4
+        ports:
+        - 27017/tcp
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - uses: actions/setup-node@v2
+      with:
+        node-version: ${{ inputs.node-version }}
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
+
+    # workaround by https://github.com/cypress-io/github-action/issues/407
+    - name: Setup yarn cache settings
+      run: yarn config set cache-folder ~/.cache/yarn
+
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+          ~/.cache/Cypress
+        key: node_modules-and-cypress-bin-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-and-cypress-bin-${{ runner.OS }}-node${{ inputs.node-version }}
+
+    - name: lerna bootstrap
+      run: |
+        npx lerna bootstrap -- --frozen-lockfile
+
+    - name: Download production files artifact
+      uses: actions/download-artifact@v2
+      with:
+        name: Production Files
+
+    - name: Extract procution files artifact
+      run: |
+        tar -xf ${{ needs.build-prod.outputs.PROD_FILES }}
+
+    - name: Determine spec expression
+      id: determine-spec-exp
+      run: |
+        SPEC=`node bin/github-actions/generate-cypress-spec-arg.js --prefix="test/cypress/integration/" --suffix="-*/**" "${{ matrix.spec-group }}"`
+        echo "::set-output name=value::$SPEC"
+
+    - name: Copy dotenv file for automatic installation
+      if: ${{ matrix.spec-group != '1' }}
+      working-directory: ./packages/app
+      run: |
+        cat config/ci/.env.local.for-auto-install >> .env.production.local
+
+    - name: Cypress Run
+      uses: cypress-io/github-action@v2
+      with:
+        working-directory: ./packages/app
+        install: false
+        spec: '${{ steps.determine-spec-exp.outputs.value }}'
+        build: |
+          cat config/ci/.env.local.for-ci >> .env.production.local
+        start: yarn server
+        wait-on: 'http://localhost:3000'
+      env:
+        MONGO_URI: mongodb://mongodb:27017/growi-vrt
+
+    - name: Upload results
+      if: always()
+      uses: actions/upload-artifact@v2
+      with:
+        name: ${{ inputs.cypress-report-artifact-name }}
+        path: |
+          packages/app/test/cypress/screenshots
+          packages/app/test/cypress/videos
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi - run-cypress (${{ inputs.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 91 - 0
.github/workflows/reusable-app-reg-suit.yml

@@ -0,0 +1,91 @@
+name: Reusable build app workflow for production
+
+on:
+  workflow_call:
+    inputs:
+      node-version:
+        required: true
+        type: string
+      checkout-ref:
+        type: string
+        default: ${{ github.head_ref }}
+      cypress-report-artifact-name:
+        required: true
+        type: string
+    secrets:
+      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID:
+        required: true
+      AWS_ACCESS_KEY_ID:
+        required: true
+      AWS_SECRET_ACCESS_KEY:
+        required: true
+      SLACK_WEBHOOK_URL:
+        required: true
+    outputs:
+      EXPECTED_IMAGES_EXIST:
+        value: ${{ jobs.run-reg-suit.outputs.EXPECTED_IMAGES_EXIST }}
+
+
+jobs:
+
+  run-reg-suit:
+    # use secrets for "VRT" environment
+    # https://github.com/weseek/growi/settings/environments/376165508/edit
+    environment: VRT
+
+    env:
+      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
+      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+    runs-on: ubuntu-latest
+
+    outputs:
+      EXPECTED_IMAGES_EXIST: ${{ steps.check-expected-images.outputs.EXPECTED_IMAGES_EXIST }}
+
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        ref: ${{ inputs.checkout-ref }}
+        fetch-depth: 0
+
+    - uses: actions/setup-node@v2
+      with:
+        node-version: ${{ inputs.node-version }}
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
+
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v2
+      with:
+        path: |
+          **/node_modules
+        key: node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-
+
+    - name: lerna bootstrap
+      run: |
+        npx lerna bootstrap -- --frozen-lockfile
+
+    - name: Download screenshots taken by cypress
+      uses: actions/download-artifact@v2
+      with:
+        name: ${{ inputs.cypress-report-artifact-name }}
+        path: packages/app/test/cypress
+
+    - name: Run reg-suit
+      working-directory: ./packages/app
+      run: |
+        yarn reg:run
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi - run-reg-suit (${{ inputs.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 20 - 1
CHANGELOG.md

@@ -1,9 +1,28 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.5.8...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v4.5.9...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v4.5.9](https://github.com/weseek/growi/compare/v4.5.8...v4.5.9) - 2022-01-21
+
+### 🚀 Improvement
+
+- imprv: 79291 make password min length 8 charactors (#5116) @kaoritokashiki
+
+### 🐛 Bug Fixes
+
+- fix: OIDC reconnection bug fix (#5104) @mudana-grune
+- fix: /_api/v3/page is broken and dump 500 error "get-page-failed TypeError: user.canDeleteCompletely is not a function" (#5103) @yuki-takei
+- fix: Default completely deletion settings label mismatched against to actual (#5102) @yuki-takei
+- fix: OIDC issuer host availability check (#5099) @mudana-grune
+
+### 🧰 Maintenance
+
+- support: Improve multistage build (#5090) @yuki-takei
+- support: Omit node-re2 (#5089) @yuki-takei
+- ci(deps-dev): bump swr from 1.0.1 to 1.1.2 (#5018) @dependabot
+
 ## [v4.5.8](https://github.com/weseek/growi/compare/v4.5.7...v4.5.8) - 2022-01-12
 
 ### 💎 Features

+ 53 - 0
bin/github-actions/generate-cypress-spec-arg.js

@@ -0,0 +1,53 @@
+/* eslint-disable no-console */
+
+/*
+ * USAGE:
+ *  node generate-cypress-spec-arg --prefix=${prefix} --suffix=${suffix} ${value}
+ *
+ * OPTIONS:
+ *  --prefix : prefix string for each items
+ *  --suffix : suffix string for each items
+ *
+ * EXAMPLE:
+ *  node generate-cypress-spec-arg --prefix=${prefix}"A" --suffix="Z" "1,3,5"
+ *  => A1Z,A3Z,A5Z
+ */
+
+const yargs = require('yargs/yargs');
+const { hideBin } = require('yargs/helpers');
+
+const argv = yargs(hideBin(process.argv)).argv;
+
+
+const printExample = () => {
+  console.group('** Usage **');
+  // eslint-disable-next-line no-template-curly-in-string
+  console.log('$ node generate-cypress-spec-arg --prefix=${prefix}"A" --suffix="Z" "1,3,5"');
+  console.log('  ==> A1Z,A3Z,A5Z');
+  console.groupEnd();
+  console.log('\n');
+};
+
+
+const { prefix, suffix, _: value } = argv;
+
+if (prefix == null) {
+  printExample();
+  throw new Error('The option "prefix" must be specified');
+}
+if (suffix == null) {
+  printExample();
+  throw new Error('The option "suffix" must be specified');
+}
+if (value.length === 0) {
+  printExample();
+  throw new Error('A value string must be specified');
+}
+
+const result = value[0]
+  .toString().split(',')
+  .map(v => v.trim())
+  .map(v => `${prefix}${v}${suffix}`)
+  .join(',');
+
+console.log(result);

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "packages": [
     "packages/*"
   ]

+ 10 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -51,11 +51,13 @@
     "tslib": "^2.3.1"
   },
   "devDependencies": {
+    "@testing-library/cypress": "^8.0.2",
     "@types/jest": "^26.0.22",
     "@types/node": "^14.14.35",
     "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
+    "cypress": "^9.2.0",
     "eslint": "^7.31.0",
     "eslint-config-weseek": "^1.1.0",
     "eslint-import-resolver-typescript": "^2.4.0",
@@ -69,6 +71,11 @@
     "lerna": "^4.0.0",
     "postcss": "^8.4.5",
     "postcss-scss": "^4.0.3",
+    "reg-keygen-git-hash-plugin": "^0.11.1",
+    "reg-notify-github-plugin": "^0.11.1",
+    "reg-notify-slack-plugin": "^0.11.0",
+    "reg-publish-s3-plugin": "^0.11.0",
+    "reg-suit": "^0.11.1",
     "rewire": "^5.0.0",
     "shipjs": "^0.24.1",
     "stylelint": "^14.2.0",
@@ -76,7 +83,8 @@
     "ts-jest": "^27.0.4",
     "ts-node": "^9.1.1",
     "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3"
+    "typescript": "^4.2.3",
+    "yargs": "^17.3.1"
   },
   "engines": {
     "node": "^12 || ^14",

+ 5 - 0
packages/app/.gitignore

@@ -2,6 +2,11 @@
 /.next/
 /out/
 
+# test
+test/cypress/screenshots
+test/cypress/videos
+.reg
+
 # dist
 /dist/
 /transpiled/

+ 7 - 0
packages/app/config/ci/.env.local.for-auto-install

@@ -0,0 +1,7 @@
+APP_SITE_URL=http://localhost:3000
+
+AUTO_INSTALL_ADMIN_USERNAME=admin
+AUTO_INSTALL_ADMIN_NAME=Admin
+AUTO_INSTALL_ADMIN_EMAIL=admin@example.com
+AUTO_INSTALL_ADMIN_PASSWORD=adminadmin
+AUTO_INSTALL_GLOBAL_LANG=zh_CN

+ 17 - 0
packages/app/cypress.json

@@ -0,0 +1,17 @@
+{
+  "baseUrl": "http://localhost:3000",
+
+  "fileServerFolder": "test/cypress",
+  "fixturesFolder": "test/cypress/fixtures",
+  "integrationFolder": "test/cypress/integration",
+  "screenshotsFolder": "test/cypress/screenshots",
+  "videosFolder": "test/cypress/videos",
+  "supportFile": "test/cypress/support/index.ts",
+  "pluginsFile": "test/cypress/plugins/index.ts",
+  "testFiles": "**/*.spec.ts",
+
+  "viewportWidth": 1440,
+  "viewportHeight": 1200,
+
+  "experimentalSessionSupport": true
+}

+ 2 - 2
packages/app/docker/README.md

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`4.5.8`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.8/docker/Dockerfile)
-* [`4.5.8-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.8/docker/Dockerfile)
+* [`4.5.9`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.9/docker/Dockerfile)
+* [`4.5.9-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.9/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 * [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 

+ 7 - 12
packages/app/jest.config.js

@@ -21,8 +21,8 @@ module.exports = {
       preset: 'ts-jest/presets/js-with-ts',
 
       rootDir: '.',
-      roots: ['<rootDir>/src'],
-      testMatch: ['<rootDir>/src/test/unit/**/*.test.ts', '<rootDir>/src/test/unit/**/*.test.js'],
+      roots: ['<rootDir>'],
+      testMatch: ['<rootDir>/test/unit/**/*.test.ts', '<rootDir>/test/unit/**/*.test.js'],
 
       testEnvironment: 'node',
 
@@ -36,23 +36,18 @@ module.exports = {
       preset: 'ts-jest/presets/js-with-ts',
 
       rootDir: '.',
-      roots: ['<rootDir>/src'],
-      testMatch: ['<rootDir>/src/test/integration/**/*.test.ts', '<rootDir>/src/test/integration/**/*.test.js'],
+      roots: ['<rootDir>'],
+      testMatch: ['<rootDir>/test/integration/**/*.test.ts', '<rootDir>/test/integration/**/*.test.js'],
 
       testEnvironment: 'node',
-      globalSetup: '<rootDir>/src/test/integration/global-setup.js',
-      globalTeardown: '<rootDir>/src/test/integration/global-teardown.js',
-      setupFilesAfterEnv: ['<rootDir>/src/test/integration/setup.js'],
+      globalSetup: '<rootDir>/test/integration/global-setup.js',
+      globalTeardown: '<rootDir>/test/integration/global-teardown.js',
+      setupFilesAfterEnv: ['<rootDir>/test/integration/setup.js'],
 
       // Automatically clear mock calls and instances between every test
       clearMocks: true,
       moduleNameMapper: MODULE_NAME_MAPPING,
     },
-    // {
-    //   displayName: 'client',
-    //   rootDir: '.',
-    //   testMatch: ['<rootDir>/src/test/client/**/*.test.js'],
-    // },
   ],
 
   // Automatically clear mock calls and instances between every test

+ 11 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -28,6 +28,7 @@
     "dev:migrate:status": "yarn dev:migrate-mongo status",
     "dev:migrate:up": "yarn dev:migrate-mongo up",
     "dev:migrate:down": "yarn dev:migrate-mongo down",
+    "cy:run": "cypress run --headless",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",
@@ -39,6 +40,7 @@
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
     "prelint:eslint": "yarn resources:plugin",
     "prelint:swagger2openapi": "yarn openapi:v3",
+    "reg:run": "reg-suit run",
     "//// misc": "",
     "console": "yarn cross-env NODE_ENV=development yarn ts-node --experimental-repl-await src/server/console.js",
     "swagger-jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
@@ -57,11 +59,11 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.5.9-RC.0",
-    "@growi/plugin-attachment-refs": "^4.5.9-RC.0",
-    "@growi/plugin-lsx": "^4.5.9-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^4.5.9-RC.0",
-    "@growi/slack": "^4.5.9-RC.0",
+    "@growi/codemirror-textlint": "^4.5.10-RC.0",
+    "@growi/plugin-attachment-refs": "^4.5.10-RC.0",
+    "@growi/plugin-lsx": "^4.5.10-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.5.10-RC.0",
+    "@growi/slack": "^4.5.10-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
@@ -100,6 +102,7 @@
     "express-session": "^1.16.1",
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
+    "extensible-custom-error": "^0.0.7",
     "graceful-fs": "^4.1.11",
     "helmet": "^4.6.0",
     "http-errors": "~1.8.0",
@@ -159,7 +162,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^4.5.9-RC.0",
+    "@growi/ui": "^4.5.10-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -179,6 +182,7 @@
     "csv-to-markdown-table": "^1.0.1",
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.1.0",
+    "eslint-plugin-cypress": "^2.12.1",
     "file-loader": "^5.0.2",
     "handsontable": "=6.2.2",
     "hard-source-webpack-plugin": "^0.13.1",

+ 25 - 0
packages/app/regconfig.json

@@ -0,0 +1,25 @@
+{
+  "core": {
+    "workingDir": ".reg",
+    "actualDir": "test/cypress/screenshots",
+    "thresholdRate": 0.001,
+    "addIgnore": true,
+    "ximgdiff": {
+      "invocationType": "client"
+    }
+  },
+  "plugins": {
+    "reg-keygen-git-hash-plugin": true,
+    "reg-notify-github-plugin": {
+      "prCommentBehavior": "new",
+      "setCommitStatus": false,
+      "clientId": "$REG_NOTIFY_GITHUB_PLUGIN_CLIENTID"
+    },
+    "reg-notify-slack-plugin": {
+      "webhookUrl": "$SLACK_WEBHOOK_URL"
+    },
+    "reg-publish-s3-plugin": {
+      "bucketName": "growi-vrt-snapshots"
+    }
+  }
+}

+ 7 - 7
packages/app/src/client/nologin.jsx

@@ -16,17 +16,17 @@ import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationF
 const i18n = i18nFactory();
 
 // render InstallerForm
-const installerFormElem = document.getElementById('installer-form');
-if (installerFormElem) {
-  const userName = installerFormElem.dataset.userName;
-  const name = installerFormElem.dataset.name;
-  const email = installerFormElem.dataset.email;
-  const csrf = installerFormElem.dataset.csrf;
+const installerFormContainerElem = document.getElementById('installer-form-container');
+if (installerFormContainerElem) {
+  const userName = installerFormContainerElem.dataset.userName;
+  const name = installerFormContainerElem.dataset.name;
+  const email = installerFormContainerElem.dataset.email;
+  const csrf = installerFormContainerElem.dataset.csrf;
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
       <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
     </I18nextProvider>,
-    installerFormElem,
+    installerFormContainerElem,
   );
 }
 

+ 14 - 4
packages/app/src/components/InstallerForm.jsx

@@ -55,8 +55,6 @@ class InstallerForm extends React.Component {
     setTimeout(() => {
       this.setState({ isSubmittingDisabled: false });
     }, 3000);
-
-    document['register-form'].submit();
   }
 
   render() {
@@ -66,7 +64,7 @@ class InstallerForm extends React.Component {
       : <span><i className="icon-fw icon-ban" />{ this.props.t('installer.unavaliable_user_id') }</span>;
 
     return (
-      <div className={`login-dialog p-3 mx-auto${hasErrorClass}`}>
+      <div data-testid="installerForm" className={`login-dialog p-3 mx-auto${hasErrorClass}`}>
         <div className="row">
           <div className="col-md-12">
             <p className="alert alert-success">
@@ -84,6 +82,7 @@ class InstallerForm extends React.Component {
                   type="button"
                   className="btn btn-secondary dropdown-toggle text-right w-100 border-0 shadow-none"
                   id="dropdownLanguage"
+                  data-testid="dropdownLanguage"
                   data-toggle="dropdown"
                   aria-haspopup="true"
                   aria-expanded="true"
@@ -100,7 +99,13 @@ class InstallerForm extends React.Component {
                 <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
                   {
                     localeMetadatas.map(meta => (
-                      <button key={meta.id} className="dropdown-item" type="button" onClick={() => { this.changeLanguage(meta) }}>
+                      <button
+                        key={meta.id}
+                        data-testid={`dropdownLanguageMenu-${meta.id}`}
+                        className="dropdown-item"
+                        type="button"
+                        onClick={() => { this.changeLanguage(meta) }}
+                      >
                         {meta.displayName}
                       </button>
                     ))
@@ -114,6 +119,7 @@ class InstallerForm extends React.Component {
                 <span className="input-group-text"><i className="icon-user" /></span>
               </div>
               <input
+                data-testid="tiUsername"
                 type="text"
                 className="form-control"
                 placeholder={this.props.t('User ID')}
@@ -130,6 +136,7 @@ class InstallerForm extends React.Component {
                 <span className="input-group-text"><i className="icon-tag" /></span>
               </div>
               <input
+                data-testid="tiName"
                 type="text"
                 className="form-control"
                 placeholder={this.props.t('Name')}
@@ -144,6 +151,7 @@ class InstallerForm extends React.Component {
                 <span className="input-group-text"><i className="icon-envelope" /></span>
               </div>
               <input
+                data-testid="tiEmail"
                 type="email"
                 className="form-control"
                 placeholder={this.props.t('Email')}
@@ -158,6 +166,7 @@ class InstallerForm extends React.Component {
                 <span className="input-group-text"><i className="icon-lock" /></span>
               </div>
               <input
+                data-testid="tiPassword"
                 type="password"
                 className="form-control"
                 placeholder={this.props.t('Password')}
@@ -170,6 +179,7 @@ class InstallerForm extends React.Component {
 
             <div className="input-group mt-4 mb-3 d-flex justify-content-center">
               <button
+                data-testid="btnSubmit"
                 type="submit"
                 className="btn-fill btn btn-register"
                 id="register"

+ 3 - 3
packages/app/src/components/LoginForm.jsx

@@ -50,7 +50,7 @@ class LoginForm extends React.Component {
               <i className="icon-user"></i>
             </span>
           </div>
-          <input type="text" className="form-control rounded-0" placeholder="Username or E-mail" name="loginForm[username]" />
+          <input type="text" className="form-control rounded-0" data-testid="tiUsernameForLogin" placeholder="Username or E-mail" name="loginForm[username]" />
           {isLdapStrategySetup && (
             <div className="input-group-append">
               <small className="input-group-text text-success">
@@ -66,12 +66,12 @@ class LoginForm extends React.Component {
               <i className="icon-lock"></i>
             </span>
           </div>
-          <input type="password" className="form-control rounded-0" placeholder="Password" name="loginForm[password]" />
+          <input type="password" className="form-control rounded-0" data-testid="tiPasswordForLogin" placeholder="Password" name="loginForm[password]" />
         </div>
 
         <div className="input-group my-4">
           <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
-          <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto">
+          <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
             <div className="eff"></div>
             <span className="btn-label">
               <i className="icon-login"></i>

+ 7 - 0
packages/app/src/interfaces/lang.ts

@@ -0,0 +1,7 @@
+export const Lang = {
+  en_US: 'en_US',
+  ja_JP: 'ja_JP',
+  zh_CN: 'zh_CN',
+} as const;
+export const AllLang = Object.values(Lang);
+export type Lang = typeof Lang[keyof typeof Lang];

+ 2 - 0
packages/app/src/interfaces/user.ts

@@ -3,6 +3,8 @@ import { HasObjectId } from '~/interfaces/has-object-id';
 export type IUser = {
   name: string;
   username: string;
+  email: string;
+  password: string;
   imageUrlCached: string;
   admin: boolean;
 }

+ 31 - 40
packages/app/src/server/crowi/index.js

@@ -22,6 +22,7 @@ import AttachmentService from '../service/attachment';
 import { SlackIntegrationService } from '../service/slack-integration';
 import { UserNotificationService } from '../service/user-notification';
 import SearchService from '../service/search';
+import { InstallerService } from '../service/installer';
 
 import Actiity from '../models/activity';
 
@@ -140,47 +141,8 @@ Crowi.prototype.init = async function() {
     this.setUpGlobalNotification(),
     this.setUpUserNotification(),
   ]);
-};
-
-Crowi.prototype.initForTest = async function() {
-  await this.setupModels();
-  await this.setupConfigManager();
-
-  // setup messaging services
-  await this.setupSocketIoService();
-
-  // // customizeService depends on AppService and XssService
-  // // passportService depends on appService
-  await Promise.all([
-    this.setUpApp(),
-    this.setUpXss(),
-    // this.setUpGrowiBridge(),
-  ]);
 
-  await Promise.all([
-    // this.scanRuntimeVersions(),
-    this.setupPassport(),
-    // this.setupSearcher(),
-    // this.setupMailer(),
-    // this.setupSlackIntegrationService(),
-    // this.setupCsrf(),
-    // this.setUpFileUpload(),
-    this.setupAttachmentService(),
-    this.setUpAcl(),
-    // this.setUpCustomize(),
-    // this.setUpRestQiitaAPI(),
-    // this.setupUserGroup(),
-    // this.setupExport(),
-    // this.setupImport(),
-    this.setupPageService(),
-    this.setupInAppNotificationService(),
-    this.setupActivityService(),
-  ]);
-
-  // globalNotification depends on slack and mailer
-  // await Promise.all([
-  //   this.setUpGlobalNotification(),
-  // ]);
+  await this.autoInstall();
 };
 
 Crowi.prototype.isPageId = function(pageId) {
@@ -413,6 +375,35 @@ Crowi.prototype.setupCsrf = async function() {
   return Promise.resolve();
 };
 
+Crowi.prototype.autoInstall = function() {
+  const isInstalled = this.configManager.getConfig('crowi', 'app:installed');
+  const username = this.configManager.getConfig('crowi', 'autoInstall:adminUsername');
+
+  if (isInstalled || username == null) {
+    return;
+  }
+
+  logger.info('Start automatic installation');
+
+  const firstAdminUserToSave = {
+    username,
+    name: this.configManager.getConfig('crowi', 'autoInstall:adminName'),
+    email: this.configManager.getConfig('crowi', 'autoInstall:adminEmail'),
+    password: this.configManager.getConfig('crowi', 'autoInstall:adminPassword'),
+    admin: true,
+  };
+  const globalLang = this.configManager.getConfig('crowi', 'autoInstall:globalLang');
+
+  const installerService = new InstallerService(this);
+
+  try {
+    installerService.install(firstAdminUserToSave, globalLang ?? 'en_US');
+  }
+  catch (err) {
+    logger.warn('Automatic installation failed.', err);
+  }
+};
+
 Crowi.prototype.getTokens = function() {
   return this.tokens;
 };

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -13,7 +13,7 @@ const rateLimit = require('express-rate-limit');
 
 const apiLimiter = rateLimit({
   windowMs: 15 * 60 * 1000, // 15 minutes
-  max: 10, // limit each IP to 5 requests per windowMs
+  max: 10, // limit each IP to 10 requests per windowMs
   message:
     'Too many requests sent from this IP, please try again after 15 minutes',
 });

+ 14 - 58
packages/app/src/server/routes/installer.js

@@ -1,59 +1,13 @@
 import loggerFactory from '~/utils/logger';
 
-module.exports = function(crowi) {
-  const logger = loggerFactory('growi:routes:installer');
-  const path = require('path');
-  const fs = require('graceful-fs');
+import { InstallerService, FailedToCreateAdminUserError } from '../service/installer';
 
-  const models = crowi.models;
-  const { appService } = crowi;
+const logger = loggerFactory('growi:routes:installer');
 
-  const User = models.User;
-  const Page = models.Page;
+module.exports = function(crowi) {
 
   const actions = {};
 
-  async function initSearchIndex() {
-    const { searchService } = crowi;
-    if (!searchService.isReachable) {
-      return;
-    }
-
-    await searchService.rebuildIndex();
-  }
-
-  async function createPage(filePath, pagePath, owner, lang) {
-    try {
-      const markdown = fs.readFileSync(filePath);
-      return Page.create(pagePath, markdown, owner, {});
-    }
-    catch (err) {
-      logger.error(`Failed to create ${pagePath}`, err);
-    }
-  }
-
-  async function createInitialPages(owner, lang) {
-    const promises = [];
-
-    // create portal page for '/'
-    promises.push(createPage(path.join(crowi.localeDir, lang, 'welcome.md'), '/', owner, lang));
-
-    // create /Sandbox/*
-    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox.md'), '/Sandbox', owner, lang));
-    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-bootstrap4.md'), '/Sandbox/Bootstrap4', owner, lang));
-    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner, lang));
-    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner, lang));
-
-    await Promise.all(promises);
-
-    try {
-      await initSearchIndex();
-    }
-    catch (err) {
-      logger.error('Failed to build Elasticsearch Indices', err);
-    }
-  }
-
   actions.index = function(req, res) {
     return res.render('installer');
   };
@@ -71,24 +25,26 @@ module.exports = function(crowi) {
     const password = registerForm.password;
     const language = registerForm['app:globalLang'] || 'en_US';
 
-    await appService.initDB(language);
+    const installerService = new InstallerService(crowi);
 
-    // create first admin user
-    // TODO: with transaction
     let adminUser;
     try {
-      adminUser = await User.createUser(name, username, email, password, language);
-      await adminUser.asyncMakeAdmin();
+      adminUser = await installerService.install({
+        name,
+        username,
+        email,
+        password,
+      }, language);
     }
     catch (err) {
-      req.form.errors.push(req.t('message.failed_to_create_admin_user', { errMessage: err.message }));
+      if (err instanceof FailedToCreateAdminUserError) {
+        req.form.errors.push(req.t('message.failed_to_create_admin_user', { errMessage: err.message }));
+      }
       return res.render('installer');
     }
-    // create initial pages
-    await createInitialPages(adminUser, language);
 
+    const appService = crowi.appService;
     appService.setupAfterInstall();
-    appService.publishPostInstallationMessage();
 
     // login with passport
     req.logIn(adminUser, (err) => {

+ 7 - 18
packages/app/src/server/service/app.ts

@@ -2,8 +2,6 @@ import { pathUtils } from '@growi/core';
 
 import loggerFactory from '~/utils/logger';
 
-import { generateConfigsForInstalling } from '../models/config';
-
 import S2sMessage from '../models/vo/s2s-message';
 import { S2sMessageHandlable } from './s2s-messaging/handlable';
 import { S2sMessagingService } from './s2s-messaging/base';
@@ -49,6 +47,12 @@ export default class AppService implements S2sMessageHandlable {
     const isDBInitialized = await this.isDBInitialized(true);
     if (isDBInitialized) {
       this.setupAfterInstall();
+
+      // remove message handler
+      const { s2sMessagingService } = this;
+      if (s2sMessagingService != null) {
+        this.s2sMessagingService.removeMessageHandler(this);
+      }
     }
   }
 
@@ -101,15 +105,6 @@ export default class AppService implements S2sMessageHandlable {
     return this.configManager.getConfig('crowi', 'app:confidential');
   }
 
-  /**
-   * Execute only once for installing application
-   */
-  async initDB(globalLang) {
-    const initialConfig = generateConfigsForInstalling();
-    initialConfig['app:globalLang'] = globalLang;
-    await this.configManager.updateConfigsInTheSameNamespace('crowi', initialConfig, true);
-  }
-
   async isDBInitialized(forceReload) {
     if (forceReload) {
       // load configs
@@ -118,16 +113,10 @@ export default class AppService implements S2sMessageHandlable {
     return this.configManager.getConfigFromDB('crowi', 'app:installed');
   }
 
-  async setupAfterInstall() {
+  async setupAfterInstall(): Promise<void> {
     await this.crowi.pluginService.autoDetectAndLoadPlugins();
     this.crowi.setupRoutesAtLast();
     this.crowi.setupGlobalErrorHandlers();
-
-    // remove message handler
-    const { s2sMessagingService } = this;
-    if (s2sMessagingService != null) {
-      this.s2sMessagingService.removeMessageHandler(this);
-    }
   }
 
 }

+ 30 - 0
packages/app/src/server/service/config-loader.ts

@@ -172,6 +172,36 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.BOOLEAN,
     default: false,
   },
+  AUTO_INSTALL_ADMIN_USERNAME: {
+    ns:      'crowi',
+    key:     'autoInstall:adminUsername',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AUTO_INSTALL_ADMIN_NAME: {
+    ns:      'crowi',
+    key:     'autoInstall:adminName',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AUTO_INSTALL_ADMIN_EMAIL: {
+    ns:      'crowi',
+    key:     'autoInstall:adminEmail',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AUTO_INSTALL_ADMIN_PASSWORD: {
+    ns:      'crowi',
+    key:     'autoInstall:adminPassword',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AUTO_INSTALL_GLOBAL_LANG: {
+    ns:      'crowi',
+    key:     'autoInstall:globalLang',
+    type:    ValueType.STRING,
+    default: null,
+  },
   S2SMSG_PUBSUB_SERVER_TYPE: {
     ns:      'crowi',
     key:     's2sMessagingPubsub:serverType',

+ 118 - 0
packages/app/src/server/service/installer.ts

@@ -0,0 +1,118 @@
+import mongoose from 'mongoose';
+import fs from 'graceful-fs';
+import path from 'path';
+import ExtensibleCustomError from 'extensible-custom-error';
+
+import { IPage } from '~/interfaces/page';
+import { IUser } from '~/interfaces/user';
+import { Lang } from '~/interfaces/lang';
+import loggerFactory from '~/utils/logger';
+
+import { generateConfigsForInstalling } from '../models/config';
+
+import SearchService from './search';
+import ConfigManager from './config-manager';
+
+const logger = loggerFactory('growi:service:installer');
+
+export class FailedToCreateAdminUserError extends ExtensibleCustomError {
+}
+
+export class InstallerService {
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  crowi: any;
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
+  constructor(crowi: any) {
+    this.crowi = crowi;
+  }
+
+  private async initSearchIndex() {
+    const searchService: SearchService = this.crowi.searchService;
+    if (!searchService.isReachable) {
+      return;
+    }
+
+    await searchService.rebuildIndex();
+  }
+
+  private async createPage(filePath, pagePath, owner): Promise<IPage|undefined> {
+
+    // TODO typescriptize models/user.js and remove eslint-disable-next-line
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const Page = mongoose.model('Page') as any;
+
+    try {
+      const markdown = fs.readFileSync(filePath);
+      return Page.create(pagePath, markdown, owner, {}) as IPage;
+    }
+    catch (err) {
+      logger.error(`Failed to create ${pagePath}`, err);
+    }
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  private async createInitialPages(owner, lang: Lang): Promise<any> {
+    const { localeDir } = this.crowi;
+
+    const promises: Promise<IPage|undefined>[] = [];
+
+    // create portal page for '/'
+    promises.push(this.createPage(path.join(localeDir, lang, 'welcome.md'), '/', owner));
+
+    // create /Sandbox/*
+    promises.push(this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox', owner));
+    promises.push(this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap4.md'), '/Sandbox/Bootstrap4', owner));
+    promises.push(this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner));
+    promises.push(this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner));
+
+    await Promise.all(promises);
+
+    try {
+      await this.initSearchIndex();
+    }
+    catch (err) {
+      logger.error('Failed to build Elasticsearch Indices', err);
+    }
+  }
+
+  /**
+   * Execute only once for installing application
+   */
+  private async initDB(globalLang: Lang): Promise<void> {
+    const configManager: ConfigManager = this.crowi.configManager;
+
+    const initialConfig = generateConfigsForInstalling();
+    initialConfig['app:globalLang'] = globalLang;
+    return configManager.updateConfigsInTheSameNamespace('crowi', initialConfig, true);
+  }
+
+  async install(firstAdminUserToSave: IUser, globalLang: Lang): Promise<IUser> {
+    await this.initDB(globalLang);
+
+    // TODO typescriptize models/user.js and remove eslint-disable-next-line
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const User = mongoose.model('User') as any;
+
+    // create first admin user
+    // TODO: with transaction
+    let adminUser;
+    try {
+      const {
+        name, username, email, password,
+      } = firstAdminUserToSave;
+      adminUser = await User.createUser(name, username, email, password, globalLang);
+      await adminUser.asyncMakeAdmin();
+    }
+    catch (err) {
+      throw new FailedToCreateAdminUserError(err);
+    }
+
+    // create initial pages
+    await this.createInitialPages(adminUser, globalLang);
+
+    return adminUser;
+  }
+
+}

+ 1 - 1
packages/app/src/server/views/installer.html

@@ -69,7 +69,7 @@
           </div>
         </div>
         <div class="col-md-12">
-          <div id="installer-form"
+          <div id="installer-form-container"
             data-user-name="{{ req.body.registerForm.username }}"
             data-name="{{ req.body.registerForm.name }}"
             data-email="{{ req.body.registerForm.email }}"

+ 0 - 18
packages/app/src/test/integration/setup-crowi.js

@@ -1,18 +0,0 @@
-import Crowi from '~/server/crowi';
-
-let _instance = null;
-
-export async function getInstance(isNewInstance) {
-  if (isNewInstance) {
-    const crowi = new Crowi();
-    await crowi.initForTest();
-    return crowi;
-  }
-
-  // initialize singleton instance
-  if (_instance == null) {
-    _instance = new Crowi();
-    await _instance.initForTest();
-  }
-  return _instance;
-}

+ 8 - 0
packages/app/test/cypress/.eslintrc.js

@@ -0,0 +1,8 @@
+module.exports = {
+  root: true,
+  extends: [
+    'weseek/typescript',
+    'plugin:cypress/recommended',
+  ],
+  plugins: ['@typescript-eslint', 'cypress'],
+};

+ 6 - 0
packages/app/test/cypress/fixtures/user-admin.json

@@ -0,0 +1,6 @@
+{
+  "username": "admin",
+  "name": "Admin",
+  "email": "admin@example.com",
+  "password": "adminadmin"
+}

+ 58 - 0
packages/app/test/cypress/integration/1-install/install.spec.ts

@@ -0,0 +1,58 @@
+context('Installer', () => {
+
+  const ssPrefix = 'installer-';
+
+  beforeEach(() => {
+    cy.visit('/');
+  })
+
+  it('successfully loads', () => {
+    cy.screenshot(`${ssPrefix}-on-load`);
+    cy.getByTestid('installerForm').should('be.visible');
+  });
+
+  it('the dropdown for language works', () => {
+    cy.getByTestid('dropdownLanguage').should('be.visible');
+
+    cy.getByTestid('dropdownLanguage').click();
+    cy.screenshot(`${ssPrefix}-open-dropdownLanguage`);
+    cy.getByTestid('dropdownLanguage').click(); // close
+
+    cy.getByTestid('dropdownLanguage').click();
+    cy.getByTestid('dropdownLanguageMenu-en_US').click();
+    cy.screenshot(`${ssPrefix}-select-en_US`);
+
+    cy.getByTestid('dropdownLanguage').click();
+    cy.getByTestid('dropdownLanguageMenu-ja_JP').click();
+    cy.screenshot(`${ssPrefix}-select-ja_JP`);
+
+    cy.getByTestid('dropdownLanguage').click();
+    cy.getByTestid('dropdownLanguageMenu-zh_CN').click();
+    cy.screenshot(`${ssPrefix}-select-zh_CN`);
+  });
+
+});
+
+context('Installing', () => {
+
+  const ssPrefix = 'installing-';
+
+  beforeEach(() => {
+    cy.visit('/');
+  })
+
+  it('has succeeded', () => {
+    cy.fixture("user-admin.json").then(user => {
+      cy.getByTestid('tiUsername').type(user.username);
+      cy.getByTestid('tiName').type(user.name);
+      cy.getByTestid('tiEmail').type(user.email);
+      cy.getByTestid('tiPassword').type(user.password);
+    });
+    cy.screenshot(`${ssPrefix}-before-submit`);
+
+    cy.getByTestid('btnSubmit').click();
+
+    cy.screenshot(`${ssPrefix}-installed`, { capture: 'viewport' });
+  });
+
+});

+ 102 - 0
packages/app/test/cypress/integration/2-advanced-examples/misc.spec.ts

@@ -0,0 +1,102 @@
+context('Misc', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/misc')
+  })
+
+  it('.end() - end the command chain', () => {
+    // https://on.cypress.io/end
+
+    // cy.end is useful when you want to end a chain of commands
+    // and force Cypress to re-query from the root element
+    cy.get('.misc-table').within(() => {
+      // ends the current chain and yields null
+      cy.contains('Cheryl').click().end()
+
+      // queries the entire table again
+      cy.contains('Charles').click()
+    })
+  })
+
+  it('cy.exec() - execute a system command', () => {
+    // execute a system command.
+    // so you can take actions necessary for
+    // your test outside the scope of Cypress.
+    // https://on.cypress.io/exec
+
+    // we can use Cypress.platform string to
+    // select appropriate command
+    // https://on.cypress/io/platform
+    cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
+
+    // on CircleCI Windows build machines we have a failure to run bash shell
+    // https://github.com/cypress-io/cypress/issues/5169
+    // so skip some of the tests by passing flag "--env circle=true"
+    const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
+
+    if (isCircleOnWindows) {
+      cy.log('Skipping test on CircleCI')
+
+      return
+    }
+
+    // cy.exec problem on Shippable CI
+    // https://github.com/cypress-io/cypress/issues/6718
+    const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
+
+    if (isShippable) {
+      cy.log('Skipping test on ShippableCI')
+
+      return
+    }
+
+    cy.exec('echo Jane Lane')
+      .its('stdout').should('contain', 'Jane Lane')
+
+    if (Cypress.platform === 'win32') {
+      cy.exec('print cypress.json')
+        .its('stderr').should('be.empty')
+    } else {
+      cy.exec('cat cypress.json')
+        .its('stderr').should('be.empty')
+
+      cy.exec('pwd')
+        .its('code').should('eq', 0)
+    }
+  })
+
+  it('cy.focused() - get the DOM element that has focus', () => {
+    // https://on.cypress.io/focused
+    cy.get('.misc-form').find('#name').click()
+    cy.focused().should('have.id', 'name')
+
+    cy.get('.misc-form').find('#description').click()
+    cy.focused().should('have.id', 'description')
+  })
+
+  context('Cypress.Screenshot', function () {
+    it('cy.screenshot() - take a screenshot', () => {
+      // https://on.cypress.io/screenshot
+      cy.screenshot('my-image')
+    })
+
+    it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
+      Cypress.Screenshot.defaults({
+        blackout: ['.foo'],
+        capture: 'viewport',
+        clip: { x: 0, y: 0, width: 200, height: 200 },
+        scale: false,
+        disableTimersAndAnimations: true,
+        screenshotOnRunFailure: true,
+        onBeforeScreenshot () { },
+        onAfterScreenshot () { },
+      })
+    })
+  })
+
+  it('cy.wrap() - wrap an object', () => {
+    // https://on.cypress.io/wrap
+    cy.wrap({ foo: 'bar' })
+      .should('have.property', 'foo')
+      .and('include', 'bar')
+  })
+})

+ 59 - 0
packages/app/test/cypress/integration/2-advanced-examples/viewport.spec.ts

@@ -0,0 +1,59 @@
+context('Viewport', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/viewport')
+  })
+
+  it('cy.viewport() - set the viewport size and dimension', () => {
+    // https://on.cypress.io/viewport
+
+    cy.get('#navbar').should('be.visible')
+    cy.viewport(320, 480)
+
+    // the navbar should have collapse since our screen is smaller
+    cy.get('#navbar').should('not.be.visible')
+    cy.get('.navbar-toggle').should('be.visible').click()
+    cy.get('.nav').find('a').should('be.visible')
+
+    // lets see what our app looks like on a super large screen
+    cy.viewport(2999, 2999)
+
+    // cy.viewport() accepts a set of preset sizes
+    // to easily set the screen to a device's width and height
+
+    // We added a cy.wait() between each viewport change so you can see
+    // the change otherwise it is a little too fast to see :)
+
+    /* eslint-disable cypress/no-unnecessary-waiting */
+    cy.viewport('macbook-15')
+    cy.wait(200)
+    cy.viewport('macbook-13')
+    cy.wait(200)
+    cy.viewport('macbook-11')
+    cy.wait(200)
+    cy.viewport('ipad-2')
+    cy.wait(200)
+    cy.viewport('ipad-mini')
+    cy.wait(200)
+    cy.viewport('iphone-6+')
+    cy.wait(200)
+    cy.viewport('iphone-6')
+    cy.wait(200)
+    cy.viewport('iphone-5')
+    cy.wait(200)
+    cy.viewport('iphone-4')
+    cy.wait(200)
+    cy.viewport('iphone-3')
+    cy.wait(200)
+
+    // cy.viewport() accepts an orientation for all presets
+    // the default orientation is 'portrait'
+    cy.viewport('ipad-2', 'portrait')
+    cy.wait(200)
+    cy.viewport('iphone-4', 'landscape')
+    cy.wait(200)
+    /* eslint-enable cypress/no-unnecessary-waiting */
+
+    // The viewport will be reset back to the default dimensions
+    // in between tests (the  default can be set in cypress.json)
+  })
+})

+ 33 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts

@@ -0,0 +1,33 @@
+const ssPrefix = 'access-to-page-';
+
+context('Access to page', () => {
+
+  let connectSid: string | undefined;
+
+  before(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+    cy.getCookie('connect.sid').then(cookie => {
+      connectSid = cookie?.value;
+    });
+  });
+
+  beforeEach(() => {
+    if (connectSid != null) {
+      cy.setCookie('connect.sid', connectSid);
+    }
+  });
+
+  it('/Sandbox is successfully loaded', () => {
+    cy.visit('/Sandbox', {  });
+    cy.screenshot(`${ssPrefix}-sandbox`, { capture: 'viewport' });
+  });
+
+  it('/Sandbox with anchor hash is successfully loaded', () => {
+    cy.visit('/Sandbox#Headers');
+    cy.screenshot(`${ssPrefix}-sandbox-headers`, { capture: 'viewport' });
+  });
+
+});

+ 22 - 0
packages/app/test/cypress/plugins/index.ts

@@ -0,0 +1,22 @@
+/// <reference types="cypress" />
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line no-unused-vars
+module.exports = (on, config) => {
+  // `on` is used to hook into various events Cypress emits
+  // `config` is the resolved Cypress config
+}

+ 39 - 0
packages/app/test/cypress/support/commands.ts

@@ -0,0 +1,39 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
+
+
+Cypress.Commands.add('getByTestid', (selector, options?) => {
+  return cy.get(`[data-testid=${selector}]`, options);
+});
+
+Cypress.Commands.add('login', (username, password) => {
+  cy.session(username, () => {
+    cy.visit('/login');
+    cy.getByTestid('tiUsernameForLogin').type(username);
+    cy.getByTestid('tiPasswordForLogin').type(password);
+    cy.getByTestid('btnSubmitForLogin').click();
+  });
+});

+ 30 - 0
packages/app/test/cypress/support/index.ts

@@ -0,0 +1,30 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+declare global {
+  // eslint-disable-next-line @typescript-eslint/no-namespace
+  namespace Cypress {
+    interface Chainable {
+       getByTestid(selector: string, options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<Element>>,
+       login(username: string, password: string): Chainable<void>,
+    }
+  }
+}

+ 13 - 0
packages/app/test/cypress/tsconfig.json

@@ -0,0 +1,13 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "noEmit": true,
+    // be explicit about types included
+    // to avoid clashing with Jest types
+    "types": ["cypress"]
+  },
+  "include": [
+    "../../node_modules/cypress",
+    "./**/*.ts"
+  ]
+}

+ 0 - 0
packages/app/src/test/integration/crowi/crowi.test.js → packages/app/test/integration/crowi/crowi.test.js


+ 0 - 0
packages/app/src/test/integration/global-setup.js → packages/app/test/integration/global-setup.js


+ 0 - 0
packages/app/src/test/integration/global-teardown.js → packages/app/test/integration/global-teardown.js


+ 0 - 0
packages/app/src/test/integration/middlewares/access-token-parser.test.js → packages/app/test/integration/middlewares/access-token-parser.test.js


+ 0 - 0
packages/app/src/test/integration/middlewares/login-required.test.js → packages/app/test/integration/middlewares/login-required.test.js


+ 1 - 1
packages/app/src/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts → packages/app/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts

@@ -2,7 +2,7 @@ import mongoose from 'mongoose';
 import { Collection } from 'mongodb';
 import { getMongoUri, mongoOptions } from '@growi/core';
 
-const migrate = require('../../../migrations/20210913153942-migrate-slack-app-integration-schema');
+const migrate = require('~/migrations/20210913153942-migrate-slack-app-integration-schema');
 
 describe('migrate-slack-app-integration-schema', () => {
 

+ 0 - 0
packages/app/src/test/integration/models/config.test.js → packages/app/test/integration/models/config.test.js


+ 0 - 0
packages/app/src/test/integration/models/page.test.js → packages/app/test/integration/models/page.test.js


+ 0 - 0
packages/app/src/test/integration/models/share-link.test.js → packages/app/test/integration/models/share-link.test.js


+ 0 - 0
packages/app/src/test/integration/models/update-post.test.js → packages/app/test/integration/models/update-post.test.js


+ 0 - 0
packages/app/src/test/integration/models/user.test.js → packages/app/test/integration/models/user.test.js


+ 0 - 0
packages/app/src/test/integration/service/acl.test.js → packages/app/test/integration/service/acl.test.js


+ 0 - 0
packages/app/src/test/integration/service/config-manager.test.js → packages/app/test/integration/service/config-manager.test.js


+ 0 - 0
packages/app/src/test/integration/service/page.test.js → packages/app/test/integration/service/page.test.js


+ 0 - 0
packages/app/src/test/integration/service/passport.test.js → packages/app/test/integration/service/passport.test.js


+ 0 - 0
packages/app/src/test/integration/service/search/search-service.test.js → packages/app/test/integration/service/search/search-service.test.js


+ 39 - 0
packages/app/test/integration/setup-crowi.js

@@ -0,0 +1,39 @@
+import Crowi from '~/server/crowi';
+
+let _instance = null;
+
+const initCrowi = async(crowi) => {
+  await crowi.setupModels();
+  await crowi.setupConfigManager();
+
+  await crowi.setupSocketIoService();
+
+  await Promise.all([
+    crowi.setUpApp(),
+    crowi.setUpXss(),
+  ]);
+
+  await Promise.all([
+    crowi.setupPassport(),
+    crowi.setupAttachmentService(),
+    crowi.setUpAcl(),
+    crowi.setupPageService(),
+    crowi.setupInAppNotificationService(),
+    crowi.setupActivityService(),
+  ]);
+};
+
+export async function getInstance(isNewInstance) {
+  if (isNewInstance) {
+    const crowi = new Crowi();
+    await initCrowi(crowi);
+    return crowi;
+  }
+
+  // initialize singleton instance
+  if (_instance == null) {
+    _instance = new Crowi();
+    await initCrowi(_instance);
+  }
+  return _instance;
+}

+ 0 - 0
packages/app/src/test/integration/setup.js → packages/app/test/integration/setup.js


+ 0 - 0
packages/app/src/test/integration/utils/slack-legacy.test.js → packages/app/test/integration/utils/slack-legacy.test.js


+ 6 - 0
packages/app/test/tsconfig.json

@@ -0,0 +1,6 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "isolatedModules": false,
+  },
+}

+ 0 - 0
packages/app/src/test/unit/middlewares/safe-redirect.test.js → packages/app/test/unit/middlewares/safe-redirect.test.js


+ 0 - 0
packages/app/src/test/unit/migrate-mongo-config.test.js → packages/app/test/unit/migrate-mongo-config.test.js


+ 0 - 0
packages/app/src/test/unit/utils/to-array-from-csv.test.js → packages/app/test/unit/utils/to-array-from-csv.test.js


+ 0 - 1
packages/app/tsconfig.build.server.json

@@ -22,6 +22,5 @@
     "src/stores",
     "src/styles",
     "src/styles-hackmd",
-    "src/test"
   ]
 }

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 2 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
@@ -19,6 +19,7 @@
     "bunyan": "^1.8.15",
     "extensible-custom-error": "^0.0.7",
     "http-errors": "^1.8.0",
+    "qs": "^6.10.2",
     "universal-bunyan": "^0.9.2",
     "url-join": "^4.0.0"
   },

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "4.5.9-slackbot-proxy.0",
+  "version": "4.5.10-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^4.5.9-RC.0",
+    "@growi/slack": "^4.5.10-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "4.5.9-RC.0",
+  "version": "4.5.10-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

Разница между файлами не показана из-за своего большого размера
+ 536 - 25
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов