Compare commits
303 Commits
feature/wo
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
cc7866089b | ||
|
c5fc734bb1 | ||
|
e6645c12a8 | ||
|
00c3e7c263 | ||
|
ea6b0d50f0 | ||
|
f73bf6fdeb | ||
|
039d634e62 | ||
|
0ce78b62a0 | ||
|
2bc11c6dfe | ||
|
e4f2cdede6 | ||
|
4b8719ab9c | ||
|
4bc8fbc557 | ||
|
206f91d766 | ||
|
8bece51a20 | ||
|
0ea1d81ad3 | ||
|
602f8f4b73 | ||
|
930976f67e | ||
|
e6b4c02e34 | ||
|
8f65b1390b | ||
|
c6239b2770 | ||
|
bf5db3f47b | ||
|
628872f27c | ||
|
dfea4b4779 | ||
|
afbd63646a | ||
|
39c80352b3 | ||
|
16b02fcbaa | ||
|
e197aa41fe | ||
|
2d6bba2224 | ||
|
5c6a45875c | ||
|
f5c2bd88f2 | ||
|
1c6908b6a5 | ||
|
99739064cc | ||
|
dc82ee9be6 | ||
|
7b710d4d13 | ||
|
2ecbc53983 | ||
|
c3f8b2a938 | ||
|
e7adb05c3d | ||
|
5e30c2898e | ||
|
7e83c926ae | ||
|
26f8526f70 | ||
|
b5bce230c1 | ||
|
624308bae8 | ||
|
28591f00a3 | ||
|
23f4dd5756 | ||
|
9392b93d10 | ||
|
da08621dc6 | ||
|
3ba41f78ad | ||
|
c8c1031c29 | ||
|
0b6d48bddc | ||
|
4e993f4d55 | ||
|
12206fd3ee | ||
|
3cc76d0b61 | ||
|
4a158ae6e8 | ||
|
c330e6000d | ||
|
d9e9770eb8 | ||
|
0745d90a5c | ||
|
e58be83214 | ||
|
9dbe7cfa2b | ||
|
15d93abea3 | ||
|
9ca27118c4 | ||
|
29b37b7ed0 | ||
|
d36e33e4df | ||
|
4ac07ef938 | ||
|
ee9a1ea9c7 | ||
|
df6e14c585 | ||
|
0b53b78cda | ||
|
6c396c6ec8 | ||
|
591a98bff1 | ||
|
9afd1d1c7c | ||
|
6b7b3a12d8 | ||
|
41d072bc90 | ||
|
358e822660 | ||
|
3388a15462 | ||
|
bd603eccfb | ||
|
7ff691471b | ||
|
ea5f99b0d2 | ||
|
c0387b1521 | ||
|
09866663f2 | ||
|
1eabbf5df2 | ||
|
7c336e2320 | ||
|
2044da4be9 | ||
|
a14d444960 | ||
|
5e85e5a3b9 | ||
|
99a1eeb356 | ||
|
f08c8bcd2a | ||
|
e42625f1aa | ||
|
b26d7b08c6 | ||
|
f82de45a03 | ||
|
e8e2a9ab2c | ||
|
b2c3d2c0c3 | ||
|
d8adb4596c | ||
|
e757da19eb | ||
|
5a8fab4e4b | ||
|
2ad6a77740 | ||
|
1a15faad42 | ||
|
d7c772e748 | ||
|
9fbbb87fa8 | ||
|
63e7de4fad | ||
|
64a1caf0dd | ||
|
33f18742dd | ||
|
ce3508f8be | ||
|
2ab95463c5 | ||
|
dc91b69bf5 | ||
|
5b96ad7c69 | ||
|
cdd26073af | ||
|
83dc99e1a8 | ||
|
335765e3ea | ||
|
d50f974c3d | ||
|
f031f5fc7c | ||
|
39b6b1a53f | ||
|
d6dc68429a | ||
|
4d71238084 | ||
|
79e06ff40c | ||
|
a7fa577837 | ||
|
393fa21d4c | ||
|
f7e5995add | ||
|
8f53b0f6f9 | ||
|
a679ad20bb | ||
|
9753215281 | ||
|
2ab96606aa | ||
|
0b42899347 | ||
|
cce508d17a | ||
|
1f1dd86f89 | ||
|
053d9751c3 | ||
|
2e1af4ae3a | ||
|
32b1c4888d | ||
|
c122589a2e | ||
|
e0537ab706 | ||
|
a428b787e7 | ||
|
6559154bca | ||
|
827497114d | ||
|
48fdb3138a | ||
|
7bb7b0c7fd | ||
|
cd75ac18bc | ||
|
70827bbd5f | ||
|
cffe037269 | ||
|
d9255db8a9 | ||
|
89b249274f | ||
|
2cd79e6091 | ||
|
90d013bce5 | ||
|
4b0f3f793e | ||
|
e0da70838a | ||
|
a2d75a4ed7 | ||
|
c70a93e54e | ||
|
dbf8c4a827 | ||
|
be9da7811d | ||
|
8a4cad1195 | ||
|
77eb0337d1 | ||
|
018e69232d | ||
|
e2e82dc3f3 | ||
|
efabb958aa | ||
|
caca627b18 | ||
|
01eb1dda73 | ||
|
7ad01f6b26 | ||
|
9448d3c111 | ||
|
b26855de90 | ||
|
b224077d19 | ||
|
725ca7bdd3 | ||
|
c363c13d37 | ||
|
1214301ae4 | ||
|
404ba360b9 | ||
|
f9d75962ed | ||
|
c904fd3932 | ||
|
f31cd0f773 | ||
|
34cc06cd9c | ||
|
5f8896eec2 | ||
|
17b84ef501 | ||
|
8db8f9ab19 | ||
|
ae6907a3f4 | ||
|
4115ff9847 | ||
|
ea342a0d0b | ||
|
b18677e709 | ||
|
1bd2968982 | ||
|
df49b8f692 | ||
|
3dc15aeb68 | ||
|
668e8a6ba7 | ||
|
de2af7931f | ||
|
a64e820e80 | ||
|
eb6ba7a1a5 | ||
|
66b7248349 | ||
|
371a3e1078 | ||
|
08feb06d9c | ||
|
89b69a9fa1 | ||
|
8c0563ca83 | ||
|
dc7542de60 | ||
|
29011194c7 | ||
|
b18d872c66 | ||
|
fd6585d80c | ||
|
9526faeec1 | ||
|
4b8bcac049 | ||
|
994a200415 | ||
|
4c85fcc47e | ||
|
38566f1f60 | ||
|
89d416becd | ||
|
de7344b192 | ||
|
2b683b0923 | ||
|
0095e3b7e0 | ||
|
39ff6acf99 | ||
|
e2687865ef | ||
|
fe3159105f | ||
|
d3378ada21 | ||
|
49f0bfbd71 | ||
|
ced773d77b | ||
|
24881d7cdc | ||
|
ae58f9e06d | ||
|
57ec0b3b7f | ||
|
627eabd8b2 | ||
|
3242ad80c4 | ||
|
8ce6dc7607 | ||
|
57918272ac | ||
|
734e33edbb | ||
|
0b0f78a3ed | ||
|
142b3b7be9 | ||
|
58ab49e974 | ||
|
21cba5946b | ||
|
4f116fdcc4 | ||
|
bf5750c634 | ||
|
f782ddf176 | ||
|
6c9044b457 | ||
|
3f7fb8b68c | ||
|
e9b85f7878 | ||
|
5e50a3a733 | ||
|
7b0c1c43c9 | ||
|
eed15cd23c | ||
|
3629796786 | ||
|
b35cad82d3 | ||
|
95917e22f0 | ||
|
f5d3f0d683 | ||
|
ac33ffa34a | ||
|
65884f80f6 | ||
|
9ccadb96f9 | ||
|
127a589b2c | ||
|
ba3b1a64e5 | ||
|
00d5e98712 | ||
|
318020ead7 | ||
|
91838194bb | ||
|
be5d48fa71 | ||
|
6319ca272e | ||
|
6350c95536 | ||
|
3984412646 | ||
|
f67a6e6a45 | ||
|
ee4049b947 | ||
|
76683371cf | ||
|
f9af6219ab | ||
|
2560f0af3d | ||
|
255bd8bab8 | ||
|
6771b831e5 | ||
|
2bca93bf12 | ||
|
c3bbd35c2c | ||
|
5d88d9ad59 | ||
|
39b25acc50 | ||
|
a58bfe79cc | ||
|
f43b7d72d4 | ||
|
be81e2a863 | ||
|
c5489daac3 | ||
|
78f2a7418b | ||
|
dd1d977fbc | ||
|
a357b49824 | ||
|
84a3b6e6fa | ||
|
0b3a8b57dd | ||
|
a39f3ed3b1 | ||
|
c07fc4341d | ||
|
05b9416cd9 | ||
|
071afbfec1 | ||
|
746bece538 | ||
|
b3b5fa9fac | ||
|
ed56816836 | ||
|
d23924347e | ||
|
15a41f1ba7 | ||
|
5c99bd759e | ||
|
968576b665 | ||
|
90243b9875 | ||
|
f86eab9a95 | ||
|
4b860732c0 | ||
|
456df124f1 | ||
|
565e02ee5c | ||
|
e4a8389cf5 | ||
|
c868ca91c2 | ||
|
eea5418734 | ||
|
70f5c45abe | ||
|
3ef5e75cba | ||
|
b8c089f3fd | ||
|
e5f70ad08c | ||
|
225e450bc9 | ||
|
e3fa804fa8 | ||
|
9390295f5b | ||
|
dfdc8025e9 | ||
|
e1f91de9ef | ||
|
203537a9ce | ||
|
0778b68ef8 | ||
|
43186b86ef | ||
|
e34e5a606b | ||
|
c32bbd9124 | ||
|
5fff688612 | ||
|
f851b0df10 | ||
|
cf9f00abf5 | ||
|
620011a19a | ||
|
54adf69db5 | ||
|
d9e0bdfb9a | ||
|
31ddefba16 | ||
|
908537ea0b | ||
|
83cfd57245 | ||
|
08f3bb0578 |
44
.github/workflows/codecov.yml
vendored
Normal file
44
.github/workflows/codecov.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
# Unit Test Coverage Report
|
||||
name: Test Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- releases/**
|
||||
paths:
|
||||
- server/**
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- releases/**
|
||||
- feature/**
|
||||
paths:
|
||||
- server/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd server && npm install
|
||||
|
||||
- name: Run tests and collect coverage
|
||||
run: cd server && npm run test:cov
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
39
.github/workflows/server-lint.yml
vendored
Normal file
39
.github/workflows/server-lint.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Lint
|
||||
name: Server Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- releases/**
|
||||
paths:
|
||||
- server/**
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- releases/**
|
||||
- feature/**
|
||||
paths:
|
||||
- server/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd server && npm install
|
||||
|
||||
- name: Lint
|
||||
run: cd server && npm run lint
|
42
.github/workflows/web-lint.yml
vendored
Normal file
42
.github/workflows/web-lint.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# Lint
|
||||
name: Web Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- releases/**
|
||||
paths:
|
||||
- web/**
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- releases/**
|
||||
- feature/**
|
||||
paths:
|
||||
- web/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd web && npm install
|
||||
|
||||
- name: Type-Check
|
||||
run: cd web && npm run type-check
|
||||
|
||||
- name: Lint
|
||||
run: cd web && npm run lint
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -25,3 +25,10 @@ pnpm-debug.log*
|
||||
*.sw?
|
||||
|
||||
.history
|
||||
|
||||
components.d.ts
|
||||
|
||||
# 默认的上传文件夹
|
||||
userUpload
|
||||
exportfile
|
||||
yarn.lock
|
133
CODE_OF_CONDUCT.md
Normal file
133
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[xiaojusurvey@gmail.com](mailto:xiaojusurvey@gmail.com).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
22
DATA_COLLECTION.md
Normal file
22
DATA_COLLECTION.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Important Disclosure re:XIAOJUSURVEY Data Collection
|
||||
|
||||
XIAOJUSURVEY is open-source software developed and maintained by XIAOJUSURVEY Team and available at https://github.com/didi/xiaoju-survey.
|
||||
We hereby state the purpose and reason for collecting data.
|
||||
|
||||
## Purpose of data collection
|
||||
|
||||
Data collected is used to help improve XIAOJUSURVEY for all users. It is important that our team understands the usage patterns as soon as possible, so we can best decide how to design future features and prioritize current work.
|
||||
|
||||
## Types of data collected
|
||||
|
||||
XIAOJUSURVEY just collects data about version's information. The data collected is subsequently reported to the XIAOJUSURVEY's backend services.
|
||||
|
||||
All data collected will be used exclusively by the XIAOJUSURVEY team for analytical purposes only. The data will be neither accessible nor sold to any third party.
|
||||
|
||||
## Sensitive data
|
||||
|
||||
XIAOJUSURVEY will never collect and/or report sensitive information, such as private keys, API keys, or passwords.
|
||||
|
||||
## How do I opt-in to or opt-out of data sharing?
|
||||
|
||||
See [docs](https://xiaojusurvey.didi.cn/docs/next/community/%E6%95%B0%E6%8D%AE%E4%B8%8A%E6%8A%A5%E5%A3%B0%E6%98%8E) for information on configuring this functionality.
|
14
Dockerfile
14
Dockerfile
@ -1,5 +1,5 @@
|
||||
# 镜像集成
|
||||
FROM node:16
|
||||
FROM node:18-slim
|
||||
|
||||
# 设置工作区间
|
||||
WORKDIR /xiaoju-survey
|
||||
@ -8,24 +8,20 @@ WORKDIR /xiaoju-survey
|
||||
COPY . /xiaoju-survey
|
||||
|
||||
# 安装nginx
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nginx
|
||||
RUN apt-get update && apt-get install -y nginx
|
||||
|
||||
RUN npm config set registry https://registry.npmjs.org/
|
||||
|
||||
# 安装项目依赖
|
||||
RUN cd /xiaoju-survey/web && npm install && npm run build
|
||||
# 用了后端服务代理启动,建议使用nginx启动
|
||||
#RUN cd /xiaoju-survey && cp -af ./web/dist/* ./server/public/
|
||||
RUN cd /xiaoju-survey/web && npm install && npm run build-only
|
||||
|
||||
# 覆盖nginx配置文件
|
||||
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
RUN cd /xiaoju-survey/server && npm install && npm run build
|
||||
|
||||
# 暴露端口 需要跟nginx的port一致
|
||||
# EXPOSE 3000
|
||||
EXPOSE 8080
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# docker入口文件,启动nginx和运行pm2启动,并保证监听不断
|
||||
CMD ["sh","docker-run.sh"]
|
||||
|
2
LICENSE
2
LICENSE
@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright (C) 2023 Beijing Didi Infinity Technology and Development Co.,Ltd. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
185
README.md
185
README.md
@ -4,68 +4,94 @@
|
||||
</p>
|
||||
<div>
|
||||
<a href="https://github.com/didi/xiaoju-survey/graphs/contributors">
|
||||
<img src="https://img.shields.io/github/last-commit/didi/xiaoju-survey?color=red" alt="commit">
|
||||
<img src="https://img.shields.io/badge/node-%3E=18-green" alt="commit">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/pulls">
|
||||
<img src="https://img.shields.io/badge/PRs-welcome-%23ffa600" alt="pr">
|
||||
<a href="https://app.codecov.io/github/didi/xiaoju-survey">
|
||||
<img src="https://img.shields.io/codecov/c/github/didi/xiaoju-survey" alt="codecov">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/issues">
|
||||
<img src="https://img.shields.io/github/issues/didi/xiaoju-survey" alt="issues">
|
||||
<img src="https://img.shields.io/github/issues/didi/xiaoju-survey" alt="issues">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/discussions">
|
||||
<img src="https://img.shields.io/badge/Discussions-%E8%AE%A8%E8%AE%BA-blue" alt="discussions-">
|
||||
<a href="https://github.com/didi/xiaoju-survey/graphs/contributors">
|
||||
<img src="https://img.shields.io/github/last-commit/didi/xiaoju-survey?color=red" alt="commit">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/pulls">
|
||||
<img src="https://img.shields.io/badge/PRs-welcome-%23ffa600" alt="pr">
|
||||
</a>
|
||||
<a href="https://xiaojusurvey.didi.cn">
|
||||
<img src="https://img.shields.io/badge/help-%E6%96%87%E6%A1%A3-blue" alt="docs">
|
||||
<img src="https://img.shields.io/badge/help-%E5%AE%98%E7%BD%91-blue" alt="docs">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/blob/main/README_EN.md">
|
||||
<img src="https://img.shields.io/badge/help-README_EN-50c62a" alt="docs">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
  **XIAOJUSURVEY**是一套轻量、安全的**问卷系统**,提供面向个人和企业的一站式产品级解决方案,快速满足各类线上调研场景。
|
||||
  **XIAOJUSURVEY**是一套轻量、安全的调研系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。
|
||||
|
||||
  系统已沉淀40+种题型,累积精选模板100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。
|
||||
  内部系统已沉淀 40+种题型,累积精选模板 100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。
|
||||
|
||||
# 简介
|
||||
本次开源主要围绕问卷生命周期提供了完整的产品化能力:
|
||||
# 功能特性
|
||||
|
||||
- 问卷管理:创、编、投、收、数据分析
|
||||
**🌈 易用**
|
||||
|
||||
- 多样化题型:单行输入框、多行输入框、单项选择、多项选择、判断题、评分、投票
|
||||
- 多类型数据采集,轻松创建调研表单:文本输入、数据选择、评分、投票、文件上传等。
|
||||
|
||||
_(更多题型将陆续开放。快速[自定义题型](https://xiaojusurvey.didi.cn/docs/next/document/%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/%E9%A2%98%E5%9E%8B%E6%89%A9%E5%B1%95))_
|
||||
- 智能逻辑编排,设计多规则动态表单:显示逻辑、跳转逻辑、选项引用、题目引用等。
|
||||
|
||||
- 用户管理:登录、注册、权限管理
|
||||
- 精细权限管理,支持高效团队协同:空间管理、多角色权限管理等。
|
||||
|
||||
- 数据安全:传输加密、脱敏等
|
||||
- 数据在线分析和导出,洞察调研结果:数据导出、回收数据管理、分题统计、交叉分析等。
|
||||
|
||||
> 查阅[官方Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
**🎨 好看**
|
||||
|
||||
- 主题自由定制,适配您的品牌:自定义颜色、背景、图片、Logo、结果页规则等。
|
||||
|
||||
- 无缝嵌入各终端,满足不同场景需求:多端嵌入式小问卷 SDK。
|
||||
|
||||
**🚀 安全、可扩展**
|
||||
|
||||
- 安全能力可扩展,提供安全相关建设的经验指导:传输加密、敏感词库、发布审查等。
|
||||
|
||||
- 自定义 Hook 配置,轻松集成多方系统与各类工具:数据推送集成、消息推送集成等。
|
||||
|
||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
|
||||
|
||||
_**(个人和企业用户均可快速构建特定领域的调研类解决方案。)**_
|
||||
1、 **全部功能**请查看 [功能介绍](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B)。
|
||||
|
||||
2、**企业**和**个人**均可快速构建特定领域的调研类解决方案。
|
||||
|
||||
# 技术
|
||||
Web端:Vue2(Vue3版本24年上半年推出)+ ElementUI
|
||||
|
||||
Server端:Nestjs + MongoDB
|
||||
**1、Web 端:Vue3 + ElementPlus**
|
||||
|
||||
架构:[架构解读](https://xiaojusurvey.didi.cn/docs/next/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E6%9E%B6%E6%9E%84)
|
||||
  C 端多端渲染:ReactNative SDK 建设中
|
||||
|
||||
**2、Server 端:NestJS + MongoDB**
|
||||
|
||||
  Java 版:建设中,[欢迎加入共建](https://github.com/didi/xiaoju-survey/issues/306)
|
||||
|
||||
**3、能力增强**
|
||||
|
||||
  在线平台:建设中、智能化问卷:规划中
|
||||
|
||||
# 项目优势
|
||||
|
||||
**一、具备全面的综合性和专业性**
|
||||
|
||||
- [制定了问卷标准化协议规范](https://xiaojusurvey.didi.cn/docs/next/agreement/%E3%80%8A%E9%97%AE%E5%8D%B7Meta%E5%8D%8F%E8%AE%AE%E3%80%8B)
|
||||
|
||||
领域标准保障概念互通,是全系统的基础和核心。基于实际业务经验,沉淀了两大类:
|
||||
|
||||
- 业务描述:问卷协议、题型协议
|
||||
- 物料描述:题型物料协议,包含题型和设置器
|
||||
|
||||
- [制定了问卷UI/UX规范](https://xiaojusurvey.didi.cn/docs/next/design/%E3%80%8A%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83%E3%80%8B)
|
||||
- [制定了问卷 UI/UX 规范](https://xiaojusurvey.didi.cn/docs/next/design/%E3%80%8A%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83%E3%80%8B)
|
||||
|
||||
设计语言是系统灵活性、一致性的基石,保障系统支撑的实际业务运转拥有极高的用户体验。包含两部分:
|
||||
|
||||
- 设计规范:灵活、降噪、统一
|
||||
- 交互规范:遵循用户行为特征,遵循产品定位,遵循成熟的用户习惯
|
||||
|
||||
@ -76,13 +102,14 @@ Server端:Nestjs + MongoDB
|
||||
- [题型物料化设计,自由定制扩展](https://xiaojusurvey.didi.cn/docs/next/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E9%A2%98%E5%9E%8B%E7%89%A9%E6%96%99%E5%8C%96%E8%AE%BE%E8%AE%A1/%E5%9F%BA%E7%A1%80%E8%AE%BE%E8%AE%A1)
|
||||
|
||||
题型是问卷最核心的组成部分,而题型可配置化能力决定了上层业务可扩展的场景以及系统自身可复用的场景。
|
||||
题型架构设计上,主打每一类题型拥有通用基础能力,每一种题型拥有原子化特性能力,并保障高度定制化。
|
||||
题型架构设计上,主打每一类题型拥有通用基础能力,每一种题型拥有原子化特性能力,并保障高度定制化。
|
||||
|
||||
- [合规建设沉淀积累,安全能力拓展性高](https://xiaojusurvey.didi.cn/docs/next/document/%E5%AE%89%E5%85%A8%E8%AE%BE%E8%AE%A1/%E6%A6%82%E8%BF%B0)
|
||||
|
||||
数据加密传输、敏感信息精细化检测、投票防刷等能力,保障问卷发布、数据回收链路安全性。
|
||||
|
||||
**二、轻量化设计,快速接入、灵活扩展**
|
||||
|
||||
- [产品级开源方案,快速产出一套调研流程](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E6%A6%82%E8%BF%B0)
|
||||
|
||||
围绕问卷生命周期提供了完整的产品化能力,包含用户管理: 登录、注册、问卷权限,问卷管理: 创、编、投、收、数据分析,可快速构建特定领域的调研类解决方案。
|
||||
@ -97,106 +124,54 @@ Server端:Nestjs + MongoDB
|
||||
|
||||
- [部署成本低,快速上线](https://xiaojusurvey.didi.cn/docs/next/document/%E5%B7%A5%E7%A8%8B%E9%83%A8%E7%BD%B2/Docker%E9%83%A8%E7%BD%B2)
|
||||
|
||||
前后端分离,提供Docker化方案,提供了完善的部署指导手册。
|
||||
前后端分离,提供 Docker 化方案,提供了完善的部署指导手册。
|
||||
|
||||
# 快速启动
|
||||
# 快速使用
|
||||
|
||||
Node版本 >= 16.x,
|
||||
[查看环境准备指导](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
|
||||
_(在线平台建设中)_
|
||||
|
||||
复制工程
|
||||
```shell
|
||||
git clone git@github.com:didi/xiaoju-survey.git
|
||||
```
|
||||
# 本地开发
|
||||
|
||||
## 服务端启动
|
||||
请查看 [本地安装手册](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) 来启动项目。
|
||||
|
||||
### 方案一、快速启动,无需安装数据库
|
||||
> _便于快速预览工程,对于正式项目需要使用方案二。_
|
||||
# 快速部署
|
||||
|
||||
#### 1、安装依赖
|
||||
```shell
|
||||
cd server
|
||||
npm install
|
||||
```
|
||||
### 服务部署
|
||||
|
||||
#### 2、启动
|
||||
```shell
|
||||
npm run local
|
||||
```
|
||||
请查看 [快速部署指导](https://xiaojusurvey.didi.cn/docs/next/document/%E5%B7%A5%E7%A8%8B%E9%83%A8%E7%BD%B2/Docker%E9%83%A8%E7%BD%B2) 。
|
||||
|
||||
> 服务运行依赖 [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server):
|
||||
>
|
||||
> 1、数据保存在内存中,重启服务会更新数据。<br />2、启动内存服务器新实例时,如果找不到MongoDB二进制文件会自动下载,因此首次可能需要一些时间。
|
||||
### 一键部署
|
||||
|
||||
### 方案二、(生产推荐)
|
||||
_(手册编写中)_
|
||||
|
||||
#### 1、启动数据库
|
||||
|
||||
> 项目使用MongoDB:[MongoDB安装指导](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
||||
|
||||
- 配置数据库,查看[MongoDB配置](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93)
|
||||
|
||||
- 启动本地数据库,查看[MongoDB启动](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E4%BA%94%E5%90%AF%E5%8A%A8)
|
||||
|
||||
#### 2、安装依赖
|
||||
```shell
|
||||
cd server
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 3、启动
|
||||
```shell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 前端启动
|
||||
### 安装依赖
|
||||
```shell
|
||||
cd web
|
||||
npm install
|
||||
```
|
||||
### 启动
|
||||
```shell
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## 访问
|
||||
### 问卷管理端
|
||||
|
||||
[http://localhost:8080/management](http://localhost:8080)
|
||||
|
||||
### 问卷投放端
|
||||
创建并发布问卷。
|
||||
|
||||
[http://localhost:8080/render/:surveyPath](http://localhost:8080/render/:surveyPath)
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 微信交流群
|
||||
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
||||
|
||||
## QQ交流群
|
||||
|
||||
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="210" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
||||
<br />
|
||||
|
||||
## Star
|
||||
如果该项目对你有帮助,star一下 ❤️❤️❤️
|
||||
|
||||
开源不易,如果该项目对你有帮助,请 star 一下 ❤️❤️❤️,你的支持是我们最大的动力。
|
||||
|
||||
[![Star History Chart](https://api.star-history.com/svg?repos=didi/xiaoju-survey&type=Date)](https://star-history.com/#didi/xiaoju-survey&Date)
|
||||
|
||||
## 记录
|
||||
[谁在使用](https://github.com/didi/xiaoju-survey/issues/64)
|
||||
## 交流群
|
||||
|
||||
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入。
|
||||
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
||||
|
||||
_任何问题和合作可以联系小助手。_
|
||||
|
||||
## 案例
|
||||
|
||||
如果你使用了该项目,请记录反馈:[我在使用](https://github.com/didi/xiaoju-survey/issues/64),你的支持是我们最大的动力。
|
||||
|
||||
## Future Tasks
|
||||
|
||||
[欢迎了解项目发展和共建](https://github.com/didi/xiaoju-survey/issues/85),你的支持是我们最大的动力。
|
||||
|
||||
## 贡献
|
||||
[贡献者指南](https://xiaojusurvey.didi.cn/docs/next/share/%E7%94%9F%E6%80%81%E5%BB%BA%E8%AE%BE)
|
||||
|
||||
## Feature
|
||||
[官方Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
如果你想成为贡献者或者扩展技术栈,请查看:[贡献者指南](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE),你的加入使我们最大的荣幸。
|
||||
|
||||
## CHANGELOG
|
||||
[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
||||
|
||||
## 分享
|
||||
[文章](https://github.com/didi/xiaoju-survey/issues)
|
||||
关注项目重大变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)。
|
||||
|
237
README_EN.md
Normal file
237
README_EN.md
Normal file
@ -0,0 +1,237 @@
|
||||
<div align=center>
|
||||
<p>
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/j8lBA6yy201698840712358.jpg" width="300" align='center' />
|
||||
</p>
|
||||
<div>
|
||||
<a href="https://github.com/didi/xiaoju-survey/graphs/contributors">
|
||||
<img src="https://img.shields.io/badge/node-%3E=18-green" alt="commit">
|
||||
</a>
|
||||
<a href="https://app.codecov.io/github/didi/xiaoju-survey">
|
||||
<img src="https://img.shields.io/codecov/c/github/didi/xiaoju-survey" alt="codecov">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/issues">
|
||||
<img src="https://img.shields.io/github/issues/didi/xiaoju-survey" alt="issues">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/graphs/contributors">
|
||||
<img src="https://img.shields.io/github/last-commit/didi/xiaoju-survey?color=red" alt="commit">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/pulls">
|
||||
<img src="https://img.shields.io/badge/PRs-welcome-%23ffa600" alt="pr">
|
||||
</a>
|
||||
<a href="https://xiaojusurvey.didi.cn">
|
||||
<img src="https://img.shields.io/badge/help-website-blue" alt="docs">
|
||||
</a>
|
||||
<a href="https://github.com/didi/xiaoju-survey/blob/main/README.md">
|
||||
<img src="https://img.shields.io/badge/help-README_ZH-50c62a" alt="docs">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
  XIAOJUSURVEY is an enterprises form builder and analytics platform to create questionnaires, exams, polls, quizzes, and analyze data online.
|
||||
|
||||
  The internal system has accumulated over 40 question types and more than 100 selected templates, suitable for market research, customer satisfaction surveys, online exams, voting, reporting, evaluations, and many other scenarios. In terms of data capabilities, it has been honed through hundreds of millions of iterations, resulting in the ability to provide online reports with per-question statistics, cross-analysis, and multi-channel analysis, quickly meeting professional analysis needs.
|
||||
|
||||
# Features
|
||||
|
||||
**🌈 Easy to use**
|
||||
|
||||
- Multi-type data collection, easy to create forms: text input, data selection, scoring, voting, file upload, etc.
|
||||
|
||||
- Smart logic arrangement, design multi-rule dynamic forms: display logic, jump logic, option reference, title reference, etc.
|
||||
|
||||
- Multiple permission management, support efficient team collaboration: space management, multi-role permission management, etc.
|
||||
|
||||
- Online data analysis and export, insight into survey results: data export, recycled data management, sub-topic statistics, cross-analysis, etc.
|
||||
|
||||
**🎨 Good-looking**
|
||||
|
||||
- Free customization of themes to adapt to your brand: custom colors, backgrounds, pictures, logos, result page rules, etc.
|
||||
|
||||
- Seamlessly embedded in various terminals to meet the needs of different scenarios: multi-terminal embedded small questionnaire SDK.
|
||||
|
||||
**🚀 Secure and scalable**
|
||||
|
||||
- Scalable security capabilities, providing experience guidance for security-related construction: encrypted transmission, data masking, etc.
|
||||
|
||||
- Customized Hook configuration, easy integration of multiple systems and various tools: data push, message push, etc.
|
||||
|
||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/508ce30f-0ae8-4f5f-84a7-e96de8238a7f" width="700" />
|
||||
|
||||
1. For more comprehensive features, please refer to the [documentation](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B).
|
||||
|
||||
2. Both individual and enterprise users can quickly build survey solutions specific to their fields.
|
||||
|
||||
# Technology
|
||||
|
||||
Web: Vue3 + ElementPlus; Multi-end rendering for C-end (planning).
|
||||
|
||||
Server: NestJS + MongoDB; Java ([under construction](https://github.com/didi/xiaoju-survey/issues/306)).
|
||||
|
||||
Online Platform: (under construction).
|
||||
|
||||
Intelligent Foundation: (planning).
|
||||
|
||||
# Project Advantages
|
||||
|
||||
**1. Comprehensive and Professional**
|
||||
|
||||
- [Standardized Protocols for Questionnaires](https://xiaojusurvey.didi.cn/docs/next/agreement/%E3%80%8A%E9%97%AE%E5%8D%B7Meta%E5%8D%8F%E8%AE%AE%E3%80%8B)
|
||||
|
||||
Ensuring concept interoperability is the foundation and core of the entire system. Based on practical business experience, two main categories have been established:
|
||||
|
||||
Business Descriptions: Questionnaire protocol, question type protocol.
|
||||
|
||||
Material Descriptions: Question type material protocol, including question types and settings.
|
||||
|
||||
- [UI/UX Standardization for Questionnaires](https://xiaojusurvey.didi.cn/docs/next/design/%E3%80%8A%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83%E3%80%8B)
|
||||
|
||||
The design language is the cornerstone of system flexibility and consistency, ensuring the system supports actual business operations with high user experience. It includes:
|
||||
|
||||
Design Standards: Flexible, noise-reducing, unified.
|
||||
|
||||
Interaction Standards: Follows user behavior characteristics, product positioning, and mature user habits.
|
||||
|
||||
- [WYSIWYG with High Consistency in Construction and Rendering](https://xiaojusurvey.didi.cn/docs/next/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E9%A2%98%E5%9E%8B%E5%9C%BA%E6%99%AF%E5%8C%96%E8%AE%BE%E8%AE%A1)
|
||||
|
||||
In practical business usage, the system includes both questionnaire generation and deployment. We design question types in a scenario-based manner to ensure high consistency from production to application.
|
||||
|
||||
- [Materialized Question Type Design for Free Customization and Extension](https://xiaojusurvey.didi.cn/docs/next/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E9%A2%98%E5%9E%8B%E7%89%A9%E6%96%99%E5%8C%96%E8%AE%BE%E8%AE%A1/%E5%9F%BA%E7%A1%80%E8%AE%BE%E8%AE%A1)
|
||||
|
||||
The core component of questionnaires is question types, and their configurability determines the extensibility of business scenarios and the system's reusability. Each question type has general capabilities and atomic characteristics, ensuring high customization.
|
||||
|
||||
- [Compliance Accumulation and High Expandability in Security Capabilities](https://xiaojusurvey.didi.cn/docs/next/document/%E5%AE%89%E5%85%A8%E8%AE%BE%E8%AE%A1/%E6%A6%82%E8%BF%B0)
|
||||
|
||||
Data encryption, sensitive information detection, and anti-vote brushing capabilities ensure the security of the questionnaire publishing and data collection process.
|
||||
Lightweight Design for Quick Integration and Flexible Expansion.
|
||||
|
||||
**2. Product-Level Open-Source Solution for Rapid Survey Process Implementation**
|
||||
|
||||
- [Product-Level Open-Source Solution for Rapid Survey Process Implementation](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E6%A6%82%E8%BF%B0)
|
||||
|
||||
Provides complete product capabilities around the questionnaire lifecycle, including user management (login, registration, questionnaire permissions) and questionnaire management (create, edit, distribute, collect, data analysis), allowing for quick construction of survey solutions in specific fields.
|
||||
|
||||
- [Out-of-the-Box Questionnaire Design to Reduce Domain Complexity](https://xiaojusurvey.didi.cn/docs/next/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E9%97%AE%E5%8D%B7%E6%90%AD%E5%BB%BA%E9%A2%86%E5%9F%9F%E5%8C%96%E8%AE%BE%E8%AE%A1)
|
||||
|
||||
High flexibility in questionnaire composition leads to high complexity in editing capabilities. We divide questionnaire editing into five sub-domains for product capability clustering and guide system modular design and development. Based on module arrangement and management, it can be used out-of-the-box.
|
||||
|
||||
- [Low Cost for Secondary Development and Easy Customization](https://xiaojusurvey.didi.cn/docs/next/document/%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/%E5%B7%A5%E7%A8%8B%E9%85%8D%E7%BD%AE%E5%8C%96)
|
||||
|
||||
The entire system is designed based on protocol standardization, function modularization, and management configuration, and provides a complete set of documentation and development and extension manuals.
|
||||
|
||||
- [Low Deployment Cost for Quick Online Launch](https://xiaojusurvey.didi.cn/docs/next/document/%E5%B7%A5%E7%A8%8B%E9%83%A8%E7%BD%B2/Docker%E9%83%A8%E7%BD%B2)
|
||||
|
||||
The front-end and back-end separation, Dockerization solutions, and complete deployment guidance manual.
|
||||
|
||||
# Quick Start
|
||||
|
||||
Node Version >= 18.x, [ check environment preparation guide.](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
|
||||
|
||||
Clone Project
|
||||
|
||||
```shell
|
||||
git clone git@github.com:didi/xiaoju-survey.git
|
||||
```
|
||||
|
||||
## Server Startup
|
||||
|
||||
### Option 1: Quick Start without Database Installation
|
||||
|
||||
> _This is convenient for quickly previewing the project. For formal projects, use Option 2._
|
||||
|
||||
#### 1.Install Dependencies
|
||||
|
||||
```shell
|
||||
cd server
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 2.Start
|
||||
|
||||
```shell
|
||||
npm run local
|
||||
```
|
||||
|
||||
> The service relies on mongodb-memory-server:
|
||||
> 1.Data is stored in memory and will be updated upon service restart.
|
||||
> 2.When starting a new instance of the memory server, it will automatically download MongoDB binaries if not found, which might take some time initially.
|
||||
|
||||
### Option 2: (Recommended for Production)
|
||||
|
||||
#### 1.Configure Database
|
||||
|
||||
> The project uses MongoDB: [MongoDB Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
||||
|
||||
Configure the database, check MongoDB configuration.
|
||||
|
||||
#### 2.Install Dependencies
|
||||
|
||||
```shell
|
||||
cd server
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 2.Install Dependencies
|
||||
|
||||
```shell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Frontend Startup
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```shell
|
||||
cd web
|
||||
npm install
|
||||
```
|
||||
|
||||
### Start
|
||||
|
||||
```shell
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## Access
|
||||
|
||||
### Questionnaire Management End
|
||||
|
||||
[http://localhost:8080/management](http://localhost:8080)
|
||||
|
||||
### Questionnaire Deployment End
|
||||
|
||||
Create and publish a questionnaire.
|
||||
|
||||
[http://localhost:8080/render/:surveyPath](http://localhost:8080/render/:surveyPath)
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Star
|
||||
|
||||
Open source is not easy. If this project helps you, please star it ❤️❤️❤️. Your support is our greatest motivation.
|
||||
|
||||
[![Star History Chart](https://api.star-history.com/svg?repos=didi/xiaoju-survey&type=Date)](https://star-history.com/#didi/xiaoju-survey&Date)
|
||||
|
||||
## WeChat Group
|
||||
|
||||
The official group will release the latest project news, construction plans, and community activities. Any questions and cooperation can contact the assistant:
|
||||
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
||||
|
||||
## Feedback
|
||||
|
||||
If you use this project, please leave feedback:[I'm using](https://github.com/didi/xiaoju-survey/issues/64), Your support is our greatest.
|
||||
|
||||
## Contribution
|
||||
|
||||
If you want to become a contributor or expand your technical stack, please check: [Contributor Guide](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE). Your participation is our greatest honor.
|
||||
|
||||
## Future Tasks
|
||||
|
||||
1. [Official Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
2. [WIP](https://github.com/didi/xiaoju-survey/labels/WIP)
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
Follow major changes: [MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
@ -15,11 +15,11 @@ services:
|
||||
- xiaoju-survey
|
||||
|
||||
xiaoju-survey:
|
||||
image: "xiaojusurvey/xiaoju-survey:1.0.4"
|
||||
image: "xiaojusurvey/xiaoju-survey:1.3.0-slim" # 最新版本:https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags
|
||||
container_name: xiaoju-survey
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080" # API端口
|
||||
- "8080:80" # API端口
|
||||
environment:
|
||||
XIAOJU_SURVEY_MONGO_URL: mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@xiaoju-survey-mongo:27017 # docker-compose 会根据容器名称自动处理
|
||||
links:
|
||||
|
@ -1,7 +1,9 @@
|
||||
#! /bin/bash
|
||||
|
||||
# 启动nginx
|
||||
echo 'nginx start'
|
||||
nginx -g 'daemon on;'
|
||||
|
||||
# 启动后端服务
|
||||
cd /xiaoju-survey/server
|
||||
npm run start:prod
|
@ -18,9 +18,9 @@ http {
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
server {
|
||||
listen 8080;
|
||||
listen 80;
|
||||
# IPv6端口
|
||||
listen [::]:8080;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
# gzip config
|
||||
gzip on;
|
||||
@ -39,6 +39,11 @@ http {
|
||||
try_files $uri $uri/ /management.html;
|
||||
}
|
||||
|
||||
location /management/preview/ {
|
||||
try_files $uri $uri/ /render.html;
|
||||
}
|
||||
|
||||
|
||||
location /render/ {
|
||||
try_files $uri $uri/ /render.html;
|
||||
}
|
||||
@ -46,6 +51,15 @@ http {
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
location /exportfile {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
# 静态文件的默认存储文件夹
|
||||
# 文件夹的配置在 server/src/modules/file/config/index.ts SERVER_LOCAL_CONFIG.FILE_KEY_PREFIX
|
||||
location /userUpload {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /500.html;
|
||||
client_max_body_size 20M;
|
||||
|
@ -1,7 +1,13 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVEY_MONGO_URL=
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
||||
|
||||
XIAOJU_SURVEY_REDIS_HOST=
|
||||
XIAOJU_SURVEY_REDIS_PORT=
|
||||
XIAOJU_SURVEY_REDIS_USERNAME=
|
||||
XIAOJU_SURVEY_REDIS_PASSWORD=
|
||||
XIAOJU_SURVEY_REDIS_DB=
|
||||
|
||||
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
@ -0,0 +1,20 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://127.0.0.1:27017
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
||||
|
||||
XIAOJU_SURVEY_REDIS_HOST=
|
||||
XIAOJU_SURVEY_REDIS_PORT=
|
||||
XIAOJU_SURVEY_REDIS_USERNAME=
|
||||
XIAOJU_SURVEY_REDIS_PASSWORD=
|
||||
XIAOJU_SURVEY_REDIS_DB=
|
||||
|
||||
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
|
||||
|
||||
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log
|
||||
|
||||
XIAOJU_SURVEY_REPORT=true
|
@ -0,0 +1,17 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
||||
|
||||
XIAOJU_SURVEY_REDIS_HOST=
|
||||
XIAOJU_SURVEY_REDIS_PORT=
|
||||
XIAOJU_SURVEY_REDIS_USERNAME=
|
||||
XIAOJU_SURVEY_REDIS_PASSWORD=
|
||||
XIAOJU_SURVEY_REDIS_DB=
|
||||
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
|
||||
|
||||
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log
|
5
server/.gitignore
vendored
5
server/.gitignore
vendored
@ -13,6 +13,7 @@ pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
yarn.lock
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@ -37,4 +38,6 @@ lerna-debug.log*
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
tmp
|
||||
tmp
|
||||
exportfile
|
||||
userUpload
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"name": "xiaoju-survey-server",
|
||||
"version": "1.3.0",
|
||||
"description": "XIAOJUSURVEY的server端",
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"src/**/__test/*.ts\"",
|
||||
"local": "ts-node ./scripts/run-local.ts",
|
||||
"start": "nest start",
|
||||
"dev": "npm run start:dev",
|
||||
@ -22,15 +22,17 @@
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/microservices": "^10.4.4",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/typeorm": "^10.0.1",
|
||||
"ali-oss": "^6.20.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
@ -41,7 +43,9 @@
|
||||
"nanoid": "^3.3.7",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-xlsx": "^0.24.0",
|
||||
"qiniu": "^7.11.1",
|
||||
"redlock": "^5.0.0-beta.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"svg-captcha": "^1.4.0",
|
||||
@ -61,8 +65,8 @@
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
@ -70,13 +74,14 @@
|
||||
"jest": "^29.5.0",
|
||||
"mongodb-memory-server": "^9.1.4",
|
||||
"prettier": "^3.0.0",
|
||||
"redis-memory-server": "^0.11.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
@ -90,12 +95,18 @@
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
"**/*.(t|j)s",
|
||||
"!**/*.module.ts",
|
||||
"!**/upgrade.*.ts"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node",
|
||||
"moduleNameMapper": {
|
||||
"^src/(.*)$": "<rootDir>/$1"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||||
import { spawn } from 'child_process';
|
||||
// import { RedisMemoryServer } from 'redis-memory-server';
|
||||
|
||||
async function startServerAndRunScript() {
|
||||
// 启动 MongoDB 内存服务器
|
||||
@ -8,12 +9,19 @@ async function startServerAndRunScript() {
|
||||
|
||||
console.log('MongoDB Memory Server started:', mongoUri);
|
||||
|
||||
// const redisServer = new RedisMemoryServer();
|
||||
// const redisHost = await redisServer.getHost();
|
||||
// const redisPort = await redisServer.getPort();
|
||||
|
||||
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
||||
const tsnode = spawn(
|
||||
'cross-env',
|
||||
[
|
||||
`XIAOJU_SURVEY_MONGO_URL=${mongoUri}`,
|
||||
// `XIAOJU_SURVEY_REDIS_HOST=${redisHost}`,
|
||||
// `XIAOJU_SURVEY_REDIS_PORT=${redisPort}`,
|
||||
'NODE_ENV=development',
|
||||
'SERVER_ENV=local',
|
||||
'npm',
|
||||
'run',
|
||||
'start:dev',
|
||||
@ -31,9 +39,10 @@ async function startServerAndRunScript() {
|
||||
console.error(data);
|
||||
});
|
||||
|
||||
tsnode.on('close', (code) => {
|
||||
tsnode.on('close', async (code) => {
|
||||
console.log(`Nodemon process exited with code ${code}`);
|
||||
mongod.stop(); // 停止 MongoDB 内存服务器
|
||||
await mongod.stop(); // 停止 MongoDB 内存服务器
|
||||
// await redisServer.stop();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,8 @@ import { SurveyResponseModule } from './modules/surveyResponse/surveyResponse.mo
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { MessageModule } from './modules/message/message.module';
|
||||
import { FileModule } from './modules/file/file.module';
|
||||
import { WorkspaceModule } from './modules/workspace/workspace.module';
|
||||
import { UpgradeModule } from './modules/upgrade/upgrade.module';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
@ -31,16 +33,24 @@ import { ClientEncrypt } from './models/clientEncrypt.entity';
|
||||
import { Word } from './models/word.entity';
|
||||
import { MessagePushingTask } from './models/messagePushingTask.entity';
|
||||
import { MessagePushingLog } from './models/messagePushingLog.entity';
|
||||
import { WorkspaceMember } from './models/workspaceMember.entity';
|
||||
import { Workspace } from './models/workspace.entity';
|
||||
import { Collaborator } from './models/collaborator.entity';
|
||||
import { DownloadTask } from './models/downloadTask.entity';
|
||||
import { Session } from './models/session.entity';
|
||||
|
||||
import { LoggerProvider } from './logger/logger.provider';
|
||||
import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
|
||||
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
||||
import { PluginManager } from './securityPlugin/pluginManager';
|
||||
import { Logger } from './logger';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({}),
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: `.env.${process.env.NODE_ENV}`, // 根据 NODE_ENV 动态加载对应的 .env 文件
|
||||
isGlobal: true, // 使配置模块在应用的任何地方可用
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
@ -74,6 +84,11 @@ import { Logger } from './logger';
|
||||
Word,
|
||||
MessagePushingTask,
|
||||
MessagePushingLog,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
Collaborator,
|
||||
DownloadTask,
|
||||
Session,
|
||||
],
|
||||
};
|
||||
},
|
||||
@ -92,6 +107,8 @@ import { Logger } from './logger';
|
||||
}),
|
||||
MessageModule,
|
||||
FileModule,
|
||||
WorkspaceModule,
|
||||
UpgradeModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
@ -106,7 +123,7 @@ import { Logger } from './logger';
|
||||
export class AppModule {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly pluginManager: PluginManager,
|
||||
) {}
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(LogRequestMiddleware).forRoutes('*');
|
||||
|
6
server/src/enums/downloadTaskStatus.ts
Normal file
6
server/src/enums/downloadTaskStatus.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum DOWNLOAD_TASK_STATUS {
|
||||
WAITING = 'waiting', // 排队中
|
||||
COMPUTING = 'computing', // 计算中
|
||||
SUCCEED = 'succeed', // 导出成功
|
||||
FAILED = 'failed', // 导出失败
|
||||
}
|
@ -1,20 +1,27 @@
|
||||
export enum EXCEPTION_CODE {
|
||||
AUTHENTICATION_FAILED = 1001, // 没有权限
|
||||
AUTHENTICATION_FAILED = 1001, // 未授权
|
||||
PARAMETER_ERROR = 1002, // 参数有误
|
||||
NO_PERMISSION = 1003, // 没有操作权限
|
||||
|
||||
USER_EXISTS = 2001, // 用户已存在
|
||||
USER_NOT_EXISTS = 2002, // 用户不存在
|
||||
USER_PASSWORD_WRONG = 2003, // 用户名或密码错误
|
||||
PASSWORD_INVALID = 2004, // 密码无效
|
||||
NO_SURVEY_PERMISSION = 3001, // 没有问卷权限
|
||||
SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错
|
||||
SURVEY_TYPE_ERROR = 3003, // 问卷类型错误
|
||||
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
||||
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
||||
SURVEY_SAVE_CONFLICT = 3006, // 问卷冲突
|
||||
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
||||
WHITELIST_ERROR = 4002, // 白名单校验错误
|
||||
|
||||
RESPONSE_SIGN_ERROR = 9001, // 签名不正确
|
||||
RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交
|
||||
RESPONSE_OVER_LIMIT = 9003, // 超出限制
|
||||
RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除
|
||||
RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除
|
||||
RESPONSE_PAUSING = 9006, // 问卷已暂停
|
||||
|
||||
UPLOAD_FILE_ERROR = 5001, // 上传文件错误
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
// 状态类型
|
||||
export enum RECORD_STATUS {
|
||||
NEW = 'new', // 新建
|
||||
EDITING = 'editing', // 编辑
|
||||
PAUSING = 'pausing', // 暂停
|
||||
NEW = 'new', // 新建 | 未发布
|
||||
PUBLISHED = 'published', // 发布
|
||||
REMOVED = 'removed', // 删除
|
||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||
EDITING = 'editing', // 编辑
|
||||
FINISHED = 'finished', // 已结束
|
||||
REMOVED = 'removed',
|
||||
}
|
||||
|
||||
export const enum RECORD_SUB_STATUS {
|
||||
DEFAULT = '', // 默认
|
||||
PAUSING = 'pausing', // 暂停
|
||||
}
|
||||
|
||||
// 历史类型
|
||||
|
37
server/src/enums/question.ts
Normal file
37
server/src/enums/question.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @description 问卷题目类型
|
||||
*/
|
||||
export enum QUESTION_TYPE {
|
||||
/**
|
||||
* 单行输入框
|
||||
*/
|
||||
TEXT = 'text',
|
||||
/**
|
||||
* 多行输入框
|
||||
*/
|
||||
TEXTAREA = 'textarea',
|
||||
/**
|
||||
* 单项选择
|
||||
*/
|
||||
RADIO = 'radio',
|
||||
/**
|
||||
* 多项选择
|
||||
*/
|
||||
CHECKBOX = 'checkbox',
|
||||
/**
|
||||
* 判断题
|
||||
*/
|
||||
BINARY_CHOICE = 'binary-choice',
|
||||
/**
|
||||
* 评分
|
||||
*/
|
||||
RADIO_STAR = 'radio-star',
|
||||
/**
|
||||
* nps评分
|
||||
*/
|
||||
RADIO_NPS = 'radio-nps',
|
||||
/**
|
||||
* 投票
|
||||
*/
|
||||
VOTE = 'vote',
|
||||
}
|
20
server/src/enums/surveyPermission.ts
Normal file
20
server/src/enums/surveyPermission.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export enum SURVEY_PERMISSION {
|
||||
SURVEY_CONF_MANAGE = 'SURVEY_CONF_MANAGE',
|
||||
SURVEY_RESPONSE_MANAGE = 'SURVEY_RESPONSE_MANAGE',
|
||||
SURVEY_COOPERATION_MANAGE = 'SURVEY_COOPERATION_MANAGE',
|
||||
}
|
||||
|
||||
export const SURVEY_PERMISSION_DESCRIPTION = {
|
||||
SURVEY_CONF_MANAGE: {
|
||||
name: '问卷配置管理',
|
||||
value: SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||
},
|
||||
surveyResponseManage: {
|
||||
name: '问卷分析管理',
|
||||
value: SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||
},
|
||||
surveyCooperatorManage: {
|
||||
name: '协作者管理',
|
||||
value: SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
},
|
||||
};
|
4
server/src/enums/surveySessionStatus.ts
Normal file
4
server/src/enums/surveySessionStatus.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum SESSION_STATUS {
|
||||
ACTIVATED = 'activated',
|
||||
DEACTIVATED = 'deactivated',
|
||||
}
|
41
server/src/enums/workspace.ts
Normal file
41
server/src/enums/workspace.ts
Normal file
@ -0,0 +1,41 @@
|
||||
export enum ROLE {
|
||||
ADMIN = 'admin',
|
||||
USER = 'user',
|
||||
}
|
||||
|
||||
export const ROLE_DESCRIPTION = {
|
||||
ADMIN: {
|
||||
name: '管理员',
|
||||
value: ROLE.ADMIN,
|
||||
},
|
||||
USER: {
|
||||
name: '用户',
|
||||
value: ROLE.USER,
|
||||
},
|
||||
};
|
||||
|
||||
export enum PERMISSION {
|
||||
READ_WORKSPACE = 'READ_WORKSPACE',
|
||||
WRITE_WORKSPACE = 'WRITE_WORKSPACE',
|
||||
READ_MEMBER = 'READ_MEMBER',
|
||||
WRITE_MEMBER = 'WRITE_MEMBER',
|
||||
READ_SURVEY = 'READ_SURVEY',
|
||||
WRITE_SURVEY = 'WRITE_SURVEY',
|
||||
}
|
||||
|
||||
export const ROLE_PERMISSION: Record<ROLE, PERMISSION[]> = {
|
||||
[ROLE.ADMIN]: [
|
||||
PERMISSION.READ_WORKSPACE,
|
||||
PERMISSION.WRITE_WORKSPACE,
|
||||
PERMISSION.READ_MEMBER,
|
||||
PERMISSION.WRITE_MEMBER,
|
||||
PERMISSION.READ_SURVEY,
|
||||
PERMISSION.WRITE_SURVEY,
|
||||
],
|
||||
[ROLE.USER]: [
|
||||
PERMISSION.READ_WORKSPACE,
|
||||
PERMISSION.READ_MEMBER,
|
||||
PERMISSION.READ_SURVEY,
|
||||
PERMISSION.WRITE_SURVEY,
|
||||
],
|
||||
};
|
55
server/src/exceptions/__test/httpExceptions.filter.spec.ts
Normal file
55
server/src/exceptions/__test/httpExceptions.filter.spec.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HttpExceptionsFilter } from '../httpExceptions.filter';
|
||||
import { ArgumentsHost } from '@nestjs/common';
|
||||
import { HttpException } from '../httpException';
|
||||
import { Response } from 'express';
|
||||
|
||||
describe('HttpExceptionsFilter', () => {
|
||||
let filter: HttpExceptionsFilter;
|
||||
let mockArgumentsHost: ArgumentsHost;
|
||||
let mockResponse: Partial<Response>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [HttpExceptionsFilter],
|
||||
}).compile();
|
||||
|
||||
filter = module.get<HttpExceptionsFilter>(HttpExceptionsFilter);
|
||||
|
||||
mockResponse = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
|
||||
mockArgumentsHost = {
|
||||
switchToHttp: jest.fn().mockReturnThis(),
|
||||
getResponse: jest.fn().mockReturnValue(mockResponse),
|
||||
} as unknown as ArgumentsHost;
|
||||
});
|
||||
|
||||
it('should return 500 status and "Internal Server Error" message for generic errors', () => {
|
||||
const genericError = new Error('Some error');
|
||||
|
||||
filter.catch(genericError, mockArgumentsHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
message: 'Internal Server Error',
|
||||
code: 500,
|
||||
errmsg: 'Some error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 status and specific message for HttpException', () => {
|
||||
const httpException = new HttpException('Specific error message', 1001);
|
||||
|
||||
filter.catch(httpException, mockArgumentsHost);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(200);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
message: 'Specific error message',
|
||||
code: 1001,
|
||||
errmsg: 'Specific error message',
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,3 @@
|
||||
// all-exceptions.filter.ts
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { HttpException } from './httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
export class NoSurveyPermissionException extends HttpException {
|
||||
export class NoPermissionException extends HttpException {
|
||||
constructor(public readonly message: string) {
|
||||
super(message, EXCEPTION_CODE.NO_SURVEY_PERMISSION);
|
||||
super(message, EXCEPTION_CODE.NO_PERMISSION);
|
||||
}
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Authtication } from './authtication';
|
||||
import { Authentication } from '../authentication.guard';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { AuthenticationException } from 'src/exceptions/authException';
|
||||
import { User } from 'src/models/user.entity';
|
||||
|
||||
jest.mock('jsonwebtoken');
|
||||
|
||||
describe('Authtication', () => {
|
||||
let guard: Authtication;
|
||||
describe('Authentication', () => {
|
||||
let guard: Authentication;
|
||||
let authService: AuthService;
|
||||
let configService: ConfigService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
Authtication,
|
||||
Authentication,
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {
|
||||
@ -31,7 +31,7 @@ describe('Authtication', () => {
|
||||
],
|
||||
}).compile();
|
||||
|
||||
guard = module.get<Authtication>(Authtication);
|
||||
guard = module.get<Authentication>(Authentication);
|
||||
authService = module.get<AuthService>(AuthService);
|
||||
configService = module.get<ConfigService>(ConfigService);
|
||||
});
|
68
server/src/guards/__test/session.guard.spec.ts
Normal file
68
server/src/guards/__test/session.guard.spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { SessionService } from 'src/modules/survey/services/session.service';
|
||||
import { SessionGuard } from '../session.guard';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
|
||||
describe('SessionGuard', () => {
|
||||
let sessionGuard: SessionGuard;
|
||||
let reflector: Reflector;
|
||||
let sessionService: SessionService;
|
||||
|
||||
beforeEach(() => {
|
||||
reflector = new Reflector();
|
||||
sessionService = {
|
||||
findOne: jest.fn(),
|
||||
} as unknown as SessionService;
|
||||
sessionGuard = new SessionGuard(reflector, sessionService);
|
||||
});
|
||||
|
||||
it('should return true when sessionId exists and sessionService returns sessionInfo', async () => {
|
||||
const mockSessionId = '12345';
|
||||
const mockSessionInfo = { id: mockSessionId, name: 'test session' };
|
||||
|
||||
const context = {
|
||||
switchToHttp: jest.fn().mockReturnThis(),
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
sessionId: mockSessionId,
|
||||
}),
|
||||
getHandler: jest.fn(),
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('sessionId');
|
||||
|
||||
jest
|
||||
.spyOn(sessionService, 'findOne')
|
||||
.mockResolvedValue(mockSessionInfo as any);
|
||||
|
||||
const result = await sessionGuard.canActivate(context);
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(reflector.get).toHaveBeenCalledWith(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
expect(sessionService.findOne).toHaveBeenCalledWith(mockSessionId);
|
||||
expect(request.sessionInfo).toEqual(mockSessionInfo);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException when sessionId is missing', async () => {
|
||||
const context = {
|
||||
switchToHttp: jest.fn().mockReturnThis(),
|
||||
getRequest: jest.fn().mockReturnValue({}),
|
||||
getHandler: jest.fn(),
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('sessionId');
|
||||
|
||||
await expect(sessionGuard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
expect(reflector.get).toHaveBeenCalledWith(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
});
|
||||
});
|
198
server/src/guards/__test/survey.guard.spec.ts
Normal file
198
server/src/guards/__test/survey.guard.spec.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
|
||||
import { SurveyGuard } from '../survey.guard';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
|
||||
describe('SurveyGuard', () => {
|
||||
let guard: SurveyGuard;
|
||||
let reflector: Reflector;
|
||||
let collaboratorService: CollaboratorService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let workspaceMemberService: WorkspaceMemberService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SurveyGuard,
|
||||
{
|
||||
provide: Reflector,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CollaboratorService,
|
||||
useValue: {
|
||||
getCollaborator: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useValue: {
|
||||
getSurveyById: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
guard = module.get<SurveyGuard>(SurveyGuard);
|
||||
reflector = module.get<Reflector>(Reflector);
|
||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
workspaceMemberService = module.get<WorkspaceMemberService>(
|
||||
WorkspaceMemberService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(guard).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow access if no surveyId is present', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest.spyOn(reflector, 'get').mockReturnValue(null);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw SurveyNotFoundException if survey does not exist', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||
jest.spyOn(surveyMetaService, 'getSurveyById').mockResolvedValue(null);
|
||||
|
||||
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||
SurveyNotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow access if user is the owner of the survey by ownerId', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
const surveyMeta = { ownerId: 'testUserId', workspaceId: null };
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow access if user is the owner of the survey by username', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
const surveyMeta = { owner: 'testUser', workspaceId: null };
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow access if user is a workspace member', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
const surveyMeta = { owner: 'anotherUser', workspaceId: 'workspaceId' };
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
jest
|
||||
.spyOn(workspaceMemberService, 'findOne')
|
||||
.mockResolvedValue({} as WorkspaceMember);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if user is not a workspace member', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
const surveyMeta = { owner: 'anotherUser', workspaceId: 'workspaceId' };
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
jest.spyOn(workspaceMemberService, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if no permissions are provided', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce(null);
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
|
||||
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if user has no matching permissions', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce(['requiredPermission']);
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
jest
|
||||
.spyOn(collaboratorService, 'getCollaborator')
|
||||
.mockResolvedValue({ permissions: [] } as Collaborator);
|
||||
|
||||
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow access if user has the required permissions', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
||||
jest
|
||||
.spyOn(reflector, 'get')
|
||||
.mockReturnValueOnce([SURVEY_PERMISSION.SURVEY_CONF_MANAGE]);
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
jest.spyOn(collaboratorService, 'getCollaborator').mockResolvedValue({
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||
} as Collaborator);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
function createMockExecutionContext(): ExecutionContext {
|
||||
return {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
user: { username: 'testUser', _id: 'testUserId' },
|
||||
params: { surveyId: 'surveyId' },
|
||||
}),
|
||||
}),
|
||||
getHandler: jest.fn(),
|
||||
} as unknown as ExecutionContext;
|
||||
}
|
||||
});
|
137
server/src/guards/__test/workspace.guard.spec.ts
Normal file
137
server/src/guards/__test/workspace.guard.spec.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceGuard } from '../workspace.guard';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { NoPermissionException } from '../../exceptions/noPermissionException';
|
||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
|
||||
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
||||
|
||||
describe('WorkspaceGuard', () => {
|
||||
let guard: WorkspaceGuard;
|
||||
let reflector: Reflector;
|
||||
let workspaceMemberService: WorkspaceMemberService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
WorkspaceGuard,
|
||||
{
|
||||
provide: Reflector,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
guard = module.get<WorkspaceGuard>(WorkspaceGuard);
|
||||
reflector = module.get<Reflector>(Reflector);
|
||||
workspaceMemberService = module.get<WorkspaceMemberService>(
|
||||
WorkspaceMemberService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(guard).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow access if no roles are defined', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest.spyOn(reflector, 'get').mockReturnValue(null);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if workspaceId is missing and optional is false', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest
|
||||
.spyOn(reflector, 'get')
|
||||
.mockReturnValueOnce([WORKSPACE_PERMISSION.READ_WORKSPACE]);
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
|
||||
|
||||
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow access if workspaceId is missing and optional is true', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest
|
||||
.spyOn(reflector, 'get')
|
||||
.mockReturnValueOnce([WORKSPACE_PERMISSION.WRITE_WORKSPACE]);
|
||||
jest
|
||||
.spyOn(reflector, 'get')
|
||||
.mockReturnValueOnce({ key: 'params.workspaceId', optional: true });
|
||||
|
||||
jest
|
||||
.spyOn(workspaceMemberService, 'findOne')
|
||||
.mockResolvedValue({ role: 'admin' } as WorkspaceMember);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if user is not a member of the workspace', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest
|
||||
.spyOn(reflector, 'get')
|
||||
.mockReturnValueOnce([WORKSPACE_PERMISSION.WRITE_WORKSPACE]);
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
|
||||
jest.spyOn(workspaceMemberService, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if user role is not allowed', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest
|
||||
.spyOn(reflector, 'get')
|
||||
.mockReturnValueOnce([WORKSPACE_PERMISSION.READ_MEMBER]);
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
|
||||
jest
|
||||
.spyOn(workspaceMemberService, 'findOne')
|
||||
.mockResolvedValue({ role: 'member' } as WorkspaceMember);
|
||||
|
||||
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow access if user role is allowed', async () => {
|
||||
const context = createMockExecutionContext();
|
||||
jest
|
||||
.spyOn(reflector, 'get')
|
||||
.mockReturnValueOnce([WORKSPACE_PERMISSION.READ_MEMBER]);
|
||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
|
||||
jest
|
||||
.spyOn(workspaceMemberService, 'findOne')
|
||||
.mockResolvedValue({ role: 'admin' } as WorkspaceMember);
|
||||
|
||||
const result = await guard.canActivate(context);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
function createMockExecutionContext(): ExecutionContext {
|
||||
return {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
user: { _id: 'testUserId' },
|
||||
params: { workspaceId: 'workspaceId' },
|
||||
}),
|
||||
}),
|
||||
getHandler: jest.fn(),
|
||||
} as unknown as ExecutionContext;
|
||||
}
|
||||
});
|
@ -3,7 +3,7 @@ import { AuthenticationException } from '../exceptions/authException';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class Authtication implements CanActivate {
|
||||
export class Authentication implements CanActivate {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
30
server/src/guards/session.guard.ts
Normal file
30
server/src/guards/session.guard.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
import { SessionService } from 'src/modules/survey/services/session.service';
|
||||
@Injectable()
|
||||
export class SessionGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly sessionService: SessionService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const sessionIdKey = this.reflector.get<string>(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
const sessionId = get(request, sessionIdKey);
|
||||
|
||||
if (!sessionId) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
const sessionInfo = await this.sessionService.findOne(sessionId);
|
||||
request.sessionInfo = sessionInfo;
|
||||
request.surveyId = sessionInfo.surveyId;
|
||||
return true;
|
||||
}
|
||||
}
|
87
server/src/guards/survey.guard.ts
Normal file
87
server/src/guards/survey.guard.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
|
||||
@Injectable()
|
||||
export class SurveyGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
const surveyIdKey = this.reflector.get<string>(
|
||||
'surveyId',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
const surveyId = get(request, surveyIdKey);
|
||||
|
||||
if (!surveyId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||
|
||||
if (!surveyMeta) {
|
||||
throw new SurveyNotFoundException('问卷不存在');
|
||||
}
|
||||
|
||||
request.surveyMeta = surveyMeta;
|
||||
|
||||
// 兼容老的问卷没有ownerId
|
||||
if (
|
||||
surveyMeta.ownerId === user._id.toString() ||
|
||||
surveyMeta.owner === user.username
|
||||
) {
|
||||
// 问卷的owner,可以访问和操作问卷
|
||||
return true;
|
||||
}
|
||||
|
||||
if (surveyMeta.workspaceId) {
|
||||
const memberInfo = await this.workspaceMemberService.findOne({
|
||||
workspaceId: surveyMeta.workspaceId,
|
||||
userId: user._id.toString(),
|
||||
});
|
||||
if (!memberInfo) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissions = this.reflector.get<string[]>(
|
||||
'surveyPermission',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
if (!Array.isArray(permissions) || permissions.length === 0) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const info = await this.collaboratorService.getCollaborator({
|
||||
surveyId,
|
||||
userId: user._id.toString(),
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
request.collaborator = info;
|
||||
if (
|
||||
permissions.some((permission) => info.permissions.includes(permission))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
}
|
72
server/src/guards/workspace.guard.ts
Normal file
72
server/src/guards/workspace.guard.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { NoPermissionException } from '../exceptions/noPermissionException';
|
||||
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { ROLE_PERMISSION as WORKSPACE_ROLE_PERMISSION } from 'src/enums/workspace';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const allowPermissions = this.reflector.get<string[]>(
|
||||
'workspacePermissions',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
if (!allowPermissions) {
|
||||
return true; // 没有定义权限,可以访问
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
const workspaceIdInfo = this.reflector.get(
|
||||
'workspaceId',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
let workspaceIdKey, optional;
|
||||
if (typeof workspaceIdInfo === 'string') {
|
||||
workspaceIdKey = workspaceIdInfo;
|
||||
optional = false;
|
||||
} else {
|
||||
workspaceIdKey = workspaceIdInfo?.key;
|
||||
optional = workspaceIdInfo?.optional || false;
|
||||
}
|
||||
|
||||
const workspaceId = get(request, workspaceIdKey);
|
||||
|
||||
if (!workspaceId && optional === false) {
|
||||
throw new NoPermissionException('没有空间权限');
|
||||
}
|
||||
|
||||
if (workspaceId) {
|
||||
const membersInfo = await this.workspaceMemberService.findOne({
|
||||
workspaceId,
|
||||
userId: user._id.toString(),
|
||||
});
|
||||
|
||||
if (!membersInfo) {
|
||||
throw new NoPermissionException('没有空间权限');
|
||||
}
|
||||
|
||||
const userPermissions = WORKSPACE_ROLE_PERMISSION[membersInfo.role] || [];
|
||||
if (
|
||||
allowPermissions.some((permission) =>
|
||||
userPermissions.includes(permission),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -94,18 +94,58 @@ export interface SubmitConf {
|
||||
msgContent: MsgContent;
|
||||
}
|
||||
|
||||
// 白名单类型
|
||||
export enum WhitelistType {
|
||||
ALL = 'ALL',
|
||||
// 空间成员
|
||||
MEMBER = 'MEMBER',
|
||||
// 自定义
|
||||
CUSTOM = 'CUSTOM',
|
||||
}
|
||||
|
||||
// 白名单用户类型
|
||||
export enum MemberType {
|
||||
// 手机号
|
||||
MOBILE = 'MOBILE',
|
||||
// 邮箱
|
||||
EMAIL = 'EMAIL',
|
||||
}
|
||||
|
||||
export interface BaseConf {
|
||||
begTime: string;
|
||||
beginTime: string;
|
||||
endTime: string;
|
||||
answerBegTime: string;
|
||||
answerEndTime: string;
|
||||
tLimit: number;
|
||||
language: string;
|
||||
// 访问密码开关
|
||||
passwordSwitch?: boolean;
|
||||
// 密码
|
||||
password?: string | null;
|
||||
// 白名单类型
|
||||
whitelistType?: WhitelistType;
|
||||
// 白名单用户类型
|
||||
memberType?: MemberType;
|
||||
// 白名单列表
|
||||
whitelist?: string[];
|
||||
// 提示语
|
||||
whitelistTip?: string;
|
||||
}
|
||||
|
||||
export interface SkinConf {
|
||||
skinColor: string;
|
||||
inputBgColor: string;
|
||||
backgroundConf: {
|
||||
color: string;
|
||||
type: string;
|
||||
image: string;
|
||||
};
|
||||
contentConf: {
|
||||
opacity: number;
|
||||
};
|
||||
themeConf: {
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BottomConf {
|
||||
|
@ -1,15 +1,18 @@
|
||||
import * as log4js from 'log4js';
|
||||
import moment from 'moment';
|
||||
import { Request } from 'express';
|
||||
import { Injectable, Scope, Inject } from '@nestjs/common';
|
||||
import { CONTEXT, RequestContext } from '@nestjs/microservices';
|
||||
|
||||
const log4jsLogger = log4js.getLogger();
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class Logger {
|
||||
private static inited = false;
|
||||
|
||||
constructor() {}
|
||||
constructor(@Inject(CONTEXT) private readonly ctx: RequestContext) {}
|
||||
|
||||
static init(config: { filename: string }) {
|
||||
if (this.inited) {
|
||||
if (Logger.inited) {
|
||||
return;
|
||||
}
|
||||
log4js.configure({
|
||||
@ -30,25 +33,26 @@ export class Logger {
|
||||
default: { appenders: ['app'], level: 'trace' },
|
||||
},
|
||||
});
|
||||
Logger.inited = true;
|
||||
}
|
||||
|
||||
_log(message, options: { dltag?: string; level: string; req?: Request }) {
|
||||
_log(message, options: { dltag?: string; level: string }) {
|
||||
const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
const level = options.level;
|
||||
const dltag = options.dltag ? `${options.dltag}||` : '';
|
||||
const traceIdStr = options?.req['traceId']
|
||||
? `traceid=${options?.req['traceId']}||`
|
||||
const level = options?.level;
|
||||
const dltag = options?.dltag ? `${options.dltag}||` : '';
|
||||
const traceIdStr = this.ctx?.['traceId']
|
||||
? `traceid=${this.ctx?.['traceId']}||`
|
||||
: '';
|
||||
return log4jsLogger[level](
|
||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
info(message, options?: { dltag?: string; req?: Request }) {
|
||||
info(message, options?: { dltag?: string }) {
|
||||
return this._log(message, { ...options, level: 'info' });
|
||||
}
|
||||
|
||||
error(message, options: { dltag?: string; req?: Request }) {
|
||||
error(message, options?: { dltag?: string }) {
|
||||
return this._log(message, { ...options, level: 'error' });
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ const getCountStr = () => {
|
||||
|
||||
export const genTraceId = ({ ip }) => {
|
||||
// ip转16位 + 当前时间戳(毫秒级)+自增序列(1000开始自增到9000)+ 当前进程id的后5位
|
||||
ip = ip.replace('::ffff:', '');
|
||||
ip = ip.replace('::ffff:', '').replace('::1', '');
|
||||
let ipArr;
|
||||
if (ip.indexOf(':') > 0) {
|
||||
if (ip.indexOf(':') >= 0) {
|
||||
ipArr = ip.split(':').map((segment) => {
|
||||
// 将IPv6每个段转为16位,并补0到长度为4
|
||||
return parseInt(segment, 16).toString(16).padStart(4, '0');
|
||||
@ -20,7 +20,9 @@ export const genTraceId = ({ ip }) => {
|
||||
} else {
|
||||
ipArr = ip
|
||||
.split('.')
|
||||
.map((item) => parseInt(item).toString(16).padStart(2, '0'));
|
||||
.map((item) =>
|
||||
item ? parseInt(item).toString(16).padStart(2, '0') : '',
|
||||
);
|
||||
}
|
||||
|
||||
return `${ipArr.join('')}${Date.now().toString()}${getCountStr()}${process.pid.toString().slice(-5)}`;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import './report';
|
||||
|
||||
async function bootstrap() {
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
@ -1,4 +1,3 @@
|
||||
// logger.middleware.ts
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Logger } from '../logger/index'; // 替换为你实际的logger路径
|
||||
@ -20,7 +19,6 @@ export class LogRequestMiddleware implements NestMiddleware {
|
||||
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
|
||||
{
|
||||
dltag: 'request_in',
|
||||
req,
|
||||
},
|
||||
);
|
||||
|
||||
@ -30,7 +28,6 @@ export class LogRequestMiddleware implements NestMiddleware {
|
||||
`status=${res.statusCode.toString()}||duration=${duration}ms`,
|
||||
{
|
||||
dltag: 'request_out',
|
||||
req,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -1,30 +0,0 @@
|
||||
import { BaseEntity } from '../base.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
describe('BaseEntity', () => {
|
||||
let baseEntity: BaseEntity;
|
||||
|
||||
beforeEach(() => {
|
||||
baseEntity = new BaseEntity();
|
||||
});
|
||||
|
||||
it('should initialize default info before insert', () => {
|
||||
const now = Date.now();
|
||||
baseEntity.initDefaultInfo();
|
||||
|
||||
expect(baseEntity.curStatus.status).toBe(RECORD_STATUS.NEW);
|
||||
expect(baseEntity.curStatus.date).toBeCloseTo(now, -3);
|
||||
expect(baseEntity.statusList).toHaveLength(1);
|
||||
expect(baseEntity.statusList[0].status).toBe(RECORD_STATUS.NEW);
|
||||
expect(baseEntity.statusList[0].date).toBeCloseTo(now, -3);
|
||||
expect(baseEntity.createDate).toBeCloseTo(now, -3);
|
||||
expect(baseEntity.updateDate).toBeCloseTo(now, -3);
|
||||
});
|
||||
|
||||
it('should update updateDate before update', () => {
|
||||
const now = Date.now();
|
||||
baseEntity.onUpdate();
|
||||
|
||||
expect(baseEntity.updateDate).toBeCloseTo(now, -3);
|
||||
});
|
||||
});
|
57
server/src/models/__test/surveyMeta.entity.spec.ts
Normal file
57
server/src/models/__test/surveyMeta.entity.spec.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { SurveyMeta } from '../surveyMeta.entity';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
|
||||
|
||||
// 模拟日期
|
||||
const mockDateNow = Date.now();
|
||||
|
||||
describe('SurveyMeta Entity', () => {
|
||||
let surveyMeta: SurveyMeta;
|
||||
|
||||
// 在每个测试之前,初始化 SurveyMeta 实例
|
||||
beforeEach(() => {
|
||||
surveyMeta = new SurveyMeta();
|
||||
// 模拟 Date.now() 返回固定的时间
|
||||
jest.spyOn(Date, 'now').mockReturnValue(mockDateNow);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks(); // 每次测试后还原所有 mock
|
||||
});
|
||||
|
||||
it('should set default curStatus and subStatus on insert when they are not provided', () => {
|
||||
surveyMeta.initDefaultInfo();
|
||||
|
||||
// 验证 curStatus 是否被初始化为默认值
|
||||
expect(surveyMeta.curStatus).toEqual({
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: mockDateNow,
|
||||
});
|
||||
|
||||
// 验证 statusList 是否包含 curStatus
|
||||
expect(surveyMeta.statusList).toEqual([
|
||||
{
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: mockDateNow,
|
||||
},
|
||||
]);
|
||||
|
||||
// 验证 subStatus 是否被初始化为默认值
|
||||
expect(surveyMeta.subStatus).toEqual({
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: mockDateNow,
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize statusList if curStatus is provided but statusList is empty', () => {
|
||||
surveyMeta.curStatus = null;
|
||||
|
||||
surveyMeta.initDefaultInfo();
|
||||
|
||||
expect(surveyMeta.statusList).toEqual([
|
||||
{
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,43 +1,13 @@
|
||||
import { Column, ObjectIdColumn, BeforeInsert, BeforeUpdate } from 'typeorm';
|
||||
import { ObjectIdColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
export class BaseEntity {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
@CreateDateColumn({ type: 'timestamp', precision: 3 })
|
||||
createdAt: Date;
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
@UpdateDateColumn({ type: 'timestamp', precision: 3 })
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'captcha' })
|
||||
export class Captcha extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
@ -6,8 +6,7 @@ import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'clientEncrypt' })
|
||||
export class ClientEncrypt extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
26
server/src/models/collaborator.entity.ts
Normal file
26
server/src/models/collaborator.entity.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'collaborator' })
|
||||
export class Collaborator extends BaseEntity {
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@Column('jsonb')
|
||||
permissions: Array<string>;
|
||||
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
}
|
48
server/src/models/downloadTask.entity.ts
Normal file
48
server/src/models/downloadTask.entity.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
|
||||
|
||||
@Entity({ name: 'downloadTask' })
|
||||
export class DownloadTask extends BaseEntity {
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
// 文件路径
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
// 文件key
|
||||
@Column()
|
||||
fileKey: string;
|
||||
|
||||
// 任务创建人
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
// 任务创建人
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
// 文件名
|
||||
@Column()
|
||||
filename: string;
|
||||
|
||||
// 文件大小
|
||||
@Column()
|
||||
fileSize: string;
|
||||
|
||||
@Column()
|
||||
params: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
|
||||
@Column()
|
||||
status: DOWNLOAD_TASK_STATUS;
|
||||
}
|
@ -27,4 +27,16 @@ export class MessagePushingTask extends BaseEntity {
|
||||
|
||||
@Column()
|
||||
ownerId: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'surveyPublish' })
|
||||
export class ResponseSchema extends BaseEntity {
|
||||
@ -15,4 +16,19 @@ export class ResponseSchema extends BaseEntity {
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
subStatus: {
|
||||
status: RECORD_SUB_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
22
server/src/models/session.entity.ts
Normal file
22
server/src/models/session.entity.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Entity, Column, Index, ObjectIdColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
|
||||
|
||||
@Entity({ name: 'session' })
|
||||
export class Session extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@Column()
|
||||
status: SESSION_STATUS;
|
||||
}
|
@ -19,4 +19,7 @@ export class SurveyHistory extends BaseEntity {
|
||||
username: string;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
@Column('string')
|
||||
sessionId: string;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { Entity, Column, BeforeInsert } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'surveyMeta' })
|
||||
export class SurveyMeta extends BaseEntity {
|
||||
@ -18,12 +19,65 @@ export class SurveyMeta extends BaseEntity {
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
owner: string;
|
||||
|
||||
@Column()
|
||||
ownerId: string;
|
||||
|
||||
@Column()
|
||||
createMethod: string;
|
||||
|
||||
@Column()
|
||||
createFrom: string;
|
||||
|
||||
@Column()
|
||||
workspaceId: string;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
subStatus: {
|
||||
status: RECORD_SUB_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS | RECORD_SUB_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
if (!this.subStatus) {
|
||||
const subStatus = { status: RECORD_SUB_STATUS.DEFAULT, date: now };
|
||||
this.subStatus = subStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export class SurveyResponse extends BaseEntity {
|
||||
data: Record<string, any>;
|
||||
|
||||
@Column()
|
||||
difTime: number;
|
||||
diffTime: number;
|
||||
|
||||
@Column()
|
||||
clientTime: number;
|
||||
@ -27,11 +27,11 @@ export class SurveyResponse extends BaseEntity {
|
||||
|
||||
@BeforeInsert()
|
||||
async onDataInsert() {
|
||||
return await pluginManager.triggerHook('beforeResponseDataCreate', this);
|
||||
return await pluginManager.triggerHook('encryptResponseData', this);
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
async onDataLoaded() {
|
||||
return await pluginManager.triggerHook('afterResponseDataReaded', this);
|
||||
return await pluginManager.triggerHook('decryptResponseData', this);
|
||||
}
|
||||
}
|
||||
|
35
server/src/models/workspace.entity.ts
Normal file
35
server/src/models/workspace.entity.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'workspace' })
|
||||
export class Workspace extends BaseEntity {
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
ownerId: string;
|
||||
|
||||
@Column()
|
||||
owner: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
description: string;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
}
|
26
server/src/models/workspaceMember.entity.ts
Normal file
26
server/src/models/workspaceMember.entity.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'workspaceMember' })
|
||||
export class WorkspaceMember extends BaseEntity {
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@Column()
|
||||
workspaceId: string;
|
||||
|
||||
@Column()
|
||||
role: string;
|
||||
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
}
|
@ -82,6 +82,22 @@ describe('AuthController', () => {
|
||||
new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException with PASSWORD_INVALID code when password is invalid', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: '无效的密码abc123',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
await expect(controller.register(mockUserInfo)).rejects.toThrow(
|
||||
new HttpException(
|
||||
'密码只能输入数字、字母、特殊字符',
|
||||
EXCEPTION_CODE.PASSWORD_INVALID,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
@ -96,12 +112,21 @@ describe('AuthController', () => {
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
jest.spyOn(userService, 'getUser').mockResolvedValue(
|
||||
Promise.resolve({
|
||||
username: 'testUser',
|
||||
_id: new ObjectId(),
|
||||
} as User),
|
||||
);
|
||||
|
||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(
|
||||
Promise.resolve({
|
||||
username: 'testUser',
|
||||
_id: new ObjectId(),
|
||||
} as User),
|
||||
);
|
||||
|
||||
jest.spyOn(authService, 'generateToken').mockResolvedValue('testToken');
|
||||
|
||||
const result = await controller.login(mockUserInfo);
|
||||
@ -143,10 +168,40 @@ describe('AuthController', () => {
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(true);
|
||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(null);
|
||||
|
||||
await expect(controller.login(mockUserInfo)).rejects.toThrow(
|
||||
new HttpException(
|
||||
'账号未注册,请进行注册',
|
||||
EXCEPTION_CODE.USER_NOT_EXISTS,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException with USER_NOT_EXISTS code when user is not found', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: 'testPassword',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(true);
|
||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(
|
||||
Promise.resolve({
|
||||
username: 'testUser',
|
||||
_id: new ObjectId(),
|
||||
} as User),
|
||||
);
|
||||
jest.spyOn(userService, 'getUser').mockResolvedValue(null);
|
||||
|
||||
await expect(controller.login(mockUserInfo)).rejects.toThrow(
|
||||
new HttpException('用户名或密码错误', EXCEPTION_CODE.USER_NOT_EXISTS),
|
||||
new HttpException(
|
||||
'用户名或密码错误',
|
||||
EXCEPTION_CODE.USER_PASSWORD_WRONG,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -165,4 +220,29 @@ describe('AuthController', () => {
|
||||
expect(typeof result.data.img).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('password strength', () => {
|
||||
it('it should return strong', async () => {
|
||||
await expect(
|
||||
controller.getPasswordStrength('abcd&1234'),
|
||||
).resolves.toEqual({
|
||||
code: 200,
|
||||
data: 'Strong',
|
||||
});
|
||||
});
|
||||
|
||||
it('it should return medium', async () => {
|
||||
await expect(controller.getPasswordStrength('abc123')).resolves.toEqual({
|
||||
code: 200,
|
||||
data: 'Medium',
|
||||
});
|
||||
});
|
||||
|
||||
it('it should return weak', async () => {
|
||||
await expect(controller.getPasswordStrength('123456')).resolves.toEqual({
|
||||
code: 200,
|
||||
data: 'Weak',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
82
server/src/modules/auth/__test/user.controller.spec.ts
Normal file
82
server/src/modules/auth/__test/user.controller.spec.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserController } from '../controllers/user.controller';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { GetUserListDto } from '../dto/getUserList.dto';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { User } from 'src/models/user.entity';
|
||||
|
||||
describe('UserController', () => {
|
||||
let userController: UserController;
|
||||
let userService: UserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [UserController],
|
||||
providers: [
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserListByUsername: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(Authentication)
|
||||
.useValue({ canActivate: jest.fn(() => true) })
|
||||
.compile();
|
||||
|
||||
userController = module.get<UserController>(UserController);
|
||||
userService = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
describe('getUserList', () => {
|
||||
it('should return a list of users', async () => {
|
||||
const mockUserList = [
|
||||
{ _id: '1', username: 'user1' },
|
||||
{ _id: '2', username: 'user2' },
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(userService, 'getUserListByUsername')
|
||||
.mockResolvedValue(mockUserList as unknown as User[]);
|
||||
|
||||
const queryInfo: GetUserListDto = {
|
||||
username: 'testuser',
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
GetUserListDto.validate = jest
|
||||
.fn()
|
||||
.mockReturnValue({ value: queryInfo, error: null });
|
||||
|
||||
const result = await userController.getUserList(queryInfo);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: mockUserList.map((item) => ({
|
||||
userId: item._id,
|
||||
username: item.username,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an HttpException if validation fails', async () => {
|
||||
const queryInfo: GetUserListDto = {
|
||||
username: 'testuser',
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
const validationError = new Error('Validation failed');
|
||||
|
||||
GetUserListDto.validate = jest
|
||||
.fn()
|
||||
.mockReturnValue({ value: null, error: validationError });
|
||||
|
||||
await expect(userController.getUserList(queryInfo)).rejects.toThrow(
|
||||
new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -5,6 +5,7 @@ import { UserService } from '../services/user.service';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { hash256 } from 'src/utils/hash256';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
@ -20,6 +21,7 @@ describe('UserService', () => {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -31,6 +33,10 @@ describe('UserService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a user', async () => {
|
||||
const userInfo = {
|
||||
username: 'testUser',
|
||||
@ -101,7 +107,7 @@ describe('UserService', () => {
|
||||
expect(user).toEqual({ ...userInfo, password: hashedPassword });
|
||||
});
|
||||
|
||||
it('should return undefined when user is not found by credentials', async () => {
|
||||
it('should return null when user is not found by credentials', async () => {
|
||||
const userInfo = {
|
||||
username: 'nonExistingUser',
|
||||
password: 'nonExistingPassword',
|
||||
@ -128,15 +134,132 @@ describe('UserService', () => {
|
||||
const userInfo = {
|
||||
username: username,
|
||||
password: 'existingPassword',
|
||||
} as User;
|
||||
curStatus: { status: 'ACTIVE' },
|
||||
} as unknown as User;
|
||||
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
||||
|
||||
const user = await service.getUserByUsername(username);
|
||||
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { username: username },
|
||||
where: {
|
||||
username: username,
|
||||
},
|
||||
});
|
||||
expect(user).toEqual(userInfo);
|
||||
});
|
||||
|
||||
it('should return null when user is not found by username', async () => {
|
||||
const username = 'nonExistingUser';
|
||||
|
||||
const findOneSpy = jest
|
||||
.spyOn(userRepository, 'findOne')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
const user = await service.getUserByUsername(username);
|
||||
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: {
|
||||
username: username,
|
||||
},
|
||||
});
|
||||
expect(user).toBe(null);
|
||||
});
|
||||
|
||||
it('should return a user by id', async () => {
|
||||
const id = '60c72b2f9b1e8a5f4b123456';
|
||||
const userInfo = {
|
||||
_id: new ObjectId(id),
|
||||
username: 'testUser',
|
||||
curStatus: { status: 'ACTIVE' },
|
||||
} as unknown as User;
|
||||
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
||||
|
||||
const user = await service.getUserById(id);
|
||||
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
},
|
||||
});
|
||||
expect(user).toEqual(userInfo);
|
||||
});
|
||||
|
||||
it('should return null when user is not found by id', async () => {
|
||||
const id = '60c72b2f9b1e8a5f4b123456';
|
||||
|
||||
const findOneSpy = jest
|
||||
.spyOn(userRepository, 'findOne')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
const user = await service.getUserById(id);
|
||||
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
},
|
||||
});
|
||||
expect(user).toBe(null);
|
||||
});
|
||||
|
||||
it('should return a list of users by username', async () => {
|
||||
const username = 'test';
|
||||
const userList = [
|
||||
{ _id: new ObjectId(), username: 'testUser1', createdAt: new Date() },
|
||||
{ _id: new ObjectId(), username: 'testUser2', createdAt: new Date() },
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(userRepository, 'find')
|
||||
.mockResolvedValue(userList as unknown as User[]);
|
||||
|
||||
const result = await service.getUserListByUsername({
|
||||
username,
|
||||
skip: 0,
|
||||
take: 10,
|
||||
});
|
||||
|
||||
expect(userRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
username: new RegExp(username),
|
||||
},
|
||||
skip: 0,
|
||||
take: 10,
|
||||
select: ['_id', 'username', 'createdAt'],
|
||||
});
|
||||
expect(result).toEqual(userList);
|
||||
});
|
||||
|
||||
it('should return a list of users by ids', async () => {
|
||||
const idList = ['60c72b2f9b1e8a5f4b123456', '60c72b2f9b1e8a5f4b123457'];
|
||||
const userList = [
|
||||
{
|
||||
_id: new ObjectId(idList[0]),
|
||||
username: 'testUser1',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
_id: new ObjectId(idList[1]),
|
||||
username: 'testUser2',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(userRepository, 'find')
|
||||
.mockResolvedValue(userList as unknown as User[]);
|
||||
|
||||
const result = await service.getUserListByIds({ idList });
|
||||
|
||||
expect(userRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
_id: {
|
||||
$in: idList.map((id) => new ObjectId(id)),
|
||||
},
|
||||
},
|
||||
select: ['_id', 'username', 'createdAt'],
|
||||
});
|
||||
expect(result).toEqual(userList);
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import { AuthService } from './services/auth.service';
|
||||
import { CaptchaService } from './services/captcha.service';
|
||||
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
import { UserController } from './controllers/user.controller';
|
||||
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { Captcha } from 'src/models/captcha.entity';
|
||||
@ -13,7 +14,7 @@ import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, Captcha]), ConfigModule],
|
||||
controllers: [AuthController],
|
||||
controllers: [AuthController, UserController],
|
||||
providers: [UserService, AuthService, CaptchaService],
|
||||
exports: [UserService, AuthService],
|
||||
})
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpCode,
|
||||
Get,
|
||||
Query,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { CaptchaService } from '../services/captcha.service';
|
||||
@ -7,6 +15,9 @@ import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { create } from 'svg-captcha';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
const passwordReg = /^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/;
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('/api/auth')
|
||||
export class AuthController {
|
||||
@ -28,6 +39,24 @@ export class AuthController {
|
||||
captcha: string;
|
||||
},
|
||||
) {
|
||||
if (!userInfo.password) {
|
||||
throw new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID);
|
||||
}
|
||||
|
||||
if (userInfo.password.length < 6 || userInfo.password.length > 16) {
|
||||
throw new HttpException(
|
||||
'密码长度在 6 到 16 个字符',
|
||||
EXCEPTION_CODE.PASSWORD_INVALID,
|
||||
);
|
||||
}
|
||||
|
||||
if (!passwordReg.test(userInfo.password)) {
|
||||
throw new HttpException(
|
||||
'密码只能输入数字、字母、特殊字符',
|
||||
EXCEPTION_CODE.PASSWORD_INVALID,
|
||||
);
|
||||
}
|
||||
|
||||
const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
|
||||
captcha: userInfo.captcha,
|
||||
id: userInfo.captchaId,
|
||||
@ -85,6 +114,16 @@ export class AuthController {
|
||||
throw new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT);
|
||||
}
|
||||
|
||||
const username = await this.userService.getUserByUsername(
|
||||
userInfo.username,
|
||||
);
|
||||
if (!username) {
|
||||
throw new HttpException(
|
||||
'账号未注册,请进行注册',
|
||||
EXCEPTION_CODE.USER_NOT_EXISTS,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userService.getUser({
|
||||
username: userInfo.username,
|
||||
password: userInfo.password,
|
||||
@ -92,7 +131,7 @@ export class AuthController {
|
||||
if (user === null) {
|
||||
throw new HttpException(
|
||||
'用户名或密码错误',
|
||||
EXCEPTION_CODE.USER_NOT_EXISTS,
|
||||
EXCEPTION_CODE.USER_PASSWORD_WRONG,
|
||||
);
|
||||
}
|
||||
let token;
|
||||
@ -152,4 +191,59 @@ export class AuthController {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码强度
|
||||
*/
|
||||
@Get('/password/strength')
|
||||
@HttpCode(200)
|
||||
async getPasswordStrength(@Query('password') password: string) {
|
||||
const numberReg = /[0-9]/.test(password);
|
||||
const letterReg = /[a-zA-Z]/.test(password);
|
||||
const symbolReg = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
|
||||
// 包含三种、且长度大于8
|
||||
if (numberReg && letterReg && symbolReg && password.length >= 8) {
|
||||
return {
|
||||
code: 200,
|
||||
data: 'Strong',
|
||||
};
|
||||
}
|
||||
|
||||
// 满足任意两种
|
||||
if ([numberReg, letterReg, symbolReg].filter(Boolean).length >= 2) {
|
||||
return {
|
||||
code: 200,
|
||||
data: 'Medium',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: 'Weak',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/verifyToken')
|
||||
@HttpCode(200)
|
||||
async verifyToken(@Request() req) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) {
|
||||
return {
|
||||
code: 200,
|
||||
data: false,
|
||||
};
|
||||
}
|
||||
try {
|
||||
await this.authService.verifyToken(token);
|
||||
return {
|
||||
code: 200,
|
||||
data: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 200,
|
||||
data: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
65
server/src/modules/auth/controllers/user.controller.ts
Normal file
65
server/src/modules/auth/controllers/user.controller.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
|
||||
import { UserService } from '../services/user.service';
|
||||
import { GetUserListDto } from '../dto/getUserList.dto';
|
||||
|
||||
@ApiTags('user')
|
||||
@ApiBearerAuth()
|
||||
@Controller('/api/user')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@UseGuards(Authentication)
|
||||
@Get('/getUserList')
|
||||
@HttpCode(200)
|
||||
async getUserList(
|
||||
@Query()
|
||||
queryInfo: GetUserListDto,
|
||||
) {
|
||||
const { value, error } = GetUserListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const userList = await this.userService.getUserListByUsername({
|
||||
username: value.username,
|
||||
skip: (value.pageIndex - 1) * value.pageSize,
|
||||
take: value.pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: userList.map((item) => {
|
||||
return {
|
||||
userId: item._id.toString(),
|
||||
username: item.username,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authentication)
|
||||
@Get('/getUserInfo')
|
||||
async getUserInfo(@Request() req) {
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
userId: req.user._id.toString(),
|
||||
username: req.user.username,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
21
server/src/modules/auth/dto/getUserList.dto.ts
Normal file
21
server/src/modules/auth/dto/getUserList.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
|
||||
export class GetUserListDto {
|
||||
@ApiProperty({ description: '用户名', required: true })
|
||||
username: string;
|
||||
|
||||
@ApiProperty({ description: '页码', required: false, default: 1 })
|
||||
pageIndex?: number;
|
||||
|
||||
@ApiProperty({ description: '每页查询数', required: false, default: 10 })
|
||||
pageSize: number;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
username: Joi.string().required(),
|
||||
pageIndex: Joi.number().allow(null).default(1),
|
||||
pageSize: Joi.number().allow(null).default(10),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import { User } from 'src/models/user.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { hash256 } from 'src/utils/hash256';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@ -56,4 +57,38 @@ export class UserService {
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async getUserById(id: string) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async getUserListByUsername({ username, skip, take }) {
|
||||
const list = await this.userRepository.find({
|
||||
where: {
|
||||
username: new RegExp(username),
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
select: ['_id', 'username', 'createdAt'],
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
async getUserListByIds({ idList }) {
|
||||
const list = await this.userRepository.find({
|
||||
where: {
|
||||
_id: {
|
||||
$in: idList.map((item) => new ObjectId(item)),
|
||||
},
|
||||
},
|
||||
select: ['_id', 'username', 'createdAt'],
|
||||
});
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,16 @@ import {
|
||||
Body,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { FileService } from '../services/file.service';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { AuthenticationException } from 'src/exceptions/authException';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@ApiTags('file')
|
||||
@Controller('/api/file')
|
||||
export class FileController {
|
||||
constructor(
|
||||
|
@ -14,13 +14,18 @@ export class FileService {
|
||||
configKey,
|
||||
file,
|
||||
pathPrefix,
|
||||
filename,
|
||||
}: {
|
||||
configKey: string;
|
||||
file: Express.Multer.File;
|
||||
pathPrefix: string;
|
||||
filename?: string;
|
||||
}) {
|
||||
const handler = this.getHandler(configKey);
|
||||
const { key } = await handler.upload(file, { pathPrefix });
|
||||
const { key } = await handler.upload(file, {
|
||||
pathPrefix,
|
||||
filename,
|
||||
});
|
||||
const url = await handler.getUrl(key);
|
||||
return {
|
||||
key,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { join, dirname } from 'path';
|
||||
import { join, dirname, sep } from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { FileUploadHandler } from './uploadHandler.interface';
|
||||
@ -12,13 +12,20 @@ export class LocalHandler implements FileUploadHandler {
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: { pathPrefix?: string },
|
||||
options?: { pathPrefix?: string; filename?: string },
|
||||
): Promise<{ key: string }> {
|
||||
const filename = await generateUniqueFilename(file.originalname);
|
||||
let filename;
|
||||
if (options?.filename) {
|
||||
filename = file.filename;
|
||||
} else {
|
||||
filename = await generateUniqueFilename(file.originalname);
|
||||
}
|
||||
const filePath = join(
|
||||
options?.pathPrefix ? options?.pathPrefix : '',
|
||||
filename,
|
||||
);
|
||||
)
|
||||
.split(sep)
|
||||
.join('/');
|
||||
const physicalPath = join(this.physicalRootPath, filePath);
|
||||
await fse.mkdir(dirname(physicalPath), { recursive: true });
|
||||
const writeStream = createWriteStream(physicalPath);
|
||||
@ -35,6 +42,10 @@ export class LocalHandler implements FileUploadHandler {
|
||||
}
|
||||
|
||||
getUrl(key: string): string {
|
||||
if (process.env.SERVER_ENV === 'local') {
|
||||
const port = process.env.PORT || 3000;
|
||||
return `http://localhost:${port}/${key}`;
|
||||
}
|
||||
return `/${key}`;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
MESSAGE_PUSHING_TYPE,
|
||||
} from 'src/enums/messagePushing';
|
||||
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
|
||||
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
|
||||
@ -38,7 +38,7 @@ describe('MessagePushingTaskController', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Authtication,
|
||||
provide: Authentication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
MESSAGE_PUSHING_TYPE,
|
||||
MESSAGE_PUSHING_HOOK,
|
||||
} from 'src/enums/messagePushing';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
describe('MessagePushingTaskDto', () => {
|
||||
let dto: MessagePushingTaskDto;
|
||||
@ -34,9 +33,9 @@ describe('MessagePushingTaskDto', () => {
|
||||
});
|
||||
|
||||
it('should have a type', () => {
|
||||
dto.type = MESSAGE_PUSHING_TYPE.HTTP; // Set your desired type here
|
||||
dto.type = MESSAGE_PUSHING_TYPE.HTTP;
|
||||
expect(dto.type).toBeDefined();
|
||||
expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP); // Adjust based on your enum
|
||||
expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP);
|
||||
});
|
||||
|
||||
it('should have a push address', () => {
|
||||
@ -46,13 +45,13 @@ describe('MessagePushingTaskDto', () => {
|
||||
});
|
||||
|
||||
it('should have a trigger hook', () => {
|
||||
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED; // Set your desired hook here
|
||||
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
|
||||
expect(dto.triggerHook).toBeDefined();
|
||||
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED); // Adjust based on your enum
|
||||
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED);
|
||||
});
|
||||
|
||||
it('should have an array of surveys', () => {
|
||||
dto.surveys = ['survey1', 'survey2']; // Set your desired surveys here
|
||||
dto.surveys = ['survey1', 'survey2'];
|
||||
expect(dto.surveys).toBeDefined();
|
||||
expect(dto.surveys).toEqual(['survey1', 'survey2']);
|
||||
});
|
||||
@ -62,13 +61,6 @@ describe('MessagePushingTaskDto', () => {
|
||||
expect(dto.owner).toBeDefined();
|
||||
expect(dto.owner).toBe('test_owner');
|
||||
});
|
||||
|
||||
it('should have current status', () => {
|
||||
dto.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
|
||||
expect(dto.curStatus).toBeDefined();
|
||||
expect(dto.curStatus.status).toEqual(RECORD_STATUS.NEW);
|
||||
expect(dto.curStatus.date).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodeDto', () => {
|
||||
|
@ -10,7 +10,6 @@ import { MessagePushingLogService } from '../services/messagePushingLog.service'
|
||||
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
|
||||
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
|
||||
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
|
||||
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
|
||||
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
|
||||
@ -118,10 +117,12 @@ describe('MessagePushingTaskService', () => {
|
||||
expect(result).toEqual(tasks);
|
||||
expect(repository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
ownerId: mockOwnerId,
|
||||
surveys: { $all: [surveyId] },
|
||||
triggerHook: hook,
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -146,10 +147,19 @@ describe('MessagePushingTaskService', () => {
|
||||
where: {
|
||||
ownerId: mockOwnerId,
|
||||
_id: new ObjectId(taskId),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should throw an error when message pushing task is not found', async () => {
|
||||
const taskId = '65afc62904d5db18534c0f78';
|
||||
jest.spyOn(repository, 'findOne').mockResolvedValue(null); // 模拟未找到任务
|
||||
const mockOwnerId = '66028642292c50f8b71a9eee';
|
||||
|
||||
await expect(service.findOne({ id: taskId, ownerId: mockOwnerId }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
@ -161,10 +171,6 @@ describe('MessagePushingTaskService', () => {
|
||||
pushAddress: 'http://update.example.com',
|
||||
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
|
||||
surveys: ['new survey id'],
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.EDITING,
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
const existingTask = new MessagePushingTask();
|
||||
existingTask._id = new ObjectId(taskId);
|
||||
@ -190,6 +196,20 @@ describe('MessagePushingTaskService', () => {
|
||||
});
|
||||
expect(repository.save).toHaveBeenCalledWith(updatedTask);
|
||||
});
|
||||
it('should throw an error if the task to be updated is not found', async () => {
|
||||
const taskId = '65afc62904d5db18534c0f78';
|
||||
const updateDto: UpdateMessagePushingTaskDto = { name: 'Updated Task' };
|
||||
jest.spyOn(repository, 'findOne').mockResolvedValue(null); // 模拟任务未找到
|
||||
const mockOwnerId = '66028642292c50f8b71a9eee';
|
||||
|
||||
await expect(
|
||||
service.update({
|
||||
ownerId: mockOwnerId,
|
||||
id: taskId,
|
||||
updateData: updateDto,
|
||||
}),
|
||||
).rejects.toThrow(`Message pushing task with id ${taskId} not found`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
@ -197,38 +217,54 @@ describe('MessagePushingTaskService', () => {
|
||||
const taskId = '65afc62904d5db18534c0f78';
|
||||
|
||||
const updateResult = { modifiedCount: 1 };
|
||||
const mockOwnerId = '66028642292c50f8b71a9eee';
|
||||
const mockOperatorId = '66028642292c50f8b71a9eee';
|
||||
const mockOperator = 'mockOperator';
|
||||
|
||||
jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult);
|
||||
|
||||
const result = await service.remove({
|
||||
id: taskId,
|
||||
ownerId: mockOwnerId,
|
||||
operatorId: mockOperatorId,
|
||||
operator: mockOperator,
|
||||
});
|
||||
|
||||
expect(result).toEqual(updateResult);
|
||||
expect(repository.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: mockOwnerId,
|
||||
_id: new ObjectId(taskId),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
},
|
||||
$push: {
|
||||
statusList: {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
isDeleted: true,
|
||||
operatorId: mockOperatorId,
|
||||
operator: mockOperator,
|
||||
deletedAt: expect.any(Date),
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
it('should throw an error if the task to be removed is not found', async () => {
|
||||
const taskId = '65afc62904d5db18534c0f78';
|
||||
jest
|
||||
.spyOn(repository, 'updateOne')
|
||||
.mockResolvedValue({ modifiedCount: 0 }); // 模拟删除失败
|
||||
const mockOperatorId = '66028642292c50f8b71a9eee';
|
||||
const mockOperator = 'mockOperator';
|
||||
|
||||
const result = await service.remove({
|
||||
id: taskId,
|
||||
operatorId: mockOperatorId,
|
||||
operator: mockOperator,
|
||||
});
|
||||
|
||||
expect(result.modifiedCount).toBe(0);
|
||||
expect(repository.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: new ObjectId(taskId),
|
||||
},
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('surveyAuthorizeTask', () => {
|
||||
@ -258,8 +294,35 @@ describe('MessagePushingTaskService', () => {
|
||||
$push: {
|
||||
surveys: surveyId,
|
||||
},
|
||||
$set: {
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
it('should not add the surveyId if it already exists in the task', async () => {
|
||||
const taskId = '65afc62904d5db18534c0f78';
|
||||
const surveyId = '65af380475b64545e5277dd9';
|
||||
const mockOwnerId = '66028642292c50f8b71a9eee';
|
||||
|
||||
jest
|
||||
.spyOn(repository, 'updateOne')
|
||||
.mockResolvedValue({ modifiedCount: 0 }); // 模拟重复添加
|
||||
const result = await service.surveyAuthorizeTask({
|
||||
taskId,
|
||||
surveyId,
|
||||
ownerId: mockOwnerId,
|
||||
});
|
||||
|
||||
expect(result.modifiedCount).toBe(0);
|
||||
expect(repository.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: new ObjectId(taskId),
|
||||
surveys: { $nin: [surveyId] }, // 确保只有不包含时才插入
|
||||
ownerId: mockOwnerId,
|
||||
},
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -37,9 +37,4 @@ describe('UpdateMessagePushingTaskDto', () => {
|
||||
dto.surveys = null;
|
||||
expect(dto.surveys).toBeNull();
|
||||
});
|
||||
|
||||
it('should have a nullable curStatus', () => {
|
||||
dto.curStatus = null;
|
||||
expect(dto.curStatus).toBeNull();
|
||||
});
|
||||
});
|
||||
|
@ -25,9 +25,9 @@ import { QueryMessagePushingTaskListDto } from '../dto/queryMessagePushingTaskLi
|
||||
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@UseGuards(Authentication)
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('messagePushingTasks')
|
||||
@Controller('/api/messagePushingTasks')
|
||||
@ -47,12 +47,10 @@ export class MessagePushingTaskController {
|
||||
req,
|
||||
@Body() createMessagePushingTaskDto: CreateMessagePushingTaskDto,
|
||||
) {
|
||||
let data;
|
||||
try {
|
||||
data = await CreateMessagePushingTaskDto.validate(
|
||||
createMessagePushingTaskDto,
|
||||
);
|
||||
} catch (error) {
|
||||
const { error, value } = CreateMessagePushingTaskDto.validate(
|
||||
createMessagePushingTaskDto,
|
||||
);
|
||||
if (error) {
|
||||
throw new HttpException(
|
||||
`参数错误: ${error.message}`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -61,7 +59,7 @@ export class MessagePushingTaskController {
|
||||
const userId = req.user._id;
|
||||
|
||||
const messagePushingTask = await this.messagePushingTaskService.create({
|
||||
...data,
|
||||
...value,
|
||||
ownerId: userId,
|
||||
});
|
||||
return {
|
||||
@ -83,10 +81,8 @@ export class MessagePushingTaskController {
|
||||
req,
|
||||
@Query() query: QueryMessagePushingTaskListDto,
|
||||
) {
|
||||
let data;
|
||||
try {
|
||||
data = await QueryMessagePushingTaskListDto.validate(query);
|
||||
} catch (error) {
|
||||
const { error, value } = QueryMessagePushingTaskListDto.validate(query);
|
||||
if (error) {
|
||||
throw new HttpException(
|
||||
`参数错误: ${error.message}`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -94,8 +90,8 @@ export class MessagePushingTaskController {
|
||||
}
|
||||
const userId = req.user._id;
|
||||
const list = await this.messagePushingTaskService.findAll({
|
||||
surveyId: data.surveyId,
|
||||
hook: data.triggerHook,
|
||||
surveyId: value.surveyId,
|
||||
hook: value.triggerHook,
|
||||
ownerId: userId,
|
||||
});
|
||||
return {
|
||||
@ -154,8 +150,9 @@ export class MessagePushingTaskController {
|
||||
async remove(@Request() req, @Param('id') id: string) {
|
||||
const userId = req.user._id;
|
||||
const res = await this.messagePushingTaskService.remove({
|
||||
ownerId: userId,
|
||||
id,
|
||||
operator: req.user.username,
|
||||
operatorId: userId,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
|
@ -29,8 +29,8 @@ export class CreateMessagePushingTaskDto {
|
||||
})
|
||||
surveys?: string[];
|
||||
|
||||
static async validate(data) {
|
||||
return await Joi.object({
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().allow(null).default(MESSAGE_PUSHING_TYPE.HTTP),
|
||||
pushAddress: Joi.string().required(),
|
||||
@ -38,6 +38,6 @@ export class CreateMessagePushingTaskDto {
|
||||
.allow(null)
|
||||
.default(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED),
|
||||
surveys: Joi.array().items(Joi.string()).allow(null).default([]),
|
||||
}).validateAsync(data);
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
MESSAGE_PUSHING_TYPE,
|
||||
MESSAGE_PUSHING_HOOK,
|
||||
} from 'src/enums/messagePushing';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
export class MessagePushingTaskDto {
|
||||
@ApiProperty({ description: '任务id' })
|
||||
@ -27,12 +26,6 @@ export class MessagePushingTaskDto {
|
||||
|
||||
@ApiProperty({ description: '所有者' })
|
||||
owner: string;
|
||||
|
||||
@ApiProperty({ description: '任务状态', required: false })
|
||||
curStatus?: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class CodeDto {
|
||||
|
@ -13,6 +13,6 @@ export class QueryMessagePushingTaskListDto {
|
||||
return Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
triggerHook: Joi.string().required(),
|
||||
}).validateAsync(data);
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import {
|
||||
MESSAGE_PUSHING_TYPE,
|
||||
MESSAGE_PUSHING_HOOK,
|
||||
@ -20,10 +19,4 @@ export class UpdateMessagePushingTaskDto {
|
||||
|
||||
@ApiProperty({ description: '绑定的问卷id', required: false })
|
||||
surveys?: string[];
|
||||
|
||||
@ApiProperty({ description: '任务状态', required: false })
|
||||
curStatus?: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
|
||||
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
|
||||
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
|
||||
import { MessagePushingLogService } from './messagePushingLog.service';
|
||||
import { httpPost } from 'src/utils/request';
|
||||
@ -44,8 +43,8 @@ export class MessagePushingTaskService {
|
||||
ownerId?: string;
|
||||
}): Promise<MessagePushingTask[]> {
|
||||
const where: Record<string, any> = {
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
};
|
||||
if (surveyId) {
|
||||
@ -64,19 +63,19 @@ export class MessagePushingTaskService {
|
||||
});
|
||||
}
|
||||
|
||||
async findOne({
|
||||
findOne({
|
||||
id,
|
||||
ownerId,
|
||||
}: {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
}): Promise<MessagePushingTask> {
|
||||
return await this.messagePushingTaskRepository.findOne({
|
||||
return this.messagePushingTaskRepository.findOne({
|
||||
where: {
|
||||
ownerId,
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -104,25 +103,25 @@ export class MessagePushingTaskService {
|
||||
return await this.messagePushingTaskRepository.save(updatedTask);
|
||||
}
|
||||
|
||||
async remove({ id, ownerId }: { id: string; ownerId: string }) {
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
};
|
||||
async remove({
|
||||
id,
|
||||
operator,
|
||||
operatorId,
|
||||
}: {
|
||||
id: string;
|
||||
operator: string;
|
||||
operatorId: string;
|
||||
}) {
|
||||
return this.messagePushingTaskRepository.updateOne(
|
||||
{
|
||||
ownerId,
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
isDeleted: true,
|
||||
operator,
|
||||
operatorId,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -147,6 +146,9 @@ export class MessagePushingTaskService {
|
||||
$push: {
|
||||
surveys: surveyId as never,
|
||||
},
|
||||
$set: {
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
9
server/src/modules/redis/redis.module.ts
Normal file
9
server/src/modules/redis/redis.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// src/redis/redis.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
@Module({
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
32
server/src/modules/redis/redis.service.ts
Normal file
32
server/src/modules/redis/redis.service.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Redis } from 'ioredis';
|
||||
import Redlock, { Lock } from 'redlock';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
private readonly redisClient: Redis;
|
||||
private readonly redlock: Redlock;
|
||||
|
||||
constructor() {
|
||||
this.redisClient = new Redis({
|
||||
host: process.env.XIAOJU_SURVEY_REDIS_HOST,
|
||||
port: parseInt(process.env.XIAOJU_SURVEY_REDIS_PORT),
|
||||
password: process.env.XIAOJU_SURVEY_REDIS_PASSWORD || undefined,
|
||||
username: process.env.XIAOJU_SURVEY_REDIS_USERNAME || undefined,
|
||||
db: parseInt(process.env.XIAOJU_SURVEY_REDIS_DB) || 0,
|
||||
});
|
||||
this.redlock = new Redlock([this.redisClient], {
|
||||
retryCount: 10,
|
||||
retryDelay: 200, // ms
|
||||
retryJitter: 200, // ms
|
||||
});
|
||||
}
|
||||
|
||||
async lockResource(resource: string, ttl: number): Promise<Lock> {
|
||||
return this.redlock.acquire([resource], ttl);
|
||||
}
|
||||
|
||||
async unlockResource(lock: Lock): Promise<void> {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
510
server/src/modules/survey/__test/collaborator.controller.spec.ts
Normal file
510
server/src/modules/survey/__test/collaborator.controller.spec.ts
Normal file
@ -0,0 +1,510 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CollaboratorController } from '../controllers/collaborator.controller';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { GetSurveyCollaboratorListDto } from '../dto/getSurveyCollaboratorList.dto';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import {
|
||||
SURVEY_PERMISSION,
|
||||
SURVEY_PERMISSION_DESCRIPTION,
|
||||
} from 'src/enums/surveyPermission';
|
||||
import { BatchSaveCollaboratorDto } from '../dto/batchSaveCollaborator.dto';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
jest.mock('src/guards/workspace.guard');
|
||||
|
||||
describe('CollaboratorController', () => {
|
||||
let controller: CollaboratorController;
|
||||
let collaboratorService: CollaboratorService;
|
||||
let logger: Logger;
|
||||
let userService: UserService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let workspaceMemberServie: WorkspaceMemberService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [CollaboratorController],
|
||||
providers: [
|
||||
{
|
||||
provide: CollaboratorService,
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
getSurveyCollaboratorList: jest.fn(),
|
||||
changeUserPermission: jest.fn(),
|
||||
deleteCollaborator: jest.fn(),
|
||||
getCollaborator: jest.fn(),
|
||||
batchDeleteBySurveyId: jest.fn(),
|
||||
batchCreate: jest.fn(),
|
||||
batchDelete: jest.fn(),
|
||||
updateById: jest.fn(),
|
||||
batchSaveCollaborator: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserById: jest.fn().mockImplementation((id) => {
|
||||
return Promise.resolve({
|
||||
_id: new ObjectId(id),
|
||||
});
|
||||
}),
|
||||
getUserListByIds: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useValue: {
|
||||
getSurveyById: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<CollaboratorController>(CollaboratorController);
|
||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||
logger = module.get<Logger>(Logger);
|
||||
userService = module.get<UserService>(UserService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
||||
WorkspaceMemberService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('addCollaborator', () => {
|
||||
it('should add a collaborator successfully', async () => {
|
||||
const userId = new ObjectId().toString();
|
||||
const reqBody: CreateCollaboratorDto = {
|
||||
surveyId: 'surveyId',
|
||||
userId: new ObjectId().toString(),
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||
};
|
||||
const req = { user: { _id: 'userId' }, surveyMeta: { ownerId: userId } };
|
||||
const result = { _id: 'collaboratorId' };
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'create')
|
||||
.mockResolvedValue(result as unknown as Collaborator);
|
||||
|
||||
const response = await controller.addCollaborator(reqBody, req);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
collaboratorId: result._id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const reqBody: CreateCollaboratorDto = {
|
||||
surveyId: '',
|
||||
userId: '',
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception if user does not exist', async () => {
|
||||
const reqBody: CreateCollaboratorDto = {
|
||||
surveyId: 'surveyId',
|
||||
userId: new ObjectId().toString(),
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||
};
|
||||
const req = {
|
||||
user: { _id: 'userId' },
|
||||
surveyMeta: { ownerId: new ObjectId().toString() },
|
||||
};
|
||||
|
||||
jest.spyOn(userService, 'getUserById').mockResolvedValue(null);
|
||||
|
||||
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception if user is the survey owner', async () => {
|
||||
const userId = new ObjectId().toString();
|
||||
const reqBody: CreateCollaboratorDto = {
|
||||
surveyId: 'surveyId',
|
||||
userId: userId,
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||
};
|
||||
const req = { user: { _id: 'userId' }, surveyMeta: { ownerId: userId } };
|
||||
|
||||
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception if user is already a collaborator', async () => {
|
||||
const userId = new ObjectId().toString();
|
||||
const reqBody: CreateCollaboratorDto = {
|
||||
surveyId: 'surveyId',
|
||||
userId: userId,
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||
};
|
||||
const req = {
|
||||
user: { _id: 'userId' },
|
||||
surveyMeta: { ownerId: new ObjectId().toString() },
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'getCollaborator')
|
||||
.mockResolvedValue({} as unknown as Collaborator);
|
||||
|
||||
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSurveyCollaboratorList', () => {
|
||||
it('should return collaborator list', async () => {
|
||||
const query = { surveyId: 'surveyId' };
|
||||
const result = [
|
||||
{ _id: 'collaboratorId', userId: 'userId', username: '' },
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'getSurveyCollaboratorList')
|
||||
.mockResolvedValue(result as unknown as Array<Collaborator>);
|
||||
|
||||
jest.spyOn(userService, 'getUserListByIds').mockResolvedValueOnce([]);
|
||||
|
||||
const response = await controller.getSurveyCollaboratorList(query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const query: GetSurveyCollaboratorListDto = {
|
||||
surveyId: '',
|
||||
};
|
||||
|
||||
await expect(controller.getSurveyCollaboratorList(query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeUserPermission', () => {
|
||||
it('should change user permission successfully', async () => {
|
||||
const reqBody = {
|
||||
surveyId: 'surveyId',
|
||||
userId: 'userId',
|
||||
permissions: ['read'],
|
||||
};
|
||||
const result = { _id: 'userId', permissions: ['read'] };
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'changeUserPermission')
|
||||
.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.changeUserPermission(reqBody);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const reqBody = {
|
||||
surveyId: '',
|
||||
userId: '',
|
||||
permissions: ['surveyManage'],
|
||||
};
|
||||
|
||||
await expect(controller.changeUserPermission(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCollaborator', () => {
|
||||
it('should delete collaborator successfully', async () => {
|
||||
const query = { surveyId: 'surveyId', userId: 'userId' };
|
||||
const result = { acknowledged: true, deletedCount: 1 };
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'deleteCollaborator')
|
||||
.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.deleteCollaborator(query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const query = { surveyId: '', userId: '' };
|
||||
|
||||
await expect(controller.deleteCollaborator(query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// 新增的测试方法
|
||||
describe('getPermissionList', () => {
|
||||
it('should return the permission list', async () => {
|
||||
const result = Object.values(SURVEY_PERMISSION_DESCRIPTION);
|
||||
|
||||
const response = await controller.getPermissionList();
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchSaveCollaborator', () => {
|
||||
it('should batch save collaborators successfully', async () => {
|
||||
const userId0 = new ObjectId().toString();
|
||||
const userId1 = new ObjectId().toString();
|
||||
const existsCollaboratorId = new ObjectId().toString();
|
||||
const surveyId = new ObjectId().toString();
|
||||
const reqBody: BatchSaveCollaboratorDto = {
|
||||
surveyId: surveyId,
|
||||
collaborators: [
|
||||
{
|
||||
userId: userId0,
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||
},
|
||||
{
|
||||
_id: existsCollaboratorId,
|
||||
userId: userId1,
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE],
|
||||
},
|
||||
],
|
||||
};
|
||||
const req = {
|
||||
user: { _id: 'requestUserId' },
|
||||
surveyMeta: { ownerId: 'ownerId' },
|
||||
};
|
||||
|
||||
const userList = [
|
||||
{ _id: new ObjectId(userId0) },
|
||||
{ _id: new ObjectId(userId1) },
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(userService, 'getUserListByIds')
|
||||
.mockResolvedValue(userList as unknown as User[]);
|
||||
jest
|
||||
.spyOn(collaboratorService, 'batchDelete')
|
||||
.mockResolvedValue({ deletedCount: 1, acknowledged: true });
|
||||
jest
|
||||
.spyOn(collaboratorService, 'batchCreate')
|
||||
.mockResolvedValue([{}] as any);
|
||||
jest.spyOn(collaboratorService, 'updateById').mockResolvedValue({});
|
||||
|
||||
const response = await controller.batchSaveCollaborator(reqBody, req);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
});
|
||||
expect(userService.getUserListByIds).toHaveBeenCalled();
|
||||
expect(collaboratorService.batchDelete).toHaveBeenCalled();
|
||||
expect(collaboratorService.batchCreate).toHaveBeenCalled();
|
||||
expect(collaboratorService.updateById).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const reqBody: BatchSaveCollaboratorDto = {
|
||||
surveyId: '',
|
||||
collaborators: [
|
||||
{
|
||||
userId: '',
|
||||
permissions: [SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE],
|
||||
},
|
||||
],
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(
|
||||
controller.batchSaveCollaborator(reqBody, req),
|
||||
).rejects.toThrow(HttpException);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserSurveyPermissions', () => {
|
||||
it('should return owner permissions if user is the owner', async () => {
|
||||
const req = {
|
||||
user: { _id: new ObjectId(), username: 'owner' },
|
||||
};
|
||||
const query = { surveyId: 'surveyId' };
|
||||
const surveyMeta = {
|
||||
ownerId: req.user._id.toString(),
|
||||
owner: req.user.username,
|
||||
workspaceId: 'workspaceId',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
|
||||
const response = await controller.getUserSurveyPermissions(req, query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
isOwner: true,
|
||||
permissions: [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default permissions if user is a workspace member', async () => {
|
||||
const req = {
|
||||
user: { _id: new ObjectId(), username: 'user' },
|
||||
};
|
||||
const query = { surveyId: 'surveyId' };
|
||||
const surveyMeta = {
|
||||
ownerId: 'ownerId',
|
||||
owner: 'owner',
|
||||
workspaceId: 'workspaceId',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue({} as any);
|
||||
|
||||
const response = await controller.getUserSurveyPermissions(req, query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
isOwner: false,
|
||||
permissions: [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return collaborator permissions if user is a collaborator', async () => {
|
||||
const req = {
|
||||
user: { _id: new ObjectId(), username: 'user' },
|
||||
};
|
||||
const query = { surveyId: 'surveyId' };
|
||||
const surveyMeta = {
|
||||
ownerId: 'ownerId',
|
||||
owner: 'owner',
|
||||
workspaceId: 'workspaceId',
|
||||
};
|
||||
const collaborator = {
|
||||
permissions: ['read', 'write'],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue(null);
|
||||
jest
|
||||
.spyOn(collaboratorService, 'getCollaborator')
|
||||
.mockResolvedValue(collaborator as Collaborator);
|
||||
|
||||
const response = await controller.getUserSurveyPermissions(req, query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
isOwner: false,
|
||||
permissions: collaborator.permissions,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty permissions if user has no permissions', async () => {
|
||||
const req = {
|
||||
user: { _id: new ObjectId(), username: 'user' },
|
||||
};
|
||||
const query = { surveyId: 'surveyId' };
|
||||
const surveyMeta = {
|
||||
ownerId: 'ownerId',
|
||||
owner: 'owner',
|
||||
workspaceId: 'workspaceId',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||
jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue(null);
|
||||
jest
|
||||
.spyOn(collaboratorService, 'getCollaborator')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
const response = await controller.getUserSurveyPermissions(req, query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
isOwner: false,
|
||||
permissions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if survey does not exist', async () => {
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const query = { surveyId: 'nonexistentSurveyId' };
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyById')
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
controller.getUserSurveyPermissions(req, query),
|
||||
).rejects.toThrow(HttpException);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
418
server/src/modules/survey/__test/collaborator.service.spec.ts
Normal file
418
server/src/modules/survey/__test/collaborator.service.spec.ts
Normal file
@ -0,0 +1,418 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Logger } from 'src/logger';
|
||||
import { InsertManyResult, ObjectId } from 'mongodb';
|
||||
|
||||
describe('CollaboratorService', () => {
|
||||
let service: CollaboratorService;
|
||||
let repository: MongoRepository<Collaborator>;
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CollaboratorService,
|
||||
{
|
||||
provide: getRepositoryToken(Collaborator),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CollaboratorService>(CollaboratorService);
|
||||
repository = module.get<MongoRepository<Collaborator>>(
|
||||
getRepositoryToken(Collaborator),
|
||||
);
|
||||
logger = module.get<Logger>(Logger);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create and save a collaborator', async () => {
|
||||
const createSpy = jest.spyOn(repository, 'create').mockReturnValue({
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
} as Collaborator);
|
||||
const collaboratorId = new ObjectId().toString();
|
||||
const saveSpy = jest.spyOn(repository, 'save').mockResolvedValue({
|
||||
_id: new ObjectId(collaboratorId),
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
} as Collaborator);
|
||||
|
||||
const result = await service.create({
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
});
|
||||
|
||||
expect(createSpy).toHaveBeenCalledWith({
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
});
|
||||
expect(saveSpy).toHaveBeenCalledWith({
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
});
|
||||
expect(result).toEqual({
|
||||
_id: new ObjectId(collaboratorId),
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCollaborator', () => {
|
||||
it('should delete a collaborator by userId and surveyId', async () => {
|
||||
const deleteOneSpy = jest
|
||||
.spyOn(repository, 'deleteOne')
|
||||
.mockResolvedValue({ acknowledged: true, deletedCount: 1 });
|
||||
|
||||
const result = await service.deleteCollaborator({
|
||||
userId: '1',
|
||||
surveyId: '1',
|
||||
});
|
||||
|
||||
expect(deleteOneSpy).toHaveBeenCalledWith({
|
||||
userId: '1',
|
||||
surveyId: '1',
|
||||
});
|
||||
expect(result).toEqual({ acknowledged: true, deletedCount: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchCreate', () => {
|
||||
it('should batch create collaborators', async () => {
|
||||
const insertManySpy = jest
|
||||
.spyOn(repository, 'insertMany')
|
||||
.mockResolvedValue({
|
||||
insertedCount: 1,
|
||||
} as unknown as InsertManyResult<Document>);
|
||||
|
||||
const result = await service.batchCreate({
|
||||
surveyId: '1',
|
||||
collaboratorList: [{ userId: '1', permissions: [] }],
|
||||
creator: 'testCreator',
|
||||
creatorId: 'testCreatorId',
|
||||
});
|
||||
|
||||
expect(insertManySpy).toHaveBeenCalledWith([
|
||||
{
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
surveyId: '1',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
creator: 'testCreator',
|
||||
creatorId: 'testCreatorId',
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({ insertedCount: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeUserPermission', () => {
|
||||
it("should update a user's permissions", async () => {
|
||||
const updateOneSpy = jest
|
||||
.spyOn(repository, 'updateOne')
|
||||
.mockResolvedValue({});
|
||||
|
||||
const result = await service.changeUserPermission({
|
||||
userId: '1',
|
||||
surveyId: '1',
|
||||
permission: 'read',
|
||||
operator: 'testOperator',
|
||||
operatorId: 'testOperatorId',
|
||||
});
|
||||
|
||||
expect(updateOneSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
permission: 'read',
|
||||
operator: 'testOperator',
|
||||
operatorId: 'testOperatorId',
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchDelete', () => {
|
||||
it('should batch delete collaborators', async () => {
|
||||
const mockResult = { acknowledged: true, deletedCount: 1 };
|
||||
const deleteManySpy = jest
|
||||
.spyOn(repository, 'deleteMany')
|
||||
.mockResolvedValue(mockResult);
|
||||
|
||||
const collaboratorId = new ObjectId().toString();
|
||||
|
||||
const result = await service.batchDelete({
|
||||
surveyId: '1',
|
||||
idList: [collaboratorId],
|
||||
});
|
||||
|
||||
const expectedQuery = {
|
||||
surveyId: '1',
|
||||
$or: [
|
||||
{
|
||||
_id: {
|
||||
$in: [new ObjectId(collaboratorId)],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(JSON.stringify(expectedQuery));
|
||||
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
it('should batch delete collaborators by idList and userIdList', async () => {
|
||||
const collaboratorId = new ObjectId().toString();
|
||||
const deleteManySpy = jest
|
||||
.spyOn(repository, 'deleteMany')
|
||||
.mockResolvedValue({ acknowledged: true, deletedCount: 2 });
|
||||
|
||||
const result = await service.batchDelete({
|
||||
idList: [collaboratorId],
|
||||
userIdList: ['user1', 'user2'],
|
||||
surveyId: '1',
|
||||
});
|
||||
|
||||
const expectedQuery = {
|
||||
surveyId: '1',
|
||||
$or: [
|
||||
{
|
||||
userId: {
|
||||
$in: ['user1', 'user2'],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: {
|
||||
$in: [new ObjectId(collaboratorId)],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
|
||||
expect(result).toEqual({ acknowledged: true, deletedCount: 2 });
|
||||
});
|
||||
|
||||
it('should handle batch delete with neIdList only', async () => {
|
||||
const neCollaboratorId = new ObjectId().toString();
|
||||
const deleteManySpy = jest
|
||||
.spyOn(repository, 'deleteMany')
|
||||
.mockResolvedValue({ acknowledged: true, deletedCount: 1 });
|
||||
|
||||
const result = await service.batchDelete({
|
||||
neIdList: [neCollaboratorId],
|
||||
surveyId: '1',
|
||||
});
|
||||
|
||||
const expectedQuery = {
|
||||
surveyId: '1',
|
||||
$or: [
|
||||
{
|
||||
_id: {
|
||||
$nin: [new ObjectId(neCollaboratorId)],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
|
||||
expect(result).toEqual({ acknowledged: true, deletedCount: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchDeleteBySurveyId', () => {
|
||||
it('should batch delete collaborators by survey id', async () => {
|
||||
const mockResult = { acknowledged: true, deletedCount: 1 };
|
||||
const deleteManySpy = jest
|
||||
.spyOn(repository, 'deleteMany')
|
||||
.mockResolvedValue(mockResult);
|
||||
|
||||
const surveyId = new ObjectId().toString();
|
||||
|
||||
const result = await service.batchDeleteBySurveyId(surveyId);
|
||||
|
||||
expect(deleteManySpy).toHaveBeenCalledWith({
|
||||
surveyId,
|
||||
});
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateById', () => {
|
||||
it('should update collaborator by id', async () => {
|
||||
const updateOneSpy = jest
|
||||
.spyOn(repository, 'updateOne')
|
||||
.mockResolvedValue({});
|
||||
const collaboratorId = new ObjectId().toString();
|
||||
const result = await service.updateById({
|
||||
collaboratorId,
|
||||
permissions: [],
|
||||
operator: 'testOperator',
|
||||
operatorId: 'testOperatorId',
|
||||
});
|
||||
|
||||
expect(updateOneSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: new ObjectId(collaboratorId),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
permissions: [],
|
||||
operator: 'testOperator',
|
||||
operatorId: 'testOperatorId',
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSurveyCollaboratorList', () => {
|
||||
it('should return a list of collaborators for a survey', async () => {
|
||||
const collaboratorId = new ObjectId().toString();
|
||||
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
|
||||
{
|
||||
_id: new ObjectId(collaboratorId),
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
},
|
||||
] as Collaborator[]);
|
||||
|
||||
const result = await service.getSurveyCollaboratorList({ surveyId: '1' });
|
||||
|
||||
expect(findSpy).toHaveBeenCalledWith({ surveyId: '1' });
|
||||
expect(result).toEqual([
|
||||
{
|
||||
_id: new ObjectId(collaboratorId),
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCollaborator', () => {
|
||||
it('should return a collaborator', async () => {
|
||||
const collaboratorId = new ObjectId().toString();
|
||||
const findOneSpy = jest.spyOn(repository, 'findOne').mockResolvedValue({
|
||||
_id: new ObjectId(collaboratorId),
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
} as Collaborator);
|
||||
|
||||
const result = await service.getCollaborator({
|
||||
userId: '1',
|
||||
surveyId: '1',
|
||||
});
|
||||
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
_id: new ObjectId(collaboratorId),
|
||||
surveyId: '1',
|
||||
userId: '1',
|
||||
permissions: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCollaboratorListByUserId', () => {
|
||||
it('should return a list of collaborators by user id', async () => {
|
||||
const userId = new ObjectId().toString();
|
||||
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
|
||||
{
|
||||
_id: '1',
|
||||
surveyId: '1',
|
||||
userId: userId,
|
||||
permissions: [],
|
||||
} as unknown as Collaborator,
|
||||
]);
|
||||
|
||||
const result = await service.getCollaboratorListByUserId({ userId });
|
||||
|
||||
expect(findSpy).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{ _id: '1', surveyId: '1', userId, permissions: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a list of collaborators by their IDs', async () => {
|
||||
const collaboratorId1 = new ObjectId().toString();
|
||||
const collaboratorId2 = new ObjectId().toString();
|
||||
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
|
||||
{
|
||||
_id: new ObjectId(collaboratorId1),
|
||||
surveyId: '1',
|
||||
userId: 'user1',
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
_id: new ObjectId(collaboratorId2),
|
||||
surveyId: '2',
|
||||
userId: 'user2',
|
||||
permissions: [],
|
||||
},
|
||||
] as Collaborator[]);
|
||||
|
||||
const result = await service.getCollaboratorListByIds({
|
||||
idList: [collaboratorId1, collaboratorId2],
|
||||
});
|
||||
|
||||
expect(findSpy).toHaveBeenCalledWith({
|
||||
_id: {
|
||||
$in: [new ObjectId(collaboratorId1), new ObjectId(collaboratorId2)],
|
||||
},
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{
|
||||
_id: new ObjectId(collaboratorId1),
|
||||
surveyId: '1',
|
||||
userId: 'user1',
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
_id: new ObjectId(collaboratorId2),
|
||||
surveyId: '2',
|
||||
userId: 'user2',
|
||||
permissions: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -8,20 +8,27 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { Logger } from 'src/logger';
|
||||
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
|
||||
jest.mock('../services/dataStatistic.service');
|
||||
jest.mock('../services/surveyMeta.service');
|
||||
jest.mock('../../surveyResponse/services/responseScheme.service');
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
|
||||
describe('DataStatisticController', () => {
|
||||
let controller: DataStatisticController;
|
||||
let dataStatisticService: DataStatisticService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let pluginManager: PluginManager;
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -32,12 +39,6 @@ describe('DataStatisticController', () => {
|
||||
ResponseSchemaService,
|
||||
PluginManagerProvider,
|
||||
ConfigService,
|
||||
{
|
||||
provide: Authtication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
@ -54,16 +55,24 @@ describe('DataStatisticController', () => {
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<DataStatisticController>(DataStatisticController);
|
||||
dataStatisticService =
|
||||
module.get<DataStatisticService>(DataStatisticService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
const pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
pluginManager = module.get<PluginManager>(PluginManager);
|
||||
logger = module.get<Logger>(Logger);
|
||||
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
@ -79,6 +88,9 @@ describe('DataStatisticController', () => {
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId,
|
||||
isMasked: false,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
user: {
|
||||
username: 'testUser',
|
||||
@ -92,26 +104,24 @@ describe('DataStatisticController', () => {
|
||||
field: 'xxx',
|
||||
title: 'xxx',
|
||||
type: 'xxx',
|
||||
diffTime: 'xxx',
|
||||
othersCode: 'xxx',
|
||||
},
|
||||
],
|
||||
listBody: [
|
||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
||||
{ diffTime: '0.5', createdAt: '2024-02-11' },
|
||||
{ diffTime: '0.5', createdAt: '2024-02-11' },
|
||||
],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
jest
|
||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce({} as any);
|
||||
jest
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
const result = await controller.data(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
@ -119,12 +129,14 @@ describe('DataStatisticController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return data table with isDesensitive', async () => {
|
||||
it('should return data table with isMasked', async () => {
|
||||
const surveyId = new ObjectId().toString();
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId,
|
||||
isDesensitive: true,
|
||||
isMasked: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
user: {
|
||||
username: 'testUser',
|
||||
@ -138,31 +150,510 @@ describe('DataStatisticController', () => {
|
||||
field: 'xxx',
|
||||
title: 'xxx',
|
||||
type: 'xxx',
|
||||
diffTime: 'xxx',
|
||||
othersCode: 'xxx',
|
||||
},
|
||||
],
|
||||
listBody: [
|
||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '15200000000' },
|
||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||
{ diffTime: '0.5', createdAt: '2024-02-11', data123: '15200000000' },
|
||||
{ diffTime: '0.5', createdAt: '2024-02-11', data123: '13800000000' },
|
||||
],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
jest
|
||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce({} as any);
|
||||
jest
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
const result = await controller.data(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: mockDataTable,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId: '',
|
||||
},
|
||||
user: {
|
||||
username: 'testUser',
|
||||
},
|
||||
};
|
||||
|
||||
await expect(controller.data(mockRequest.query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregationStatis', () => {
|
||||
it('should return aggregation statistics', async () => {
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId: new ObjectId().toString(),
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponseSchema = {
|
||||
_id: new ObjectId('6659c3283b1cb279bc2e2b0c'),
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
date: 1717159136024,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: 'published',
|
||||
date: 1717158851823,
|
||||
},
|
||||
],
|
||||
createdAt: 1717158851823,
|
||||
updatedAt: 1717159136025,
|
||||
title: '问卷调研',
|
||||
surveyPath: 'ZdGNzTTR',
|
||||
code: {
|
||||
bannerConf: {
|
||||
titleConfig: {
|
||||
mainTitle:
|
||||
'<h3 style="text-align: center">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>',
|
||||
subTitle: '',
|
||||
applyTitle: '',
|
||||
},
|
||||
bannerConfig: {
|
||||
bgImage: '/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp',
|
||||
bgImageAllowJump: false,
|
||||
bgImageJumpLink: '',
|
||||
videoLink: '',
|
||||
postImg: '',
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
beginTime: '2024-05-31 20:31:36',
|
||||
endTime: '2034-05-31 20:31:36',
|
||||
language: 'chinese',
|
||||
showVoteProcess: 'allow',
|
||||
tLimit: 0,
|
||||
answerBegTime: '00:00:00',
|
||||
answerEndTime: '23:59:59',
|
||||
answerLimitTime: 0,
|
||||
},
|
||||
bottomConf: {
|
||||
logoImage: '/imgs/Logo.webp',
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
submitConf: {
|
||||
submitTitle: '提交',
|
||||
msgContent: {
|
||||
msg_200: '提交成功',
|
||||
msg_9001: '您来晚了,感谢支持问卷~',
|
||||
msg_9002: '请勿多次提交!',
|
||||
msg_9003: '您来晚了,已经满额!',
|
||||
msg_9004: '提交失败!',
|
||||
},
|
||||
confirmAgain: {
|
||||
is_again: true,
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
link: '',
|
||||
},
|
||||
logicConf: {
|
||||
showLogicConf: [],
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [
|
||||
{
|
||||
isRequired: true,
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'radio',
|
||||
placeholderDesc: '',
|
||||
field: 'data515',
|
||||
title: '标题2',
|
||||
placeholder: '',
|
||||
randomSort: false,
|
||||
checked: false,
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
imageUrl: '',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115019',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
imageUrl: '',
|
||||
others: false,
|
||||
mustOthers: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '115020',
|
||||
},
|
||||
],
|
||||
importKey: 'single',
|
||||
importData: '',
|
||||
cOption: '',
|
||||
cOptions: [],
|
||||
star: 5,
|
||||
exclude: false,
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data893',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'checkbox',
|
||||
placeholderDesc: '',
|
||||
sLimit: 0,
|
||||
mhLimit: 0,
|
||||
title: '标题2',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
randomSort: false,
|
||||
showLeftNum: true,
|
||||
innerRandom: false,
|
||||
checked: false,
|
||||
selectType: 'radio',
|
||||
sortWay: 'v',
|
||||
noNps: '',
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
starMin: 1,
|
||||
starMax: 5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
minMsg: '极不满意',
|
||||
maxMsg: '十分满意',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '466671',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '095415',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
optionOrigin: '',
|
||||
originType: 'selected',
|
||||
matrixOptionsRely: '',
|
||||
numberRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '1000',
|
||||
value: 1000,
|
||||
},
|
||||
},
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data820',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'radio-nps',
|
||||
placeholderDesc: '',
|
||||
sLimit: 0,
|
||||
mhLimit: 0,
|
||||
title: '标题3',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
randomSort: false,
|
||||
showLeftNum: true,
|
||||
innerRandom: false,
|
||||
checked: false,
|
||||
selectType: 'radio',
|
||||
sortWay: 'v',
|
||||
noNps: '',
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
starMin: 1,
|
||||
starMax: 5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
minMsg: '极不满意',
|
||||
maxMsg: '十分满意',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '268884',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '371166',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
optionOrigin: '',
|
||||
originType: 'selected',
|
||||
matrixOptionsRely: '',
|
||||
numberRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '1000',
|
||||
value: 1000,
|
||||
},
|
||||
},
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data549',
|
||||
showIndex: true,
|
||||
showType: true,
|
||||
showSpliter: true,
|
||||
type: 'radio-star',
|
||||
placeholderDesc: '',
|
||||
sLimit: 0,
|
||||
mhLimit: 0,
|
||||
title: '标题4',
|
||||
placeholder: '',
|
||||
valid: '',
|
||||
isRequired: true,
|
||||
randomSort: false,
|
||||
showLeftNum: true,
|
||||
innerRandom: false,
|
||||
checked: false,
|
||||
selectType: 'radio',
|
||||
sortWay: 'v',
|
||||
noNps: '',
|
||||
minNum: '',
|
||||
maxNum: '',
|
||||
starStyle: 'star',
|
||||
starMin: 1,
|
||||
starMax: 5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
minMsg: '极不满意',
|
||||
maxMsg: '十分满意',
|
||||
rangeConfig: {},
|
||||
options: [
|
||||
{
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '274183',
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: '',
|
||||
hash: '842967',
|
||||
},
|
||||
],
|
||||
star: 5,
|
||||
optionOrigin: '',
|
||||
originType: 'selected',
|
||||
matrixOptionsRely: '',
|
||||
numberRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '1000',
|
||||
value: 1000,
|
||||
},
|
||||
},
|
||||
textRange: {
|
||||
min: {
|
||||
placeholder: '0',
|
||||
value: 0,
|
||||
},
|
||||
max: {
|
||||
placeholder: '500',
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pageId: '6659c3283b1cb279bc2e2b0c',
|
||||
};
|
||||
|
||||
const mockAggregationResult = [
|
||||
{
|
||||
field: 'data515',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '115019',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
id: '115020',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data893',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '466671',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
id: '095415',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data820',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '8',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data549',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '5',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce(mockResponseSchema as any);
|
||||
jest
|
||||
.spyOn(dataStatisticService, 'aggregationStatis')
|
||||
.mockResolvedValueOnce(mockAggregationResult);
|
||||
|
||||
const result = await controller.aggregationStatis(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: expect.any(Array),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId: '',
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
controller.aggregationStatis(mockRequest.query),
|
||||
).rejects.toThrow(HttpException);
|
||||
});
|
||||
|
||||
it('should return empty data if response schema does not exist', async () => {
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId: new ObjectId().toString(),
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await controller.aggregationStatis(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ import { cloneDeep } from 'lodash';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
|
||||
describe('DataStatisticService', () => {
|
||||
@ -34,9 +34,7 @@ describe('DataStatisticService', () => {
|
||||
surveyResponseRepository = module.get<MongoRepository<SurveyResponse>>(
|
||||
getRepositoryToken(SurveyResponse),
|
||||
);
|
||||
const manager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
const manager = module.get<PluginManager>(PluginManager);
|
||||
manager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
@ -70,7 +68,7 @@ describe('DataStatisticService', () => {
|
||||
data413: 3,
|
||||
data863: '109239',
|
||||
},
|
||||
difTime: 21278,
|
||||
diffTime: 21278,
|
||||
clientTime: 1710340862733.0,
|
||||
secretKeys: [],
|
||||
optionTextAndId: {
|
||||
@ -153,8 +151,8 @@ describe('DataStatisticService', () => {
|
||||
date: 1710340863123.0,
|
||||
},
|
||||
],
|
||||
createDate: 1710340863123.0,
|
||||
updateDate: 1710340863123.0,
|
||||
createdAt: 1710340863123.0,
|
||||
updatedAt: 1710340863123.0,
|
||||
},
|
||||
] as unknown as Array<SurveyResponse>;
|
||||
|
||||
@ -197,15 +195,14 @@ describe('DataStatisticService', () => {
|
||||
data413_3: expect.any(String),
|
||||
data413: expect.any(Number),
|
||||
data863: expect.any(String),
|
||||
data413_custom: expect.any(String),
|
||||
difTime: expect.any(String),
|
||||
createDate: expect.any(String),
|
||||
diffTime: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return desensitive table data', async () => {
|
||||
it('should return desensitized table data', async () => {
|
||||
const mockSchema = cloneDeep(mockSensitiveResponseSchema);
|
||||
const surveyResponseList: Array<SurveyResponse> = [
|
||||
{
|
||||
@ -221,7 +218,7 @@ describe('DataStatisticService', () => {
|
||||
'U2FsdGVkX19bRmf3uEmXAJ/6zXd1Znr3cZsD5v4Nocr2v5XG1taXluz8cohFkDyH',
|
||||
data770: 'U2FsdGVkX18ldQMhJjFXO8aerjftZLpFnRQ4/FVcCLI=',
|
||||
},
|
||||
difTime: 806707,
|
||||
diffTime: 806707,
|
||||
clientTime: 1710400229573.0,
|
||||
secretKeys: ['data458', 'data450', 'data405', 'data770'],
|
||||
optionTextAndId: {
|
||||
@ -276,8 +273,8 @@ describe('DataStatisticService', () => {
|
||||
date: 1710400232161.0,
|
||||
},
|
||||
],
|
||||
createDate: 1710400232161.0,
|
||||
updateDate: 1710400232161.0,
|
||||
createdAt: 1710400232161.0,
|
||||
updatedAt: 1710400232161.0,
|
||||
},
|
||||
] as unknown as Array<SurveyResponse>;
|
||||
|
||||
@ -298,16 +295,173 @@ describe('DataStatisticService', () => {
|
||||
expect(result.listBody).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
createDate: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
data405: expect.any(String),
|
||||
data450: expect.any(String),
|
||||
data458: expect.any(String),
|
||||
data515: expect.any(String),
|
||||
data770: expect.any(String),
|
||||
difTime: expect.any(String),
|
||||
diffTime: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregationStatis', () => {
|
||||
it('should return correct aggregation data', async () => {
|
||||
const surveyId = '65afc62904d5db18534c0f78';
|
||||
const mockAggregationResult = {
|
||||
data515: [
|
||||
{
|
||||
count: 1,
|
||||
data: {
|
||||
data515: '115019',
|
||||
},
|
||||
},
|
||||
{
|
||||
count: 1,
|
||||
data: {
|
||||
data515: '115020',
|
||||
},
|
||||
},
|
||||
],
|
||||
data893: [
|
||||
{
|
||||
count: 1,
|
||||
data: {
|
||||
data893: ['466671'],
|
||||
},
|
||||
},
|
||||
{
|
||||
count: 1,
|
||||
data: {
|
||||
data893: ['466671', '095415'],
|
||||
},
|
||||
},
|
||||
],
|
||||
data820: [
|
||||
{
|
||||
count: 1,
|
||||
data: {
|
||||
data820: 8,
|
||||
},
|
||||
},
|
||||
],
|
||||
data549: [
|
||||
{
|
||||
count: 1,
|
||||
data: {
|
||||
data549: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fieldList = Object.keys(mockAggregationResult);
|
||||
|
||||
jest.spyOn(surveyResponseRepository, 'aggregate').mockReturnValue({
|
||||
next: jest.fn().mockResolvedValue(mockAggregationResult),
|
||||
} as any);
|
||||
|
||||
const result = await service.aggregationStatis({
|
||||
surveyId,
|
||||
fieldList,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
field: 'data515',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '115019',
|
||||
count: 1,
|
||||
},
|
||||
{
|
||||
id: '115020',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data893',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '466671',
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
id: '095415',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data820',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '8',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data549',
|
||||
data: {
|
||||
aggregation: [
|
||||
{
|
||||
id: '5',
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
submitionCount: 1,
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty aggregation data when no responses', async () => {
|
||||
const surveyId = '65afc62904d5db18534c0f78';
|
||||
const fieldList = ['data458', 'data515'];
|
||||
|
||||
jest.spyOn(surveyResponseRepository, 'aggregate').mockReturnValue({
|
||||
next: jest.fn().mockResolvedValue({}),
|
||||
} as any);
|
||||
|
||||
const result = await service.aggregationStatis({
|
||||
surveyId,
|
||||
fieldList,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
field: 'data458',
|
||||
data: {
|
||||
aggregation: [],
|
||||
submitionCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'data515',
|
||||
data: {
|
||||
aggregation: [],
|
||||
submitionCount: 0,
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
264
server/src/modules/survey/__test/downloadTask.controller.spec.ts
Normal file
264
server/src/modules/survey/__test/downloadTask.controller.spec.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { DownloadTaskController } from '../controllers/downloadTask.controller';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { DownloadTaskService } from '../services/downloadTask.service';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
|
||||
describe('DownloadTaskController', () => {
|
||||
let controller: DownloadTaskController;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let downloadTaskService: DownloadTaskService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [DownloadTaskController],
|
||||
providers: [
|
||||
{
|
||||
provide: ResponseSchemaService,
|
||||
useValue: {
|
||||
getResponseSchemaByPageId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DownloadTaskService,
|
||||
useValue: {
|
||||
createDownloadTask: jest.fn(),
|
||||
processDownloadTask: jest.fn(),
|
||||
getDownloadTaskList: jest.fn(),
|
||||
getDownloadTaskById: jest.fn(),
|
||||
deleteDownloadTask: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
varifytoken() {
|
||||
return {};
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: CollaboratorService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: Authentication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: SurveyGuard,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<DownloadTaskController>(DownloadTaskController);
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
downloadTaskService = module.get<DownloadTaskService>(DownloadTaskService);
|
||||
});
|
||||
|
||||
describe('createTask', () => {
|
||||
it('should create a download task successfully', async () => {
|
||||
const mockReqBody = {
|
||||
surveyId: new ObjectId().toString(),
|
||||
isMasked: false,
|
||||
};
|
||||
const mockReq = { user: { _id: 'mockUserId', username: 'mockUsername' } };
|
||||
const mockTaskId = 'mockTaskId';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValue({} as any);
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'createDownloadTask')
|
||||
.mockResolvedValue(mockTaskId);
|
||||
|
||||
const result = await controller.createTask(mockReqBody, mockReq);
|
||||
|
||||
expect(
|
||||
responseSchemaService.getResponseSchemaByPageId,
|
||||
).toHaveBeenCalledWith(mockReqBody.surveyId);
|
||||
expect(downloadTaskService.createDownloadTask).toHaveBeenCalledWith({
|
||||
surveyId: mockReqBody.surveyId,
|
||||
responseSchema: {},
|
||||
creatorId: mockReq.user._id.toString(),
|
||||
creator: mockReq.user.username,
|
||||
params: { isMasked: mockReqBody.isMasked },
|
||||
});
|
||||
expect(downloadTaskService.processDownloadTask).toHaveBeenCalledWith({
|
||||
taskId: mockTaskId,
|
||||
});
|
||||
expect(result).toEqual({ code: 200, data: { taskId: mockTaskId } });
|
||||
});
|
||||
|
||||
it('should throw HttpException if validation fails', async () => {
|
||||
const mockReqBody: any = { isMasked: false };
|
||||
const mockReq = { user: { _id: 'mockUserId', username: 'mockUsername' } };
|
||||
|
||||
await expect(controller.createTask(mockReqBody, mockReq)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadList', () => {
|
||||
it('should return the download task list', async () => {
|
||||
const mockQueryInfo = { pageIndex: 1, pageSize: 10 };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
const mockTaskList: any = {
|
||||
total: 1,
|
||||
list: [
|
||||
{
|
||||
_id: 'mockTaskId',
|
||||
curStatus: 'completed',
|
||||
filename: 'mockFile.csv',
|
||||
url: 'http://mock-url.com',
|
||||
fileSize: 1024,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
};
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskList')
|
||||
.mockResolvedValue(mockTaskList);
|
||||
|
||||
const result = await controller.downloadList(mockQueryInfo, mockReq);
|
||||
|
||||
expect(downloadTaskService.getDownloadTaskList).toHaveBeenCalledWith({
|
||||
creatorId: mockReq.user._id.toString(),
|
||||
pageIndex: mockQueryInfo.pageIndex,
|
||||
pageSize: mockQueryInfo.pageSize,
|
||||
});
|
||||
expect(result.data.total).toEqual(mockTaskList.total);
|
||||
expect(result.data.list[0].taskId).toEqual(
|
||||
mockTaskList.list[0]._id.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException if validation fails', async () => {
|
||||
const mockQueryInfo: any = { pageIndex: 'invalid', pageSize: 10 };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
|
||||
await expect(
|
||||
controller.downloadList(mockQueryInfo, mockReq),
|
||||
).rejects.toThrow(HttpException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadTask', () => {
|
||||
it('should return a download task', async () => {
|
||||
const mockQuery = { taskId: 'mockTaskId' };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
const mockTaskInfo: any = {
|
||||
_id: 'mockTaskId',
|
||||
creatorId: 'mockUserId',
|
||||
curStatus: 'completed',
|
||||
};
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(mockTaskInfo);
|
||||
|
||||
const result = await controller.getDownloadTask(mockQuery, mockReq);
|
||||
|
||||
expect(downloadTaskService.getDownloadTaskById).toHaveBeenCalledWith({
|
||||
taskId: mockQuery.taskId,
|
||||
});
|
||||
expect(result.data.taskId).toEqual(mockTaskInfo._id.toString());
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if user has no permission', async () => {
|
||||
const mockQuery = { taskId: 'mockTaskId' };
|
||||
const mockReq = { user: { _id: new ObjectId() } };
|
||||
const mockTaskInfo: any = {
|
||||
_id: 'mockTaskId',
|
||||
creatorId: 'mockUserId',
|
||||
curStatus: 'completed',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(mockTaskInfo);
|
||||
|
||||
await expect(
|
||||
controller.getDownloadTask(mockQuery, mockReq),
|
||||
).rejects.toThrow(new NoPermissionException('没有权限'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFileByName', () => {
|
||||
it('should delete a download task successfully', async () => {
|
||||
const mockBody = { taskId: 'mockTaskId' };
|
||||
const mockUserId = new ObjectId();
|
||||
const mockReq = {
|
||||
user: { _id: mockUserId, username: 'mockUsername' },
|
||||
};
|
||||
const mockTaskInfo: any = {
|
||||
_id: new ObjectId(),
|
||||
creatorId: mockUserId.toString(),
|
||||
};
|
||||
const mockDelRes = { modifiedCount: 1 };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(mockTaskInfo);
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'deleteDownloadTask')
|
||||
.mockResolvedValue(mockDelRes);
|
||||
|
||||
const result = await controller.deleteFileByName(mockBody, mockReq);
|
||||
|
||||
expect(downloadTaskService.deleteDownloadTask).toHaveBeenCalledWith({
|
||||
taskId: mockBody.taskId,
|
||||
operator: mockReq.user.username,
|
||||
operatorId: mockReq.user._id.toString(),
|
||||
});
|
||||
expect(result).toEqual({ code: 200, data: true });
|
||||
});
|
||||
|
||||
it('should throw HttpException if task does not exist', async () => {
|
||||
const mockBody = { taskId: 'mockTaskId' };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
controller.deleteFileByName(mockBody, mockReq),
|
||||
).rejects.toThrow(
|
||||
new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
245
server/src/modules/survey/__test/downloadTask.service.spec.ts
Normal file
245
server/src/modules/survey/__test/downloadTask.service.spec.ts
Normal file
@ -0,0 +1,245 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DownloadTaskService } from '../services/downloadTask.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DownloadTask } from 'src/models/downloadTask.entity';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { ResponseSchemaService } from 'src/modules/surveyResponse/services/responseScheme.service';
|
||||
import { DataStatisticService } from '../services/dataStatistic.service';
|
||||
import { FileService } from 'src/modules/file/services/file.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
|
||||
|
||||
describe('DownloadTaskService', () => {
|
||||
let service: DownloadTaskService;
|
||||
let downloadTaskRepository: MongoRepository<DownloadTask>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DownloadTaskService,
|
||||
{
|
||||
provide: getRepositoryToken(DownloadTask),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(SurveyResponse),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
{
|
||||
provide: ResponseSchemaService,
|
||||
useValue: {
|
||||
getResponseSchemaByPageId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DataStatisticService,
|
||||
useValue: {
|
||||
getDataTable: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FileService,
|
||||
useValue: {
|
||||
upload: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DownloadTaskService>(DownloadTaskService);
|
||||
downloadTaskRepository = module.get<MongoRepository<DownloadTask>>(
|
||||
getRepositoryToken(DownloadTask),
|
||||
);
|
||||
});
|
||||
|
||||
describe('createDownloadTask', () => {
|
||||
it('should create and save a download task', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
const mockDownloadTask = { _id: new ObjectId(mockTaskId) };
|
||||
const mockParams: any = {
|
||||
surveyId: 'survey1',
|
||||
responseSchema: { title: 'test-title', surveyPath: '/path' },
|
||||
creatorId: 'creator1',
|
||||
creator: 'creatorName',
|
||||
params: { isMasked: true },
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'create')
|
||||
.mockReturnValue(mockDownloadTask as any);
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'save')
|
||||
.mockResolvedValue(mockDownloadTask as any);
|
||||
|
||||
const result = await service.createDownloadTask(mockParams);
|
||||
|
||||
expect(downloadTaskRepository.create).toHaveBeenCalledWith({
|
||||
surveyId: mockParams.surveyId,
|
||||
surveyPath: mockParams.responseSchema.surveyPath,
|
||||
fileSize: '计算中',
|
||||
creatorId: mockParams.creatorId,
|
||||
creator: mockParams.creator,
|
||||
params: {
|
||||
...mockParams.params,
|
||||
title: mockParams.responseSchema.title,
|
||||
},
|
||||
filename: expect.any(String),
|
||||
status: DOWNLOAD_TASK_STATUS.WAITING,
|
||||
});
|
||||
expect(downloadTaskRepository.save).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockTaskId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadTaskList', () => {
|
||||
it('should return task list and total count', async () => {
|
||||
const mockCreatorId = 'creator1';
|
||||
const mockTasks = [{ _id: '1' }, { _id: '2' }];
|
||||
const mockTotal = 2;
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'findAndCount')
|
||||
.mockResolvedValue([mockTasks as any, mockTotal]);
|
||||
|
||||
const result = await service.getDownloadTaskList({
|
||||
creatorId: mockCreatorId,
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
expect(downloadTaskRepository.findAndCount).toHaveBeenCalledWith({
|
||||
where: {
|
||||
creatorId: mockCreatorId,
|
||||
isDeleted: { $ne: true },
|
||||
},
|
||||
take: 10,
|
||||
skip: 0,
|
||||
order: { createdAt: -1 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
total: mockTotal,
|
||||
list: mockTasks,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadTaskById', () => {
|
||||
it('should return task by id', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
const mockTask = { _id: new ObjectId(mockTaskId) };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'find')
|
||||
.mockResolvedValue([mockTask as any]);
|
||||
|
||||
const result = await service.getDownloadTaskById({ taskId: mockTaskId });
|
||||
|
||||
expect(downloadTaskRepository.find).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(mockTaskId) },
|
||||
});
|
||||
expect(result).toEqual(mockTask);
|
||||
});
|
||||
|
||||
it('should return null if task is not found', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
|
||||
jest.spyOn(downloadTaskRepository, 'find').mockResolvedValue([]);
|
||||
|
||||
const result = await service.getDownloadTaskById({ taskId: mockTaskId });
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDownloadTask', () => {
|
||||
it('should mark task as deleted and set deletedAt', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
const mockOperator = 'operatorName';
|
||||
const mockOperatorId = 'operatorId1';
|
||||
const mockUpdateResult = { matchedCount: 1 };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'updateOne')
|
||||
.mockResolvedValue(mockUpdateResult as any);
|
||||
|
||||
const result = await service.deleteDownloadTask({
|
||||
taskId: mockTaskId,
|
||||
operator: mockOperator,
|
||||
operatorId: mockOperatorId,
|
||||
});
|
||||
|
||||
expect(downloadTaskRepository.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: new ObjectId(mockTaskId) },
|
||||
{
|
||||
$set: {
|
||||
isDeleted: true,
|
||||
operator: mockOperator,
|
||||
operatorId: mockOperatorId,
|
||||
deletedAt: expect.any(Date),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockUpdateResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processDownloadTask', () => {
|
||||
it('should push task to queue and execute if not executing', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
jest.spyOn(service, 'executeTask').mockImplementation(jest.fn());
|
||||
|
||||
service.processDownloadTask({ taskId: mockTaskId });
|
||||
|
||||
expect(DownloadTaskService.taskList).toContain(mockTaskId);
|
||||
expect(service.executeTask).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle already executing case', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
DownloadTaskService.isExecuting = true;
|
||||
jest.spyOn(service, 'executeTask').mockImplementation(jest.fn());
|
||||
|
||||
service.processDownloadTask({ taskId: mockTaskId });
|
||||
|
||||
expect(DownloadTaskService.taskList).toContain(mockTaskId);
|
||||
expect(service.executeTask).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeTask', () => {
|
||||
it('should process and execute tasks in queue', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
DownloadTaskService.taskList.push(mockTaskId);
|
||||
|
||||
jest.spyOn(service, 'getDownloadTaskById').mockResolvedValue({
|
||||
_id: new ObjectId(mockTaskId),
|
||||
isDeleted: false,
|
||||
} as any);
|
||||
|
||||
jest.spyOn(service, 'handleDownloadTask').mockResolvedValue(undefined);
|
||||
|
||||
await service.executeTask();
|
||||
|
||||
expect(service.getDownloadTaskById).toHaveBeenCalledWith({
|
||||
taskId: mockTaskId,
|
||||
});
|
||||
expect(service.handleDownloadTask).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stop executing when queue is empty', async () => {
|
||||
DownloadTaskService.taskList = [];
|
||||
await service.executeTask();
|
||||
expect(DownloadTaskService.isExecuting).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -14,8 +14,8 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
date: 1710399368439,
|
||||
},
|
||||
],
|
||||
createDate: 1710399368440,
|
||||
updateDate: 1710399368440,
|
||||
createdAt: 1710399368440,
|
||||
updatedAt: 1710399368440,
|
||||
title: '加密全流程',
|
||||
surveyPath: 'EBzdmnSp',
|
||||
code: {
|
||||
@ -32,7 +32,7 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-03-14 14:54:41',
|
||||
beginTime: '2024-03-14 14:54:41',
|
||||
endTime: '2034-03-14 14:54:41',
|
||||
language: 'chinese',
|
||||
tLimit: 0,
|
||||
@ -44,6 +44,17 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
@ -284,7 +295,7 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
pageId: '65f29f3192862d6a9067ad1c',
|
||||
} as ResponseSchema;
|
||||
} as unknown as ResponseSchema;
|
||||
|
||||
export const mockResponseSchema: ResponseSchema = {
|
||||
_id: new ObjectId('65b0d46e04d5db18534c0f7c'),
|
||||
@ -315,7 +326,7 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-01-23 21:59:05',
|
||||
beginTime: '2024-01-23 21:59:05',
|
||||
endTime: '2034-01-23 21:59:05',
|
||||
language: 'chinese',
|
||||
tLimit: 0,
|
||||
@ -327,6 +338,17 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
@ -632,6 +654,6 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
pageId: '65afc62904d5db18534c0f78',
|
||||
createDate: 1710340841289,
|
||||
updateDate: 1710340841289.0,
|
||||
} as ResponseSchema;
|
||||
createdAt: 1710340841289,
|
||||
updatedAt: 1710340841289.0,
|
||||
} as unknown as ResponseSchema;
|
||||
|
87
server/src/modules/survey/__test/session.controller.spec.ts
Normal file
87
server/src/modules/survey/__test/session.controller.spec.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SessionController } from '../controllers/session.controller';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SessionGuard } from 'src/guards/session.guard';
|
||||
|
||||
describe('SessionController', () => {
|
||||
let controller: SessionController;
|
||||
let sessionService: jest.Mocked<SessionService>;
|
||||
let logger: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SessionController],
|
||||
providers: [
|
||||
{
|
||||
provide: SessionService,
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
updateSessionToEditing: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(Authentication)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(SurveyGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(SessionGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<SessionController>(SessionController);
|
||||
sessionService = module.get<jest.Mocked<SessionService>>(SessionService);
|
||||
logger = module.get<jest.Mocked<Logger>>(Logger);
|
||||
});
|
||||
|
||||
it('should create a session', async () => {
|
||||
const reqBody = { surveyId: '123' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const session: any = { _id: 'sessionId' };
|
||||
|
||||
sessionService.create.mockResolvedValue(session);
|
||||
|
||||
const result = await controller.create(reqBody, req);
|
||||
|
||||
expect(sessionService.create).toHaveBeenCalledWith({
|
||||
surveyId: '123',
|
||||
userId: 'userId',
|
||||
});
|
||||
expect(result).toEqual({ code: 200, data: { sessionId: 'sessionId' } });
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const reqBody = { surveyId: null };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
try {
|
||||
await controller.create(reqBody, req);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('should seize a session', async () => {
|
||||
const req = {
|
||||
sessionInfo: { _id: 'sessionId', surveyId: 'surveyId' },
|
||||
};
|
||||
|
||||
await controller.seize(req);
|
||||
|
||||
expect(sessionService.updateSessionToEditing).toHaveBeenCalledWith({
|
||||
sessionId: 'sessionId',
|
||||
surveyId: 'surveyId',
|
||||
});
|
||||
});
|
||||
});
|
144
server/src/modules/survey/__test/session.service.spec.ts
Normal file
144
server/src/modules/survey/__test/session.service.spec.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { Session } from 'src/models/session.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
|
||||
|
||||
describe('SessionService', () => {
|
||||
let service: SessionService;
|
||||
let repository: MongoRepository<Session>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SessionService,
|
||||
{
|
||||
provide: getRepositoryToken(Session),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SessionService>(SessionService);
|
||||
repository = module.get<MongoRepository<Session>>(
|
||||
getRepositoryToken(Session),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create and save a new session', async () => {
|
||||
const mockSession = {
|
||||
surveyId: 'survey123',
|
||||
userId: 'user123',
|
||||
status: SESSION_STATUS.DEACTIVATED,
|
||||
};
|
||||
|
||||
const createdSession: any = { ...mockSession, _id: new ObjectId() };
|
||||
jest.spyOn(repository, 'create').mockReturnValue(createdSession);
|
||||
jest.spyOn(repository, 'save').mockResolvedValue(createdSession);
|
||||
|
||||
const result = await service.create({
|
||||
surveyId: mockSession.surveyId,
|
||||
userId: mockSession.userId,
|
||||
});
|
||||
|
||||
expect(result).toEqual(createdSession);
|
||||
expect(repository.create).toHaveBeenCalledWith(mockSession);
|
||||
expect(repository.save).toHaveBeenCalledWith(createdSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find a session by id', async () => {
|
||||
const sessionId = '65afc62904d5db18534c0f78';
|
||||
const foundSession = {
|
||||
_id: new ObjectId(sessionId),
|
||||
surveyId: 'survey123',
|
||||
userId: 'user123',
|
||||
status: SESSION_STATUS.ACTIVATED,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(repository, 'findOne')
|
||||
.mockResolvedValue(foundSession as Session);
|
||||
|
||||
const result = await service.findOne(sessionId);
|
||||
|
||||
expect(result).toEqual(foundSession);
|
||||
expect(repository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(sessionId) },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findLatestEditingOne', () => {
|
||||
it('should find the latest editing session for a survey', async () => {
|
||||
const surveyId = 'survey123';
|
||||
const latestSession = {
|
||||
_id: new ObjectId(),
|
||||
surveyId: surveyId,
|
||||
userId: 'user123',
|
||||
status: SESSION_STATUS.ACTIVATED,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(repository, 'findOne')
|
||||
.mockResolvedValue(latestSession as Session);
|
||||
|
||||
const result = await service.findLatestEditingOne({ surveyId });
|
||||
|
||||
expect(result).toEqual(latestSession);
|
||||
expect(repository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyId,
|
||||
status: SESSION_STATUS.ACTIVATED,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSessionToEditing', () => {
|
||||
it('should update a session to editing and deactivate other sessions', async () => {
|
||||
const sessionId = '65afc62904d5db18534c0f78';
|
||||
const surveyId = 'survey123';
|
||||
|
||||
const updateResult: any = { affected: 1 };
|
||||
const updateManyResult = { modifiedCount: 1 };
|
||||
|
||||
jest.spyOn(repository, 'update').mockResolvedValue(updateResult);
|
||||
jest.spyOn(repository, 'updateMany').mockResolvedValue(updateManyResult);
|
||||
|
||||
const result = await service.updateSessionToEditing({
|
||||
sessionId,
|
||||
surveyId,
|
||||
});
|
||||
|
||||
expect(result).toEqual([updateResult, updateManyResult]);
|
||||
expect(repository.update).toHaveBeenCalledWith(
|
||||
{ _id: new ObjectId(sessionId) },
|
||||
{
|
||||
status: SESSION_STATUS.ACTIVATED,
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
);
|
||||
expect(repository.updateMany).toHaveBeenCalledWith(
|
||||
{
|
||||
surveyId,
|
||||
_id: { $ne: new ObjectId(sessionId) },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: SESSION_STATUS.DEACTIVATED,
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -5,28 +5,29 @@ import { SurveyConfService } from '../services/surveyConf.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
import { ContentSecurityService } from '../services/contentSecurity.service';
|
||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { UserService } from '../../auth/services/user.service';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
|
||||
jest.mock('../services/surveyMeta.service');
|
||||
jest.mock('../services/surveyConf.service');
|
||||
jest.mock('../../surveyResponse/services/responseScheme.service');
|
||||
jest.mock('../services/contentSecurity.service');
|
||||
jest.mock('../services/surveyHistory.service');
|
||||
|
||||
jest.mock('src/guards/authtication');
|
||||
jest.mock('../services/session.service');
|
||||
jest.mock('../../auth/services/user.service');
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
jest.mock('src/guards/workspace.guard');
|
||||
|
||||
describe('SurveyController', () => {
|
||||
let controller: SurveyController;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let surveyConfService: SurveyConfService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let contentSecurityService: ContentSecurityService;
|
||||
let surveyHistoryService: SurveyHistoryService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -37,7 +38,21 @@ describe('SurveyController', () => {
|
||||
ResponseSchemaService,
|
||||
ContentSecurityService,
|
||||
SurveyHistoryService,
|
||||
LoggerProvider,
|
||||
SessionService,
|
||||
UserService,
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Authentication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -47,17 +62,11 @@ describe('SurveyController', () => {
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
contentSecurityService = module.get<ContentSecurityService>(
|
||||
ContentSecurityService,
|
||||
);
|
||||
surveyHistoryService =
|
||||
module.get<SurveyHistoryService>(SurveyHistoryService);
|
||||
});
|
||||
|
||||
describe('getBannerData', () => {
|
||||
it('should return banner data', async () => {
|
||||
const result = await controller.getBannerData();
|
||||
|
||||
expect(result.code).toBe(200);
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
@ -69,36 +78,19 @@ describe('SurveyController', () => {
|
||||
surveyType: 'normal',
|
||||
remark: '问卷调研',
|
||||
title: '问卷调研',
|
||||
} as SurveyMeta;
|
||||
};
|
||||
|
||||
const newId = new ObjectId();
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'createSurveyMeta')
|
||||
.mockImplementation(() => {
|
||||
const result = {
|
||||
_id: newId,
|
||||
} as SurveyMeta;
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
jest
|
||||
.spyOn(surveyConfService, 'createSurveyConf')
|
||||
.mockImplementation(
|
||||
(params: {
|
||||
surveyId: string;
|
||||
surveyType: string;
|
||||
createMethod: string;
|
||||
createFrom: string;
|
||||
}) => {
|
||||
const result = {
|
||||
_id: new ObjectId(),
|
||||
pageId: params.surveyId,
|
||||
code: {},
|
||||
} as SurveyConf;
|
||||
return Promise.resolve(result);
|
||||
},
|
||||
);
|
||||
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
|
||||
_id: newId,
|
||||
} as any);
|
||||
|
||||
jest.spyOn(surveyConfService, 'createSurveyConf').mockResolvedValue({
|
||||
_id: new ObjectId(),
|
||||
} as any);
|
||||
|
||||
const result = await controller.createSurvey(surveyInfo, {
|
||||
user: { username: 'testUser' },
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@ -109,13 +101,15 @@ describe('SurveyController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if validation fails', async () => {
|
||||
const surveyInfo = {}; // Invalid data
|
||||
await expect(
|
||||
controller.createSurvey(surveyInfo as any, { user: {} }),
|
||||
).rejects.toThrow(HttpException);
|
||||
});
|
||||
|
||||
it('should create a new survey by copy', async () => {
|
||||
const existsSurveyId = new ObjectId();
|
||||
const existsSurveyMeta = {
|
||||
_id: existsSurveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
const params = {
|
||||
surveyType: 'normal',
|
||||
remark: '问卷调研',
|
||||
@ -123,20 +117,16 @@ describe('SurveyController', () => {
|
||||
createMethod: 'copy',
|
||||
createFrom: existsSurveyId.toString(),
|
||||
};
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(existsSurveyMeta));
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'createSurveyMeta')
|
||||
.mockImplementation(() => {
|
||||
const result = {
|
||||
_id: new ObjectId(),
|
||||
} as SurveyMeta;
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
const request = {
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
surveyMeta: { _id: existsSurveyId, surveyType: 'exam' },
|
||||
};
|
||||
|
||||
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
|
||||
_id: new ObjectId(),
|
||||
} as any);
|
||||
|
||||
const request = { user: { username: 'testUser' } }; // 模拟请求对象,根据实际情况进行调整
|
||||
const result = await controller.createSurvey(params, request);
|
||||
expect(result?.data?.id).toBeDefined();
|
||||
});
|
||||
@ -145,50 +135,30 @@ describe('SurveyController', () => {
|
||||
describe('updateConf', () => {
|
||||
it('should update survey configuration', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
jest
|
||||
.spyOn(surveyConfService, 'saveSurveyConf')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(surveyHistoryService, 'addHistory')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const reqBody = {
|
||||
surveyId: surveyId.toString(),
|
||||
configData: {
|
||||
bannerConf: {
|
||||
titleConfig: {},
|
||||
bannerConfig: {},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-01-23 21:59:05',
|
||||
endTime: '2034-01-23 21:59:05',
|
||||
},
|
||||
bottomConf: { logoImage: '/imgs/Logo.webp', logoImageWidth: '60%' },
|
||||
skinConf: { skinColor: '#4a4c5b', inputBgColor: '#ffffff' },
|
||||
submitConf: {},
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
},
|
||||
/* ... your config data here ... */
|
||||
},
|
||||
sessionId: 'mock-session-id',
|
||||
};
|
||||
|
||||
const result = await controller.updateConf(reqBody, {
|
||||
user: { username: 'testUser', _id: 'testUserId' },
|
||||
surveyMeta: { _id: surveyId },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if validation fails', async () => {
|
||||
const reqBody = {}; // Invalid data
|
||||
await expect(
|
||||
controller.updateConf(reqBody, { user: {} }),
|
||||
).rejects.toThrow(HttpException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSurvey', () => {
|
||||
@ -198,11 +168,8 @@ describe('SurveyController', () => {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'deleteSurveyMeta')
|
||||
.mockResolvedValue(undefined);
|
||||
@ -210,14 +177,11 @@ describe('SurveyController', () => {
|
||||
.spyOn(responseSchemaService, 'deleteResponseSchema')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.deleteSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser' } },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
const result = await controller.deleteSurvey({
|
||||
surveyMeta,
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
});
|
||||
expect(result).toEqual({ code: 200 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -228,124 +192,102 @@ describe('SurveyController', () => {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
|
||||
const request = { user: { username: 'testUser' } };
|
||||
.mockResolvedValue({} as any);
|
||||
const result = await controller.getSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
request,
|
||||
{
|
||||
surveyMeta,
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result?.data?.surveyMetaRes).toBeDefined();
|
||||
expect(result?.data?.surveyConfRes).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishSurvey', () => {
|
||||
it('should publish a survey success', async () => {
|
||||
it('should publish a survey successfully', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
isDeleted: false,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
.mockResolvedValue({
|
||||
code: {},
|
||||
} as any);
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyContentByCode')
|
||||
.mockResolvedValue({
|
||||
text: '题目1',
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(contentSecurityService, 'isForbiddenContent')
|
||||
.mockResolvedValue(false);
|
||||
.mockResolvedValue({ text: '' });
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'publishSurveyMeta')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'publishResponseSchema')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(surveyHistoryService, 'addHistory')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.publishSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser', _id: 'testUserId' } },
|
||||
{ surveyMeta, user: { username: 'testUser', _id: new ObjectId() } },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
});
|
||||
expect(result.code).toBe(200);
|
||||
});
|
||||
|
||||
it('should not publish a survey with forbidden content', async () => {
|
||||
it('should throw an error if the survey is deleted', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'normal',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyContentByCode')
|
||||
.mockResolvedValue({
|
||||
text: '违禁词',
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(contentSecurityService, 'isForbiddenContent')
|
||||
.mockResolvedValue(true);
|
||||
const surveyMeta = { _id: surveyId, isDeleted: true };
|
||||
|
||||
await expect(
|
||||
controller.publishSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser', _id: 'testUserId' } },
|
||||
{ surveyMeta, user: { username: 'testUser' } },
|
||||
),
|
||||
).rejects.toThrow(
|
||||
new HttpException(
|
||||
'问卷存在非法关键字,不允许发布',
|
||||
EXCEPTION_CODE.SURVEY_CONTENT_NOT_ALLOW,
|
||||
),
|
||||
);
|
||||
).rejects.toThrow(HttpException);
|
||||
});
|
||||
});
|
||||
|
||||
// New tests for additional methods
|
||||
describe('pausingSurvey', () => {
|
||||
it('should pause the survey successfully', async () => {
|
||||
const surveyMeta = { surveyPath: 'some/path' };
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'pausingSurveyMeta')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'pausingResponseSchema')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.pausingSurvey({
|
||||
surveyMeta,
|
||||
user: { username: 'testUser' },
|
||||
});
|
||||
expect(result.code).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPreviewSchema', () => {
|
||||
it('should get the preview schema successfully', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue({} as any);
|
||||
jest.spyOn(surveyMetaService, 'getSurveyById').mockResolvedValue({
|
||||
title: 'Test Survey',
|
||||
surveyPath: 'some/path',
|
||||
} as any);
|
||||
|
||||
const result = await controller.getPreviewSchema({
|
||||
surveyPath: surveyId.toString(),
|
||||
});
|
||||
expect(result.code).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,13 +6,16 @@ import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { Logger } from 'src/logger';
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
jest.mock('src/guards/workspace.guard');
|
||||
|
||||
describe('SurveyHistoryController', () => {
|
||||
let controller: SurveyHistoryController;
|
||||
let surveyHistoryService: SurveyHistoryService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -25,18 +28,6 @@ describe('SurveyHistoryController', () => {
|
||||
getHistoryList: jest.fn().mockResolvedValue('mockHistoryList'),
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
checkSurveyAccess: jest.fn().mockResolvedValue({}),
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: Authtication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
@ -53,25 +44,29 @@ describe('SurveyHistoryController', () => {
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useClass: jest.fn().mockImplementation(() => ({})),
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SurveyHistoryController>(SurveyHistoryController);
|
||||
surveyHistoryService =
|
||||
module.get<SurveyHistoryService>(SurveyHistoryService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
});
|
||||
|
||||
it('should return history list when query is valid', async () => {
|
||||
const req = { user: { username: 'testUser' } };
|
||||
const queryInfo = { surveyId: 'survey123', historyType: 'published' };
|
||||
|
||||
await controller.getList(queryInfo, req);
|
||||
|
||||
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledWith({
|
||||
surveyId: queryInfo.surveyId,
|
||||
username: req.user.username,
|
||||
});
|
||||
await controller.getList(queryInfo);
|
||||
|
||||
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({
|
||||
surveyId: queryInfo.surveyId,
|
||||
@ -79,6 +74,5 @@ describe('SurveyHistoryController', () => {
|
||||
});
|
||||
|
||||
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledTimes(1);
|
||||
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -42,7 +42,7 @@ describe('SurveyHistoryService', () => {
|
||||
msgContent: undefined,
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '',
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
@ -78,7 +78,12 @@ describe('SurveyHistoryService', () => {
|
||||
.spyOn(repository, 'save')
|
||||
.mockResolvedValueOnce({} as SurveyHistory);
|
||||
|
||||
await service.addHistory({ surveyId, schema, type, user });
|
||||
await service.addHistory({
|
||||
surveyId,
|
||||
schema,
|
||||
type,
|
||||
user,
|
||||
});
|
||||
|
||||
expect(spyCreate).toHaveBeenCalledWith({
|
||||
pageId: surveyId,
|
||||
@ -116,9 +121,9 @@ describe('SurveyHistoryService', () => {
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
createDate: -1,
|
||||
createdAt: -1,
|
||||
},
|
||||
select: ['createDate', 'operator', 'type', '_id'],
|
||||
select: ['createdAt', 'operator', 'type', '_id'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyMetaController } from '../controllers/surveyMeta.controller';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
jest.mock('src/guards/workspace.guard');
|
||||
|
||||
describe('SurveyMetaController', () => {
|
||||
let controller: SurveyMetaController;
|
||||
@ -18,21 +22,26 @@ describe('SurveyMetaController', () => {
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useValue: {
|
||||
checkSurveyAccess: jest.fn().mockResolvedValue({}),
|
||||
editSurveyMeta: jest.fn().mockResolvedValue(undefined),
|
||||
getSurveyMetaList: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ count: 0, data: [] }),
|
||||
},
|
||||
},
|
||||
LoggerProvider,
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error() {},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CollaboratorService,
|
||||
useValue: {
|
||||
getCollaboratorListByUserId: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(Authtication)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SurveyMetaController>(SurveyMetaController);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
@ -44,33 +53,31 @@ describe('SurveyMetaController', () => {
|
||||
title: 'Test title',
|
||||
surveyId: 'test-survey-id',
|
||||
};
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
},
|
||||
};
|
||||
|
||||
const survey = {
|
||||
title: '',
|
||||
remark: '',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockImplementation(() => {
|
||||
return Promise.resolve(survey) as Promise<SurveyMeta>;
|
||||
});
|
||||
const mockUser = {
|
||||
username: 'test-user',
|
||||
_id: new ObjectId(),
|
||||
};
|
||||
|
||||
const req = {
|
||||
user: mockUser,
|
||||
surveyMeta: survey,
|
||||
};
|
||||
|
||||
const result = await controller.updateMeta(reqBody, req);
|
||||
|
||||
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledWith({
|
||||
surveyId: reqBody.surveyId,
|
||||
username: req.user.username,
|
||||
});
|
||||
|
||||
expect(surveyMetaService.editSurveyMeta).toHaveBeenCalledWith({
|
||||
title: reqBody.title,
|
||||
remark: reqBody.remark,
|
||||
operator: mockUser.username,
|
||||
operatorId: mockUser._id.toString(),
|
||||
survey: {
|
||||
title: reqBody.title,
|
||||
remark: reqBody.remark,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({ code: 200 });
|
||||
@ -91,7 +98,6 @@ describe('SurveyMetaController', () => {
|
||||
expect(error.code).toBe(EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
expect(surveyMetaService.checkSurveyAccess).not.toHaveBeenCalled();
|
||||
expect(surveyMetaService.editSurveyMeta).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -100,65 +106,74 @@ describe('SurveyMetaController', () => {
|
||||
curPage: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
const userId = new ObjectId().toString();
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
_id: new ObjectId(userId),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyMetaList')
|
||||
.mockImplementation(() => {
|
||||
const date = new Date().getTime();
|
||||
return Promise.resolve({
|
||||
count: 10,
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
createDate: date,
|
||||
updateDate: date,
|
||||
curStatus: {
|
||||
date: date,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const result = await controller.getList(queryInfo, req);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'getSurveyMetaList')
|
||||
.mockImplementation(() => {
|
||||
const date = new Date().getTime();
|
||||
return Promise.resolve({
|
||||
count: 10,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
createDate: expect.stringMatching(
|
||||
data: [
|
||||
{
|
||||
_id: new ObjectId(),
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
curStatus: {
|
||||
date: date,
|
||||
},
|
||||
subStatus: {
|
||||
date: date,
|
||||
},
|
||||
surveyType: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const result = await controller.getList(queryInfo, req);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
count: 10,
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
createdAt: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
curStatus: expect.objectContaining({
|
||||
date: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
updateDate: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
curStatus: expect.objectContaining({
|
||||
date: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(surveyMetaService.getSurveyMetaList).toHaveBeenCalledWith({
|
||||
pageNum: queryInfo.curPage,
|
||||
pageSize: queryInfo.pageSize,
|
||||
username: req.user.username,
|
||||
filter: {},
|
||||
order: {},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
subStatus: expect.objectContaining({
|
||||
date: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
}),
|
||||
surveyType: 'normal',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(surveyMetaService.getSurveyMetaList).toHaveBeenCalledWith({
|
||||
pageNum: queryInfo.curPage,
|
||||
pageSize: queryInfo.pageSize,
|
||||
username: req.user.username,
|
||||
filter: {},
|
||||
order: {},
|
||||
surveyIdList: [],
|
||||
userId,
|
||||
workspaceId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get survey meta list with filter and order', async () => {
|
||||
@ -175,27 +190,48 @@ describe('SurveyMetaController', () => {
|
||||
condition: [{ field: 'surveyType', value: 'normal' }],
|
||||
},
|
||||
]),
|
||||
order: JSON.stringify([{ field: 'createDate', value: -1 }]),
|
||||
order: JSON.stringify([{ field: 'createdAt', value: -1 }]),
|
||||
};
|
||||
const userId = new ObjectId().toString();
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
_id: new ObjectId(userId),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await controller.getList(queryInfo, req);
|
||||
|
||||
expect(result.code).toEqual(200);
|
||||
expect(surveyMetaService.getSurveyMetaList).toHaveBeenCalledWith({
|
||||
pageNum: queryInfo.curPage,
|
||||
pageSize: queryInfo.pageSize,
|
||||
username: req.user.username,
|
||||
surveyIdList: [],
|
||||
userId,
|
||||
filter: { surveyType: 'normal', title: { $regex: 'hahah' } },
|
||||
order: { createdAt: -1 },
|
||||
workspaceId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Joi validation in getList', async () => {
|
||||
const invalidQueryInfo: any = {
|
||||
curPage: 'invalid',
|
||||
pageSize: 10,
|
||||
};
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
_id: new ObjectId(),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await controller.getList(queryInfo, req);
|
||||
|
||||
expect(result.code).toEqual(200);
|
||||
expect(surveyMetaService.getSurveyMetaList).toHaveBeenCalledWith({
|
||||
pageNum: queryInfo.curPage,
|
||||
pageSize: queryInfo.pageSize,
|
||||
username: req.user.username,
|
||||
filter: { surveyType: 'normal', title: { $regex: 'hahah' } },
|
||||
order: { createDate: -1 },
|
||||
});
|
||||
await controller.getList(invalidQueryInfo, req);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(error.code).toBe(EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -2,20 +2,16 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { NoSurveyPermissionException } from 'src/exceptions/noSurveyPermissionException';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyUtilPlugin } from 'src/securityPlugin/surveyUtilPlugin';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
describe('SurveyMetaService', () => {
|
||||
let service: SurveyMetaService;
|
||||
let surveyRepository: MongoRepository<SurveyMeta>;
|
||||
let pluginManager: XiaojuSurveyPluginManager;
|
||||
let pluginManager: PluginManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -28,10 +24,11 @@ describe('SurveyMetaService', () => {
|
||||
count: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
updateOne: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
},
|
||||
},
|
||||
PluginManagerProvider,
|
||||
PluginManager,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -39,67 +36,19 @@ describe('SurveyMetaService', () => {
|
||||
surveyRepository = module.get<MongoRepository<SurveyMeta>>(
|
||||
getRepositoryToken(SurveyMeta),
|
||||
);
|
||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
pluginManager.registerPlugin(new SurveyUtilPlugin());
|
||||
pluginManager = module.get<PluginManager>(PluginManager);
|
||||
});
|
||||
|
||||
describe('getNewSurveyPath', () => {
|
||||
it('should generate a new survey path', async () => {
|
||||
jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(1);
|
||||
jest.spyOn(pluginManager, 'triggerHook').mockResolvedValueOnce('path1');
|
||||
jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(0);
|
||||
|
||||
const surveyPath = await service.getNewSurveyPath();
|
||||
|
||||
expect(typeof surveyPath).toBe('string');
|
||||
expect(surveyRepository.count).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSurveyAccess', () => {
|
||||
it('should return survey when user has access', async () => {
|
||||
const surveyId = new ObjectId().toHexString();
|
||||
const username = 'testUser';
|
||||
const survey = { owner: username } as SurveyMeta;
|
||||
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(survey);
|
||||
|
||||
const result = await service.checkSurveyAccess({ surveyId, username });
|
||||
|
||||
expect(result).toBe(survey);
|
||||
expect(surveyRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(surveyId) },
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw SurveyNotFoundException when survey not found', async () => {
|
||||
const surveyId = new ObjectId().toHexString();
|
||||
const username = 'testUser';
|
||||
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.checkSurveyAccess({ surveyId, username }),
|
||||
).rejects.toThrow(SurveyNotFoundException);
|
||||
|
||||
expect(surveyRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(surveyId) },
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NoSurveyPermissionException when user has no access', async () => {
|
||||
const surveyId = new ObjectId().toHexString();
|
||||
const username = 'testUser';
|
||||
const surveyOwner = 'otherUser';
|
||||
const survey = { owner: surveyOwner } as SurveyMeta;
|
||||
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(survey);
|
||||
|
||||
await expect(
|
||||
service.checkSurveyAccess({ surveyId, username }),
|
||||
).rejects.toThrow(NoSurveyPermissionException);
|
||||
|
||||
expect(surveyRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(surveyId) },
|
||||
});
|
||||
expect(surveyPath).toBe('path1');
|
||||
expect(pluginManager.triggerHook).toHaveBeenCalledTimes(1);
|
||||
expect(surveyRepository.count).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -110,16 +59,14 @@ describe('SurveyMetaService', () => {
|
||||
remark: 'This is a test survey',
|
||||
surveyType: 'normal',
|
||||
username: 'testUser',
|
||||
userId: new ObjectId().toString(),
|
||||
createMethod: '',
|
||||
createFrom: '',
|
||||
workspaceId: 'workspace1',
|
||||
};
|
||||
const newSurvey = new SurveyMeta();
|
||||
|
||||
const mockedSurveyPath = 'mockedSurveyPath';
|
||||
jest
|
||||
.spyOn(service, 'getNewSurveyPath')
|
||||
.mockResolvedValue(mockedSurveyPath);
|
||||
|
||||
jest.spyOn(service, 'getNewSurveyPath').mockResolvedValue('path1');
|
||||
jest
|
||||
.spyOn(surveyRepository, 'create')
|
||||
.mockImplementation(() => newSurvey);
|
||||
@ -131,134 +78,167 @@ describe('SurveyMetaService', () => {
|
||||
title: params.title,
|
||||
remark: params.remark,
|
||||
surveyType: params.surveyType,
|
||||
surveyPath: mockedSurveyPath,
|
||||
surveyPath: 'path1',
|
||||
creator: params.username,
|
||||
creatorId: params.userId,
|
||||
owner: params.username,
|
||||
ownerId: params.userId,
|
||||
createMethod: params.createMethod,
|
||||
createFrom: params.createFrom,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey);
|
||||
expect(result).toEqual(newSurvey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editSurveyMeta', () => {
|
||||
it('should edit a survey meta and return it if in NEW or EDITING status', async () => {
|
||||
describe('pausingSurveyMeta', () => {
|
||||
it('should throw an exception if survey is in NEW status', async () => {
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
|
||||
|
||||
await expect(service.pausingSurveyMeta(survey)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pause a survey and update subStatus', async () => {
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
|
||||
survey.statusList = [];
|
||||
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
|
||||
|
||||
const result = await service.editSurveyMeta(survey);
|
||||
const result = await service.pausingSurveyMeta(survey);
|
||||
|
||||
expect(survey.curStatus.status).toEqual(RECORD_STATUS.EDITING);
|
||||
expect(survey.subStatus.status).toBe(RECORD_SUB_STATUS.PAUSING);
|
||||
expect(survey.statusList.length).toBe(1);
|
||||
expect(survey.statusList[0].status).toEqual(RECORD_STATUS.EDITING);
|
||||
expect(survey.statusList[0].status).toBe(RECORD_SUB_STATUS.PAUSING);
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
|
||||
expect(result).toEqual(survey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editSurveyMeta', () => {
|
||||
it('should edit a survey meta and return it', async () => {
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
|
||||
survey.statusList = [];
|
||||
|
||||
const operator = 'editor';
|
||||
const operatorId = 'editorId';
|
||||
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
|
||||
|
||||
const result = await service.editSurveyMeta({
|
||||
survey,
|
||||
operator,
|
||||
operatorId,
|
||||
});
|
||||
|
||||
expect(survey.curStatus.status).toBe(RECORD_STATUS.EDITING);
|
||||
expect(survey.statusList.length).toBe(1);
|
||||
expect(survey.statusList[0].status).toBe(RECORD_STATUS.EDITING);
|
||||
expect(survey.operator).toBe(operator);
|
||||
expect(survey.operatorId).toBe(operatorId);
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
|
||||
expect(result).toEqual(survey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSurveyMeta', () => {
|
||||
it('should delete survey meta and update status', async () => {
|
||||
// 准备假的SurveyMeta对象
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
|
||||
survey.statusList = [];
|
||||
it('should mark a survey as deleted', async () => {
|
||||
const surveyId = new ObjectId().toString();
|
||||
const operator = 'deleter';
|
||||
const operatorId = 'deleterId';
|
||||
|
||||
// 模拟save方法
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
|
||||
jest.spyOn(surveyRepository, 'updateOne').mockResolvedValue({
|
||||
matchedCount: 1,
|
||||
modifiedCount: 1,
|
||||
acknowledged: true,
|
||||
});
|
||||
|
||||
// 调用要测试的方法
|
||||
const result = await service.deleteSurveyMeta(survey);
|
||||
const result = await service.deleteSurveyMeta({
|
||||
surveyId,
|
||||
operator,
|
||||
operatorId,
|
||||
});
|
||||
|
||||
// 验证结果
|
||||
expect(result).toBe(survey);
|
||||
expect(survey.curStatus.status).toBe(RECORD_STATUS.REMOVED);
|
||||
expect(survey.statusList.length).toBe(1);
|
||||
expect(survey.statusList[0].status).toBe(RECORD_STATUS.REMOVED);
|
||||
expect(surveyRepository.save).toHaveBeenCalledTimes(1);
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
|
||||
});
|
||||
|
||||
it('should throw exception when survey is already removed', async () => {
|
||||
// 准备假的SurveyMeta对象,其状态已设置为REMOVED
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.REMOVED, date: Date.now() };
|
||||
|
||||
// 调用要测试的方法并期待异常
|
||||
await expect(service.deleteSurveyMeta(survey)).rejects.toThrow(
|
||||
HttpException,
|
||||
expect(surveyRepository.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: new ObjectId(surveyId) },
|
||||
{
|
||||
$set: {
|
||||
isDeleted: true,
|
||||
operator,
|
||||
operatorId,
|
||||
deletedAt: expect.any(Date),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// 验证save方法没有被调用
|
||||
expect(surveyRepository.save).not.toHaveBeenCalled();
|
||||
expect(result.matchedCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSurveyMetaList', () => {
|
||||
it('should return a list of survey metadata', async () => {
|
||||
// 准备模拟数据
|
||||
const mockData = [
|
||||
{ _id: 1, title: 'Survey 1' },
|
||||
{ _id: 2, title: 'Survey 2' },
|
||||
] as unknown as Array<SurveyMeta>;
|
||||
const mockCount = 2;
|
||||
const mockCount = 1;
|
||||
|
||||
jest
|
||||
.spyOn(surveyRepository, 'findAndCount')
|
||||
.mockResolvedValue([mockData, mockCount]);
|
||||
|
||||
// 调用方法并检查返回值
|
||||
const condition = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
userId: 'testUserId',
|
||||
username: 'testUser',
|
||||
filter: {},
|
||||
order: {},
|
||||
};
|
||||
|
||||
const result = await service.getSurveyMetaList(condition);
|
||||
|
||||
// 验证返回值
|
||||
expect(result).toEqual({ data: mockData, count: mockCount });
|
||||
// 验证repository方法被正确调用
|
||||
expect(surveyRepository.findAndCount).toHaveBeenCalledWith({
|
||||
where: {
|
||||
owner: 'testUser',
|
||||
'curStatus.status': { $ne: 'removed' },
|
||||
},
|
||||
skip: 0,
|
||||
take: 10,
|
||||
order: { createDate: -1 },
|
||||
});
|
||||
expect(surveyRepository.findAndCount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishSurveyMeta', () => {
|
||||
it('should publish a survey meta and add status to statusList', async () => {
|
||||
// 准备模拟数据
|
||||
const surveyMeta = {
|
||||
id: 1,
|
||||
title: 'Test Survey',
|
||||
statusList: [],
|
||||
} as unknown as SurveyMeta;
|
||||
const savedSurveyMeta = {
|
||||
...surveyMeta,
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
} as unknown as SurveyMeta;
|
||||
it('should publish a survey and update curStatus', async () => {
|
||||
const surveyMeta = new SurveyMeta();
|
||||
surveyMeta.statusList = [];
|
||||
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(savedSurveyMeta);
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(surveyMeta);
|
||||
|
||||
// 调用方法并检查返回值
|
||||
const result = await service.publishSurveyMeta({ surveyMeta });
|
||||
|
||||
// 验证返回值
|
||||
expect(result).toEqual(savedSurveyMeta);
|
||||
// 验证repository方法被正确调用
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(savedSurveyMeta);
|
||||
expect(surveyMeta.curStatus.status).toBe(RECORD_STATUS.PUBLISHED);
|
||||
expect(surveyMeta.statusList.length).toBe(1);
|
||||
expect(surveyMeta.statusList[0].status).toBe(RECORD_STATUS.PUBLISHED);
|
||||
expect(surveyRepository.save).toHaveBeenCalledWith(surveyMeta);
|
||||
expect(result).toEqual(surveyMeta);
|
||||
});
|
||||
});
|
||||
|
||||
describe('countSurveyMetaByWorkspaceId', () => {
|
||||
it('should return the count of surveys in a workspace', async () => {
|
||||
const workspaceId = 'workspace1';
|
||||
const mockCount = 5;
|
||||
|
||||
jest.spyOn(surveyRepository, 'count').mockResolvedValue(mockCount);
|
||||
|
||||
const result = await service.countSurveyMetaByWorkspaceId({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
expect(result).toBe(mockCount);
|
||||
expect(surveyRepository.count).toHaveBeenCalledWith({
|
||||
workspaceId,
|
||||
isDeleted: { $ne: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
378
server/src/modules/survey/controllers/collaborator.controller.ts
Normal file
378
server/src/modules/survey/controllers/collaborator.controller.ts
Normal file
@ -0,0 +1,378 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
Post,
|
||||
Query,
|
||||
Request,
|
||||
SetMetadata,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import * as Joi from 'joi';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import {
|
||||
SURVEY_PERMISSION,
|
||||
SURVEY_PERMISSION_DESCRIPTION,
|
||||
} from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
|
||||
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
|
||||
import { ChangeUserPermissionDto } from '../dto/changeUserPermission.dto';
|
||||
import { GetSurveyCollaboratorListDto } from '../dto/getSurveyCollaboratorList.dto';
|
||||
import { BatchSaveCollaboratorDto } from '../dto/batchSaveCollaborator.dto';
|
||||
import { splitCollaborators } from '../utils/splitCollaborator';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
|
||||
@UseGuards(Authentication)
|
||||
@ApiTags('collaborator')
|
||||
@ApiBearerAuth()
|
||||
@Controller('/api/collaborator')
|
||||
export class CollaboratorController {
|
||||
constructor(
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
private readonly logger: Logger,
|
||||
private readonly userService: UserService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly workspaceMemberServie: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
@Get('getPermissionList')
|
||||
@HttpCode(200)
|
||||
async getPermissionList() {
|
||||
const vals = Object.values(SURVEY_PERMISSION_DESCRIPTION);
|
||||
return {
|
||||
code: 200,
|
||||
data: vals,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async addCollaborator(
|
||||
@Body() reqBody: CreateCollaboratorDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { error, value } = CreateCollaboratorDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException(
|
||||
'系统错误,请联系管理员',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await this.userService.getUserById(value.userId);
|
||||
if (!user) {
|
||||
throw new HttpException('用户不存在', EXCEPTION_CODE.USER_NOT_EXISTS);
|
||||
}
|
||||
|
||||
if (user._id.toString() === req.surveyMeta.ownerId) {
|
||||
throw new HttpException(
|
||||
'不能给问卷所有者授权',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const collaborator = await this.collaboratorService.getCollaborator({
|
||||
userId: value.userId,
|
||||
surveyId: value.surveyId,
|
||||
});
|
||||
|
||||
if (collaborator) {
|
||||
throw new HttpException(
|
||||
'用户已经是协作者',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const res = await this.collaboratorService.create(value);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
collaboratorId: res._id.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('batchSave')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async batchSaveCollaborator(
|
||||
@Body() reqBody: BatchSaveCollaboratorDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { error, value } = BatchSaveCollaboratorDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException(
|
||||
'系统错误,请联系管理员',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(value.collaborators) && value.collaborators.length > 0) {
|
||||
const collaboratorUserIdList = value.collaborators.map(
|
||||
(item) => item.userId,
|
||||
);
|
||||
for (const collaboratorUserId of collaboratorUserIdList) {
|
||||
if (collaboratorUserId === req.surveyMeta.ownerId) {
|
||||
throw new HttpException(
|
||||
'不能给问卷所有者授权',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
// 不能有重复的userId
|
||||
const userIdSet = new Set(collaboratorUserIdList);
|
||||
if (collaboratorUserIdList.length !== Array.from(userIdSet).length) {
|
||||
throw new HttpException(
|
||||
'不能重复添加用户',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
const userList = await this.userService.getUserListByIds({
|
||||
idList: collaboratorUserIdList,
|
||||
});
|
||||
const userInfoMap = userList.reduce((pre, cur) => {
|
||||
const id = cur._id.toString();
|
||||
pre[id] = cur;
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
for (const collaborator of value.collaborators) {
|
||||
if (!userInfoMap[collaborator.userId]) {
|
||||
throw new HttpException(
|
||||
`用户id: {${collaborator.userId}} 不存在`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value.collaborators) && value.collaborators.length > 0) {
|
||||
const { newCollaborator, existsCollaborator } = splitCollaborators(
|
||||
value.collaborators,
|
||||
);
|
||||
const collaboratorIdList = existsCollaborator.map((item) => item._id);
|
||||
const newCollaboratorUserIdList = newCollaborator.map(
|
||||
(item) => item.userId,
|
||||
);
|
||||
const delRes = await this.collaboratorService.batchDelete({
|
||||
surveyId: value.surveyId,
|
||||
idList: [],
|
||||
neIdList: collaboratorIdList,
|
||||
userIdList: newCollaboratorUserIdList,
|
||||
});
|
||||
this.logger.info('batchDelete:' + JSON.stringify(delRes));
|
||||
const username = req.user.username;
|
||||
const userId = req.user._id.toString();
|
||||
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
|
||||
const insertRes = await this.collaboratorService.batchCreate({
|
||||
surveyId: value.surveyId,
|
||||
collaboratorList: newCollaborator,
|
||||
creator: username,
|
||||
creatorId: userId,
|
||||
});
|
||||
this.logger.info(`${JSON.stringify(insertRes)}`);
|
||||
}
|
||||
if (Array.isArray(existsCollaborator) && existsCollaborator.length > 0) {
|
||||
const updateRes = await Promise.all(
|
||||
existsCollaborator.map((item) =>
|
||||
this.collaboratorService.updateById({
|
||||
collaboratorId: item._id,
|
||||
permissions: item.permissions,
|
||||
operator: username,
|
||||
operatorId: userId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.logger.info(`${JSON.stringify(updateRes)}`);
|
||||
}
|
||||
} else {
|
||||
// 删除所有协作者
|
||||
const delRes = await this.collaboratorService.batchDeleteBySurveyId(
|
||||
value.surveyId,
|
||||
);
|
||||
this.logger.info(JSON.stringify(delRes));
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'query.surveyId')
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async getSurveyCollaboratorList(
|
||||
@Query() query: GetSurveyCollaboratorListDto,
|
||||
) {
|
||||
const { error, value } = GetSurveyCollaboratorListDto.validate(query);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const res = await this.collaboratorService.getSurveyCollaboratorList(value);
|
||||
|
||||
const userIdList = res.map((item) => item.userId);
|
||||
const userList = await this.userService.getUserListByIds({
|
||||
idList: userIdList,
|
||||
});
|
||||
const userInfoMap = userList.reduce((pre, cur) => {
|
||||
const id = cur._id.toString();
|
||||
pre[id] = cur;
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: res.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
username: userInfoMap[item.userId]?.username || '',
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('changeUserPermission')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async changeUserPermission(@Body() reqBody: ChangeUserPermissionDto) {
|
||||
const { error, value } = Joi.object({
|
||||
surveyId: Joi.string(),
|
||||
userId: Joi.string(),
|
||||
permissions: Joi.array().items(Joi.string().required()),
|
||||
}).validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const res = await this.collaboratorService.changeUserPermission(value);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: res,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('deleteCollaborator')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async deleteCollaborator(@Query() query) {
|
||||
const { error, value } = Joi.object({
|
||||
surveyId: Joi.string(),
|
||||
userId: Joi.string(),
|
||||
}).validate(query);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const res = await this.collaboratorService.deleteCollaborator(value);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: res,
|
||||
};
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Get('permissions')
|
||||
async getUserSurveyPermissions(@Request() req, @Query() query) {
|
||||
const user = req.user;
|
||||
const userId = user._id.toString();
|
||||
const surveyId = query.surveyId;
|
||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||
|
||||
if (!surveyMeta) {
|
||||
this.logger.error(`问卷不存在: ${surveyId}`);
|
||||
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 问卷owner,有问卷的权限
|
||||
if (
|
||||
surveyMeta?.ownerId === userId ||
|
||||
surveyMeta?.owner === req.user.username
|
||||
) {
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
isOwner: true,
|
||||
permissions: [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
// 有空间权限,默认也有所有权限
|
||||
if (surveyMeta.workspaceId) {
|
||||
const memberInfo = await this.workspaceMemberServie.findOne({
|
||||
workspaceId: surveyMeta.workspaceId,
|
||||
userId,
|
||||
});
|
||||
if (memberInfo) {
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
isOwner: false,
|
||||
permissions: [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const colloborator = await this.collaboratorService.getCollaborator({
|
||||
surveyId,
|
||||
userId,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
isOwner: false,
|
||||
permissions: colloborator?.permissions || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -4,49 +4,57 @@ import {
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
SetMetadata,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { DataStatisticService } from '../services/dataStatistic.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { AggregationStatisDto } from '../dto/aggregationStatis.dto';
|
||||
import { handleAggretionData } from '../utils';
|
||||
import { QUESTION_TYPE } from 'src/enums/question';
|
||||
|
||||
@ApiTags('survey')
|
||||
@ApiBearerAuth()
|
||||
@Controller('/api/survey/dataStatistic')
|
||||
export class DataStatisticController {
|
||||
constructor(
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly pluginManager: PluginManager,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Get('/dataTable')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'query.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async data(
|
||||
@Query()
|
||||
queryInfo,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
const { value, error } = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isDesensitive: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||
isMasked: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||
page: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(10),
|
||||
}).validateAsync(queryInfo);
|
||||
const { surveyId, isDesensitive, page, pageSize } = validationResult;
|
||||
const username = req.user.username;
|
||||
await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
}).validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { surveyId, isMasked, page, pageSize } = value;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const { total, listHead, listBody } =
|
||||
@ -57,10 +65,10 @@ export class DataStatisticController {
|
||||
pageSize,
|
||||
});
|
||||
|
||||
if (isDesensitive) {
|
||||
if (isMasked) {
|
||||
// 脱敏
|
||||
listBody.forEach((item) => {
|
||||
this.pluginManager.triggerHook('desensitiveData', item);
|
||||
this.pluginManager.triggerHook('maskData', item);
|
||||
});
|
||||
}
|
||||
|
||||
@ -73,4 +81,50 @@ export class DataStatisticController {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/aggregationStatis')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async aggregationStatis(@Query() queryInfo: AggregationStatisDto) {
|
||||
// 聚合统计
|
||||
const { value, error } = AggregationStatisDto.validate(queryInfo);
|
||||
if (error) {
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(
|
||||
value.surveyId,
|
||||
);
|
||||
if (!responseSchema) {
|
||||
return {
|
||||
code: 200,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
const allowQuestionType = [
|
||||
QUESTION_TYPE.RADIO,
|
||||
QUESTION_TYPE.CHECKBOX,
|
||||
QUESTION_TYPE.BINARY_CHOICE,
|
||||
QUESTION_TYPE.RADIO_STAR,
|
||||
QUESTION_TYPE.RADIO_NPS,
|
||||
QUESTION_TYPE.VOTE,
|
||||
];
|
||||
const fieldList = responseSchema.code.dataConf.dataList
|
||||
.filter((item) => allowQuestionType.includes(item.type as QUESTION_TYPE))
|
||||
.map((item) => item.field);
|
||||
const dataMap = responseSchema.code.dataConf.dataList.reduce((pre, cur) => {
|
||||
pre[cur.field] = cur;
|
||||
return pre;
|
||||
}, {});
|
||||
const res = await this.dataStatisticService.aggregationStatis({
|
||||
surveyId: value.surveyId,
|
||||
fieldList,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: res.map((item) => {
|
||||
return handleAggretionData({ item, dataMap });
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
189
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
189
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
Post,
|
||||
Body,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { DownloadTaskService } from '../services/downloadTask.service';
|
||||
import {
|
||||
GetDownloadTaskDto,
|
||||
CreateDownloadDto,
|
||||
GetDownloadTaskListDto,
|
||||
DeleteDownloadTaskDto,
|
||||
} from '../dto/downloadTask.dto';
|
||||
import moment from 'moment';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
|
||||
@ApiTags('downloadTask')
|
||||
@ApiBearerAuth()
|
||||
@Controller('/api/downloadTask')
|
||||
export class DownloadTaskController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly downloadTaskService: DownloadTaskService,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@Post('/createTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async createTask(
|
||||
@Body()
|
||||
reqBody: CreateDownloadDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = CreateDownloadDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { surveyId, isMasked } = value;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const id = await this.downloadTaskService.createDownloadTask({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
creatorId: req.user._id.toString(),
|
||||
creator: req.user.username,
|
||||
params: { isMasked },
|
||||
});
|
||||
this.downloadTaskService.processDownloadTask({ taskId: id });
|
||||
return {
|
||||
code: 200,
|
||||
data: { taskId: id },
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/getDownloadTaskList')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async downloadList(
|
||||
@Query()
|
||||
queryInfo: GetDownloadTaskListDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = GetDownloadTaskListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { pageIndex, pageSize } = value;
|
||||
const { total, list } = await this.downloadTaskService.getDownloadTaskList({
|
||||
creatorId: req.user._id.toString(),
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
total: total,
|
||||
list: list.map((data) => {
|
||||
const item: Record<string, any> = {};
|
||||
item.taskId = data._id.toString();
|
||||
item.status = data.status;
|
||||
item.filename = data.filename;
|
||||
item.url = data.url;
|
||||
const fmt = 'YYYY-MM-DD HH:mm:ss';
|
||||
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
let unitIndex = 0;
|
||||
let size = Number(data.fileSize);
|
||||
if (isNaN(size)) {
|
||||
item.fileSize = data.fileSize;
|
||||
} else {
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
item.fileSize = `${size.toFixed()} ${units[unitIndex]}`;
|
||||
}
|
||||
item.createdAt = moment(data.createdAt).format(fmt);
|
||||
return item;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/getDownloadTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async getDownloadTask(@Query() query: GetDownloadTaskDto, @Request() req) {
|
||||
const { value, error } = GetDownloadTaskDto.validate(query);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const taskInfo = await this.downloadTaskService.getDownloadTaskById({
|
||||
taskId: value.taskId,
|
||||
});
|
||||
|
||||
if (!taskInfo) {
|
||||
throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
if (taskInfo.creatorId !== req.user._id.toString()) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
const res: Record<string, any> = {
|
||||
...taskInfo,
|
||||
};
|
||||
res.taskId = taskInfo._id.toString();
|
||||
delete res._id;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: res,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/deleteDownloadTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async deleteFileByName(@Body() body: DeleteDownloadTaskDto, @Request() req) {
|
||||
const { value, error } = DeleteDownloadTaskDto.validate(body);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { taskId } = value;
|
||||
|
||||
const taskInfo = await this.downloadTaskService.getDownloadTaskById({
|
||||
taskId,
|
||||
});
|
||||
|
||||
if (!taskInfo) {
|
||||
throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
if (taskInfo.creatorId !== req.user._id.toString()) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const delRes = await this.downloadTaskService.deleteDownloadTask({
|
||||
taskId,
|
||||
operator: req.user.username,
|
||||
operatorId: req.user._id.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: delRes.modifiedCount === 1,
|
||||
};
|
||||
}
|
||||
}
|
90
server/src/modules/survey/controllers/session.controller.ts
Normal file
90
server/src/modules/survey/controllers/session.controller.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SessionService } from '../services/session.service';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { SessionGuard } from 'src/guards/session.guard';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/session')
|
||||
export class SessionController {
|
||||
constructor(
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@Post('/create')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async create(
|
||||
@Body()
|
||||
reqBody: {
|
||||
surveyId: string;
|
||||
},
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(reqBody);
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const surveyId = value.surveyId;
|
||||
const session = await this.sessionService.create({
|
||||
surveyId,
|
||||
userId: req.user._id.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
sessionId: session._id.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/seize')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SessionGuard, SurveyGuard)
|
||||
@SetMetadata('sessionId', 'body.sessionId')
|
||||
@SetMetadata('surveyId', 'surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async seize(
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const sessionInfo = req.sessionInfo;
|
||||
|
||||
await this.sessionService.updateSessionToEditing({
|
||||
sessionId: sessionInfo._id.toString(),
|
||||
surveyId: sessionInfo.surveyId,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
}
|
@ -7,7 +7,10 @@ import {
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
SetMetadata,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { SurveyConfService } from '../services/surveyConf.service';
|
||||
@ -16,14 +19,20 @@ import { ContentSecurityService } from '../services/contentSecurity.service';
|
||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
|
||||
import BannerData from '../template/banner/index.json';
|
||||
import { CreateSurveyDto } from '../dto/createSurvey.dto';
|
||||
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { HISTORY_TYPE } from 'src/enums';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { Logger } from 'src/logger';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
|
||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/survey')
|
||||
@ -35,6 +44,8 @@ export class SurveyController {
|
||||
private readonly contentSecurityService: ContentSecurityService,
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
private readonly logger: Logger,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Get('/getBannerData')
|
||||
@ -46,66 +57,55 @@ export class SurveyController {
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Post('/createSurvey')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.createFrom')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(WorkspaceGuard)
|
||||
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.READ_SURVEY])
|
||||
@SetMetadata('workspaceId', { key: 'body.workspaceId', optional: true })
|
||||
@UseGuards(Authentication)
|
||||
async createSurvey(
|
||||
@Body()
|
||||
reqBody,
|
||||
reqBody: CreateSurveyDto,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
let validationResult;
|
||||
try {
|
||||
validationResult = await Joi.object({
|
||||
title: Joi.string().required(),
|
||||
remark: Joi.string().allow(null, '').default(''),
|
||||
surveyType: Joi.string().when('createMethod', {
|
||||
is: 'copy',
|
||||
then: Joi.allow(null),
|
||||
otherwise: Joi.required(),
|
||||
}),
|
||||
createMethod: Joi.string().allow(null).default('basic'),
|
||||
createFrom: Joi.string().when('createMethod', {
|
||||
is: 'copy',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.allow(null),
|
||||
}),
|
||||
}).validateAsync(reqBody);
|
||||
} catch (error) {
|
||||
this.logger.error(`createSurvey_parameter error: ${error.message}`, {
|
||||
req,
|
||||
});
|
||||
const { error, value } = CreateSurveyDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(`createSurvey_parameter error: ${error.message}`);
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const { title, remark, createMethod, createFrom } = validationResult;
|
||||
const { title, remark, createMethod, createFrom } = value;
|
||||
|
||||
const username = req.user.username;
|
||||
let surveyType = '';
|
||||
let surveyType = '',
|
||||
workspaceId = null;
|
||||
if (createMethod === 'copy') {
|
||||
const survey = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId: createFrom,
|
||||
username,
|
||||
});
|
||||
const survey = req.surveyMeta;
|
||||
surveyType = survey.surveyType;
|
||||
workspaceId = survey.workspaceId;
|
||||
} else {
|
||||
surveyType = validationResult.surveyType;
|
||||
surveyType = value.surveyType;
|
||||
workspaceId = value.workspaceId;
|
||||
}
|
||||
|
||||
const surveyMeta = await this.surveyMetaService.createSurveyMeta({
|
||||
title,
|
||||
remark,
|
||||
surveyType,
|
||||
username,
|
||||
username: req.user.username,
|
||||
userId: req.user._id.toString(),
|
||||
createMethod,
|
||||
createFrom,
|
||||
workspaceId,
|
||||
});
|
||||
await this.surveyConfService.createSurveyConf({
|
||||
surveyId: surveyMeta._id.toString(),
|
||||
surveyType: surveyType,
|
||||
createMethod: validationResult.createMethod,
|
||||
createFrom: validationResult.createFrom,
|
||||
createMethod: value.createMethod,
|
||||
createFrom: value.createFrom,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
@ -115,26 +115,58 @@ export class SurveyController {
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Post('/updateConf')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async updateConf(
|
||||
@Body()
|
||||
surveyInfo,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
configData: Joi.any().required(),
|
||||
}).validateAsync(surveyInfo);
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
await this.surveyMetaService.checkSurveyAccess({
|
||||
sessionId: Joi.string().required(),
|
||||
}).validate(surveyInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const sessionId = value.sessionId;
|
||||
const surveyId = value.surveyId;
|
||||
const latestEditingOne = await this.sessionService.findLatestEditingOne({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
const configData = validationResult.configData;
|
||||
|
||||
if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) {
|
||||
const curSession = await this.sessionService.findOne(sessionId);
|
||||
if (curSession.createdAt <= latestEditingOne.updatedAt) {
|
||||
// 在当前用户打开之后,被其他页面保存过了
|
||||
const isSameOperator =
|
||||
latestEditingOne.userId === req.user._id.toString();
|
||||
let preOperator;
|
||||
if (!isSameOperator) {
|
||||
preOperator = await this.userService.getUserById(
|
||||
latestEditingOne.userId,
|
||||
);
|
||||
}
|
||||
return {
|
||||
code: EXCEPTION_CODE.SURVEY_SAVE_CONFLICT,
|
||||
errmsg: isSameOperator
|
||||
? '当前问卷已在其它页面开启编辑,刷新以获取最新内容'
|
||||
: `当前问卷已由 ${preOperator.username} 编辑,刷新以获取最新内容`,
|
||||
};
|
||||
}
|
||||
}
|
||||
await this.sessionService.updateSessionToEditing({ sessionId, surveyId });
|
||||
|
||||
const username = req.user.username;
|
||||
|
||||
const configData = value.configData;
|
||||
await this.surveyConfService.saveSurveyConf({
|
||||
surveyId,
|
||||
schema: configData,
|
||||
@ -153,23 +185,45 @@ export class SurveyController {
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@HttpCode(200)
|
||||
@Post('/deleteSurvey')
|
||||
async deleteSurvey(@Body() reqBody, @Request() req) {
|
||||
const validationResult = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validateAsync(reqBody, { allowUnknown: true });
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const survey = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async deleteSurvey(@Request() req) {
|
||||
const surveyMeta = req.surveyMeta;
|
||||
|
||||
await this.surveyMetaService.deleteSurveyMeta(survey);
|
||||
await this.responseSchemaService.deleteResponseSchema({
|
||||
surveyPath: survey.surveyPath,
|
||||
const delMetaRes = await this.surveyMetaService.deleteSurveyMeta({
|
||||
surveyId: surveyMeta._id.toString(),
|
||||
operator: req.user.username,
|
||||
operatorId: req.user._id.toString(),
|
||||
});
|
||||
const delResponseRes =
|
||||
await this.responseSchemaService.deleteResponseSchema({
|
||||
surveyPath: surveyMeta.surveyPath,
|
||||
});
|
||||
|
||||
this.logger.info(JSON.stringify(delMetaRes));
|
||||
this.logger.info(JSON.stringify(delResponseRes));
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Post('/pausingSurvey')
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async pausingSurvey(@Request() req) {
|
||||
const surveyMeta = req.surveyMeta;
|
||||
|
||||
await this.surveyMetaService.pausingSurveyMeta(surveyMeta);
|
||||
await this.responseSchemaService.pausingResponseSchema({
|
||||
surveyPath: surveyMeta.surveyPath,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -177,9 +231,16 @@ export class SurveyController {
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Get('/getSurvey')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'query.surveyId')
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||
])
|
||||
@UseGuards(Authentication)
|
||||
async getSurvey(
|
||||
@Query()
|
||||
queryInfo: {
|
||||
@ -188,19 +249,28 @@ export class SurveyController {
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validateAsync(queryInfo);
|
||||
}).validate(queryInfo);
|
||||
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const surveyMeta = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const surveyId = value.surveyId;
|
||||
const surveyMeta = req.surveyMeta;
|
||||
const surveyConf =
|
||||
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
|
||||
|
||||
surveyMeta.currentUserId = req.user._id.toString();
|
||||
if (req.collaborator) {
|
||||
surveyMeta.isCollaborated = true;
|
||||
surveyMeta.currentPermission = req.collaborator.permissions;
|
||||
} else {
|
||||
surveyMeta.isCollaborated = false;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
@ -210,24 +280,64 @@ export class SurveyController {
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Get('/getPreviewSchema')
|
||||
@HttpCode(200)
|
||||
async getPreviewSchema(
|
||||
@Query()
|
||||
queryInfo: {
|
||||
surveyPath: string;
|
||||
},
|
||||
) {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate({ surveyId: queryInfo.surveyPath });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const surveyId = value.surveyId;
|
||||
const surveyConf =
|
||||
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
|
||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
...surveyConf,
|
||||
title: surveyMeta?.title,
|
||||
surveyPath: surveyMeta?.surveyPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/publishSurvey')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async publishSurvey(
|
||||
@Body()
|
||||
surveyInfo,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validateAsync(surveyInfo);
|
||||
}).validate(surveyInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const surveyMeta = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
const surveyId = value.surveyId;
|
||||
const surveyMeta = req.surveyMeta;
|
||||
if (surveyMeta.isDeleted) {
|
||||
throw new HttpException(
|
||||
'问卷已删除,无法发布',
|
||||
EXCEPTION_CODE.SURVEY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const surveyConf =
|
||||
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
|
||||
|
||||
@ -253,7 +363,8 @@ export class SurveyController {
|
||||
pageId: surveyId,
|
||||
});
|
||||
|
||||
await this.surveyHistoryService.addHistory({
|
||||
// 添加发布历史可以异步添加
|
||||
this.surveyHistoryService.addHistory({
|
||||
surveyId,
|
||||
schema: surveyConf.code,
|
||||
type: HISTORY_TYPE.PUBLISH_HIS,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user