mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 23:03:45 -04:00
Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d9851c9e | ||
|
|
fbfb959de2 | ||
|
|
449b32d4bc | ||
|
|
a3c479ff92 | ||
|
|
51c97adce4 | ||
|
|
cde8a0d97f | ||
|
|
e257a10fdb | ||
|
|
6df9c5519a | ||
|
|
33a50b7e7e | ||
|
|
5029af9c5f | ||
|
|
7b61c70f6d | ||
|
|
87059494a3 | ||
|
|
e7f348e34e | ||
|
|
f953b5c10b | ||
|
|
d75f179a46 | ||
|
|
87713a7803 | ||
|
|
ac7091ae06 | ||
|
|
f4f3aa80f1 | ||
|
|
fa82a30907 | ||
|
|
2e81ef25f1 | ||
|
|
12ef6a891b | ||
|
|
fb3f5e5ace | ||
|
|
9537500fe1 | ||
|
|
bb423ba0e4 | ||
|
|
21db5cb481 | ||
|
|
078425f1b2 | ||
|
|
77e3b56b65 | ||
|
|
2ed23e05fa | ||
|
|
486a4a81f7 | ||
|
|
9a0f0c0892 | ||
|
|
4abed529d3 | ||
|
|
4293a8c24b | ||
|
|
60721eefb2 | ||
|
|
e5c8650df0 | ||
|
|
8e6d6584ea | ||
|
|
077f3ac1a3 | ||
|
|
04b8e21769 | ||
|
|
bc672db79a | ||
|
|
dc7be2d334 | ||
|
|
08c9f60010 | ||
|
|
0bb52e72f7 | ||
|
|
593c2ac024 | ||
|
|
8712424b89 | ||
|
|
c408d895b1 | ||
|
|
33c2121c8d | ||
|
|
fa151e79d3 | ||
|
|
58e65baa47 | ||
|
|
7bc57d8380 | ||
|
|
eed3e71113 | ||
|
|
08d98468c5 | ||
|
|
0ee66a264e | ||
|
|
c10f8ceb91 | ||
|
|
e3cb85271c | ||
|
|
2c43f3e1e7 | ||
|
|
29ea3a600a | ||
|
|
3ace0e80e8 | ||
|
|
79f460b9a2 | ||
|
|
7d41846b0a | ||
|
|
d8997f64cb | ||
|
|
8a9139633d | ||
|
|
a5191b440e | ||
|
|
c7d58905b5 | ||
|
|
b6df79b836 | ||
|
|
9975113eff | ||
|
|
e8431845b1 | ||
|
|
a3e51f06e3 | ||
|
|
ce447db6b5 | ||
|
|
8c14212e10 | ||
|
|
9976e085c1 | ||
|
|
6da973dd0c | ||
|
|
1dcf86e5ba | ||
|
|
3232e783e3 | ||
|
|
c66d82a06d | ||
|
|
a46ff44f99 | ||
|
|
669a95bfa9 | ||
|
|
4287fbae85 | ||
|
|
f2845eab91 | ||
|
|
40702e7832 | ||
|
|
9909bd41cb | ||
|
|
0dec7e1987 | ||
|
|
c5965fff89 | ||
|
|
53a1b4c415 | ||
|
|
773551e41d | ||
|
|
2df0e3df88 | ||
|
|
df39c2ee70 | ||
|
|
0bfd4d47b9 | ||
|
|
097617cfa0 | ||
|
|
b2968166df | ||
|
|
9116144de4 | ||
|
|
47e5e8bb28 | ||
|
|
32cae16045 | ||
|
|
4e4c946acf | ||
|
|
3e06b61eba | ||
|
|
04e2376829 | ||
|
|
709ef3f4ad | ||
|
|
2e7de541aa | ||
|
|
dc073ef0f9 | ||
|
|
a677df2fdd | ||
|
|
58c005194c | ||
|
|
16a5c3e408 | ||
|
|
8a4fb040e1 | ||
|
|
eab140e51d | ||
|
|
009d4bf91f | ||
|
|
ed094bbeca | ||
|
|
704bcb4619 | ||
|
|
fbdc594fa9 | ||
|
|
6a511adcf5 | ||
|
|
d55b04d22d | ||
|
|
96d0c90f8c | ||
|
|
2253a734b1 | ||
|
|
dca08a6d81 | ||
|
|
8a4dc44fbc | ||
|
|
e231f016d6 | ||
|
|
05250285c0 | ||
|
|
ce9ac99894 | ||
|
|
f76c39bd1c | ||
|
|
9ccb8d002b | ||
|
|
d112d4e739 | ||
|
|
7dfec75ac0 | ||
|
|
3fd27131e2 | ||
|
|
355fd7e6d1 | ||
|
|
c0b8df4ef9 | ||
|
|
cc3af74676 | ||
|
|
40a1e079a1 | ||
|
|
764769366c | ||
|
|
b433668628 | ||
|
|
cf27794776 | ||
|
|
238fed617f | ||
|
|
0d026d36d1 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
63
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Bug description
|
||||||
|
description: |
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: |
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: |
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: |
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Info
|
||||||
|
description: |
|
||||||
|
- Browser: [e.g. chrome, safari]
|
||||||
|
- Device (if mobile): [e.g. iPhone6]
|
||||||
|
- Build info
|
||||||
|
placeholder: |
|
||||||
|
Build info from `Settings` -> `About`
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: |
|
||||||
|
Add any other context about the problem here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for contributing!
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
40
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
40
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea or improvement for this project
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem
|
||||||
|
description: |
|
||||||
|
A clear and concise description of what the problem is.
|
||||||
|
placeholder: I'm always frustrated when [...]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Solution
|
||||||
|
description: |
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternatives
|
||||||
|
description: |
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: |
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for contributing!
|
||||||
36
.github/ISSUE_TEMPLATE/other.yml
vendored
36
.github/ISSUE_TEMPLATE/other.yml
vendored
@@ -1,15 +1,25 @@
|
|||||||
name: Other
|
name: Other
|
||||||
description: Any other question or issue
|
description: Use this only if no other issue type fits.
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue
|
label: Issue
|
||||||
description: >
|
description: |
|
||||||
A clear and concise description of the question or issue
|
A clear and concise description of the question or issue
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: markdown
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
value: >
|
label: Info
|
||||||
Thanks for contributing!
|
description: |
|
||||||
|
- Browser: [e.g. chrome, safari]
|
||||||
|
- Device (if mobile): [e.g. iPhone6]
|
||||||
|
- Build info
|
||||||
|
placeholder: |
|
||||||
|
Build info from `Settings` -> `About`
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for contributing!
|
||||||
|
|||||||
70
.github/workflows/cd-android-live-update.yml
vendored
Normal file
70
.github/workflows/cd-android-live-update.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: CD Android Live Update
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
paths:
|
||||||
|
- "android/capawesome.json"
|
||||||
|
- ".github/workflows/cd-android-live-update.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # we need full history for git log
|
||||||
|
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Read current version
|
||||||
|
id: current
|
||||||
|
run: |
|
||||||
|
current=$(jq -r '.version' android/capawesome.json)
|
||||||
|
echo "version=$current" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Read previous version
|
||||||
|
id: previous
|
||||||
|
run: |
|
||||||
|
# Get previous commit’s package.json (if it existed)
|
||||||
|
if git show HEAD^:android/capawesome.json >/dev/null 2>&1; then
|
||||||
|
previous=$(git show HEAD^:android/capawesome.json | jq -r '.version')
|
||||||
|
else
|
||||||
|
previous="none"
|
||||||
|
fi
|
||||||
|
echo "version=$previous" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check version change
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
echo "current=${{ steps.current.outputs.version }}"
|
||||||
|
echo "previous=${{ steps.previous.outputs.version }}"
|
||||||
|
if [ "${{ steps.current.outputs.version }}" = "${{ steps.previous.outputs.version }}" ]; then
|
||||||
|
echo "changed=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "changed=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.check.outputs.changed == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.check.outputs.changed == 'true'
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Deploy Live Update
|
||||||
|
if: steps.check.outputs.changed == 'true'
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_INSTANCE_ID: ${{ secrets.NEXT_PUBLIC_SUPABASE_INSTANCE_ID }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
|
||||||
|
CAPAWESOME_TOKEN: ${{ secrets.CAPAWESOME_TOKEN }}
|
||||||
|
commitRef: ${{ github.head_ref || github.ref_name }}
|
||||||
|
commitSha: ${{ github.sha }}
|
||||||
|
run: yarn android-live-update
|
||||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -5,12 +5,13 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Debug Jest Tests",
|
"name": "Debug Current Test",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"--inspect-brk",
|
"--inspect-brk",
|
||||||
"${workspaceRoot}/node_modules/.bin/jest",
|
"${workspaceRoot}/node_modules/.bin/jest",
|
||||||
|
"${fileBasename}",
|
||||||
"--runInBand"
|
"--runInBand"
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
[](https://github.com/CompassConnections/Compass/actions/workflows/cd-api.yml)
|
[](https://github.com/CompassConnections/Compass/actions/workflows/cd-api.yml)
|
||||||
[](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
|
[](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/CompassConnections/Compass)
|
[](https://codecov.io/gh/CompassConnections/Compass)
|
||||||
[](https://www.compassmeet.com/stats)
|
[](https://www.compassmeet.com/stats)
|
||||||
|
|
||||||
# Compass
|
# Compass
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ Everything is open to anyone for collaboration, but the following ones are parti
|
|||||||
- [x] Add profile fields (intellectual interests, cause areas, personality type, etc.)
|
- [x] Add profile fields (intellectual interests, cause areas, personality type, etc.)
|
||||||
- [ ] Add profile fields: conflict style
|
- [ ] Add profile fields: conflict style
|
||||||
- [ ] Add profile fields: timezone
|
- [ ] Add profile fields: timezone
|
||||||
|
- [ ] Add translations: Italian, Dutch, Hindi, Chinese, etc.
|
||||||
- [x] Add filters to search through remaining profile fields (politics, religion, education level, etc.)
|
- [x] Add filters to search through remaining profile fields (politics, religion, education level, etc.)
|
||||||
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
||||||
- [ ] Clean up terms and conditions (convert to Markdown)
|
- [ ] Clean up terms and conditions (convert to Markdown)
|
||||||
@@ -156,7 +157,7 @@ If you are new to Typescript or the open-source space, you could start with smal
|
|||||||
There is a lof of documentation in the [docs](docs) folder and across the repo, namely:
|
There is a lof of documentation in the [docs](docs) folder and across the repo, namely:
|
||||||
- [Next.js.md](docs/Next.js.md) for core fundamentals about our web / page-rendering framework.
|
- [Next.js.md](docs/Next.js.md) for core fundamentals about our web / page-rendering framework.
|
||||||
- [knowledge.md](docs/knowledge.md) for general information about the project structure.
|
- [knowledge.md](docs/knowledge.md) for general information about the project structure.
|
||||||
- [development.md](docs/development.md) for additional instructions, such as adding new profile fields.
|
- [development.md](docs/development.md) for additional instructions, such as adding new profile fields or languages.
|
||||||
- [web](web) for the web.
|
- [web](web) for the web.
|
||||||
- [backend/api](backend/api) for the backend API.
|
- [backend/api](backend/api) for the backend API.
|
||||||
- [android](android) for the Android app.
|
- [android](android) for the Android app.
|
||||||
|
|||||||
@@ -267,9 +267,9 @@ yarn build-sync-android
|
|||||||
|
|
||||||
## Live Updates
|
## Live Updates
|
||||||
|
|
||||||
To avoid releasing to the app stores after every code update in the web pages, we build the new bundle and store it in Capawesome Cloud (an alternative to Ionic).
|
To avoid releasing to the app stores after every code update in the web pages, we build the new bundle and store it in Capawesome Cloud (an alternative to Ionic). To add a new update, increment the version number in [capawesome.json](capawesome.json) and push to main (or make a PR to main). A GitHub Action will automatically build the new bundle and push it to Capawesome.
|
||||||
|
|
||||||
First, you need to do this one-time setup:
|
You can also do so locally if you have admin access. First, you need to do this one-time setup:
|
||||||
```
|
```
|
||||||
npm install -g @capawesome/cli@latest
|
npm install -g @capawesome/cli@latest
|
||||||
npx @capawesome/cli login
|
npx @capawesome/cli login
|
||||||
@@ -277,12 +277,10 @@ npx @capawesome/cli login
|
|||||||
|
|
||||||
Then, run this to build your local assets and push them to Capawesome. Once done, each mobile app user will receive a notice that there is a new update available, which they can approve to download.
|
Then, run this to build your local assets and push them to Capawesome. Once done, each mobile app user will receive a notice that there is a new update available, which they can approve to download.
|
||||||
```
|
```
|
||||||
yarn build-web-view
|
yarn android:live-update
|
||||||
npx @capawesome/cli apps:bundles:create --path web/out
|
|
||||||
```
|
```
|
||||||
|
|
||||||
That's all. So you should run the lines above every time you want your web updates pushed to main (which essentially updates the web app) to update the mobile app as well.
|
That's all. So you should run the lines above every time you want your web updates pushed to main (which essentially updates the web app) to update the mobile app as well.
|
||||||
Maybe we should add it to our CD. For example we set a file with `{liveUpdateVersion: 1}` and run the live update each time a push to main increments that counter.
|
|
||||||
There is a limit of 100 monthly active user per month, though. So we may need to pay or create our custom limit as we scale. Next plan is $9 / month and allows 1000 MAUs.
|
There is a limit of 100 monthly active user per month, though. So we may need to pay or create our custom limit as we scale. Next plan is $9 / month and allows 1000 MAUs.
|
||||||
|
|
||||||
- ∞ Live Updates
|
- ∞ Live Updates
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ android {
|
|||||||
applicationId "com.compassconnections.app"
|
applicationId "com.compassconnections.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 14
|
versionCode 18
|
||||||
versionName "1.1.3"
|
versionName "1.1.6"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
3
android/capawesome.json
Normal file
3
android/capawesome.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"version": 23
|
||||||
|
}
|
||||||
@@ -27,13 +27,16 @@ SERVICE_NAME="api"
|
|||||||
GIT_REVISION=$(git rev-parse --short HEAD)
|
GIT_REVISION=$(git rev-parse --short HEAD)
|
||||||
GIT_COMMIT_DATE=$(git log -1 --format=%ci)
|
GIT_COMMIT_DATE=$(git log -1 --format=%ci)
|
||||||
GIT_COMMIT_AUTHOR=$(git log -1 --format='%an')
|
GIT_COMMIT_AUTHOR=$(git log -1 --format='%an')
|
||||||
|
GIT_COMMIT_MESSAGE=$(git log -1 --format='%s')
|
||||||
|
echo "Git commit message: ${GIT_COMMIT_MESSAGE}"
|
||||||
|
|
||||||
cat > metadata.json << EOF
|
cat > metadata.json << EOF
|
||||||
{
|
{
|
||||||
"git": {
|
"git": {
|
||||||
"revision": "${GIT_REVISION}",
|
"revision": "${GIT_REVISION}",
|
||||||
"commitDate": "${GIT_COMMIT_DATE}",
|
"commitDate": "${GIT_COMMIT_DATE}",
|
||||||
"author": "${GIT_COMMIT_AUTHOR}"
|
"author": "${GIT_COMMIT_AUTHOR}",
|
||||||
|
"message": "${GIT_COMMIT_MESSAGE}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"git": {
|
"git": {
|
||||||
"revision": "91f69ed",
|
"revision": "704bcb4",
|
||||||
"commitDate": "2025-12-04 20:51:09+0100",
|
"commitDate": "2025-12-15 13:38:09 +0200",
|
||||||
"author": "MartinBraquet"
|
"author": "MartinBraquet",
|
||||||
|
"message": "Increase API docs font size on mobile"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@compass/api",
|
"name": "@compass/api",
|
||||||
"description": "Backend API endpoints",
|
"description": "Backend API endpoints",
|
||||||
"version": "1.0.11",
|
"version": "1.0.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch:serve": "tsx watch src/serve.ts",
|
"watch:serve": "tsx watch src/serve.ts",
|
||||||
|
|||||||
@@ -518,6 +518,12 @@ app.get(
|
|||||||
swaggerUi.setup(swaggerDocument, {
|
swaggerUi.setup(swaggerDocument, {
|
||||||
customSiteTitle: 'Compass API Docs',
|
customSiteTitle: 'Compass API Docs',
|
||||||
customCssUrl: '/swagger.css',
|
customCssUrl: '/swagger.css',
|
||||||
|
customJs: `
|
||||||
|
const meta = document.createElement('meta');
|
||||||
|
meta.name = 'viewport';
|
||||||
|
meta.content = 'width=device-width, initial-scale=1';
|
||||||
|
document.head.appendChild(meta);
|
||||||
|
`,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
app.use(rootPath, swaggerUi.serve)
|
app.use(rootPath, swaggerUi.serve)
|
||||||
|
|||||||
@@ -3,6 +3,25 @@ import {Notification} from 'common/notifications'
|
|||||||
import {insertNotificationToSupabase} from 'shared/supabase/notifications'
|
import {insertNotificationToSupabase} from 'shared/supabase/notifications'
|
||||||
import {tryCatch} from "common/util/try-catch";
|
import {tryCatch} from "common/util/try-catch";
|
||||||
import {Row} from "common/supabase/utils";
|
import {Row} from "common/supabase/utils";
|
||||||
|
import {ANDROID_APP_URL} from "common/constants";
|
||||||
|
|
||||||
|
export const createAndroidReleaseNotifications = async () => {
|
||||||
|
const createdTime = Date.now();
|
||||||
|
const id = `android-release-${createdTime}`
|
||||||
|
const notification: Notification = {
|
||||||
|
id,
|
||||||
|
userId: 'todo',
|
||||||
|
createdTime: createdTime,
|
||||||
|
isSeen: false,
|
||||||
|
sourceType: 'info',
|
||||||
|
sourceUpdateType: 'created',
|
||||||
|
sourceSlug: ANDROID_APP_URL,
|
||||||
|
sourceUserAvatarUrl: 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fcompass-192.png?alt=media&token=9fd251c5-fc43-4375-b629-1a8f4bbe8185',
|
||||||
|
title: 'Android App Released on Google Play',
|
||||||
|
sourceText: 'The Compass Android app is now publicly available on Google Play! Download it today to stay connected on the go.',
|
||||||
|
}
|
||||||
|
return await createNotifications(notification)
|
||||||
|
}
|
||||||
|
|
||||||
export const createAndroidTestNotifications = async () => {
|
export const createAndroidTestNotifications = async () => {
|
||||||
const createdTime = Date.now();
|
const createdTime = Date.now();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { APIHandler } from './helpers/endpoint'
|
import { APIHandler } from './helpers/endpoint'
|
||||||
import {git} from './../metadata.json'
|
import {git} from './../metadata.json'
|
||||||
|
import {version as pkgVersion} from './../package.json'
|
||||||
|
|
||||||
export const health: APIHandler<'health'> = async (_, auth) => {
|
export const health: APIHandler<'health'> = async (_, auth) => {
|
||||||
return {
|
return {
|
||||||
message: 'Server is working.',
|
message: 'Server is working.',
|
||||||
uid: auth?.uid,
|
uid: auth?.uid,
|
||||||
git: git,
|
git: git,
|
||||||
|
version: pkgVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
body {
|
body {
|
||||||
background-color: #1e1e1e !important;
|
background-color: #1e1e1e !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
|
|
||||||
}
|
}
|
||||||
.swagger-ui p,
|
.swagger-ui p,
|
||||||
h1,
|
h1,
|
||||||
@@ -62,3 +63,44 @@
|
|||||||
color: #1e90ff !important;
|
color: #1e90ff !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Increase font sizes on mobile for better readability */
|
||||||
|
/* Still not working though */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
.swagger-ui {
|
||||||
|
font-size: 32px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common text elements */
|
||||||
|
.swagger-ui p,
|
||||||
|
.swagger-ui label,
|
||||||
|
.swagger-ui .btn,
|
||||||
|
.swagger-ui .parameter__name,
|
||||||
|
.swagger-ui .parameter__type,
|
||||||
|
.swagger-ui .parameter__in,
|
||||||
|
.swagger-ui .response-control-media-type__title,
|
||||||
|
.swagger-ui table thead tr td,
|
||||||
|
.swagger-ui table thead tr th,
|
||||||
|
.swagger-ui table tbody tr td,
|
||||||
|
.swagger-ui .tab li,
|
||||||
|
.swagger-ui .response-col_links,
|
||||||
|
.swagger-ui .opblock-summary-path,
|
||||||
|
.swagger-ui .opblock-summary-description {
|
||||||
|
font-size: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings scale */
|
||||||
|
.swagger-ui h1 { font-size: 1.75rem !important; }
|
||||||
|
.swagger-ui h2 { font-size: 1.5rem !important; }
|
||||||
|
.swagger-ui h3 { font-size: 1.25rem !important; }
|
||||||
|
.swagger-ui h4 { font-size: 1.125rem !important; }
|
||||||
|
.swagger-ui h5, .swagger-ui h6 { font-size: 1rem !important; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { throwErrorIfNotMod } from "shared/helpers/auth";
|
|||||||
import * as constants from "common/envs/constants";
|
import * as constants from "common/envs/constants";
|
||||||
import * as supabaseUsers from "shared/supabase/users";
|
import * as supabaseUsers from "shared/supabase/users";
|
||||||
import * as sharedAnalytics from "shared/analytics";
|
import * as sharedAnalytics from "shared/analytics";
|
||||||
import { } from "shared/helpers/auth";
|
import { AuthedUser } from "api/helpers/endpoint"
|
||||||
import { APIError, AuthedUser } from "api/helpers/endpoint"
|
|
||||||
|
|
||||||
|
|
||||||
describe('banUser', () => {
|
describe('banUser', () => {
|
||||||
@@ -24,13 +23,13 @@ describe('banUser', () => {
|
|||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
|
||||||
it('ban a user successfully', async () => {
|
describe('when given valid input', () => {
|
||||||
|
it('should ban a user successfully', async () => {
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
userId: '123',
|
userId: '123',
|
||||||
unban: false
|
unban: false
|
||||||
@@ -42,15 +41,25 @@ describe('banUser', () => {
|
|||||||
|
|
||||||
await banUser(mockUser, mockAuth, mockReq);
|
await banUser(mockUser, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(throwErrorIfNotMod).toBeCalledTimes(1);
|
||||||
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(constants.isAdminId).toBeCalledTimes(1);
|
||||||
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
|
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
|
||||||
expect(sharedAnalytics.trackPublicEvent)
|
expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(1);
|
||||||
.toBeCalledWith(mockAuth.uid, 'ban user', {userId: mockUser.userId});
|
expect(sharedAnalytics.trackPublicEvent).toBeCalledWith(
|
||||||
expect(supabaseUsers.updateUser)
|
mockAuth.uid,
|
||||||
.toBeCalledWith(mockPg, mockUser.userId, {isBannedFromPosting: true});
|
'ban user',
|
||||||
|
{userId: mockUser.userId}
|
||||||
|
);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledWith(
|
||||||
|
mockPg,
|
||||||
|
mockUser.userId,
|
||||||
|
{isBannedFromPosting: true}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unban a user successfully', async () => {
|
it('should unban a user successfully', async () => {
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
userId: '123',
|
userId: '123',
|
||||||
unban: true
|
unban: true
|
||||||
@@ -64,13 +73,20 @@ describe('banUser', () => {
|
|||||||
|
|
||||||
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
||||||
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
|
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
|
||||||
expect(sharedAnalytics.trackPublicEvent)
|
expect(sharedAnalytics.trackPublicEvent).toBeCalledWith(
|
||||||
.toBeCalledWith(mockAuth.uid, 'ban user', {userId: mockUser.userId});
|
mockAuth.uid,
|
||||||
expect(supabaseUsers.updateUser)
|
'ban user',
|
||||||
.toBeCalledWith(mockPg, mockUser.userId, {isBannedFromPosting: false});
|
{userId: mockUser.userId}
|
||||||
|
);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledWith(
|
||||||
|
mockPg,
|
||||||
|
mockUser.userId,
|
||||||
|
{isBannedFromPosting: false}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
it('throw and error if the ban requester is not a mod or admin', async () => {
|
describe('when an error occurs', () => {
|
||||||
|
it('throw if the ban requester is not a mod or admin', async () => {
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
userId: '123',
|
userId: '123',
|
||||||
unban: false
|
unban: false
|
||||||
@@ -79,21 +95,16 @@ describe('banUser', () => {
|
|||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
|
|
||||||
(throwErrorIfNotMod as jest.Mock).mockRejectedValue(
|
(throwErrorIfNotMod as jest.Mock).mockRejectedValue(
|
||||||
new APIError(
|
new Error(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`)
|
||||||
403,
|
|
||||||
`User ${mockAuth.uid} must be an admin or trusted to perform this action.`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(banUser(mockUser, mockAuth, mockReq))
|
await expect(banUser(mockUser, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`);
|
.toThrowError(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`);
|
||||||
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
||||||
expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(0);
|
|
||||||
expect(supabaseUsers.updateUser).toBeCalledTimes(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the ban target is an admin', async () => {
|
it('throw if the ban target is an admin', async () => {
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
userId: '123',
|
userId: '123',
|
||||||
unban: false
|
unban: false
|
||||||
@@ -108,8 +119,6 @@ describe('banUser', () => {
|
|||||||
.toThrowError('Cannot ban admin');
|
.toThrowError('Cannot ban admin');
|
||||||
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
|
||||||
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
|
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
|
||||||
expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(0);
|
|
||||||
expect(supabaseUsers.updateUser).toBeCalledTimes(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -23,37 +23,35 @@ describe('blockUser', () => {
|
|||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg)
|
.mockReturnValue(mockPg)
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('block the user successfully', async () => {
|
it('block the user successfully', async () => {
|
||||||
const mockParams = { id: '123' }
|
const mockParams = { id: '123' }
|
||||||
const mockAuth = {uid: '321'} as AuthedUser;
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
|
|
||||||
(supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null);
|
|
||||||
|
|
||||||
await blockUserModule.blockUser(mockParams, mockAuth, mockReq)
|
await blockUserModule.blockUser(mockParams, mockAuth, mockReq)
|
||||||
|
|
||||||
expect(mockPg.tx).toHaveBeenCalledTimes(1)
|
expect(mockPg.tx).toHaveBeenCalledTimes(1);
|
||||||
|
expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(2);
|
||||||
expect(supabaseUsers.updatePrivateUser)
|
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
|
||||||
.toHaveBeenCalledWith(
|
1,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
mockAuth.uid,
|
mockAuth.uid,
|
||||||
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
|
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
|
||||||
);
|
);
|
||||||
expect(supabaseUsers.updatePrivateUser)
|
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
|
||||||
.toHaveBeenCalledWith(
|
2,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
mockParams.id,
|
mockParams.id,
|
||||||
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
|
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
it('throw an error if the user tries to block themselves', async () => {
|
it('throw an error if the user tries to block themselves', async () => {
|
||||||
const mockParams = { id: '123' }
|
const mockParams = { id: '123' }
|
||||||
const mockAuth = {uid: '123'} as AuthedUser;
|
const mockAuth = {uid: '123'} as AuthedUser;
|
||||||
@@ -61,12 +59,9 @@ describe('blockUser', () => {
|
|||||||
|
|
||||||
expect(blockUserModule.blockUser(mockParams, mockAuth, mockReq))
|
expect(blockUserModule.blockUser(mockParams, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('You cannot block yourself')
|
.toThrowError('You cannot block yourself');
|
||||||
|
|
||||||
expect(mockPg.tx).toHaveBeenCalledTimes(0)
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('unblockUser', () => {
|
describe('unblockUser', () => {
|
||||||
@@ -84,35 +79,32 @@ describe('unblockUser', () => {
|
|||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg)
|
.mockReturnValue(mockPg)
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('block the user successfully', async () => {
|
it('should block the user successfully', async () => {
|
||||||
const mockParams = { id: '123' }
|
const mockParams = { id: '123' }
|
||||||
const mockAuth = {uid: '321'} as AuthedUser;
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
|
|
||||||
(supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null);
|
|
||||||
|
|
||||||
await blockUserModule.unblockUser(mockParams, mockAuth, mockReq)
|
await blockUserModule.unblockUser(mockParams, mockAuth, mockReq)
|
||||||
|
|
||||||
expect(mockPg.tx).toHaveBeenCalledTimes(1)
|
expect(mockPg.tx).toHaveBeenCalledTimes(1);
|
||||||
|
expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(2);
|
||||||
expect(supabaseUsers.updatePrivateUser)
|
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
|
||||||
.toHaveBeenCalledWith(
|
1,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
mockAuth.uid,
|
mockAuth.uid,
|
||||||
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
|
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
|
||||||
);
|
);
|
||||||
expect(supabaseUsers.updatePrivateUser)
|
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
|
||||||
.toHaveBeenCalledWith(
|
2,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
mockParams.id,
|
mockParams.id,
|
||||||
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
|
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,41 @@
|
|||||||
import * as supabaseInit from "shared/supabase/init";
|
jest.mock('shared/supabase/init');
|
||||||
import {getCompatibleProfiles} from "api/compatible-profiles";
|
|
||||||
|
import {getCompatibleProfiles} from "api/compatible-profiles";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
jest.mock('shared/supabase/init')
|
|
||||||
|
|
||||||
describe('getCompatibleProfiles', () => {
|
describe('getCompatibleProfiles', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
const mockPg = {
|
mockPg = {
|
||||||
none: jest.fn().mockResolvedValue(null),
|
map: jest.fn().mockResolvedValue([]),
|
||||||
one: jest.fn().mockResolvedValue(null),
|
};
|
||||||
oneOrNone: jest.fn().mockResolvedValue(null),
|
|
||||||
any: jest.fn().mockResolvedValue([]),
|
|
||||||
map: jest.fn().mockResolvedValue([["abc", {score: 0.69}]]),
|
|
||||||
} as any;
|
|
||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('successfully get compatible profiles when supplied with a valid user Id', async () => {
|
it('should successfully get compatible profiles', async () => {
|
||||||
const results = await getCompatibleProfiles("123");
|
const mockProps = '123';
|
||||||
expect(results.status).toEqual('success');
|
const mockScores = ["abc", { score: 0.69 }];
|
||||||
expect(results.profileCompatibilityScores).toEqual({"abc": {score: 0.69}});
|
const mockScoresFromEntries = {"abc": { score: 0.69 }};
|
||||||
});
|
|
||||||
|
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue([mockScores]);
|
||||||
|
|
||||||
|
const results = await getCompatibleProfiles(mockProps);
|
||||||
|
const [sql, param, fn] = mockPg.map.mock.calls[0];
|
||||||
|
|
||||||
|
expect(results.status).toEqual('success');
|
||||||
|
expect(results.profileCompatibilityScores).toEqual(mockScoresFromEntries);
|
||||||
|
expect(mockPg.map).toBeCalledTimes(1);
|
||||||
|
expect(sql).toContain('select *');
|
||||||
|
expect(sql).toContain('from compatibility_scores');
|
||||||
|
expect(param).toStrictEqual([mockProps]);
|
||||||
|
expect(fn).toEqual(expect.any(Function));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ describe('contact', () => {
|
|||||||
let mockPg: any;
|
let mockPg: any;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
mockPg = {
|
mockPg = {
|
||||||
oneOrNone: jest.fn(),
|
oneOrNone: jest.fn(),
|
||||||
};
|
};
|
||||||
@@ -22,13 +21,12 @@ describe('contact', () => {
|
|||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('send a discord message to the user', async () => {
|
it('should send a discord message to the user', async () => {
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
content: {
|
content: {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
@@ -52,29 +50,42 @@ describe('contact', () => {
|
|||||||
const mockReturnData = {} as any;
|
const mockReturnData = {} as any;
|
||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValue({ data: mockReturnData, error: null });
|
(tryCatch as jest.Mock).mockResolvedValue({ data: mockReturnData, error: null });
|
||||||
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser);
|
|
||||||
(sendDiscordMessage as jest.Mock).mockResolvedValue(null);
|
|
||||||
|
|
||||||
const results = await contact(mockProps, mockAuth, mockReq);
|
const results = await contact(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.success).toBe(true);
|
||||||
|
expect(results.result).toStrictEqual({});
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
expect(supabaseUtils.insert).toBeCalledTimes(1)
|
expect(supabaseUtils.insert).toBeCalledTimes(1)
|
||||||
expect(supabaseUtils.insert).toBeCalledWith(
|
expect(supabaseUtils.insert).toBeCalledWith(
|
||||||
mockPg,
|
expect.any(Object),
|
||||||
'contact',
|
'contact',
|
||||||
{
|
{
|
||||||
user_id: mockProps.userId,
|
user_id: mockProps.userId,
|
||||||
content: JSON.stringify(mockProps.content)
|
content: JSON.stringify(mockProps.content)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
expect(results.success).toBe(true);
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser);
|
||||||
|
|
||||||
await results.continue();
|
await results.continue();
|
||||||
|
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select name from users where id = $1'),
|
||||||
|
[mockProps.userId]
|
||||||
|
);
|
||||||
|
expect(sendDiscordMessage).toBeCalledTimes(1);
|
||||||
expect(sendDiscordMessage).toBeCalledWith(
|
expect(sendDiscordMessage).toBeCalledWith(
|
||||||
expect.stringContaining(`New message from ${mockDbUser.name}`),
|
expect.stringContaining(`New message from ${mockDbUser.name}`),
|
||||||
'contact'
|
'contact'
|
||||||
)
|
);
|
||||||
expect(sendDiscordMessage).toBeCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
it('throw an error if the inser function fails', async () => {
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the insert function fails', async () => {
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
content: {
|
content: {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
@@ -100,15 +111,59 @@ describe('contact', () => {
|
|||||||
expect(contact(mockProps, mockAuth, mockReq))
|
expect(contact(mockProps, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Failed to submit contact message');
|
.toThrowError('Failed to submit contact message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to send discord message', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
content: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Error test message'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
userId: '123'
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockDbUser = { name: 'Humphrey Mocker' };
|
||||||
|
const mockReturnData = {} as any;
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({ data: mockReturnData, error: null });
|
||||||
|
|
||||||
|
const results = await contact(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.success).toBe(true);
|
||||||
|
expect(results.result).toStrictEqual({});
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
expect(supabaseUtils.insert).toBeCalledTimes(1)
|
expect(supabaseUtils.insert).toBeCalledTimes(1)
|
||||||
expect(supabaseUtils.insert).toBeCalledWith(
|
expect(supabaseUtils.insert).toBeCalledWith(
|
||||||
mockPg,
|
expect.any(Object),
|
||||||
'contact',
|
'contact',
|
||||||
{
|
{
|
||||||
user_id: mockProps.userId,
|
user_id: mockProps.userId,
|
||||||
content: JSON.stringify(mockProps.content)
|
content: JSON.stringify(mockProps.content)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser);
|
||||||
|
(sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Unable to send message'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledTimes(1);
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('Failed to send discord contact'),
|
||||||
|
expect.objectContaining({name: 'Error'})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -8,7 +8,6 @@ describe('createBookmarkedSearch', () => {
|
|||||||
let mockPg: any;
|
let mockPg: any;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
mockPg = {
|
mockPg = {
|
||||||
one: jest.fn(),
|
one: jest.fn(),
|
||||||
};
|
};
|
||||||
@@ -16,13 +15,12 @@ describe('createBookmarkedSearch', () => {
|
|||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('insert a bookmarked search into the database', async () => {
|
it('should insert a bookmarked search into the database', async () => {
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
search_filters: 'mock_search_filters',
|
search_filters: 'mock_search_filters',
|
||||||
location: 'mock_location',
|
location: 'mock_location',
|
||||||
@@ -30,9 +28,14 @@ describe('createBookmarkedSearch', () => {
|
|||||||
};
|
};
|
||||||
const mockAuth = { uid: '321' } as AuthedUser;
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
|
const mockInserted = "mockInsertedReturn";
|
||||||
|
|
||||||
await createBookmarkedSearch(mockProps, mockAuth, mockReq)
|
(mockPg.one as jest.Mock).mockResolvedValue(mockInserted);
|
||||||
expect(mockPg.one).toBeCalledTimes(1)
|
|
||||||
|
const result = await createBookmarkedSearch(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockInserted);
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
expect(mockPg.one).toHaveBeenCalledWith(
|
expect(mockPg.one).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('INSERT INTO bookmarked_searches'),
|
expect.stringContaining('INSERT INTO bookmarked_searches'),
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -15,32 +15,26 @@ import * as supabaseNotifications from "shared/supabase/notifications";
|
|||||||
import * as emailHelpers from "email/functions/helpers";
|
import * as emailHelpers from "email/functions/helpers";
|
||||||
import * as websocketHelpers from "shared/websockets/helpers";
|
import * as websocketHelpers from "shared/websockets/helpers";
|
||||||
import { convertComment } from "common/supabase/comment";
|
import { convertComment } from "common/supabase/comment";
|
||||||
|
import { richTextToString } from "common/util/parse";
|
||||||
|
|
||||||
describe('createComment', () => {
|
describe('createComment', () => {
|
||||||
let mockPg: any;
|
let mockPg: any;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
mockPg = {
|
mockPg = {
|
||||||
one: jest.fn()
|
one: jest.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
(supabaseNotifications.insertNotificationToSupabase as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
|
||||||
(emailHelpers.sendNewEndorsementEmail as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
|
||||||
(convertComment as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('successfully create a comment with information provided', async () => {
|
it('should successfully create a comment', async () => {
|
||||||
const mockUserId = {
|
const mockUserId = {
|
||||||
userId: '123',
|
userId: '123',
|
||||||
blockedUserIds: ['111']
|
blockedUserIds: ['111']
|
||||||
@@ -74,12 +68,17 @@ describe('createComment', () => {
|
|||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockReplyToCommentId = {} as any;
|
const mockReplyToCommentId = {} as any;
|
||||||
const mockComment = {id: 12};
|
const mockComment = {id: 12};
|
||||||
const mockNotificationDestination = {} as any;
|
const mockNotificationDestination = {
|
||||||
|
sendToBrowser: true,
|
||||||
|
sendToMobile: false,
|
||||||
|
sendToEmail: true
|
||||||
|
};
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
userId: mockUserId.userId,
|
userId: mockUserId.userId,
|
||||||
content: mockContent.content,
|
content: mockContent.content,
|
||||||
replyToCommentId: mockReplyToCommentId
|
replyToCommentId: mockReplyToCommentId
|
||||||
};
|
};
|
||||||
|
const mockConvertCommentReturn = 'mockConverComment';
|
||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock)
|
||||||
.mockResolvedValueOnce(mockCreator)
|
.mockResolvedValueOnce(mockCreator)
|
||||||
@@ -90,24 +89,51 @@ describe('createComment', () => {
|
|||||||
(mockPg.one as jest.Mock).mockResolvedValue(mockComment);
|
(mockPg.one as jest.Mock).mockResolvedValue(mockComment);
|
||||||
(notificationPrefs.getNotificationDestinationsForUser as jest.Mock)
|
(notificationPrefs.getNotificationDestinationsForUser as jest.Mock)
|
||||||
.mockReturnValue(mockNotificationDestination);
|
.mockReturnValue(mockNotificationDestination);
|
||||||
|
(convertComment as jest.Mock).mockReturnValue(mockConvertCommentReturn);
|
||||||
|
|
||||||
const results = await createComment(mockProps, mockAuth, mockReq);
|
const results = await createComment(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
expect(results.status).toBe('success');
|
expect(results.status).toBe('success');
|
||||||
expect(sharedUtils.getUser).toBeCalledTimes(2);
|
expect(sharedUtils.getUser).toBeCalledTimes(2);
|
||||||
expect(sharedUtils.getUser).toBeCalledWith(mockUserId.userId);
|
expect(sharedUtils.getUser).toHaveBeenNthCalledWith(1, mockAuth.uid);
|
||||||
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
expect(sharedUtils.getUser).toHaveBeenNthCalledWith(2, mockUserId.userId);
|
||||||
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
|
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
|
||||||
|
expect(sharedUtils.getPrivateUser).toHaveBeenNthCalledWith(1, mockProps.userId);
|
||||||
|
expect(sharedUtils.getPrivateUser).toHaveBeenNthCalledWith(2, mockOnUser.id);
|
||||||
expect(mockPg.one).toBeCalledTimes(1);
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
expect(mockPg.one).toBeCalledWith(
|
expect(mockPg.one).toBeCalledWith(
|
||||||
expect.stringContaining('insert into profile_comments'),
|
expect.stringContaining('insert into profile_comments'),
|
||||||
expect.arrayContaining([mockCreator.id])
|
[
|
||||||
|
mockCreator.id,
|
||||||
|
mockCreator.name,
|
||||||
|
mockCreator.username,
|
||||||
|
mockCreator.avatarUrl,
|
||||||
|
mockProps.userId,
|
||||||
|
mockProps.content,
|
||||||
|
mockProps.replyToCommentId
|
||||||
|
]
|
||||||
);
|
);
|
||||||
expect(websocketHelpers.broadcastUpdatedComment).toBeCalledTimes(1)
|
expect(notificationPrefs.getNotificationDestinationsForUser).toBeCalledTimes(1);
|
||||||
|
expect(notificationPrefs.getNotificationDestinationsForUser).toBeCalledWith(mockOnUser, 'new_endorsement');
|
||||||
|
expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledTimes(1);
|
||||||
|
expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
expect(emailHelpers.sendNewEndorsementEmail).toBeCalledTimes(1);
|
||||||
|
expect(emailHelpers.sendNewEndorsementEmail).toBeCalledWith(
|
||||||
|
mockOnUser,
|
||||||
|
mockCreator,
|
||||||
|
mockOnUser,
|
||||||
|
richTextToString(mockProps.content)
|
||||||
|
);
|
||||||
|
expect(websocketHelpers.broadcastUpdatedComment).toBeCalledTimes(1);
|
||||||
|
expect(websocketHelpers.broadcastUpdatedComment).toBeCalledWith(mockConvertCommentReturn);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('throw an error if there is no user matching the userId', async () => {
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if there is no user matching the userId', async () => {
|
||||||
const mockAuth = { uid: '321' } as AuthedUser;
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockReplyToCommentId = {} as any;
|
const mockReplyToCommentId = {} as any;
|
||||||
@@ -147,14 +173,16 @@ describe('createComment', () => {
|
|||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock)
|
||||||
.mockResolvedValueOnce(mockCreator)
|
.mockResolvedValueOnce(mockCreator)
|
||||||
.mockResolvedValueOnce(null);
|
.mockResolvedValueOnce(false);
|
||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
(sharedUtils.getPrivateUser as jest.Mock)
|
||||||
.mockResolvedValue(mockUserId);
|
.mockResolvedValue(mockUserId);
|
||||||
|
|
||||||
expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('User not found');
|
expect(createComment( mockProps, mockAuth, mockReq ))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('User not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if there is no account associated with the authId', async () => {
|
it('throw if there is no account associated with the authId', async () => {
|
||||||
const mockAuth = { uid: '321' } as AuthedUser;
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockReplyToCommentId = {} as any;
|
const mockReplyToCommentId = {} as any;
|
||||||
@@ -188,10 +216,12 @@ describe('createComment', () => {
|
|||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock)
|
||||||
.mockResolvedValueOnce(null);
|
.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('Your account was not found');
|
expect(createComment( mockProps, mockAuth, mockReq ))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('Your account was not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the account is banned from posting', async () => {
|
it('throw if the account is banned from posting', async () => {
|
||||||
const mockAuth = { uid: '321' } as AuthedUser;
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockReplyToCommentId = {} as any;
|
const mockReplyToCommentId = {} as any;
|
||||||
@@ -232,10 +262,12 @@ describe('createComment', () => {
|
|||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock)
|
||||||
.mockResolvedValueOnce(mockCreator);
|
.mockResolvedValueOnce(mockCreator);
|
||||||
|
|
||||||
expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('You are banned');
|
expect(createComment( mockProps, mockAuth, mockReq ))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('You are banned');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the other user is not found', async () => {
|
it('throw if the other user is not found', async () => {
|
||||||
const mockAuth = { uid: '321' } as AuthedUser;
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockReplyToCommentId = {} as any;
|
const mockReplyToCommentId = {} as any;
|
||||||
@@ -278,10 +310,12 @@ describe('createComment', () => {
|
|||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
(sharedUtils.getPrivateUser as jest.Mock)
|
||||||
.mockResolvedValue(null);
|
.mockResolvedValue(null);
|
||||||
|
|
||||||
expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('Other user not found');
|
expect(createComment( mockProps, mockAuth, mockReq ))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('Other user not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the user has blocked you', async () => {
|
it('throw if the user has blocked you', async () => {
|
||||||
const mockAuth = { uid: '321' } as AuthedUser;
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockReplyToCommentId = {} as any;
|
const mockReplyToCommentId = {} as any;
|
||||||
@@ -324,10 +358,12 @@ describe('createComment', () => {
|
|||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
(sharedUtils.getPrivateUser as jest.Mock)
|
||||||
.mockResolvedValue(mockUserId);
|
.mockResolvedValue(mockUserId);
|
||||||
|
|
||||||
expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('User has blocked you');
|
expect(createComment( mockProps, mockAuth, mockReq ))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('User has blocked you');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the comment is too long', async () => {
|
it('throw if the comment is too long', async () => {
|
||||||
const mockAuth = { uid: '321' } as AuthedUser;
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockReplyToCommentId = {} as any;
|
const mockReplyToCommentId = {} as any;
|
||||||
@@ -369,9 +405,10 @@ describe('createComment', () => {
|
|||||||
.mockResolvedValueOnce(mockCreator);
|
.mockResolvedValueOnce(mockCreator);
|
||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
(sharedUtils.getPrivateUser as jest.Mock)
|
||||||
.mockResolvedValue(mockUserId);
|
.mockResolvedValue(mockUserId);
|
||||||
console.log(JSON.stringify(mockContent.content).length);
|
|
||||||
|
|
||||||
expect(createComment( mockProps, mockAuth, mockReq )).rejects.toThrowError('Comment is too long');
|
expect(createComment( mockProps, mockAuth, mockReq ))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('Comment is too long');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -18,12 +18,12 @@ describe('createCompatibilityQuestion', () => {
|
|||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
describe('should', () => {
|
|
||||||
it('successfully create compatibility questions', async () => {
|
describe('when given valid input', () => {
|
||||||
|
it('should successfully create compatibility questions', async () => {
|
||||||
const mockQuestion = {} as any;
|
const mockQuestion = {} as any;
|
||||||
const mockOptions = {} as any;
|
const mockOptions = {} as any;
|
||||||
const mockProps = {options:mockOptions, question:mockQuestion};
|
const mockProps = {options:mockOptions, question:mockQuestion};
|
||||||
@@ -41,31 +41,45 @@ describe('createCompatibilityQuestion', () => {
|
|||||||
multiple_choice_options: {"first_choice":"first_answer"},
|
multiple_choice_options: {"first_choice":"first_answer"},
|
||||||
question: "mockQuestion"
|
question: "mockQuestion"
|
||||||
};
|
};
|
||||||
|
|
||||||
(shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
(shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
(supabaseUtils.insert as jest.Mock).mockResolvedValue(mockData);
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValue({data:mockData, error: null});
|
(tryCatch as jest.Mock).mockResolvedValue({data:mockData, error: null});
|
||||||
|
|
||||||
const results = await createCompatibilityQuestion(mockProps, mockAuth, mockReq);
|
const results = await createCompatibilityQuestion(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
expect(results.question).toEqual(mockData);
|
expect(results.question).toEqual(mockData);
|
||||||
|
expect(shareUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(shareUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'compatibility_prompts',
|
||||||
|
{
|
||||||
|
creator_id: mockCreator.id,
|
||||||
|
question: mockQuestion,
|
||||||
|
answer_type: 'compatibility_multiple_choice',
|
||||||
|
multiple_choice_options: mockOptions
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
it('throws an error if the account does not exist', async () => {
|
describe('when an error occurs', () => {
|
||||||
|
it('throws if the account does not exist', async () => {
|
||||||
const mockQuestion = {} as any;
|
const mockQuestion = {} as any;
|
||||||
const mockOptions = {} as any;
|
const mockOptions = {} as any;
|
||||||
const mockProps = {options:mockOptions, question:mockQuestion};
|
const mockProps = {options:mockOptions, question:mockQuestion};
|
||||||
const mockAuth = {uid: '321'} as AuthedUser;
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
(shareUtils.getUser as jest.Mock).mockResolvedValue(null);
|
|
||||||
|
(shareUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq))
|
expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Your account was not found')
|
.toThrowError('Your account was not found');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if unable to create the question', async () => {
|
it('throws if unable to create the question', async () => {
|
||||||
const mockQuestion = {} as any;
|
const mockQuestion = {} as any;
|
||||||
const mockOptions = {} as any;
|
const mockOptions = {} as any;
|
||||||
const mockProps = {options:mockOptions, question:mockQuestion};
|
const mockProps = {options:mockOptions, question:mockQuestion};
|
||||||
@@ -74,23 +88,13 @@ describe('createCompatibilityQuestion', () => {
|
|||||||
const mockCreator = {
|
const mockCreator = {
|
||||||
id: '123',
|
id: '123',
|
||||||
};
|
};
|
||||||
const mockData = {
|
|
||||||
answer_type: "mockAnswerType",
|
|
||||||
category: "mockCategory",
|
|
||||||
created_time: "mockCreatedTime",
|
|
||||||
id: 1,
|
|
||||||
importance_score: 1,
|
|
||||||
multiple_choice_options: {"first_choice":"first_answer"},
|
|
||||||
question: "mockQuestion"
|
|
||||||
};
|
|
||||||
(shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
(shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
(supabaseUtils.insert as jest.Mock).mockResolvedValue(mockData);
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValue({data:null, error: Error});
|
(tryCatch as jest.Mock).mockResolvedValue({data:null, error: Error});
|
||||||
|
|
||||||
expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq))
|
expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Error creating question')
|
.toThrowError('Error creating question');
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -8,25 +8,23 @@ import { tryCatch } from "common/util/try-catch";
|
|||||||
import * as supabaseNotifications from "shared/supabase/notifications";
|
import * as supabaseNotifications from "shared/supabase/notifications";
|
||||||
import { Notification } from "common/notifications";
|
import { Notification } from "common/notifications";
|
||||||
|
|
||||||
type MockNotificationUser = Pick<Notification, 'userId'>;
|
|
||||||
|
|
||||||
describe('createNotifications', () => {
|
describe('createNotifications', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
const mockPg = {
|
mockPg = {
|
||||||
many: jest.fn().mockReturnValue(null)
|
many: jest.fn().mockReturnValue(null)
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('sucessfully create a notification', async () => {
|
it('should sucessfully create a notification', async () => {
|
||||||
const mockUsers = [
|
const mockUsers = [
|
||||||
{
|
{
|
||||||
created_time: "mockCreatedTime",
|
created_time: "mockCreatedTime",
|
||||||
@@ -39,60 +37,89 @@ describe('createNotifications', () => {
|
|||||||
];
|
];
|
||||||
const mockNotification = {
|
const mockNotification = {
|
||||||
userId: "mockUserId"
|
userId: "mockUserId"
|
||||||
} as MockNotificationUser;
|
} as Notification;
|
||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null});
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null});
|
||||||
(supabaseNotifications.insertNotificationToSupabase as jest.Mock)
|
jest.spyOn(createNotificationModules, 'createNotification');
|
||||||
.mockResolvedValue(null);
|
|
||||||
|
const results = await createNotificationModules.createNotifications(mockNotification);
|
||||||
|
|
||||||
const results = await createNotificationModules.createNotifications(mockNotification as Notification);
|
|
||||||
expect(results?.success).toBeTruthy;
|
expect(results?.success).toBeTruthy;
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.many).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.many).toBeCalledWith('select * from users');
|
||||||
|
expect(createNotificationModules.createNotification).toBeCalledTimes(1);
|
||||||
|
expect(createNotificationModules.createNotification).toBeCalledWith(
|
||||||
|
mockUsers[0],
|
||||||
|
mockNotification,
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledTimes(1);
|
||||||
|
expect(supabaseNotifications.insertNotificationToSupabase).toBeCalledWith(
|
||||||
|
mockNotification,
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('throws an error if its unable to fetch users', async () => {
|
describe('when an error occurs', () => {
|
||||||
const mockUsers = [
|
it('should throw if its unable to fetch users', async () => {
|
||||||
{
|
|
||||||
created_time: "mockCreatedTime",
|
|
||||||
data: {"mockData": "mockDataJson"},
|
|
||||||
id: "mockId",
|
|
||||||
name: "mockName",
|
|
||||||
name_user_vector: "mockNUV",
|
|
||||||
username: "mockUsername"
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const mockNotification = {
|
const mockNotification = {
|
||||||
userId: "mockUserId"
|
userId: "mockUserId"
|
||||||
} as MockNotificationUser;
|
} as Notification;
|
||||||
|
|
||||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:Error});
|
(tryCatch as jest.Mock).mockResolvedValue({data: null, error:Error});
|
||||||
|
|
||||||
await createNotificationModules.createNotifications(mockNotification as Notification)
|
await createNotificationModules.createNotifications(mockNotification);
|
||||||
expect(errorSpy).toHaveBeenCalledWith('Error fetching users', expect.objectContaining({name: 'Error'}))
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
'Error fetching users',
|
||||||
|
expect.objectContaining({name: 'Error'})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if there are no users', async () => {
|
it('should throw if there are no users', async () => {
|
||||||
const mockUsers = [
|
|
||||||
{
|
|
||||||
created_time: "mockCreatedTime",
|
|
||||||
data: {"mockData": "mockDataJson"},
|
|
||||||
id: "mockId",
|
|
||||||
name: "mockName",
|
|
||||||
name_user_vector: "mockNUV",
|
|
||||||
username: "mockUsername"
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const mockNotification = {
|
const mockNotification = {
|
||||||
userId: "mockUserId"
|
userId: "mockUserId"
|
||||||
} as MockNotificationUser;
|
} as Notification;
|
||||||
|
|
||||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValue({data: null, error:null});
|
(tryCatch as jest.Mock).mockResolvedValue({data: null, error:null});
|
||||||
|
|
||||||
await createNotificationModules.createNotifications(mockNotification as Notification)
|
await createNotificationModules.createNotifications(mockNotification);
|
||||||
expect(errorSpy).toHaveBeenCalledWith('No users found')
|
expect(errorSpy).toBeCalledWith('No users found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to create notification', async () => {
|
||||||
|
const mockUsers = [
|
||||||
|
{
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockData": "mockDataJson"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_user_vector: "mockNUV",
|
||||||
|
username: "mockUsername"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockNotification = {
|
||||||
|
userId: "mockUserId"
|
||||||
|
} as Notification;
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null});
|
||||||
|
jest.spyOn(createNotificationModules, 'createNotification').mockRejectedValue(new Error('Creation failure'));
|
||||||
|
|
||||||
|
await createNotificationModules.createNotifications(mockNotification);
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
'Failed to create notification',
|
||||||
|
expect.objectContaining({name: 'Error'}),
|
||||||
|
mockUsers[0]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -23,13 +23,12 @@ describe('createPrivateUserMessageChannel', () => {
|
|||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg)
|
.mockReturnValue(mockPg)
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks()
|
jest.restoreAllMocks()
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('successfully create a private user message channel (currentChannel)', async () => {
|
it('should successfully create a private user message channel (currentChannel)', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
userIds: ["123"]
|
userIds: ["123"]
|
||||||
};
|
};
|
||||||
@@ -55,29 +54,27 @@ describe('createPrivateUserMessageChannel', () => {
|
|||||||
isBannedFromPosting: false
|
isBannedFromPosting: false
|
||||||
};
|
};
|
||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
.mockResolvedValue(mockCreator);
|
(utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers);
|
||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockCurrentChannel);
|
||||||
.mockResolvedValue(mockUserIds);
|
|
||||||
(utilArrayModules.filterDefined as jest.Mock)
|
|
||||||
.mockReturnValue(mockPrivateUsers);
|
|
||||||
(mockPg.oneOrNone as jest.Mock)
|
|
||||||
.mockResolvedValue(mockCurrentChannel);
|
|
||||||
(privateMessageModules.addUsersToPrivateMessageChannel as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
|
||||||
|
|
||||||
const results = await createPrivateUserMessageChannel(mockBody, mockAuth, mockReq);
|
const results = await createPrivateUserMessageChannel(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.status).toBe('success');
|
||||||
|
expect(results.channelId).toBe(444);
|
||||||
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
|
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
|
||||||
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]);
|
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]);
|
||||||
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]);
|
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]);
|
||||||
expect(results.status).toBe('success');
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
expect(results.channelId).toBe(444)
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select channel_id from private_user_message_channel_members'),
|
||||||
|
[mockUserIds]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully create a private user message channel (channel)', async () => {
|
it('should successfully create a private user message channel (channel)', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
userIds: ["123"]
|
userIds: ["123"]
|
||||||
};
|
};
|
||||||
@@ -103,45 +100,54 @@ describe('createPrivateUserMessageChannel', () => {
|
|||||||
isBannedFromPosting: false
|
isBannedFromPosting: false
|
||||||
};
|
};
|
||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
.mockResolvedValue(mockCreator);
|
(utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers);
|
||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
.mockResolvedValue(mockUserIds);
|
(mockPg.one as jest.Mock).mockResolvedValue(mockChannel);
|
||||||
(utilArrayModules.filterDefined as jest.Mock)
|
|
||||||
.mockReturnValue(mockPrivateUsers);
|
|
||||||
(mockPg.oneOrNone as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
|
||||||
(mockPg.one as jest.Mock)
|
|
||||||
.mockResolvedValue(mockChannel);
|
|
||||||
(privateMessageModules.addUsersToPrivateMessageChannel as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
|
||||||
|
|
||||||
const results = await createPrivateUserMessageChannel(mockBody, mockAuth, mockReq);
|
const results = await createPrivateUserMessageChannel(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.status).toBe('success');
|
||||||
|
expect(results.channelId).toBe(333);
|
||||||
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
|
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
|
||||||
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]);
|
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]);
|
||||||
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]);
|
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]);
|
||||||
expect(results.status).toBe('success');
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
expect(results.channelId).toBe(333)
|
expect(mockPg.one).toBeCalledWith(
|
||||||
|
expect.stringContaining('insert into private_user_message_channels default values returning id')
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('insert into private_user_message_channel_members (channel_id, user_id, role, status)'),
|
||||||
|
[mockChannel.id, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(privateMessageModules.addUsersToPrivateMessageChannel).toBeCalledTimes(1);
|
||||||
|
expect(privateMessageModules.addUsersToPrivateMessageChannel).toBeCalledWith(
|
||||||
|
[mockUserIds[0]],
|
||||||
|
mockChannel.id,
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
it('throw an error if the user account doesnt exist', async () => {
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the user account doesnt exist', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
userIds: ["123"]
|
userIds: ["123"]
|
||||||
};
|
};
|
||||||
const mockAuth = {uid: '321'} as AuthedUser;
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
(sharedUtils.getUser as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq))
|
expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Your account was not found');
|
.toThrowError('Your account was not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the authId is banned from posting', async () => {
|
it('should throw if the authId is banned from posting', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
userIds: ["123"]
|
userIds: ["123"]
|
||||||
};
|
};
|
||||||
@@ -151,21 +157,17 @@ describe('createPrivateUserMessageChannel', () => {
|
|||||||
isBannedFromPosting: true
|
isBannedFromPosting: true
|
||||||
};
|
};
|
||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
.mockResolvedValue(mockCreator);
|
|
||||||
|
|
||||||
expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq))
|
expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('You are banned');
|
.toThrowError('You are banned');
|
||||||
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
|
||||||
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the array lengths dont match (privateUsers, userIds)', async () => {
|
it('should throw if the array lengths dont match (privateUsers, userIds)', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
userIds: ["123"]
|
userIds: ["123"]
|
||||||
};
|
};
|
||||||
const mockUserIds = ['123'];
|
|
||||||
const mockPrivateUsers = [
|
const mockPrivateUsers = [
|
||||||
{
|
{
|
||||||
id: '123',
|
id: '123',
|
||||||
@@ -181,8 +183,6 @@ describe('createPrivateUserMessageChannel', () => {
|
|||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock)
|
||||||
.mockResolvedValue(mockCreator);
|
.mockResolvedValue(mockCreator);
|
||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
|
||||||
.mockResolvedValue(mockUserIds);
|
|
||||||
(utilArrayModules.filterDefined as jest.Mock)
|
(utilArrayModules.filterDefined as jest.Mock)
|
||||||
.mockReturnValue(mockPrivateUsers);
|
.mockReturnValue(mockPrivateUsers);
|
||||||
|
|
||||||
@@ -191,11 +191,10 @@ describe('createPrivateUserMessageChannel', () => {
|
|||||||
.toThrowError(`Private user ${mockAuth.uid} not found`);
|
.toThrowError(`Private user ${mockAuth.uid} not found`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if there is a blocked user in the userId list', async () => {
|
it('should throw if there is a blocked user in the userId list', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
userIds: ["123"]
|
userIds: ["123"]
|
||||||
};
|
};
|
||||||
const mockUserIds = ['321'];
|
|
||||||
const mockPrivateUsers = [
|
const mockPrivateUsers = [
|
||||||
{
|
{
|
||||||
id: '123',
|
id: '123',
|
||||||
@@ -216,8 +215,6 @@ describe('createPrivateUserMessageChannel', () => {
|
|||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock)
|
(sharedUtils.getUser as jest.Mock)
|
||||||
.mockResolvedValue(mockCreator);
|
.mockResolvedValue(mockCreator);
|
||||||
(sharedUtils.getPrivateUser as jest.Mock)
|
|
||||||
.mockResolvedValue(mockUserIds);
|
|
||||||
(utilArrayModules.filterDefined as jest.Mock)
|
(utilArrayModules.filterDefined as jest.Mock)
|
||||||
.mockReturnValue(mockPrivateUsers);
|
.mockReturnValue(mockPrivateUsers);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('createPrivateUserMessage', () => {
|
|||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('successfully create a private user message', async () => {
|
it('successfully create a private user message', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH-8))},
|
content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH-8))},
|
||||||
@@ -36,10 +36,12 @@ describe('createPrivateUserMessage', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
(helpersPrivateMessagesModules.createPrivateUserMessageMain as jest.Mock)
|
|
||||||
.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await createPrivateUserMessage(mockBody, mockAuth, mockReq);
|
await createPrivateUserMessage(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledTimes(1);
|
||||||
expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledWith(
|
expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledWith(
|
||||||
mockCreator,
|
mockCreator,
|
||||||
mockBody.channelId,
|
mockBody.channelId,
|
||||||
@@ -48,8 +50,9 @@ describe('createPrivateUserMessage', () => {
|
|||||||
'private'
|
'private'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
it('throw an error if the content is too long', async () => {
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the content is too long', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH))},
|
content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH))},
|
||||||
channelId: 123
|
channelId: 123
|
||||||
@@ -62,7 +65,7 @@ describe('createPrivateUserMessage', () => {
|
|||||||
.toThrowError(`Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`);
|
.toThrowError(`Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the user does not exist', async () => {
|
it('should throw if the user does not exist', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
content: {"mockJson": "mockJsonContent"},
|
content: {"mockJson": "mockJsonContent"},
|
||||||
channelId: 123
|
channelId: 123
|
||||||
@@ -70,14 +73,14 @@ describe('createPrivateUserMessage', () => {
|
|||||||
const mockAuth = {uid: '321'} as AuthedUser;
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
|
|
||||||
(sharedUtils.getUser as jest.Mock).mockResolvedValue(null);
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
expect(createPrivateUserMessage(mockBody, mockAuth, mockReq))
|
expect(createPrivateUserMessage(mockBody, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError(`Your account was not found`);
|
.toThrowError(`Your account was not found`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if the user does not exist', async () => {
|
it('should throw if the user does not exist', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
content: {"mockJson": "mockJsonContent"},
|
content: {"mockJson": "mockJsonContent"},
|
||||||
channelId: 123
|
channelId: 123
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ jest.mock('shared/supabase/utils');
|
|||||||
jest.mock('common/util/try-catch');
|
jest.mock('common/util/try-catch');
|
||||||
jest.mock('shared/analytics');
|
jest.mock('shared/analytics');
|
||||||
jest.mock('common/discord/core');
|
jest.mock('common/discord/core');
|
||||||
|
jest.mock('common/util/time');
|
||||||
|
|
||||||
import { createProfile } from "api/create-profile";
|
import { createProfile } from "api/create-profile";
|
||||||
import * as supabaseInit from "shared/supabase/init";
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
@@ -20,25 +21,189 @@ import { AuthedUser } from "api/helpers/endpoint";
|
|||||||
|
|
||||||
describe('createProfile', () => {
|
describe('createProfile', () => {
|
||||||
let mockPg = {} as any;
|
let mockPg = {} as any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
mockPg = {
|
mockPg = {
|
||||||
oneOrNone: jest.fn().mockReturnValue(null),
|
oneOrNone: jest.fn(),
|
||||||
one: jest.fn()
|
one: jest.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when given valid input', () => {
|
||||||
it('successfully create a profile', async () => {
|
it('should successfully create a profile', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
city: "mockCity",
|
||||||
|
gender: "mockGender",
|
||||||
|
looking_for_matches: true,
|
||||||
|
photo_urls: ["mockPhotoUrl1"],
|
||||||
|
pinned_url: "mockPinnedUrl",
|
||||||
|
pref_gender: ["mockPrefGender"],
|
||||||
|
pref_relation_styles: ["mockPrefRelationStyles"],
|
||||||
|
visibility: 'public' as "public" | "member",
|
||||||
|
wants_kids_strength: 2,
|
||||||
|
};
|
||||||
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockNProfiles = 10
|
||||||
|
const mockData = {
|
||||||
|
age: 30,
|
||||||
|
city: "mockCity"
|
||||||
|
};
|
||||||
|
const mockUser = {
|
||||||
|
createdTime: Date.now(),
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUserName"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null});
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null});
|
||||||
|
|
||||||
|
const results: any = await createProfile(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.result).toEqual(mockData);
|
||||||
|
expect(tryCatch).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select id from profiles where user_id = $1'),
|
||||||
|
[mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1);
|
||||||
|
expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockBody);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
mockAuth.uid,
|
||||||
|
{avatarUrl: mockBody.pinned_url}
|
||||||
|
);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'profiles',
|
||||||
|
expect.objectContaining({user_id: mockAuth.uid})
|
||||||
|
);
|
||||||
|
|
||||||
|
(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles);
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(sharedAnalytics.track).toBeCalledTimes(1);
|
||||||
|
expect(sharedAnalytics.track).toBeCalledWith(
|
||||||
|
mockAuth.uid,
|
||||||
|
'create profile',
|
||||||
|
{username: mockUser.username}
|
||||||
|
);
|
||||||
|
expect(sendDiscordMessage).toBeCalledTimes(1);
|
||||||
|
expect(sendDiscordMessage).toBeCalledWith(
|
||||||
|
expect.stringContaining(mockUser.name && mockUser.username),
|
||||||
|
'members'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully create milestone profile', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
city: "mockCity",
|
||||||
|
gender: "mockGender",
|
||||||
|
looking_for_matches: true,
|
||||||
|
photo_urls: ["mockPhotoUrl1"],
|
||||||
|
pinned_url: "mockPinnedUrl",
|
||||||
|
pref_gender: ["mockPrefGender"],
|
||||||
|
pref_relation_styles: ["mockPrefRelationStyles"],
|
||||||
|
visibility: 'public' as "public" | "member",
|
||||||
|
wants_kids_strength: 2,
|
||||||
|
};
|
||||||
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockNProfiles = 15
|
||||||
|
const mockData = {
|
||||||
|
age: 30,
|
||||||
|
city: "mockCity"
|
||||||
|
};
|
||||||
|
const mockUser = {
|
||||||
|
createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUserName"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null});
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null});
|
||||||
|
|
||||||
|
const results: any = await createProfile(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.result).toEqual(mockData);
|
||||||
|
|
||||||
|
(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles);
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.one).toBeCalledWith(
|
||||||
|
expect.stringContaining('SELECT count(*) FROM profiles'),
|
||||||
|
[],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(sendDiscordMessage).toBeCalledTimes(2);
|
||||||
|
expect(sendDiscordMessage).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining(String(mockNProfiles)),
|
||||||
|
'general'
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if it failed to track create profile', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
city: "mockCity",
|
||||||
|
gender: "mockGender",
|
||||||
|
looking_for_matches: true,
|
||||||
|
photo_urls: ["mockPhotoUrl1"],
|
||||||
|
pinned_url: "mockPinnedUrl",
|
||||||
|
pref_gender: ["mockPrefGender"],
|
||||||
|
pref_relation_styles: ["mockPrefRelationStyles"],
|
||||||
|
visibility: 'public' as "public" | "member",
|
||||||
|
wants_kids_strength: 2,
|
||||||
|
};
|
||||||
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
age: 30,
|
||||||
|
city: "mockCity"
|
||||||
|
};
|
||||||
|
const mockUser = {
|
||||||
|
createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUserName"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null});
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null});
|
||||||
|
|
||||||
|
const results: any = await createProfile(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Track error'));
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
'Failed to track create profile',
|
||||||
|
expect.objectContaining({name: 'Error'})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if it failed to send discord new profile', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
city: "mockCity",
|
city: "mockCity",
|
||||||
gender: "mockGender",
|
gender: "mockGender",
|
||||||
@@ -52,7 +217,6 @@ describe('createProfile', () => {
|
|||||||
};
|
};
|
||||||
const mockAuth = {uid: '321'} as AuthedUser;
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockExistingUser = {id: "mockExistingUserId"};
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
age: 30,
|
age: 30,
|
||||||
city: "mockCity"
|
city: "mockCity"
|
||||||
@@ -65,18 +229,25 @@ describe('createProfile', () => {
|
|||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
|
||||||
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
(supabaseUsers.updateUser as jest.Mock).mockReturnValue(null);
|
|
||||||
(supabaseUtils.insert as jest.Mock).mockReturnValue(null);
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null});
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null});
|
||||||
(sharedAnalytics.track as jest.Mock).mockResolvedValue(null);
|
|
||||||
(sendDiscordMessage as jest.Mock).mockResolvedValueOnce(null);
|
|
||||||
(mockPg.one as jest.Mock).mockReturnValue(10);
|
|
||||||
|
|
||||||
const results: any = await createProfile(mockBody, mockAuth, mockReq);
|
const results: any = await createProfile(mockBody, mockAuth, mockReq);
|
||||||
expect(results.result).toEqual(mockData)
|
|
||||||
|
expect(results.result).toEqual(mockData);
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
(sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Sending error'));
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
'Failed to send discord new profile',
|
||||||
|
expect.objectContaining({name: 'Error'})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if the profile already exists', async () => {
|
it('should throw if it failed to send discord user milestone', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
city: "mockCity",
|
city: "mockCity",
|
||||||
gender: "mockGender",
|
gender: "mockGender",
|
||||||
@@ -90,16 +261,69 @@ describe('createProfile', () => {
|
|||||||
};
|
};
|
||||||
const mockAuth = {uid: '321'} as AuthedUser;
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const mockExistingUser = {id: "mockExistingUserId"};
|
const mockNProfiles = 15
|
||||||
|
const mockData = {
|
||||||
|
age: 30,
|
||||||
|
city: "mockCity"
|
||||||
|
};
|
||||||
|
const mockUser = {
|
||||||
|
createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUserName"
|
||||||
|
};
|
||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockExistingUser, error: null});
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null});
|
||||||
|
|
||||||
|
const results: any = await createProfile(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.result).toEqual(mockData);
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
(sendDiscordMessage as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockRejectedValueOnce(new Error('Discord error'));
|
||||||
|
(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles);
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(sendDiscordMessage).toBeCalledTimes(2);
|
||||||
|
expect(sendDiscordMessage).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining(String(mockNProfiles)),
|
||||||
|
'general'
|
||||||
|
);
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
'Failed to send discord user milestone',
|
||||||
|
expect.objectContaining({name: 'Error'})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the user already exists', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
city: "mockCity",
|
||||||
|
gender: "mockGender",
|
||||||
|
looking_for_matches: true,
|
||||||
|
photo_urls: ["mockPhotoUrl1"],
|
||||||
|
pinned_url: "mockPinnedUrl",
|
||||||
|
pref_gender: ["mockPrefGender"],
|
||||||
|
pref_relation_styles: ["mockPrefRelationStyles"],
|
||||||
|
visibility: 'public' as "public" | "member",
|
||||||
|
wants_kids_strength: 2,
|
||||||
|
};
|
||||||
|
const mockAuth = {uid: '321'} as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: true, error: null});
|
||||||
|
|
||||||
await expect(createProfile(mockBody, mockAuth, mockReq))
|
await expect(createProfile(mockBody, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('User already exists');
|
.toThrowError('User already exists');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if the user already exists', async () => {
|
it('should throw if unable to find the account', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
city: "mockCity",
|
city: "mockCity",
|
||||||
gender: "mockGender",
|
gender: "mockGender",
|
||||||
@@ -115,16 +339,14 @@ describe('createProfile', () => {
|
|||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
|
||||||
(sharedUtils.getUser as jest.Mock).mockResolvedValue(null);
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(createProfile(mockBody, mockAuth, mockReq))
|
await expect(createProfile(mockBody, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Your account was not found');
|
.toThrowError('Your account was not found');
|
||||||
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
|
||||||
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if anything unexpected happens when creating the user', async () => {
|
it('should throw if anything unexpected happens when creating the user', async () => {
|
||||||
const mockBody = {
|
const mockBody = {
|
||||||
city: "mockCity",
|
city: "mockCity",
|
||||||
gender: "mockGender",
|
gender: "mockGender",
|
||||||
@@ -146,15 +368,11 @@ describe('createProfile', () => {
|
|||||||
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
|
||||||
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
(supabaseUsers.updateUser as jest.Mock).mockReturnValue(null);
|
|
||||||
(supabaseUtils.insert as jest.Mock).mockReturnValue(null);
|
|
||||||
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: Error});
|
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: Error});
|
||||||
|
|
||||||
await expect(createProfile(mockBody, mockAuth, mockReq))
|
await expect(createProfile(mockBody, mockAuth, mockReq))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Error creating user');
|
.toThrowError('Error creating user');
|
||||||
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
|
||||||
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,835 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/supabase/utils');
|
||||||
|
jest.mock('common/supabase/users');
|
||||||
|
jest.mock('email/functions/helpers');
|
||||||
|
jest.mock('api/set-last-online-time');
|
||||||
|
jest.mock('firebase-admin', () => ({
|
||||||
|
auth: jest.fn()
|
||||||
|
}));
|
||||||
|
jest.mock('shared/utils');
|
||||||
|
jest.mock('shared/analytics');
|
||||||
|
jest.mock('shared/firebase-utils');
|
||||||
|
jest.mock('shared/helpers/generate-and-update-avatar-urls');
|
||||||
|
jest.mock('common/util/object');
|
||||||
|
jest.mock('common/user-notification-preferences');
|
||||||
|
jest.mock('common/util/clean-username');
|
||||||
|
jest.mock('shared/monitoring/log');
|
||||||
|
jest.mock('common/hosting/constants');
|
||||||
|
|
||||||
|
import { createUser } from "api/create-user";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as supabaseUtils from "shared/supabase/utils";
|
||||||
|
import * as supabaseUsers from "common/supabase/users";
|
||||||
|
import * as emailHelpers from "email/functions/helpers";
|
||||||
|
import * as apiSetLastTimeOnline from "api/set-last-online-time";
|
||||||
|
import * as firebaseAdmin from "firebase-admin";
|
||||||
|
import * as sharedUtils from "shared/utils";
|
||||||
|
import * as sharedAnalytics from "shared/analytics";
|
||||||
|
import * as firebaseUtils from "shared/firebase-utils";
|
||||||
|
import * as avatarHelpers from "shared/helpers/generate-and-update-avatar-urls";
|
||||||
|
import * as objectUtils from "common/util/object";
|
||||||
|
import * as userNotificationPref from "common/user-notification-preferences";
|
||||||
|
import * as usernameUtils from "common/util/clean-username";
|
||||||
|
import * as hostingConstants from "common/hosting/constants";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
|
||||||
describe('createUser', () => {
|
describe('createUser', () => {
|
||||||
describe('should', () => {
|
const originalIsLocal = (hostingConstants as any).IS_LOCAL;
|
||||||
it('', async () => {
|
let mockPg = {} as any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
one: jest.fn(),
|
||||||
|
tx: jest.fn(async (cb) => {
|
||||||
|
const mockTx = {} as any;
|
||||||
|
return cb(mockTx)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
Object.defineProperty(hostingConstants, 'IS_LOCAL', {
|
||||||
|
value: originalIsLocal,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should successfully create a user', async () => {
|
||||||
|
Object.defineProperty(hostingConstants, 'IS_LOCAL', {
|
||||||
|
value: false,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
const results: any = await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.result.user).toEqual(mockNewUserRow);
|
||||||
|
expect(results.result.privateUser).toEqual(mockPrivateUserRow);
|
||||||
|
expect(mockGetUser).toBeCalledTimes(2);
|
||||||
|
expect(mockGetUser).toHaveBeenNthCalledWith(1, mockAuth.uid);
|
||||||
|
expect(mockReq.get).toBeCalledTimes(1);
|
||||||
|
expect(mockReq.get).toBeCalledWith(Object.keys(mockReferer.headers)[0]);
|
||||||
|
expect(sharedAnalytics.getIp).toBeCalledTimes(1);
|
||||||
|
expect(sharedAnalytics.getIp).toBeCalledWith(mockReq);
|
||||||
|
expect(mockGetUser).toHaveBeenNthCalledWith(2, mockAuth.uid);
|
||||||
|
expect(usernameUtils.cleanDisplayName).toBeCalledTimes(1);
|
||||||
|
expect(usernameUtils.cleanDisplayName).toHaveBeenCalledWith(mockFbUser.displayName);
|
||||||
|
expect(usernameUtils.cleanUsername).toBeCalledTimes(1);
|
||||||
|
expect(usernameUtils.cleanUsername).toBeCalledWith(mockFbUser.displayName);
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.tx).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toHaveBeenCalledWith(
|
||||||
|
mockAuth.uid,
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
expect(userNotificationPref.getDefaultNotificationPreferences).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledTimes(2);
|
||||||
|
expect(supabaseUtils.insert).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.any(Object),
|
||||||
|
'users',
|
||||||
|
expect.objectContaining(
|
||||||
|
{
|
||||||
|
id: mockAuth.uid,
|
||||||
|
name: mockFbUser.displayName,
|
||||||
|
username: mockFbUser.displayName,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(supabaseUtils.insert).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.any(Object),
|
||||||
|
'private_users',
|
||||||
|
expect.objectContaining(
|
||||||
|
{
|
||||||
|
id: mockAuth.uid,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
(sharedAnalytics.track as jest.Mock).mockResolvedValue(null);
|
||||||
|
(emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null);
|
||||||
|
(apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(sharedAnalytics.track).toBeCalledTimes(1);
|
||||||
|
expect(sharedAnalytics.track).toBeCalledWith(
|
||||||
|
mockAuth.uid,
|
||||||
|
'create profile',
|
||||||
|
{username: mockNewUserRow.username}
|
||||||
|
);
|
||||||
|
expect(emailHelpers.sendWelcomeEmail).toBeCalledTimes(1);
|
||||||
|
expect(emailHelpers.sendWelcomeEmail).toBeCalledWith(mockNewUserRow, mockPrivateUserRow);
|
||||||
|
expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledTimes(1);
|
||||||
|
expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a device token when creating a user', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'password'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(supabaseUtils.insert).not.toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.any(Object),
|
||||||
|
'private_users',
|
||||||
|
{
|
||||||
|
id: expect.any(String),
|
||||||
|
data: expect.objectContaining(
|
||||||
|
{
|
||||||
|
initialDeviceToken: mockProps.deviceToken
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a avatar Url when creating a user', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'password'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockAvatarUrl = "mockGeneratedAvatarUrl"
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(avatarHelpers.generateAvatarUrl as jest.Mock).mockResolvedValue(mockAvatarUrl);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1);
|
||||||
|
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
avatarUrl: mockAvatarUrl,
|
||||||
|
isBannedFromPosting: false,
|
||||||
|
link: expect.any(Object)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow a username that already exists when creating a user', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(1);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledTimes(2);
|
||||||
|
expect(supabaseUtils.insert).not.toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.any(Object),
|
||||||
|
'users',
|
||||||
|
expect.objectContaining(
|
||||||
|
{
|
||||||
|
id: mockAuth.uid,
|
||||||
|
name: mockFbUser.displayName,
|
||||||
|
username: mockFbUser.displayName,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully create a user who is banned from posting if there ip/device token is banned', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1);
|
||||||
|
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
avatarUrl: mockFbUser.photoURL,
|
||||||
|
isBannedFromPosting: true,
|
||||||
|
link: expect.any(Object)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the user already exists', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
expect(createUser(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('User already exists');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the username is already taken', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
expect(createUser(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrowError('Username already taken');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if failed to track create profile', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
const results: any = await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Tracking failed'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith('Failed to track create profile', expect.any(Error));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if failed to send a welcome email', async () => {
|
||||||
|
Object.defineProperty(hostingConstants, 'IS_LOCAL', {
|
||||||
|
value: false,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
const results: any = await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
(sharedAnalytics.track as jest.Mock).mockResolvedValue(null);
|
||||||
|
(emailHelpers.sendWelcomeEmail as jest.Mock).mockRejectedValue(new Error('Welcome email failed'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith('Failed to sendWelcomeEmail', expect.any(Error));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if failed to set last time online', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
deviceToken: "mockDeviceToken",
|
||||||
|
adminToken: "mockAdminToken"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReferer = {
|
||||||
|
headers: {
|
||||||
|
'referer': 'mockReferer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockReq = { get: jest.fn().mockReturnValue(mockReferer)} as any;
|
||||||
|
const mockFirebaseUser = {
|
||||||
|
providerData: [
|
||||||
|
{
|
||||||
|
providerId: 'passwords'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const mockFbUser = {
|
||||||
|
email: "mockEmail@mockServer.com",
|
||||||
|
displayName: "mockDisplayName",
|
||||||
|
photoURL: "mockPhotoUrl"
|
||||||
|
};
|
||||||
|
const mockIp = "mockIP";
|
||||||
|
const mockBucket = {} as any;
|
||||||
|
const mockNewUserRow = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockNewUserJson": "mockNewUserJsonData"},
|
||||||
|
id: "mockNewUserId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockPrivateUserRow = {
|
||||||
|
data: {"mockPrivateUserJson" : "mockPrivateUserJsonData"},
|
||||||
|
id: "mockPrivateUserId"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetUser = jest.fn()
|
||||||
|
.mockResolvedValueOnce(mockFirebaseUser)
|
||||||
|
.mockResolvedValueOnce(mockFbUser);
|
||||||
|
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
getUser: mockGetUser
|
||||||
|
});
|
||||||
|
(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket);
|
||||||
|
(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(0);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false);
|
||||||
|
(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow);
|
||||||
|
(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow);
|
||||||
|
|
||||||
|
const results: any = await createUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
(sharedAnalytics.track as jest.Mock).mockResolvedValue(null);
|
||||||
|
(emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null);
|
||||||
|
(apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockRejectedValue(new Error('Failed to set last online time'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith('Failed to set last online time', expect.any(Error));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
98
backend/api/tests/unit/create-vote.unit.test.ts
Normal file
98
backend/api/tests/unit/create-vote.unit.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/utils');
|
||||||
|
jest.mock('shared/supabase/utils');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
|
||||||
|
import { createVote } from "api/create-vote";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as sharedUtils from "shared/utils";
|
||||||
|
import * as supabaseUtils from "shared/supabase/utils";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('createVote', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
const mockPg = {} as any;
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg)
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should successfully creates a vote', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
title: 'mockTitle',
|
||||||
|
description: {'mockDescription': 'mockDescriptionValue'},
|
||||||
|
isAnonymous: true
|
||||||
|
};
|
||||||
|
const mockCreator = {id: '123'};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
creator_id: mockCreator.id,
|
||||||
|
title: 'mockTitle',
|
||||||
|
description: {'mockDescription': 'mockDescriptionValue'},
|
||||||
|
is_anonymous: true,
|
||||||
|
status: 'voting_open'
|
||||||
|
};
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockData , error: null});
|
||||||
|
|
||||||
|
const result = await createVote(mockProps, mockAuth, mockReq);
|
||||||
|
expect(result.data).toEqual(mockData);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'votes',
|
||||||
|
{
|
||||||
|
creator_id: mockCreator.id,
|
||||||
|
title: mockProps.title,
|
||||||
|
description: mockProps.description,
|
||||||
|
is_anonymous: mockProps.isAnonymous,
|
||||||
|
status: 'voting_open'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the account was not found', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
title: 'mockTitle',
|
||||||
|
description: {'mockDescription': 'mockDescriptionValue'},
|
||||||
|
isAnonymous: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(createVote(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Your account was not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to create a question', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
title: 'mockTitle',
|
||||||
|
description: {'mockDescription': 'mockDescriptionValue'},
|
||||||
|
isAnonymous: true
|
||||||
|
};
|
||||||
|
const mockCreator = {id: '123'};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: null , error: Error});
|
||||||
|
|
||||||
|
expect(createVote(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Error creating question');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
43
backend/api/tests/unit/delete-bookmarked-search.unit.test.ts
Normal file
43
backend/api/tests/unit/delete-bookmarked-search.unit.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { deleteBookmarkedSearch } from "api/delete-bookmarked-search";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('deleteBookmarkedSearch', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn(),
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should successfully deletes a bookmarked search', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
id: 123
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
const result = await deleteBookmarkedSearch(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({});
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('DELETE FROM bookmarked_searches'),
|
||||||
|
[
|
||||||
|
mockProps.id,
|
||||||
|
mockAuth.uid
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/compatibility/compute-scores');
|
||||||
|
|
||||||
|
import { deleteCompatibilityAnswer } from "api/delete-compatibility-answer";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import { recomputeCompatibilityScoresForUser } from "shared/compatibility/compute-scores";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('deleteCompatibilityAnswers', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should successfully delete compatibility answers', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
id: 123
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(true);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const results: any = await deleteCompatibilityAnswer(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.status).toBe('success');
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining(`SELECT *`),
|
||||||
|
[mockProps.id, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('DELETE'),
|
||||||
|
[mockProps.id, mockAuth.uid]
|
||||||
|
);
|
||||||
|
|
||||||
|
await results.continue();
|
||||||
|
|
||||||
|
(recomputeCompatibilityScoresForUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
expect(recomputeCompatibilityScoresForUser).toBeCalledTimes(1);
|
||||||
|
expect(recomputeCompatibilityScoresForUser).toBeCalledWith(mockAuth.uid, expect.any(Object));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the user is not the answers author', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
id: 123
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(deleteCompatibilityAnswer(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Item not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
121
backend/api/tests/unit/delete-me.unit.test.ts
Normal file
121
backend/api/tests/unit/delete-me.unit.test.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/utils');
|
||||||
|
jest.mock('firebase-admin', () => ({
|
||||||
|
auth: jest.fn()
|
||||||
|
}));
|
||||||
|
jest.mock('shared/firebase-utils');
|
||||||
|
|
||||||
|
import { deleteMe } from "api/delete-me";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as sharedUtils from "shared/utils";
|
||||||
|
import * as firebaseAdmin from "firebase-admin";
|
||||||
|
import * as firebaseUtils from "shared/firebase-utils";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('deleteMe', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg)
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should delete the user account from supabase and firebase', async () => {
|
||||||
|
const mockUser = {
|
||||||
|
id: "mockId",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockRef = {} as any;
|
||||||
|
|
||||||
|
const mockDeleteUser = jest.fn().mockResolvedValue(null);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(firebaseUtils.deleteUserFiles as jest.Mock).mockResolvedValue(null);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
deleteUser: mockDeleteUser
|
||||||
|
});
|
||||||
|
const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await deleteMe(mockRef, mockAuth, mockRef);
|
||||||
|
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('DELETE FROM users WHERE id = $1'),
|
||||||
|
[mockUser.id]
|
||||||
|
);
|
||||||
|
expect(firebaseUtils.deleteUserFiles).toBeCalledTimes(1);
|
||||||
|
expect(firebaseUtils.deleteUserFiles).toBeCalledWith(mockUser.username);
|
||||||
|
expect(mockDeleteUser).toBeCalledTimes(1);
|
||||||
|
expect(mockDeleteUser).toBeCalledWith(mockUser.id);
|
||||||
|
|
||||||
|
expect(debugSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining(mockUser.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the user account was not found', async () => {
|
||||||
|
const mockUser = {
|
||||||
|
id: "mockId",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockRef = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(deleteMe(mockRef, mockAuth, mockRef))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Your account was not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if there is no userId', async () => {
|
||||||
|
const mockUser = {
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockRef = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
|
||||||
|
expect(deleteMe(mockRef, mockAuth, mockRef))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Invalid user ID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to remove user from firebase auth', async () => {
|
||||||
|
const mockUser = {
|
||||||
|
id: "mockId",
|
||||||
|
username: "mockUsername"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockRef = {} as any;
|
||||||
|
|
||||||
|
const mockDeleteUser = jest.fn().mockRejectedValue(new Error('Error during deletion'));
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(firebaseUtils.deleteUserFiles as jest.Mock).mockResolvedValue(null);
|
||||||
|
(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||||
|
deleteUser: mockDeleteUser
|
||||||
|
});
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await deleteMe(mockRef, mockAuth, mockRef);
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('Error deleting user from Firebase Auth:'),
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
100
backend/api/tests/unit/delete-message.unit.test.ts
Normal file
100
backend/api/tests/unit/delete-message.unit.test.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('api/helpers/private-messages');
|
||||||
|
|
||||||
|
import { deleteMessage } from "api/delete-message";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as messageHelpers from "api/helpers/private-messages";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('deleteMessage', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should delete a message', async () => {
|
||||||
|
const mockMessageId = {
|
||||||
|
messageId: 123
|
||||||
|
};
|
||||||
|
const mockMessage = {
|
||||||
|
channel_id: "mockChannelId"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const results = await deleteMessage(mockMessageId, mockAuth, mockReq);
|
||||||
|
expect(results.success).toBeTruthy();
|
||||||
|
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('SELECT *'),
|
||||||
|
[mockMessageId.messageId, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('DELETE'),
|
||||||
|
[mockMessageId.messageId, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1);
|
||||||
|
expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
mockMessage.channel_id,
|
||||||
|
mockAuth.uid
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the message was not found', async () => {
|
||||||
|
const mockMessageId = {
|
||||||
|
messageId: 123
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(deleteMessage(mockMessageId, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Message not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the message was not broadcasted', async () => {
|
||||||
|
const mockMessageId = {
|
||||||
|
messageId: 123
|
||||||
|
};
|
||||||
|
const mockMessage = {
|
||||||
|
channel_id: "mockChannelId"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(messageHelpers.broadcastPrivateMessages as jest.Mock).mockRejectedValue(new Error('Broadcast Error'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await deleteMessage(mockMessageId, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledTimes(1);
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('broadcastPrivateMessages failed'),
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
126
backend/api/tests/unit/edit-message.unit.test.ts
Normal file
126
backend/api/tests/unit/edit-message.unit.test.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/encryption');
|
||||||
|
jest.mock('api/helpers/private-messages');
|
||||||
|
|
||||||
|
import { editMessage } from "api/edit-message";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as encryptionModules from "shared/encryption";
|
||||||
|
import * as messageHelpers from "api/helpers/private-messages";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('editMessage', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should edit the messages associated with the messageId', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
messageId: 123,
|
||||||
|
content: {'mockContent' : 'mockContentValue'}
|
||||||
|
};
|
||||||
|
const mockPlainTextContent = JSON.stringify(mockProps.content)
|
||||||
|
const mockMessage = {
|
||||||
|
channel_id: "mockChannelId"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockCipher = "mockCipherText";
|
||||||
|
const mockIV = "mockIV";
|
||||||
|
const mockTag = "mockTag";
|
||||||
|
const mockEncryption = {
|
||||||
|
ciphertext: mockCipher,
|
||||||
|
iv: mockIV,
|
||||||
|
tag: mockTag
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage);
|
||||||
|
(encryptionModules.encryptMessage as jest.Mock).mockReturnValue(mockEncryption);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await editMessage(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('SELECT *'),
|
||||||
|
[mockProps.messageId, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(encryptionModules.encryptMessage).toBeCalledTimes(1);
|
||||||
|
expect(encryptionModules.encryptMessage).toBeCalledWith(mockPlainTextContent);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('UPDATE private_user_messages'),
|
||||||
|
[mockCipher, mockIV, mockTag, mockProps.messageId]
|
||||||
|
);
|
||||||
|
expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1);
|
||||||
|
expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
mockMessage.channel_id,
|
||||||
|
mockAuth.uid
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if there is an issue with the message', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
messageId: 123,
|
||||||
|
content: {'mockContent' : 'mockContentValue'}
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(editMessage(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Message not found or cannot be edited');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the message broadcast failed', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
messageId: 123,
|
||||||
|
content: {'mockContent' : 'mockContentValue'}
|
||||||
|
};
|
||||||
|
const mockMessage = {
|
||||||
|
channel_id: "mockChannelId"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockCipher = "mockCipherText";
|
||||||
|
const mockIV = "mockIV";
|
||||||
|
const mockTag = "mockTag";
|
||||||
|
const mockEncryption = {
|
||||||
|
ciphertext: mockCipher,
|
||||||
|
iv: mockIV,
|
||||||
|
tag: mockTag
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage);
|
||||||
|
(encryptionModules.encryptMessage as jest.Mock).mockReturnValue(mockEncryption);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(messageHelpers.broadcastPrivateMessages as jest.Mock).mockRejectedValue(new Error('Broadcast Error'));
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await editMessage(mockProps, mockAuth, mockReq);
|
||||||
|
expect(errorSpy).toBeCalledTimes(1);
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('broadcastPrivateMessages failed'),
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import * as compatibililtyQuestionsModules from "api/get-compatibililty-questions";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('getCompatibilityQuestions', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
manyOrNone: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should get compatibility questions', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = {} as any;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockQuestions = {
|
||||||
|
answer_type: "mockAnswerTypes",
|
||||||
|
category: "mockCategory",
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
id: "mockId",
|
||||||
|
importance_score: 123,
|
||||||
|
multiple_choice_options: {"mockChoice" : "mockChoiceValue"},
|
||||||
|
question: "mockQuestion",
|
||||||
|
answer_count: 10,
|
||||||
|
score: 20
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.manyOrNone as jest.Mock).mockResolvedValue(mockQuestions);
|
||||||
|
|
||||||
|
const results: any = await compatibililtyQuestionsModules.getCompatibilityQuestions(mockProps, mockAuth, mockReq);
|
||||||
|
const [sql, params] = (mockPg.manyOrNone as jest.Mock).mock.calls[0];
|
||||||
|
|
||||||
|
expect(results.status).toBe('success');
|
||||||
|
expect(results.questions).toBe(mockQuestions);
|
||||||
|
expect(sql).toEqual(
|
||||||
|
expect.stringContaining('compatibility_prompts.*')
|
||||||
|
);
|
||||||
|
expect(sql).toEqual(
|
||||||
|
expect.stringContaining('COUNT(compatibility_answers.question_id) as answer_count')
|
||||||
|
);
|
||||||
|
expect(sql).toEqual(
|
||||||
|
expect.stringContaining('AVG(POWER(compatibility_answers.importance + 1 + CASE WHEN compatibility_answers.explanation IS NULL THEN 1 ELSE 0 END, 2)) as score')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
|
||||||
|
import { getCurrentPrivateUser } from "api/get-current-private-user";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('getCurrentPrivateUser', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should get current private user', async () => {
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId"
|
||||||
|
};
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null});
|
||||||
|
|
||||||
|
const result = await getCurrentPrivateUser(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockData.data);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select * from private_users where id = $1'),
|
||||||
|
[mockAuth.uid]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if unable to get users private data', async () => {
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId"
|
||||||
|
};
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: Error});
|
||||||
|
|
||||||
|
expect(getCurrentPrivateUser(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Error fetching private user data: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to find user account', async () => {
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: null, error: null});
|
||||||
|
|
||||||
|
expect(getCurrentPrivateUser(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Your account was not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
82
backend/api/tests/unit/get-likes-and-ships.unit.test.ts
Normal file
82
backend/api/tests/unit/get-likes-and-ships.unit.test.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import * as likesAndShips from "api/get-likes-and-ships";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('getLikesAndShips', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
map: jest.fn(),
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should get all likes recieved/given an any ships', async () => {
|
||||||
|
const mockProps = {userId: "mockUserId"};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockLikesGiven = {
|
||||||
|
user_id: "mockUser_Id_likes_given",
|
||||||
|
created_Time: 123
|
||||||
|
};
|
||||||
|
const mockLikesReceived = {
|
||||||
|
user_id: "mockUser_Id_likes_received",
|
||||||
|
created_Time: 1234
|
||||||
|
};
|
||||||
|
const mockShips = {
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
target_id: "mockTargetId",
|
||||||
|
target1_id: "mockTarget1Id",
|
||||||
|
target2_id: "mockTarget2Id",
|
||||||
|
target3_id: "mockTarget3Id",
|
||||||
|
created_time: 12345
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(likesAndShips, 'getLikesAndShipsMain');
|
||||||
|
(mockPg.map as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockLikesGiven)
|
||||||
|
.mockResolvedValueOnce(mockLikesReceived)
|
||||||
|
.mockResolvedValueOnce(mockShips);
|
||||||
|
|
||||||
|
|
||||||
|
const result: any = await likesAndShips.getLikesAndShips(mockProps, mockAuth, mockReq);
|
||||||
|
const [sql1, params1, fn1] = (mockPg.map as jest.Mock).mock.calls[0];
|
||||||
|
const [sql2, params2, fn2] = (mockPg.map as jest.Mock).mock.calls[1];
|
||||||
|
const [sql3, params3, fn3] = (mockPg.map as jest.Mock).mock.calls[2];
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(result.likesGiven).toBe(mockLikesGiven);
|
||||||
|
expect(result.likesReceived).toBe(mockLikesReceived);
|
||||||
|
expect(result.ships).toBe(mockShips);
|
||||||
|
|
||||||
|
expect(likesAndShips.getLikesAndShipsMain).toBeCalledTimes(1);
|
||||||
|
expect(likesAndShips.getLikesAndShipsMain).toBeCalledWith(mockProps.userId);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining(sql1),
|
||||||
|
[mockProps.userId],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining(sql2),
|
||||||
|
[mockProps.userId],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
expect.stringContaining(sql3),
|
||||||
|
[mockProps.userId],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
backend/api/tests/unit/get-me.unit.test.ts
Normal file
29
backend/api/tests/unit/get-me.unit.test.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
jest.mock('api/get-user');
|
||||||
|
|
||||||
|
import { getMe } from "api/get-me";
|
||||||
|
import { getUser } from "api/get-user";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('getMe', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should get the user', async () => {
|
||||||
|
const mockProps = {};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(getUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await getMe(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(getUser).toBeCalledTimes(1);
|
||||||
|
expect(getUser).toBeCalledWith({id: mockAuth.uid});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
40
backend/api/tests/unit/get-messages-count.unit.test.ts
Normal file
40
backend/api/tests/unit/get-messages-count.unit.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { getMessagesCount } from "api/get-messages-count";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('getMessagesCount', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
one: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should get message count', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockResults = { count: "10"};
|
||||||
|
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(mockResults);
|
||||||
|
|
||||||
|
const result: any = await getMessagesCount(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.count).toBe(Number(mockResults.count));
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.one).toBeCalledWith(
|
||||||
|
expect.stringContaining('SELECT COUNT(*) AS count'),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
44
backend/api/tests/unit/get-notifications.unit.test.ts
Normal file
44
backend/api/tests/unit/get-notifications.unit.test.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { getNotifications } from "api/get-notifications";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('getNotifications', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
map: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should user notifications', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
after: 2
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockNotifications = {} as any;
|
||||||
|
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue(mockNotifications);
|
||||||
|
|
||||||
|
const result = await getNotifications(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockNotifications);
|
||||||
|
expect(mockPg.map).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.map).toBeCalledWith(
|
||||||
|
expect.stringContaining('select data from user_notifications'),
|
||||||
|
[mockAuth.uid, mockProps.limit, mockProps.after],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
74
backend/api/tests/unit/get-options.unit.test.ts
Normal file
74
backend/api/tests/unit/get-options.unit.test.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
|
||||||
|
import { getOptions } from "api/get-options";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('getOptions', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
manyOrNone: jest.fn(),
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return valid options', async () => {
|
||||||
|
const mockTable = "causes";
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = [
|
||||||
|
{ name: "mockName" },
|
||||||
|
];
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||||
|
(mockPg.manyOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null});
|
||||||
|
|
||||||
|
const result: any = await getOptions({table: mockTable}, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.names).toContain(mockData[0].name);
|
||||||
|
expect(mockPg.manyOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.manyOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('SELECT interests.name')
|
||||||
|
);
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the table is invalid', async () => {
|
||||||
|
const mockTable = "causes";
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(getOptions({table: mockTable}, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Invalid table');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to get profile options', async () => {
|
||||||
|
const mockTable = "causes";
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||||
|
(mockPg.manyOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error});
|
||||||
|
|
||||||
|
expect(getOptions({table: mockTable}, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Error getting profile options');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
289
backend/api/tests/unit/get-private-messages.unit.test.ts
Normal file
289
backend/api/tests/unit/get-private-messages.unit.test.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
jest.mock('shared/supabase/messages');
|
||||||
|
|
||||||
|
import * as getPrivateMessages from "api/get-private-messages";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('getChannelMemberships', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
map: jest.fn(),
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return channel memberships', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
channelId: 1,
|
||||||
|
createdTime: "mockCreatedTime",
|
||||||
|
lastUpdatedTime: "mockLastUpdatedTime"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockChannels = [
|
||||||
|
{
|
||||||
|
channel_id: 123,
|
||||||
|
notify_after_time: "mockNotifyAfterTime",
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
last_updated_time: "mockLastUpdatedTime"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const mockMembers = [
|
||||||
|
{
|
||||||
|
channel_id: 1234,
|
||||||
|
user_id: "mockUserId"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
(mockPg.map as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockChannels)
|
||||||
|
.mockResolvedValueOnce(mockMembers);
|
||||||
|
|
||||||
|
const results: any = await getPrivateMessages.getChannelMemberships(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.channels).toBe(mockChannels);
|
||||||
|
expect(Object.keys(results.memberIdsByChannelId)[0]).toBe(String(mockMembers[0].channel_id));
|
||||||
|
expect(Object.values(results.memberIdsByChannelId)[0]).toContain(mockMembers[0].user_id);
|
||||||
|
|
||||||
|
expect(mockPg.map).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining('select channel_id, notify_after_time, pumcm.created_time, last_updated_time'),
|
||||||
|
[mockAuth.uid, mockProps.channelId, mockProps.limit],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining('select channel_id, user_id'),
|
||||||
|
[mockAuth.uid, [mockChannels[0].channel_id]],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return channel memberships if there is no channelId', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
createdTime: "mockCreatedTime",
|
||||||
|
lastUpdatedTime: "mockLastUpdatedTime"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockChannels = [
|
||||||
|
{
|
||||||
|
channel_id: 123,
|
||||||
|
notify_after_time: "mockNotifyAfterTime",
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
last_updated_time: "mockLastUpdatedTime"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const mockMembers = [
|
||||||
|
{
|
||||||
|
channel_id: 1234,
|
||||||
|
user_id: "mockUserId"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
(mockPg.map as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockChannels)
|
||||||
|
.mockResolvedValueOnce(mockMembers);
|
||||||
|
|
||||||
|
const results: any = await getPrivateMessages.getChannelMemberships(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.channels).toBe(mockChannels);
|
||||||
|
expect(Object.keys(results.memberIdsByChannelId)[0]).toBe(String(mockMembers[0].channel_id));
|
||||||
|
expect(Object.values(results.memberIdsByChannelId)[0]).toContain(mockMembers[0].user_id);
|
||||||
|
|
||||||
|
expect(mockPg.map).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining('with latest_channels as (select distinct on (pumc.id) pumc.id as channel_id'),
|
||||||
|
[mockAuth.uid, mockProps.createdTime, mockProps.limit, mockProps.lastUpdatedTime],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining('select channel_id, user_id'),
|
||||||
|
[mockAuth.uid, [mockChannels[0].channel_id]],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return nothing if there are no channels', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
channelId: 1,
|
||||||
|
createdTime: "mockCreatedTime",
|
||||||
|
lastUpdatedTime: "mockLastUpdatedTime"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const results: any = await getPrivateMessages.getChannelMemberships(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
console.log(results);
|
||||||
|
|
||||||
|
expect(results).toStrictEqual({ channels: [], memberIdsByChannelId: {} });
|
||||||
|
|
||||||
|
expect(mockPg.map).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining('select channel_id, notify_after_time, pumcm.created_time, last_updated_time'),
|
||||||
|
[mockAuth.uid, mockProps.channelId, mockProps.limit],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getChannelMessagesEndpoint', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
map: jest.fn(),
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return the channel messages endpoint', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
channelId: 1,
|
||||||
|
id: 123
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = ['mockResult'] as any;
|
||||||
|
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null});
|
||||||
|
|
||||||
|
const result = await getPrivateMessages.getChannelMessagesEndpoint(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockData);
|
||||||
|
expect(mockPg.map).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.map).toBeCalledWith(
|
||||||
|
expect.stringContaining('select *, created_time as created_time_ts'),
|
||||||
|
[mockProps.channelId, mockAuth.uid, mockProps.limit, mockProps.id],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if unable to get messages', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
channelId: 1,
|
||||||
|
id: 123
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = ['mockResult'] as any;
|
||||||
|
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error});
|
||||||
|
|
||||||
|
expect(getPrivateMessages.getChannelMessagesEndpoint(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Error getting messages');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLastSeenChannelTime', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
map: jest.fn(),
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return the last seen channel time', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
channelIds: [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUnseens = [
|
||||||
|
[1, "mockString"]
|
||||||
|
];
|
||||||
|
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue(mockUnseens);
|
||||||
|
|
||||||
|
const result = await getPrivateMessages.getLastSeenChannelTime(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockUnseens);
|
||||||
|
expect(mockPg.map).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.map).toBeCalledWith(
|
||||||
|
expect.stringContaining('select distinct on (channel_id) channel_id, created_time'),
|
||||||
|
[mockProps.channelIds, mockAuth.uid],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setChannelLastSeenTime', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn(),
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should set channel last seen time', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
channelId: 1
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await getPrivateMessages.setChannelLastSeenTime(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('insert into private_user_seen_message_channels (user_id, channel_id)'),
|
||||||
|
[mockAuth.uid, mockProps.channelId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
53
backend/api/tests/unit/get-profile-answers.unit.test.ts
Normal file
53
backend/api/tests/unit/get-profile-answers.unit.test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { getProfileAnswers } from "api/get-profile-answers";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('getProfileAnswers', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
manyOrNone: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should get the answers for the userId', async () => {
|
||||||
|
const mockProps = { userId: "mockUserId" };
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockAnswers = [
|
||||||
|
{
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
explanation: "mockExplanation",
|
||||||
|
id: 123,
|
||||||
|
importance: 10,
|
||||||
|
multiple_choice: 1234,
|
||||||
|
pref_choices: [1, 2, 3],
|
||||||
|
question_id: 12345
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
(mockPg.manyOrNone as jest.Mock).mockResolvedValue(mockAnswers);
|
||||||
|
|
||||||
|
const result: any = await getProfileAnswers(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(result.answers).toBe(mockAnswers);
|
||||||
|
expect(mockPg.manyOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.manyOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select * from compatibility_answers'),
|
||||||
|
[mockProps.userId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as profilesModule from "api/get-profiles";
|
import * as profilesModule from "api/get-profiles";
|
||||||
import { Profile } from "common/profiles/profile";
|
import { Profile } from "common/profiles/profile";
|
||||||
import * as supabaseInit from "shared/supabase/init";
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as sqlBuilder from "shared/supabase/sql-builder";
|
||||||
|
|
||||||
describe('getProfiles', () => {
|
describe('getProfiles', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -11,8 +12,8 @@ describe('getProfiles', () => {
|
|||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should fetch the user profiles', () => {
|
describe('when given valid input', () => {
|
||||||
it('successfully', async ()=> {
|
it('should successfully return profile information and count', async ()=> {
|
||||||
const mockProfiles = [
|
const mockProfiles = [
|
||||||
{
|
{
|
||||||
diet: ['Jonathon Hammon'],
|
diet: ['Jonathon Hammon'],
|
||||||
@@ -27,19 +28,15 @@ describe('getProfiles', () => {
|
|||||||
has_kids: 2,
|
has_kids: 2,
|
||||||
}
|
}
|
||||||
] as Profile [];
|
] as Profile [];
|
||||||
|
|
||||||
jest.spyOn(profilesModule, 'loadProfiles').mockResolvedValue({profiles: mockProfiles, count: 3});
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
limit: 2,
|
limit: 2,
|
||||||
orderBy: "last_online_time" as const,
|
orderBy: "last_online_time" as const,
|
||||||
};
|
};
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const results = await profilesModule.getProfiles(props, mockReq, mockReq);
|
|
||||||
|
|
||||||
if('continue' in results) {
|
jest.spyOn(profilesModule, 'loadProfiles').mockResolvedValue({profiles: mockProfiles, count: 3});
|
||||||
throw new Error('Expected direct response')
|
|
||||||
};
|
const results: any = await profilesModule.getProfiles(props, mockReq, mockReq);
|
||||||
|
|
||||||
expect(results.status).toEqual('success');
|
expect(results.status).toEqual('success');
|
||||||
expect(results.profiles).toEqual(mockProfiles);
|
expect(results.profiles).toEqual(mockProfiles);
|
||||||
@@ -47,8 +44,10 @@ describe('getProfiles', () => {
|
|||||||
expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props);
|
expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props);
|
||||||
expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1);
|
expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('unsuccessfully', async () => {
|
describe('when an error occurs', () => {
|
||||||
|
it('should not return profile information', async () => {
|
||||||
jest.spyOn(profilesModule, 'loadProfiles').mockRejectedValue(null);
|
jest.spyOn(profilesModule, 'loadProfiles').mockRejectedValue(null);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -56,278 +55,274 @@ describe('getProfiles', () => {
|
|||||||
orderBy: "last_online_time" as const,
|
orderBy: "last_online_time" as const,
|
||||||
};
|
};
|
||||||
const mockReq = {} as any;
|
const mockReq = {} as any;
|
||||||
const results = await profilesModule.getProfiles(props, mockReq, mockReq);
|
const results: any = await profilesModule.getProfiles(props, mockReq, mockReq);
|
||||||
|
|
||||||
if('continue' in results) {
|
|
||||||
throw new Error('Expected direct response')
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(results.status).toEqual('fail');
|
expect(results.status).toEqual('fail');
|
||||||
expect(results.profiles).toEqual([]);
|
expect(results.profiles).toEqual([]);
|
||||||
expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props);
|
expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props);
|
||||||
expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1);
|
expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadProfiles', () => {
|
describe('loadProfiles', () => {
|
||||||
let mockPg: any;
|
let mockPg: any;
|
||||||
|
beforeEach(() => {
|
||||||
describe('should call pg.map with an SQL query', () => {
|
jest.clearAllMocks();
|
||||||
beforeEach(() => {
|
mockPg = {
|
||||||
jest.clearAllMocks();
|
map: jest.fn(),
|
||||||
mockPg = {
|
one: jest.fn()
|
||||||
map: jest.fn().mockResolvedValue([]),
|
};
|
||||||
one: jest.fn().mockResolvedValue(1),
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.spyOn(supabaseInit, 'createSupabaseDirectClient')
|
|
||||||
.mockReturnValue(mockPg);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully', async () => {
|
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
limit: 10,
|
|
||||||
name: 'John',
|
|
||||||
is_smoker: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
|
||||||
|
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
|
||||||
expect(query).toContain('select');
|
|
||||||
expect(query).toContain('from profiles');
|
|
||||||
expect(query).toContain('where');
|
|
||||||
expect(query).toContain('limit 10');
|
|
||||||
expect(query).toContain(`John`);
|
|
||||||
expect(query).toContain(`is_smoker`);
|
|
||||||
expect(query).not.toContain(`gender`);
|
|
||||||
expect(query).not.toContain(`education_level`);
|
|
||||||
expect(query).not.toContain(`pref_gender`);
|
|
||||||
expect(query).not.toContain(`age`);
|
|
||||||
expect(query).not.toContain(`drinks_per_month`);
|
|
||||||
expect(query).not.toContain(`pref_relation_styles`);
|
|
||||||
expect(query).not.toContain(`pref_romantic_styles`);
|
|
||||||
expect(query).not.toContain(`diet`);
|
|
||||||
expect(query).not.toContain(`political_beliefs`);
|
|
||||||
expect(query).not.toContain(`religion`);
|
|
||||||
expect(query).not.toContain(`has_kids`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a gender filter', async () => {
|
jest.spyOn(supabaseInit, 'createSupabaseDirectClient')
|
||||||
await profilesModule.loadProfiles({
|
.mockReturnValue(mockPg);
|
||||||
genders: ['Electrical_gender'],
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
describe('should call pg.map with an SQL query', () => {
|
||||||
|
it('successfully', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
name: 'John',
|
||||||
|
is_smoker: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue([]);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(1);
|
||||||
|
jest.spyOn(sqlBuilder, 'renderSql');
|
||||||
|
jest.spyOn(sqlBuilder, 'select');
|
||||||
|
jest.spyOn(sqlBuilder, 'from');
|
||||||
|
jest.spyOn(sqlBuilder, 'where');
|
||||||
|
jest.spyOn(sqlBuilder, 'join');
|
||||||
|
|
||||||
|
await profilesModule.loadProfiles(mockProps);
|
||||||
|
|
||||||
|
const [query, values, cb] = mockPg.map.mock.calls[0];
|
||||||
|
|
||||||
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain('select');
|
||||||
|
expect(query).toContain('from profiles');
|
||||||
|
expect(query).toContain('where');
|
||||||
|
expect(query).toContain('limit 10');
|
||||||
|
expect(query).toContain(`John`);
|
||||||
|
expect(query).toContain(`is_smoker`);
|
||||||
|
expect(query).not.toContain(`gender`);
|
||||||
|
expect(query).not.toContain(`education_level`);
|
||||||
|
expect(query).not.toContain(`pref_gender`);
|
||||||
|
expect(query).not.toContain(`age`);
|
||||||
|
expect(query).not.toContain(`drinks_per_month`);
|
||||||
|
expect(query).not.toContain(`pref_relation_styles`);
|
||||||
|
expect(query).not.toContain(`pref_romantic_styles`);
|
||||||
|
expect(query).not.toContain(`diet`);
|
||||||
|
expect(query).not.toContain(`political_beliefs`);
|
||||||
|
expect(query).not.toContain(`religion`);
|
||||||
|
expect(query).not.toContain(`has_kids`);
|
||||||
|
expect(sqlBuilder.renderSql).toBeCalledTimes(3);
|
||||||
|
expect(sqlBuilder.select).toBeCalledTimes(3);
|
||||||
|
expect(sqlBuilder.from).toBeCalledTimes(2);
|
||||||
|
expect(sqlBuilder.where).toBeCalledTimes(8);
|
||||||
|
expect(sqlBuilder.join).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('that contains a gender filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
|
genders: ['Electrical_gender'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
|
|
||||||
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`gender`);
|
||||||
|
expect(query).toContain(`Electrical_gender`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('that contains a education level filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
|
education_levels: ['High School'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
|
|
||||||
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`education_level`);
|
||||||
|
expect(query).toContain(`High School`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a prefer gender filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
pref_gender: ['female'],
|
||||||
expect(query).toContain(`gender`);
|
});
|
||||||
expect(query).toContain(`Electrical_gender`);
|
|
||||||
});
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
|
console.log(query);
|
||||||
it('that contains a education level filter', async () => {
|
|
||||||
await profilesModule.loadProfiles({
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
education_levels: ['High School'],
|
expect(query).toContain(`pref_gender`);
|
||||||
|
expect(query).toContain(`female`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a minimum age filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
pref_age_min: 20,
|
||||||
expect(query).toContain(`education_level`);
|
});
|
||||||
expect(query).toContain(`High School`);
|
|
||||||
});
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
|
|
||||||
it('that contains a prefer gender filter', async () => {
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
await profilesModule.loadProfiles({
|
expect(query).toContain(`age`);
|
||||||
pref_gender: ['female'],
|
expect(query).toContain(`>= 20`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a maximum age filter', async () => {
|
||||||
console.log(query);
|
await profilesModule.loadProfiles({
|
||||||
|
pref_age_max: 40,
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
});
|
||||||
expect(query).toContain(`pref_gender`);
|
|
||||||
expect(query).toContain(`female`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a minimum age filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
pref_age_min: 20,
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`age`);
|
||||||
|
expect(query).toContain(`<= 40`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a minimum drinks per month filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
drinks_min: 4,
|
||||||
expect(query).toContain(`age`);
|
});
|
||||||
expect(query).toContain(`>= 20`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a maximum age filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
pref_age_max: 40,
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`drinks_per_month`);
|
||||||
|
expect(query).toContain('4');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a maximum drinks per month filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
drinks_max: 20,
|
||||||
expect(query).toContain(`age`);
|
});
|
||||||
expect(query).toContain(`<= 40`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a minimum drinks per month filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
drinks_min: 4,
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`drinks_per_month`);
|
||||||
|
expect(query).toContain('20');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a relationship style filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
pref_relation_styles: ['Chill and relaxing'],
|
||||||
expect(query).toContain(`drinks_per_month`);
|
});
|
||||||
expect(query).toContain('4');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a maximum drinks per month filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
drinks_max: 20,
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`pref_relation_styles`);
|
||||||
|
expect(query).toContain('Chill and relaxing');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a romantic style filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
pref_romantic_styles: ['Sexy'],
|
||||||
expect(query).toContain(`drinks_per_month`);
|
});
|
||||||
expect(query).toContain('20');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a relationship style filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
pref_relation_styles: ['Chill and relaxing'],
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`pref_romantic_styles`);
|
||||||
|
expect(query).toContain('Sexy');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a diet filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
diet: ['Glutton'],
|
||||||
expect(query).toContain(`pref_relation_styles`);
|
});
|
||||||
expect(query).toContain('Chill and relaxing');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a romantic style filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
pref_romantic_styles: ['Sexy'],
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`diet`);
|
||||||
|
expect(query).toContain('Glutton');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a political beliefs filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
political_beliefs: ['For the people'],
|
||||||
expect(query).toContain(`pref_romantic_styles`);
|
});
|
||||||
expect(query).toContain('Sexy');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a diet filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
diet: ['Glutton'],
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`political_beliefs`);
|
||||||
|
expect(query).toContain('For the people');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a religion filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
religion: ['The blood god'],
|
||||||
expect(query).toContain(`diet`);
|
});
|
||||||
expect(query).toContain('Glutton');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a political beliefs filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
political_beliefs: ['For the people'],
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`religion`);
|
||||||
|
expect(query).toContain('The blood god');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('that contains a has kids filter', async () => {
|
||||||
|
await profilesModule.loadProfiles({
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
has_kids: 3,
|
||||||
expect(query).toContain(`political_beliefs`);
|
});
|
||||||
expect(query).toContain('For the people');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('that contains a religion filter', async () => {
|
const [query, values, cb] = mockPg.map.mock.calls[0]
|
||||||
await profilesModule.loadProfiles({
|
|
||||||
religion: ['The blood god'],
|
expect(mockPg.map.mock.calls).toHaveLength(1)
|
||||||
|
expect(query).toContain(`has_kids`);
|
||||||
|
expect(query).toContain('> 0');
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
it('should return profiles from the database', async () => {
|
||||||
|
const mockProfiles = [
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
{
|
||||||
expect(query).toContain(`religion`);
|
diet: ['Jonathon Hammon'],
|
||||||
expect(query).toContain('The blood god');
|
is_smoker: true,
|
||||||
});
|
has_kids: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
diet: ['Joseph Hammon'],
|
||||||
|
is_smoker: false,
|
||||||
|
has_kids: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
diet: ['Jolene Hammon'],
|
||||||
|
is_smoker: true,
|
||||||
|
has_kids: 2,
|
||||||
|
}
|
||||||
|
] as Profile [];
|
||||||
|
const props = {} as any;
|
||||||
|
|
||||||
it('that contains a has kids filter', async () => {
|
(mockPg.map as jest.Mock).mockResolvedValue(mockProfiles);
|
||||||
await profilesModule.loadProfiles({
|
(mockPg.one as jest.Mock).mockResolvedValue(1);
|
||||||
has_kids: 3,
|
|
||||||
|
const results = await profilesModule.loadProfiles(props);
|
||||||
|
|
||||||
|
expect(results).toEqual({profiles: mockProfiles, count: 1});
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, values, cb] = mockPg.map.mock.calls[0]
|
|
||||||
|
|
||||||
expect(mockPg.map.mock.calls).toHaveLength(1)
|
|
||||||
expect(query).toContain(`has_kids`);
|
|
||||||
expect(query).toContain('> 0');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should', () => {
|
describe('when an error occurs', () => {
|
||||||
beforeEach(() => {
|
it('throw if there is no compatability', async () => {
|
||||||
jest.clearAllMocks();
|
|
||||||
mockPg = {
|
|
||||||
map: jest.fn(),
|
|
||||||
one: jest.fn().mockResolvedValue(1),
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.spyOn(supabaseInit, 'createSupabaseDirectClient')
|
|
||||||
.mockReturnValue(mockPg)
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('return profiles from the database', async () => {
|
|
||||||
const mockProfiles = [
|
|
||||||
{
|
|
||||||
diet: ['Jonathon Hammon'],
|
|
||||||
is_smoker: true,
|
|
||||||
has_kids: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
diet: ['Joseph Hammon'],
|
|
||||||
is_smoker: false,
|
|
||||||
has_kids: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
diet: ['Jolene Hammon'],
|
|
||||||
is_smoker: true,
|
|
||||||
has_kids: 2,
|
|
||||||
}
|
|
||||||
] as Profile [];
|
|
||||||
|
|
||||||
mockPg.map.mockResolvedValue(mockProfiles);
|
|
||||||
const props = {} as any;
|
|
||||||
const results = await profilesModule.loadProfiles(props);
|
|
||||||
|
|
||||||
expect(results).toEqual({profiles: mockProfiles, count: 1});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throw an error if there is no compatability', async () => {
|
|
||||||
const props = {
|
const props = {
|
||||||
orderBy: 'compatibility_score'
|
orderBy: 'compatibility_score'
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(profilesModule.loadProfiles(props))
|
expect(profilesModule.loadProfiles(props))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Incompatible with user ID')
|
.toThrowError('Incompatible with user ID')
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
@@ -1,163 +1,90 @@
|
|||||||
jest.mock("shared/supabase/init");
|
jest.mock("shared/supabase/init");
|
||||||
|
jest.mock("common/supabase/users");
|
||||||
|
jest.mock("common/api/user-types");
|
||||||
|
|
||||||
import { getUser } from "api/get-user";
|
import { getUser } from "api/get-user";
|
||||||
import { createSupabaseDirectClient } from "shared/supabase/init";
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
import { toUserAPIResponse } from "common/api/user-types";
|
import { toUserAPIResponse } from "common/api/user-types";
|
||||||
import { convertUser } from "common/supabase/users";
|
|
||||||
import { APIError } from "common/api/utils";
|
|
||||||
|
|
||||||
|
|
||||||
jest.spyOn(require("common/supabase/users"), 'convertUser')
|
|
||||||
jest.spyOn(require("common/api/user-types"), 'toUserAPIResponse')
|
|
||||||
|
|
||||||
describe('getUser', () =>{
|
describe('getUser', () =>{
|
||||||
let mockPg: any;
|
let mockPg: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
mockPg = {
|
mockPg = {
|
||||||
oneOrNone: jest.fn(),
|
oneOrNone: jest.fn(),
|
||||||
};
|
};
|
||||||
(createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg);
|
|
||||||
|
|
||||||
jest.clearAllMocks();
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when fetching by id', () => {
|
describe('when given valid input', () => {
|
||||||
it('should fetch user successfully by id', async () => {
|
describe('and fetching by id', () => {
|
||||||
const mockDbUser = {
|
it('should fetch user successfully by id', async () => {
|
||||||
created_time: '2025-11-11T16:42:05.188Z',
|
const mockProps = {id: "mockId"};
|
||||||
data: { link: {}, avatarUrl: "", isBannedFromPosting: false },
|
const mockUser = {} as any;
|
||||||
id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP',
|
|
||||||
name: 'Franklin Buckridge',
|
|
||||||
name_username_vector: "'buckridg':2,4 'franklin':1,3",
|
|
||||||
username: 'Franky_Buck'
|
|
||||||
};
|
|
||||||
const mockConvertedUser = {
|
|
||||||
created_time: new Date(),
|
|
||||||
id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP',
|
|
||||||
name: 'Franklin Buckridge',
|
|
||||||
name_username_vector: "'buckridg':2,4 'franklin':1,3",
|
|
||||||
username: 'Franky_Buck'
|
|
||||||
|
|
||||||
};
|
|
||||||
const mockApiResponse = {
|
|
||||||
created_time: '2025-11-11T16:42:05.188Z',
|
|
||||||
data: { link: {}, avatarUrl: "", isBannedFromPosting: false },
|
|
||||||
id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP',
|
|
||||||
name: 'Franklin Buckridge',
|
|
||||||
username: 'Franky_Buck'
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => {
|
|
||||||
const result = cb(mockDbUser);
|
|
||||||
return Promise.resolve(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
(convertUser as jest.Mock).mockReturnValue(mockConvertedUser);
|
|
||||||
( toUserAPIResponse as jest.Mock).mockReturnValue(mockApiResponse);
|
|
||||||
|
|
||||||
const result = await getUser({id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP'})
|
|
||||||
|
|
||||||
expect(mockPg.oneOrNone).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('where id = $1'),
|
|
||||||
['feUaIfcxVmJZHJOVVfawLTTPgZiP'],
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(convertUser).toHaveBeenCalledWith(mockDbUser);
|
|
||||||
expect(toUserAPIResponse).toHaveBeenCalledWith(mockConvertedUser);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockApiResponse);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw 404 when user is not found by id', async () => {
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser);
|
||||||
mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => {
|
(toUserAPIResponse as jest.Mock).mockReturnValue('mockApiResponse');
|
||||||
return Promise.resolve(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
(convertUser as jest.Mock).mockReturnValue(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await getUser({id: '3333'});
|
|
||||||
fail('Should have thrown');
|
|
||||||
} catch (error) {
|
|
||||||
const apiError = error as APIError;
|
|
||||||
expect(apiError.code).toBe(404)
|
|
||||||
expect(apiError.message).toBe('User not found')
|
|
||||||
expect(apiError.details).toBeUndefined()
|
|
||||||
expect(apiError.name).toBe('APIError')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
const result = await getUser(mockProps);
|
||||||
|
|
||||||
describe('when fetching by username', () => {
|
expect(result).toBe('mockApiResponse');
|
||||||
it('should fetch user successfully by username', async () => {
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
const mockDbUser = {
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
created_time: '2025-11-11T16:42:05.188Z',
|
expect.stringContaining('select * from users'),
|
||||||
data: { link: {}, avatarUrl: "", isBannedFromPosting: false },
|
[mockProps.id],
|
||||||
id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP',
|
expect.any(Function)
|
||||||
name: 'Franklin Buckridge',
|
);
|
||||||
name_username_vector: "'buckridg':2,4 'franklin':1,3",
|
expect(toUserAPIResponse).toBeCalledTimes(1);
|
||||||
username: 'Franky_Buck'
|
expect(toUserAPIResponse).toBeCalledWith(mockUser);
|
||||||
};
|
|
||||||
const mockConvertedUser = {
|
|
||||||
created_time: new Date(),
|
|
||||||
id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP',
|
|
||||||
name: 'Franklin Buckridge',
|
|
||||||
name_username_vector: "'buckridg':2,4 'franklin':1,3",
|
|
||||||
username: 'Franky_Buck'
|
|
||||||
|
|
||||||
};
|
|
||||||
const mockApiResponse = {
|
|
||||||
created_time: '2025-11-11T16:42:05.188Z',
|
|
||||||
data: { link: {}, avatarUrl: "", isBannedFromPosting: false },
|
|
||||||
id: 'feUaIfcxVmJZHJOVVfawLTTPgZiP',
|
|
||||||
name: 'Franklin Buckridge',
|
|
||||||
username: 'Franky_Buck'
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => {
|
|
||||||
const result = cb(mockDbUser);
|
|
||||||
return Promise.resolve(result);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
(convertUser as jest.Mock).mockReturnValue(mockConvertedUser);
|
|
||||||
(toUserAPIResponse as jest.Mock).mockReturnValue(mockApiResponse);
|
|
||||||
|
|
||||||
const result = await getUser({username: 'Franky_Buck'})
|
|
||||||
|
|
||||||
expect(mockPg.oneOrNone).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('where username = $1'),
|
|
||||||
['Franky_Buck'],
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(convertUser).toHaveBeenCalledWith(mockDbUser);
|
|
||||||
expect(toUserAPIResponse).toHaveBeenCalledWith(mockConvertedUser);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockApiResponse);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw 404 when user is not found by id', async () => {
|
describe('when fetching by username', () => {
|
||||||
mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => {
|
it('should fetch user successfully by username', async () => {
|
||||||
return Promise.resolve(null);
|
const mockProps = {username: "mockUsername"};
|
||||||
});
|
const mockUser = {} as any;
|
||||||
|
|
||||||
(convertUser as jest.Mock).mockReturnValue(null)
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
|
||||||
try {
|
await getUser(mockProps)
|
||||||
await getUser({username: '3333'});
|
|
||||||
fail('Should have thrown');
|
expect(mockPg.oneOrNone).toHaveBeenCalledWith(
|
||||||
} catch (error) {
|
expect.stringContaining('where username = $1'),
|
||||||
const apiError = error as APIError;
|
[mockProps.username],
|
||||||
expect(apiError.code).toBe(404)
|
expect.any(Function)
|
||||||
expect(apiError.message).toBe('User not found')
|
);
|
||||||
expect(apiError.details).toBeUndefined()
|
});
|
||||||
expect(apiError.name).toBe('APIError')
|
});
|
||||||
}
|
});
|
||||||
})
|
|
||||||
})
|
describe('when an error occurs', () => {
|
||||||
})
|
describe('and fetching by id', () => {
|
||||||
|
it('should throw when user is not found by id', async () => {
|
||||||
|
const mockProps = {id: "mockId"};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(getUser(mockProps))
|
||||||
|
.rejects
|
||||||
|
.toThrow('User not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when fetching by username', () => {
|
||||||
|
it('should throw when user is not found by id', async () => {
|
||||||
|
const mockProps = {username: "mockUsername"};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(getUser(mockProps))
|
||||||
|
.rejects
|
||||||
|
.toThrow('User not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
57
backend/api/tests/unit/has-free-like.unit.test.ts
Normal file
57
backend/api/tests/unit/has-free-like.unit.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import * as freeLikeModule from "api/has-free-like";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('hasFreeLike', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return if the user has a free like', async () => {
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockProps = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn( freeLikeModule, 'getHasFreeLike');
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result: any = await freeLikeModule.hasFreeLike(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(result.hasFreeLike).toBeTruthy();
|
||||||
|
expect(freeLikeModule.getHasFreeLike).toBeCalledTimes(1);
|
||||||
|
expect(freeLikeModule.getHasFreeLike).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('from profile_likes'),
|
||||||
|
[mockAuth.uid]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if the user does not have a free like', async () => {
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockProps = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn( freeLikeModule, 'getHasFreeLike');
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result: any = await freeLikeModule.hasFreeLike(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.hasFreeLike).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
16
backend/api/tests/unit/health-unit.test.ts
Normal file
16
backend/api/tests/unit/health-unit.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { health } from "api/health";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('health', () => {
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return the servers status(Health)', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
const result: any = await health(mockProps, mockAuth, mockReq);
|
||||||
|
expect(result.message).toBe('Server is working.');
|
||||||
|
expect(result.uid).toBe(mockAuth.uid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
165
backend/api/tests/unit/hide-comment.unit.test.ts
Normal file
165
backend/api/tests/unit/hide-comment.unit.test.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/supabase/comment');
|
||||||
|
jest.mock('shared/websockets/helpers');
|
||||||
|
|
||||||
|
import { hideComment } from "api/hide-comment";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as envConsts from "common/envs/constants";
|
||||||
|
import { convertComment } from "common/supabase/comment";
|
||||||
|
import * as websocketHelpers from "shared/websockets/helpers";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('hideComment', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should successfully hide the comment if the user is an admin', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
commentId: "mockCommentId",
|
||||||
|
hide: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockComment = {
|
||||||
|
content: { "mockContent": "mockContentValue" },
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
hidden: false,
|
||||||
|
id: 123,
|
||||||
|
on_user_id: "4321",
|
||||||
|
reply_to_comment_id: null,
|
||||||
|
user_avatar_url: "mockAvatarUrl",
|
||||||
|
user_id: "4321",
|
||||||
|
user_name: "mockUserName",
|
||||||
|
user_username: "mockUserUsername",
|
||||||
|
};
|
||||||
|
const mockConvertedComment = "mockConvertedCommentValue";
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment);
|
||||||
|
jest.spyOn(envConsts, 'isAdminId').mockReturnValue(true);
|
||||||
|
(convertComment as jest.Mock).mockReturnValue(mockConvertedComment);
|
||||||
|
|
||||||
|
await hideComment(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select * from profile_comments where id = $1'),
|
||||||
|
[mockProps.commentId]
|
||||||
|
);
|
||||||
|
expect(envConsts.isAdminId).toBeCalledTimes(1);
|
||||||
|
expect(envConsts.isAdminId).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(convertComment).toBeCalledTimes(1);
|
||||||
|
expect(convertComment).toBeCalledWith(mockComment);
|
||||||
|
expect(websocketHelpers.broadcastUpdatedComment).toBeCalledTimes(1);
|
||||||
|
expect(websocketHelpers.broadcastUpdatedComment).toBeCalledWith(mockConvertedComment);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully hide the comment if the user is the one who made the comment', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
commentId: "mockCommentId",
|
||||||
|
hide: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockComment = {
|
||||||
|
content: { "mockContent": "mockContentValue" },
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
hidden: false,
|
||||||
|
id: 123,
|
||||||
|
on_user_id: "4321",
|
||||||
|
reply_to_comment_id: null,
|
||||||
|
user_avatar_url: "mockAvatarUrl",
|
||||||
|
user_id: "321",
|
||||||
|
user_name: "mockUserName",
|
||||||
|
user_username: "mockUserUsername",
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment);
|
||||||
|
jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false);
|
||||||
|
|
||||||
|
await hideComment(mockProps, mockAuth, mockReq);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully hide the comment if the user is the one who is being commented on', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
commentId: "mockCommentId",
|
||||||
|
hide: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockComment = {
|
||||||
|
content: { "mockContent": "mockContentValue" },
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
hidden: false,
|
||||||
|
id: 123,
|
||||||
|
on_user_id: "321",
|
||||||
|
reply_to_comment_id: null,
|
||||||
|
user_avatar_url: "mockAvatarUrl",
|
||||||
|
user_id: "4321",
|
||||||
|
user_name: "mockUserName",
|
||||||
|
user_username: "mockUserUsername",
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment);
|
||||||
|
jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false);
|
||||||
|
|
||||||
|
await hideComment(mockProps, mockAuth, mockReq);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the comment was not found', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
commentId: "mockCommentId",
|
||||||
|
hide: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(hideComment(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Comment not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the user is not an admin, the comments author or the one being commented on', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
commentId: "mockCommentId",
|
||||||
|
hide: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockComment = {
|
||||||
|
content: { "mockContent": "mockContentValue" },
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
hidden: false,
|
||||||
|
id: 123,
|
||||||
|
on_user_id: "4321",
|
||||||
|
reply_to_comment_id: null,
|
||||||
|
user_avatar_url: "mockAvatarUrl",
|
||||||
|
user_id: "4321",
|
||||||
|
user_name: "mockUserName",
|
||||||
|
user_username: "mockUserUsername",
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment);
|
||||||
|
jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(hideComment(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('You are not allowed to hide this comment');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/utils');
|
||||||
|
jest.mock('api/helpers/private-messages');
|
||||||
|
|
||||||
|
import { leavePrivateUserMessageChannel } from "api/leave-private-user-message-channel";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as sharedUtils from "shared/utils";
|
||||||
|
import * as messageHelpers from "api/helpers/private-messages";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('leavePrivateUserMessageChannel', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should leave a private message channel', async () => {
|
||||||
|
const mockProps = { channelId: 123 };
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUser = { name: "mockName" };
|
||||||
|
const mockLeaveChatContent = "mockLeaveChatContentValue";
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(true);
|
||||||
|
(messageHelpers.leaveChatContent as jest.Mock).mockReturnValue(mockLeaveChatContent);
|
||||||
|
|
||||||
|
const results = await leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.status).toBe('success');
|
||||||
|
expect(results.channelId).toBe(mockProps.channelId);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select status from private_user_message_channel_members'),
|
||||||
|
[mockProps.channelId, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('update private_user_message_channel_members'),
|
||||||
|
[mockProps.channelId, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(messageHelpers.leaveChatContent).toBeCalledTimes(1);
|
||||||
|
expect(messageHelpers.leaveChatContent).toBeCalledWith(mockUser.name);
|
||||||
|
expect(messageHelpers.insertPrivateMessage).toBeCalledTimes(1);
|
||||||
|
expect(messageHelpers.insertPrivateMessage).toBeCalledWith(
|
||||||
|
mockLeaveChatContent,
|
||||||
|
mockProps.channelId,
|
||||||
|
mockAuth.uid,
|
||||||
|
'system_status',
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the account was not found', async () => {
|
||||||
|
const mockProps = { channelId: 123 };
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Your account was not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if you are not a member', async () => {
|
||||||
|
const mockProps = { channelId: 123 };
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUser = { name: "mockName" };
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
|
||||||
|
expect(leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('You are not authorized to post to this channel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
187
backend/api/tests/unit/like-profile.unit.test.ts
Normal file
187
backend/api/tests/unit/like-profile.unit.test.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/create-profile-notification');
|
||||||
|
jest.mock('api/has-free-like');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
|
||||||
|
import { likeProfile } from "api/like-profile";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as profileNotifiction from "shared/create-profile-notification";
|
||||||
|
import * as likeModules from "api/has-free-like";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('likeProfile', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
one: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should like the selected profile', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
likeId: "mockLikeId",
|
||||||
|
target_id: "mockTargetId"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: false})
|
||||||
|
.mockResolvedValueOnce({data: mockData, error: null});
|
||||||
|
(likeModules.getHasFreeLike as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result: any = await likeProfile(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.result.status).toBe('success');
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select * from profile_likes where creator_id = $1 and target_id = $2'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId]
|
||||||
|
);
|
||||||
|
expect(tryCatch).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.one).toBeCalledWith(
|
||||||
|
expect.stringContaining('insert into profile_likes (creator_id, target_id) values ($1, $2) returning *'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId]
|
||||||
|
);
|
||||||
|
|
||||||
|
(profileNotifiction.createProfileLikeNotification as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await result.continue();
|
||||||
|
|
||||||
|
expect(profileNotifiction.createProfileLikeNotification).toBeCalledTimes(1);
|
||||||
|
expect(profileNotifiction.createProfileLikeNotification).toBeCalledWith(mockData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if there is already a like', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: true});
|
||||||
|
|
||||||
|
const result: any = await likeProfile(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a like', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
likeId: "mockLikeId",
|
||||||
|
target_id: "mockTargetId"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null});
|
||||||
|
|
||||||
|
const result: any = await likeProfile(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('delete from profile_likes where creator_id = $1 and target_id = $2'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if failed to remove like', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
likeId: "mockLikeId",
|
||||||
|
target_id: "mockTargetId"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: mockData, error: Error});
|
||||||
|
|
||||||
|
expect(likeProfile(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to remove like: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if user has already used their free like', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
likeId: "mockLikeId",
|
||||||
|
target_id: "mockTargetId"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: false})
|
||||||
|
.mockResolvedValueOnce({data: mockData, error: null});
|
||||||
|
(likeModules.getHasFreeLike as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
|
||||||
|
expect(likeProfile(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('You already liked someone today!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if failed to add like', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
likeId: "mockLikeId",
|
||||||
|
target_id: "mockTargetId"
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: false})
|
||||||
|
.mockResolvedValueOnce({data: mockData, error: Error});
|
||||||
|
(likeModules.getHasFreeLike as jest.Mock).mockResolvedValue(true);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(likeProfile(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to add like: ');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { markAllNotifsRead } from "api/mark-all-notifications-read";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('markAllNotifsRead', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should mark all notifications as read', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
await markAllNotifsRead(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('update user_notifications'),
|
||||||
|
[mockAuth.uid]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
139
backend/api/tests/unit/react-to-message.unit.test.ts
Normal file
139
backend/api/tests/unit/react-to-message.unit.test.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('api/helpers/private-messages');
|
||||||
|
|
||||||
|
import { reactToMessage } from "api/react-to-message";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as messageHelpers from "api/helpers/private-messages";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('reactToMessage', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return success', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
messageId: 123,
|
||||||
|
reaction: "mockReaction",
|
||||||
|
toDelete: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockMessage = { channel_id: "mockChannelId"};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await reactToMessage(mockProps, mockAuth, mockReq);
|
||||||
|
const [sql, params] = mockPg.oneOrNone.mock.calls[0]
|
||||||
|
const [sql1, params1] = mockPg.none.mock.calls[0]
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(params).toEqual([mockAuth.uid, mockProps.messageId])
|
||||||
|
expect(sql).toEqual(
|
||||||
|
expect.stringContaining('SELECT *')
|
||||||
|
);
|
||||||
|
expect(sql).toEqual(
|
||||||
|
expect.stringContaining('FROM private_user_message_channel_members m')
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(params1).toEqual([mockProps.reaction, mockAuth.uid, mockProps.messageId])
|
||||||
|
expect(sql1).toEqual(
|
||||||
|
expect.stringContaining('UPDATE private_user_messages')
|
||||||
|
);
|
||||||
|
expect(sql1).toEqual(
|
||||||
|
expect.stringContaining('SET reactions =')
|
||||||
|
);
|
||||||
|
expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1);
|
||||||
|
expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
mockMessage.channel_id,
|
||||||
|
mockAuth.uid
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success when removing a reaction', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
messageId: 123,
|
||||||
|
reaction: "mockReaction",
|
||||||
|
toDelete: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockMessage = { channel_id: "mockChannelId"};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await reactToMessage(mockProps, mockAuth, mockReq);
|
||||||
|
const [sql, params] = mockPg.oneOrNone.mock.calls[0]
|
||||||
|
const [sql1, params1] = mockPg.none.mock.calls[0]
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(params1).toEqual([mockProps.reaction, mockProps.messageId, mockAuth.uid])
|
||||||
|
expect(sql1).toEqual(
|
||||||
|
expect.stringContaining('UPDATE private_user_messages')
|
||||||
|
);
|
||||||
|
expect(sql1).toEqual(
|
||||||
|
expect.stringContaining('SET reactions = reactions - $1')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if user does not have the authorization to react', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
messageId: 123,
|
||||||
|
reaction: "mockReaction",
|
||||||
|
toDelete: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(reactToMessage(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Not authorized to react to this message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
messageId: 123,
|
||||||
|
reaction: "mockReaction",
|
||||||
|
toDelete: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockMessage = { channel_id: "mockChannelId"};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(messageHelpers.broadcastPrivateMessages as jest.Mock).mockRejectedValue(new Error('Broadcast error'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await reactToMessage(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('broadcastPrivateMessages failed'),
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
75
backend/api/tests/unit/remove-pinned-photo.unit.test.ts
Normal file
75
backend/api/tests/unit/remove-pinned-photo.unit.test.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/envs/constants');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
|
||||||
|
import { removePinnedPhoto } from "api/remove-pinned-photo";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as envConstants from "common/envs/constants";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('removePinnedPhoto', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return success', async () => {
|
||||||
|
const mockBody = { userId: "mockUserId"};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(envConstants, 'isAdminId').mockReturnValue(true);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({error: null});
|
||||||
|
|
||||||
|
const result: any = await removePinnedPhoto(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(envConstants.isAdminId).toBeCalledTimes(1);
|
||||||
|
expect(envConstants.isAdminId).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('update profiles set pinned_url = null where user_id = $1'),
|
||||||
|
[mockBody.userId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if user auth is not an admin', async () => {
|
||||||
|
const mockBody = { userId: "mockUserId"};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(envConstants, 'isAdminId').mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(removePinnedPhoto(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Only admins can remove pinned photo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if failed to remove the pinned photo', async () => {
|
||||||
|
const mockBody = { userId: "mockUserId"};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(envConstants, 'isAdminId').mockReturnValue(true);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({error: Error});
|
||||||
|
|
||||||
|
expect(removePinnedPhoto(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to remove pinned photo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
225
backend/api/tests/unit/report.unit.test.ts
Normal file
225
backend/api/tests/unit/report.unit.test.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
jest.mock('shared/supabase/utils');
|
||||||
|
jest.mock('common/discord/core');
|
||||||
|
|
||||||
|
import { report } from "api/report";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import * as supabaseUtils from "shared/supabase/utils";
|
||||||
|
import { sendDiscordMessage } from "common/discord/core";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('report', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should successfully file a report', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
contentOwnerId: "mockContentOwnerId",
|
||||||
|
contentType: "user" as "user" | "comment" | "contract",
|
||||||
|
contentId: "mockContentId",
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockReporter = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername",
|
||||||
|
};
|
||||||
|
const mockReported = {
|
||||||
|
created_time: "mockCreatedTimeReported",
|
||||||
|
data: {"mockDataReported" : "mockDataValueReported"},
|
||||||
|
id: "mockIdReported",
|
||||||
|
name: "mockNameReported",
|
||||||
|
name_username_vector: "mockNameUsernameVectorReported",
|
||||||
|
username: "mockUsernameReported",
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null});
|
||||||
|
|
||||||
|
const result = await report(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(result.result).toStrictEqual({});
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock)
|
||||||
|
.mockReturnValueOnce(null)
|
||||||
|
.mockReturnValueOnce(null);
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: mockReporter, error: null})
|
||||||
|
.mockResolvedValueOnce({data: mockReported, error: null});
|
||||||
|
(sendDiscordMessage as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await result.continue();
|
||||||
|
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.oneOrNone).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining('select * from users where id = $1'),
|
||||||
|
[mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockPg.oneOrNone).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining('select * from users where id = $1'),
|
||||||
|
[mockBody.contentOwnerId]
|
||||||
|
);
|
||||||
|
expect(sendDiscordMessage).toBeCalledTimes(1);
|
||||||
|
expect(sendDiscordMessage).toBeCalledWith(
|
||||||
|
expect.stringContaining('**New Report**'),
|
||||||
|
'reports'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if failed to create the report', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
contentOwnerId: "mockContentOwnerId",
|
||||||
|
contentType: "user" as "user" | "comment" | "contract",
|
||||||
|
contentId: "mockContentId",
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error});
|
||||||
|
|
||||||
|
expect(report(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to create report: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to get information about the user', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
contentOwnerId: "mockContentOwnerId",
|
||||||
|
contentType: "user" as "user" | "comment" | "contract",
|
||||||
|
contentId: "mockContentId",
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null});
|
||||||
|
|
||||||
|
const result = await report(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock)
|
||||||
|
.mockReturnValueOnce(null);
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: null, error: Error});
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await result.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('Failed to get user for report'),
|
||||||
|
expect.objectContaining({name: 'Error'})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to get information about the user being reported', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
contentOwnerId: "mockContentOwnerId",
|
||||||
|
contentType: "user" as "user" | "comment" | "contract",
|
||||||
|
contentId: "mockContentId",
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockReporter = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername",
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null});
|
||||||
|
|
||||||
|
const result = await report(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock)
|
||||||
|
.mockReturnValueOnce(null)
|
||||||
|
.mockReturnValueOnce(null);
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: mockReporter, error: null})
|
||||||
|
.mockResolvedValueOnce({data: null, error: Error});
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await result.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('Failed to get reported user for report'),
|
||||||
|
expect.objectContaining({name: 'Error'})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if failed to send discord report', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
contentOwnerId: "mockContentOwnerId",
|
||||||
|
contentType: "user" as "user" | "comment" | "contract",
|
||||||
|
contentId: "mockContentId",
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockReporter = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername",
|
||||||
|
};
|
||||||
|
const mockReported = {
|
||||||
|
created_time: "mockCreatedTimeReported",
|
||||||
|
data: {"mockDataReported" : "mockDataValueReported"},
|
||||||
|
id: "mockIdReported",
|
||||||
|
name: "mockNameReported",
|
||||||
|
name_username_vector: "mockNameUsernameVectorReported",
|
||||||
|
username: "mockUsernameReported",
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null});
|
||||||
|
|
||||||
|
const result = await report(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock)
|
||||||
|
.mockReturnValueOnce(null)
|
||||||
|
.mockReturnValueOnce(null);
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: mockReporter, error: null})
|
||||||
|
.mockResolvedValueOnce({data: mockReported, error: null});
|
||||||
|
(sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Discord error'));
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
await result.continue();
|
||||||
|
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining('Failed to send discord reports'),
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
70
backend/api/tests/unit/save-subscription-mobile.unit.test.ts
Normal file
70
backend/api/tests/unit/save-subscription-mobile.unit.test.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import { saveSubscriptionMobile } from "api/save-subscription-mobile";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('saveSubscriptionMobile', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return success after saving the subscription', async () => {
|
||||||
|
const mockBody = { token: "mockToken" };
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await saveSubscriptionMobile(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('insert into push_subscriptions_mobile(token, platform, user_id)'),
|
||||||
|
[mockBody.token, 'android', mockAuth.uid]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if token is invalid', async () => {
|
||||||
|
const mockBody = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
expect(saveSubscriptionMobile(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Invalid subscription object');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to save subscription', async () => {
|
||||||
|
const mockBody = { token: "mockToken" };
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.none as jest.Mock).mockRejectedValue(new Error('Saving error'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
expect(saveSubscriptionMobile(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to save subscription');
|
||||||
|
// expect(errorSpy).toBeCalledTimes(1);
|
||||||
|
// expect(errorSpy).toBeCalledWith(
|
||||||
|
// expect.stringContaining('Error saving subscription'),
|
||||||
|
// expect.any(Error)
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
118
backend/api/tests/unit/save-subscription.unit.test.ts
Normal file
118
backend/api/tests/unit/save-subscription.unit.test.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import { saveSubscription } from "api/save-subscription";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('saveSubscription', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should save user subscription', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
subscription: {
|
||||||
|
endpoint: "mockEndpoint",
|
||||||
|
keys: "mockKeys",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExists = { id: "mockId" };
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockExists);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await saveSubscription(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select id from push_subscriptions where endpoint = $1'),
|
||||||
|
[mockBody.subscription.endpoint]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('update push_subscriptions set keys = $1, user_id = $2 where id = $3'),
|
||||||
|
[mockBody.subscription.keys, mockAuth.uid, mockExists.id]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save user subscription even if this is their first one', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
subscription: {
|
||||||
|
endpoint: "mockEndpoint",
|
||||||
|
keys: "mockKeys",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await saveSubscription(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select id from push_subscriptions where endpoint = $1'),
|
||||||
|
[mockBody.subscription.endpoint]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('insert into push_subscriptions(endpoint, keys, user_id) values($1, $2, $3)'),
|
||||||
|
[mockBody.subscription.endpoint, mockBody.subscription.keys, mockAuth.uid]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the subscription object is invalid', async () => {
|
||||||
|
const mockBody = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
expect(saveSubscription(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Invalid subscription object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to save subscription', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
subscription: {
|
||||||
|
endpoint: "mockEndpoint",
|
||||||
|
keys: "mockKeys",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExists = { id: "mockId" };
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockExists);
|
||||||
|
(mockPg.none as jest.Mock).mockRejectedValue(new Error('Saving error'));
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
expect(saveSubscription(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to save subscription');
|
||||||
|
|
||||||
|
// expect(errorSpy).toBeCalledTimes(1);
|
||||||
|
// expect(errorSpy).toBeCalledWith(
|
||||||
|
// expect.stringContaining('Error saving subscription'),
|
||||||
|
// expect.any(Error)
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
36
backend/api/tests/unit/search-location.unit.test.ts
Normal file
36
backend/api/tests/unit/search-location.unit.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
jest.mock('common/geodb');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import { searchLocation } from "api/search-location";
|
||||||
|
import * as geodbModules from "common/geodb";
|
||||||
|
|
||||||
|
describe('searchLocation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return search location', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
term: "mockTerm",
|
||||||
|
limit: 15
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockReturn = "Pass";
|
||||||
|
|
||||||
|
(geodbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn);
|
||||||
|
|
||||||
|
const result = await searchLocation(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockReturn);
|
||||||
|
expect(geodbModules.geodbFetch).toBeCalledTimes(1);
|
||||||
|
expect(geodbModules.geodbFetch).toBeCalledWith(
|
||||||
|
expect.stringContaining(`/cities?namePrefix=${mockBody.term}&limit=${mockBody.limit}&offset=0&sort=-population`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
72
backend/api/tests/unit/search-near-city.unit.test.ts
Normal file
72
backend/api/tests/unit/search-near-city.unit.test.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
jest.mock('common/geodb');
|
||||||
|
|
||||||
|
import * as citySearchModules from "api/search-near-city";
|
||||||
|
import * as geoDbModules from "common/geodb";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('searchNearCity', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return locations near a city', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
radius: 123,
|
||||||
|
cityId: "mockCityId"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockReturn = "Pass";
|
||||||
|
|
||||||
|
(geoDbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn);
|
||||||
|
|
||||||
|
const result = await citySearchModules.searchNearCity(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockReturn);
|
||||||
|
expect(geoDbModules.geodbFetch).toBeCalledTimes(1);
|
||||||
|
expect(geoDbModules.geodbFetch).toBeCalledWith(
|
||||||
|
expect.stringContaining(`/cities/${mockBody.cityId}/nearbyCities?radius=${mockBody.radius}&offset=0&sort=-population&limit=100`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNearCity', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return locations near a city', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
radius: 123,
|
||||||
|
cityId: "mockCityId"
|
||||||
|
};
|
||||||
|
const mockReturn = {
|
||||||
|
status: "mockStatus",
|
||||||
|
data: {
|
||||||
|
data: [
|
||||||
|
{ id: "mockId" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(geoDbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn);
|
||||||
|
|
||||||
|
const result = await citySearchModules.getNearbyCities(mockBody.cityId, mockBody.radius);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual([mockReturn.data.data[0].id]);
|
||||||
|
expect(geoDbModules.geodbFetch).toBeCalledTimes(1);
|
||||||
|
expect(geoDbModules.geodbFetch).toBeCalledWith(
|
||||||
|
expect.stringContaining(`/cities/${mockBody.cityId}/nearbyCities?radius=${mockBody.radius}&offset=0&sort=-population&limit=100`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
154
backend/api/tests/unit/search-users.unit.test.ts
Normal file
154
backend/api/tests/unit/search-users.unit.test.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/helpers/search');
|
||||||
|
jest.mock('shared/supabase/sql-builder');
|
||||||
|
jest.mock('common/supabase/users');
|
||||||
|
jest.mock('common/api/user-types');
|
||||||
|
|
||||||
|
import { searchUsers } from "api/search-users";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as searchHelpers from "shared/helpers/search";
|
||||||
|
import * as sqlBuilderModules from "shared/supabase/sql-builder";
|
||||||
|
import * as supabaseUsers from "common/supabase/users";
|
||||||
|
import { toUserAPIResponse } from "common/api/user-types";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('searchUsers', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
map: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return an array of uniq users', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
term: "mockTerm",
|
||||||
|
limit: 10,
|
||||||
|
page: 1
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockSearchAllSql = "mockSQL";
|
||||||
|
const mockAllUser = [
|
||||||
|
{id: "mockId 1"},
|
||||||
|
{id: "mockId 2"},
|
||||||
|
{id: "mockId 3"},
|
||||||
|
];
|
||||||
|
|
||||||
|
(sqlBuilderModules.renderSql as jest.Mock).mockReturnValue(mockSearchAllSql);
|
||||||
|
(sqlBuilderModules.select as jest.Mock).mockReturnValue('Select');
|
||||||
|
(sqlBuilderModules.from as jest.Mock).mockReturnValue('From');
|
||||||
|
(sqlBuilderModules.where as jest.Mock).mockReturnValue('Where');
|
||||||
|
(searchHelpers.constructPrefixTsQuery as jest.Mock).mockReturnValue('ConstructPrefix');
|
||||||
|
(sqlBuilderModules.orderBy as jest.Mock).mockReturnValue('OrderBy');
|
||||||
|
(sqlBuilderModules.limit as jest.Mock).mockReturnValue('Limit');
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue(mockAllUser);
|
||||||
|
(toUserAPIResponse as jest.Mock)
|
||||||
|
.mockReturnValueOnce(mockAllUser[0].id)
|
||||||
|
.mockReturnValueOnce(mockAllUser[1].id)
|
||||||
|
.mockReturnValueOnce(mockAllUser[2].id);
|
||||||
|
|
||||||
|
const result: any = await searchUsers(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result[0]).toContain(mockAllUser[0].id);
|
||||||
|
expect(result[1]).toContain(mockAllUser[1].id);
|
||||||
|
expect(result[2]).toContain(mockAllUser[2].id);
|
||||||
|
|
||||||
|
expect(sqlBuilderModules.renderSql).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.renderSql).toBeCalledWith(
|
||||||
|
['Select', 'From'],
|
||||||
|
['Where', 'OrderBy'],
|
||||||
|
'Limit'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sqlBuilderModules.select).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.select).toBeCalledWith('*');
|
||||||
|
expect(sqlBuilderModules.from).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.from).toBeCalledWith('users');
|
||||||
|
expect(sqlBuilderModules.where).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.where).toBeCalledWith(
|
||||||
|
expect.stringContaining("name_username_vector @@ websearch_to_tsquery('english', $1)"),
|
||||||
|
[mockProps.term, 'ConstructPrefix']
|
||||||
|
);
|
||||||
|
expect(sqlBuilderModules.orderBy).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.orderBy).toBeCalledWith(
|
||||||
|
expect.stringContaining("ts_rank(name_username_vector, websearch_to_tsquery($1)) desc,"),
|
||||||
|
[mockProps.term]
|
||||||
|
);
|
||||||
|
expect(sqlBuilderModules.limit).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.limit).toBeCalledWith(mockProps.limit, mockProps.page * mockProps.limit);
|
||||||
|
expect(mockPg.map).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.map).toBeCalledWith(
|
||||||
|
mockSearchAllSql,
|
||||||
|
null,
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array of uniq users if no term is supplied', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
limit: 10,
|
||||||
|
page: 1
|
||||||
|
} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockSearchAllSql = "mockSQL";
|
||||||
|
const mockAllUser = [
|
||||||
|
{id: "mockId 1"},
|
||||||
|
{id: "mockId 2"},
|
||||||
|
{id: "mockId 3"},
|
||||||
|
];
|
||||||
|
|
||||||
|
(sqlBuilderModules.renderSql as jest.Mock).mockReturnValue(mockSearchAllSql);
|
||||||
|
(sqlBuilderModules.select as jest.Mock).mockReturnValue('Select');
|
||||||
|
(sqlBuilderModules.from as jest.Mock).mockReturnValue('From');
|
||||||
|
(sqlBuilderModules.orderBy as jest.Mock).mockReturnValue('OrderBy');
|
||||||
|
(sqlBuilderModules.limit as jest.Mock).mockReturnValue('Limit');
|
||||||
|
(supabaseUsers.convertUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
(mockPg.map as jest.Mock).mockResolvedValue(mockAllUser);
|
||||||
|
(toUserAPIResponse as jest.Mock)
|
||||||
|
.mockReturnValueOnce(mockAllUser[0].id)
|
||||||
|
.mockReturnValueOnce(mockAllUser[1].id)
|
||||||
|
.mockReturnValueOnce(mockAllUser[2].id);
|
||||||
|
|
||||||
|
const result: any = await searchUsers(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result[0]).toContain(mockAllUser[0].id);
|
||||||
|
expect(result[1]).toContain(mockAllUser[1].id);
|
||||||
|
expect(result[2]).toContain(mockAllUser[2].id);
|
||||||
|
|
||||||
|
expect(sqlBuilderModules.renderSql).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.renderSql).toBeCalledWith(
|
||||||
|
['Select', 'From'],
|
||||||
|
'OrderBy',
|
||||||
|
'Limit'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sqlBuilderModules.select).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.select).toBeCalledWith('*');
|
||||||
|
expect(sqlBuilderModules.from).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.from).toBeCalledWith('users');
|
||||||
|
expect(sqlBuilderModules.orderBy).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.orderBy).toBeCalledWith(
|
||||||
|
expect.stringMatching(`data->'creatorTraders'->'allTime' desc nulls last`)
|
||||||
|
);
|
||||||
|
expect(sqlBuilderModules.limit).toBeCalledTimes(1);
|
||||||
|
expect(sqlBuilderModules.limit).toBeCalledWith(mockProps.limit, mockProps.page * mockProps.limit);
|
||||||
|
expect(mockPg.map).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.map).toBeCalledWith(
|
||||||
|
mockSearchAllSql,
|
||||||
|
null,
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
314
backend/api/tests/unit/send-search-notifications.unit.test.ts
Normal file
314
backend/api/tests/unit/send-search-notifications.unit.test.ts
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/supabase/sql-builder');
|
||||||
|
jest.mock('api/get-profiles');
|
||||||
|
jest.mock('email/functions/helpers');
|
||||||
|
jest.mock('lodash');
|
||||||
|
|
||||||
|
import * as searchNotificationModules from "api/send-search-notifications";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as sqlBuilderModules from "shared/supabase/sql-builder";
|
||||||
|
import * as profileModules from "api/get-profiles";
|
||||||
|
import * as helperModules from "email/functions/helpers";
|
||||||
|
import * as lodashModules from "lodash";
|
||||||
|
|
||||||
|
describe('sendSearchNotification', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
map: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should send search notification emails', async () => {
|
||||||
|
const mockSearchQuery = "mockSqlQuery";
|
||||||
|
const mockSearches = [
|
||||||
|
{
|
||||||
|
created_time: "mockSearchCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
id: 123,
|
||||||
|
last_notified_at: null,
|
||||||
|
location: {"mockLocation" : "mockLocationValue"},
|
||||||
|
search_filters: null,
|
||||||
|
search_name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
created_time: "mockCreatedTime1",
|
||||||
|
creator_id: "mockCreatorId1",
|
||||||
|
id: 1234,
|
||||||
|
last_notified_at: null,
|
||||||
|
location: {"mockLocation1" : "mockLocationValue1"},
|
||||||
|
search_filters: null,
|
||||||
|
search_name: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const _mockUsers = [
|
||||||
|
{
|
||||||
|
created_time: "mockUserCreatedTime",
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
created_time: "mockUserCreatedTime1",
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1",
|
||||||
|
name: "mockName1",
|
||||||
|
name_username_vector: "mockNameUsernameVector1",
|
||||||
|
username: "mockUsername1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockUsers = {
|
||||||
|
"user1": {
|
||||||
|
created_time: "mockUserCreatedTime",
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername",
|
||||||
|
},
|
||||||
|
"user2": {
|
||||||
|
created_time: "mockUserCreatedTime1",
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1",
|
||||||
|
name: "mockName1",
|
||||||
|
name_username_vector: "mockNameUsernameVector1",
|
||||||
|
username: "mockUsername1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const _mockPrivateUsers = [
|
||||||
|
{
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockPrivateUsers = {
|
||||||
|
"privateUser1": {
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId"
|
||||||
|
},
|
||||||
|
"privateUser2": {
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockProfiles = [
|
||||||
|
{
|
||||||
|
name: "mockProfileName",
|
||||||
|
username: "mockProfileUsername"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mockProfileName1",
|
||||||
|
username: "mockProfileUsername1"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockProps = [
|
||||||
|
{
|
||||||
|
skipId: "mockCreatorId",
|
||||||
|
lastModificationWithin: '24 hours',
|
||||||
|
shortBio: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipId: "mockCreatorId1",
|
||||||
|
lastModificationWithin: '24 hours',
|
||||||
|
shortBio: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
(sqlBuilderModules.renderSql as jest.Mock)
|
||||||
|
.mockReturnValueOnce(mockSearchQuery)
|
||||||
|
.mockReturnValueOnce('usersRenderSql')
|
||||||
|
.mockReturnValueOnce('privateUsersRenderSql');
|
||||||
|
(sqlBuilderModules.select as jest.Mock).mockReturnValue('Select');
|
||||||
|
(sqlBuilderModules.from as jest.Mock).mockReturnValue('From');
|
||||||
|
(mockPg.map as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockSearches)
|
||||||
|
.mockResolvedValueOnce(_mockUsers)
|
||||||
|
.mockResolvedValueOnce(_mockPrivateUsers);
|
||||||
|
(lodashModules.keyBy as jest.Mock)
|
||||||
|
.mockReturnValueOnce(mockUsers)
|
||||||
|
.mockReturnValueOnce(mockPrivateUsers);
|
||||||
|
(profileModules.loadProfiles as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({profiles: mockProfiles})
|
||||||
|
.mockResolvedValueOnce({profiles: mockProfiles});
|
||||||
|
jest.spyOn(searchNotificationModules, 'notifyBookmarkedSearch');
|
||||||
|
(helperModules.sendSearchAlertsEmail as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await searchNotificationModules.sendSearchNotifications();
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(sqlBuilderModules.renderSql).toBeCalledTimes(3);
|
||||||
|
expect(sqlBuilderModules.renderSql).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'Select',
|
||||||
|
'From'
|
||||||
|
);
|
||||||
|
expect(sqlBuilderModules.renderSql).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'Select',
|
||||||
|
'From'
|
||||||
|
);
|
||||||
|
expect(sqlBuilderModules.renderSql).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
'Select',
|
||||||
|
'From'
|
||||||
|
);
|
||||||
|
expect(mockPg.map).toBeCalledTimes(3);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
mockSearchQuery,
|
||||||
|
[],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'usersRenderSql',
|
||||||
|
[],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockPg.map).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
'privateUsersRenderSql',
|
||||||
|
[],
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(profileModules.loadProfiles).toBeCalledTimes(2);
|
||||||
|
expect(profileModules.loadProfiles).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
mockProps[0]
|
||||||
|
);
|
||||||
|
expect(profileModules.loadProfiles).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
mockProps[1]
|
||||||
|
);
|
||||||
|
expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledTimes(1);
|
||||||
|
expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledWith({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send search notification emails when there is a matching creator_id entry in private users', async () => {
|
||||||
|
const mockSearchQuery = "mockSqlQuery";
|
||||||
|
const mockSearches = [
|
||||||
|
{
|
||||||
|
created_time: "mockSearchCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
id: 123,
|
||||||
|
last_notified_at: null,
|
||||||
|
location: {"mockLocation" : "mockLocationValue"},
|
||||||
|
search_filters: null,
|
||||||
|
search_name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
created_time: "mockCreatedTime1",
|
||||||
|
creator_id: "mockCreatorId1",
|
||||||
|
id: 1234,
|
||||||
|
last_notified_at: null,
|
||||||
|
location: {"mockLocation1" : "mockLocationValue1"},
|
||||||
|
search_filters: null,
|
||||||
|
search_name: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const _mockUsers = [
|
||||||
|
{
|
||||||
|
created_time: "mockUserCreatedTime",
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
created_time: "mockUserCreatedTime1",
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1",
|
||||||
|
name: "mockName1",
|
||||||
|
name_username_vector: "mockNameUsernameVector1",
|
||||||
|
username: "mockUsername1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockUsers = {
|
||||||
|
"user1": {
|
||||||
|
created_time: "mockUserCreatedTime",
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId",
|
||||||
|
name: "mockName",
|
||||||
|
name_username_vector: "mockNameUsernameVector",
|
||||||
|
username: "mockUsername",
|
||||||
|
},
|
||||||
|
"user2": {
|
||||||
|
created_time: "mockUserCreatedTime1",
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1",
|
||||||
|
name: "mockName1",
|
||||||
|
name_username_vector: "mockNameUsernameVector1",
|
||||||
|
username: "mockUsername1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const _mockPrivateUsers = [
|
||||||
|
{
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockPrivateUsers = {
|
||||||
|
"mockCreatorId": {
|
||||||
|
data: {"mockData" : "mockDataValue"},
|
||||||
|
id: "mockId"
|
||||||
|
},
|
||||||
|
"mockCreatorId1": {
|
||||||
|
data: {"mockData1" : "mockDataValue1"},
|
||||||
|
id: "mockId1"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockProfiles = [
|
||||||
|
{
|
||||||
|
name: "mockProfileName",
|
||||||
|
username: "mockProfileUsername"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mockProfileName1",
|
||||||
|
username: "mockProfileUsername1"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
(sqlBuilderModules.renderSql as jest.Mock)
|
||||||
|
.mockReturnValueOnce(mockSearchQuery)
|
||||||
|
.mockReturnValueOnce('usersRenderSql')
|
||||||
|
.mockReturnValueOnce('privateUsersRenderSql');
|
||||||
|
(sqlBuilderModules.select as jest.Mock).mockReturnValue('Select');
|
||||||
|
(sqlBuilderModules.from as jest.Mock).mockReturnValue('From');
|
||||||
|
(mockPg.map as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockSearches)
|
||||||
|
.mockResolvedValueOnce(_mockUsers)
|
||||||
|
.mockResolvedValueOnce(_mockPrivateUsers);
|
||||||
|
(lodashModules.keyBy as jest.Mock)
|
||||||
|
.mockReturnValueOnce(mockUsers)
|
||||||
|
.mockReturnValueOnce(mockPrivateUsers);
|
||||||
|
(profileModules.loadProfiles as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({profiles: mockProfiles})
|
||||||
|
.mockResolvedValueOnce({profiles: mockProfiles});
|
||||||
|
jest.spyOn(searchNotificationModules, 'notifyBookmarkedSearch');
|
||||||
|
(helperModules.sendSearchAlertsEmail as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await searchNotificationModules.sendSearchNotifications();
|
||||||
|
|
||||||
|
expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledTimes(1);
|
||||||
|
expect(searchNotificationModules.notifyBookmarkedSearch).not.toBeCalledWith({});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/compatibility/compute-scores');
|
||||||
|
|
||||||
|
import { setCompatibilityAnswer } from "api/set-compatibility-answer";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import { recomputeCompatibilityScoresForUser } from "shared/compatibility/compute-scores";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('setCompatibilityAnswer', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
one: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should set compatibility answers', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
questionId: 1,
|
||||||
|
multipleChoice: 2,
|
||||||
|
prefChoices: [1,2,3,4,5],
|
||||||
|
importance: 1,
|
||||||
|
explanation: "mockExplanation"
|
||||||
|
};
|
||||||
|
const mockResult = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
explanation: "mockExplanation",
|
||||||
|
id: 123,
|
||||||
|
importance: 1,
|
||||||
|
multipleChoice: 2,
|
||||||
|
prefChoices: [1,2,3,4,5],
|
||||||
|
questionId: 1,
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(mockResult);
|
||||||
|
(recomputeCompatibilityScoresForUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result: any = await setCompatibilityAnswer(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.result).toBe(mockResult);
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.one).toBeCalledWith(
|
||||||
|
{
|
||||||
|
text: expect.stringContaining('INSERT INTO compatibility_answers'),
|
||||||
|
values: [
|
||||||
|
mockAuth.uid,
|
||||||
|
mockProps.questionId,
|
||||||
|
mockProps.multipleChoice,
|
||||||
|
mockProps.prefChoices,
|
||||||
|
mockProps.importance,
|
||||||
|
mockProps.explanation,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await result.continue();
|
||||||
|
|
||||||
|
expect(recomputeCompatibilityScoresForUser).toBeCalledTimes(1);
|
||||||
|
expect(recomputeCompatibilityScoresForUser).toBeCalledWith(mockAuth.uid, expect.any(Object));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,34 +1,56 @@
|
|||||||
jest.mock('shared/supabase/init');
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
import * as setLastTimeOnlineModule from "api/set-last-online-time";
|
import * as setLastTimeOnlineModule from "api/set-last-online-time";
|
||||||
import * as supabaseInit from "shared/supabase/init";
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
describe('Should', () => {
|
describe('setLastOnlineTimeUser', () => {
|
||||||
let mockPg: any;
|
let mockPg: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
mockPg = {
|
mockPg = {
|
||||||
none: jest.fn(),
|
none: jest.fn(),
|
||||||
};
|
};
|
||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
jest.clearAllMocks();
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('change the users last online time', async () => {
|
describe('when given valid input', () => {
|
||||||
const mockProfile = {user_id: 'Jonathon'};
|
it('should change the users last online time', async () => {
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
await setLastTimeOnlineModule.setLastOnlineTimeUser(mockProfile.user_id);
|
const mockReq = {} as any;
|
||||||
|
const mockProps = {} as any;
|
||||||
|
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
jest.spyOn(setLastTimeOnlineModule, 'setLastOnlineTimeUser');
|
||||||
|
|
||||||
expect(mockPg.none).toBeCalledTimes(1);
|
await setLastTimeOnlineModule.setLastOnlineTime(mockProps, mockAuth, mockReq);
|
||||||
|
const [query, userId] = mockPg.none.mock.calls[0];
|
||||||
|
|
||||||
|
expect(setLastTimeOnlineModule.setLastOnlineTimeUser).toBeCalledTimes(1);
|
||||||
|
expect(setLastTimeOnlineModule.setLastOnlineTimeUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(userId).toContain(mockAuth.uid);
|
||||||
|
expect(query).toContain("VALUES ($1, now())");
|
||||||
|
expect(query).toContain("ON CONFLICT (user_id)");
|
||||||
|
expect(query).toContain("DO UPDATE");
|
||||||
|
expect(query).toContain("user_activity.last_online_time < now() - interval '1 minute'");
|
||||||
|
});
|
||||||
|
|
||||||
const [query, userId] = mockPg.none.mock.calls[0];
|
it('should return if there is no auth', async () => {
|
||||||
|
const mockAuth = { } as any;
|
||||||
expect(userId).toContain(mockProfile.user_id);
|
const mockReq = {} as any;
|
||||||
expect(query).toContain("VALUES ($1, now())")
|
const mockProps = {} as any;
|
||||||
expect(query).toContain("ON CONFLICT (user_id)")
|
|
||||||
expect(query).toContain("DO UPDATE")
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
expect(query).toContain("user_activity.last_online_time < now() - interval '1 minute'")
|
jest.spyOn(setLastTimeOnlineModule, 'setLastOnlineTimeUser');
|
||||||
|
|
||||||
|
await setLastTimeOnlineModule.setLastOnlineTime(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(setLastTimeOnlineModule.setLastOnlineTimeUser).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
227
backend/api/tests/unit/ship-profiles.unit.test.ts
Normal file
227
backend/api/tests/unit/ship-profiles.unit.test.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
jest.mock('shared/supabase/utils');
|
||||||
|
jest.mock('shared/create-profile-notification');
|
||||||
|
|
||||||
|
import { shipProfiles } from "api/ship-profiles";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import * as supabaseUtils from "shared/supabase/utils";
|
||||||
|
import * as profileNotificationModules from "shared/create-profile-notification";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('shipProfiles', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return success if the profile ship already exists', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId1: "mockTargetUserId1",
|
||||||
|
targetUserId2: "mockTargetUserId2",
|
||||||
|
remove: false,
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExisting = {
|
||||||
|
data: { ship_id : "mockShipId" },
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue(mockExisting);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result: any = await shipProfiles(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select ship_id from profile_ships'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId1, mockProps.targetUserId2]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success if trying to remove a profile ship that already exists', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId1: "mockTargetUserId1",
|
||||||
|
targetUserId2: "mockTargetUserId2",
|
||||||
|
remove: true,
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExisting = {
|
||||||
|
data: { ship_id : "mockShipId" },
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockExisting)
|
||||||
|
.mockResolvedValueOnce({error: null});
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result: any = await shipProfiles(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(tryCatch).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select ship_id from profile_ships'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId1, mockProps.targetUserId2]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('delete from profile_ships where ship_id = $1'),
|
||||||
|
[mockExisting.data.ship_id]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success when creating a new profile ship', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId1: "mockTargetUserId1",
|
||||||
|
targetUserId2: "mockTargetUserId2",
|
||||||
|
remove: false,
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExisting = {
|
||||||
|
data: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
const mockData = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
ship_id: "mockShipId",
|
||||||
|
target1_id: "mockTarget1Id",
|
||||||
|
target2_id: "mockTarget2Id",
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockExisting)
|
||||||
|
.mockResolvedValueOnce({data: mockData, error: null});
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockReturnValue(null);
|
||||||
|
|
||||||
|
const result: any = await shipProfiles(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.result.status).toBe('success');
|
||||||
|
expect(tryCatch).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select ship_id from profile_ships'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId1, mockProps.targetUserId2]
|
||||||
|
);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'profile_ships',
|
||||||
|
{
|
||||||
|
creator_id: mockAuth.uid,
|
||||||
|
target1_id: mockProps.targetUserId1,
|
||||||
|
target2_id: mockProps.targetUserId2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
(profileNotificationModules.createProfileShipNotification as jest.Mock).mockReturnValue(null);
|
||||||
|
|
||||||
|
await result.continue();
|
||||||
|
|
||||||
|
expect(profileNotificationModules.createProfileShipNotification).toBeCalledTimes(2);
|
||||||
|
expect(profileNotificationModules.createProfileShipNotification).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
mockData,
|
||||||
|
mockData.target1_id
|
||||||
|
);
|
||||||
|
expect(profileNotificationModules.createProfileShipNotification).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
mockData,
|
||||||
|
mockData.target2_id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if unable to check ship', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId1: "mockTargetUserId1",
|
||||||
|
targetUserId2: "mockTargetUserId2",
|
||||||
|
remove: false,
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExisting = {
|
||||||
|
data: null,
|
||||||
|
error: Error
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue(mockExisting);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(shipProfiles(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Error when checking ship: ');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to remove a profile ship that already exists', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId1: "mockTargetUserId1",
|
||||||
|
targetUserId2: "mockTargetUserId2",
|
||||||
|
remove: true,
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExisting = {
|
||||||
|
data: { ship_id : "mockShipId" },
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockExisting)
|
||||||
|
.mockResolvedValueOnce({error: Error});
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
expect(shipProfiles(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to remove ship: ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success when creating a new profile ship', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId1: "mockTargetUserId1",
|
||||||
|
targetUserId2: "mockTargetUserId2",
|
||||||
|
remove: false,
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExisting = {
|
||||||
|
data: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockExisting)
|
||||||
|
.mockResolvedValueOnce({data: null, error: Error});
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockReturnValue(null);
|
||||||
|
|
||||||
|
expect(shipProfiles(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to create ship: ');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
146
backend/api/tests/unit/star-profile.unit.test.ts
Normal file
146
backend/api/tests/unit/star-profile.unit.test.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/supabase/utils');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import { starProfile } from "api/star-profile";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as supabaseUtils from "shared/supabase/utils";
|
||||||
|
|
||||||
|
describe('startProfile', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn(),
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return success when trying to star a profile for the first time', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: null})
|
||||||
|
.mockResolvedValueOnce({error: null});
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockReturnValue(null);
|
||||||
|
|
||||||
|
const result: any = await starProfile(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(tryCatch).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select * from profile_stars where creator_id = $1 and target_id = $2'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId]
|
||||||
|
);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'profile_stars',
|
||||||
|
{
|
||||||
|
creator_id: mockAuth.uid,
|
||||||
|
target_id: mockProps.targetUserId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success if the profile already has a star', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockExisting = {
|
||||||
|
created_time: "mockCreatedTime",
|
||||||
|
creator_id: "mockCreatorId",
|
||||||
|
star_id: "mockStarId",
|
||||||
|
target_id: "mockTarget",
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: mockExisting});
|
||||||
|
|
||||||
|
const result: any = await starProfile(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.insert).not.toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success when trying to remove a star', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({error: null});
|
||||||
|
|
||||||
|
const result: any = await starProfile(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.status).toBe('success');
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('delete from profile_stars where creator_id = $1 and target_id = $2'),
|
||||||
|
[mockAuth.uid, mockProps.targetUserId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if unable to remove star', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValueOnce({error: Error});
|
||||||
|
|
||||||
|
expect(starProfile(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to remove star');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to add a star', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
targetUserId: "mockTargetUserId",
|
||||||
|
remove: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null);
|
||||||
|
(tryCatch as jest.Mock)
|
||||||
|
.mockResolvedValueOnce({data: null})
|
||||||
|
.mockResolvedValueOnce({error: Error});
|
||||||
|
(supabaseUtils.insert as jest.Mock).mockReturnValue(null);
|
||||||
|
|
||||||
|
expect(starProfile(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Failed to add star');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
255
backend/api/tests/unit/update-me.unit.test.ts
Normal file
255
backend/api/tests/unit/update-me.unit.test.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
jest.mock('common/api/user-types');
|
||||||
|
jest.mock('common/util/clean-username');
|
||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('common/util/object');
|
||||||
|
jest.mock('lodash');
|
||||||
|
jest.mock('shared/utils');
|
||||||
|
jest.mock('shared/supabase/users');
|
||||||
|
jest.mock('shared/websockets/helpers');
|
||||||
|
jest.mock('common/envs/constants');
|
||||||
|
|
||||||
|
import { updateMe } from "api/update-me";
|
||||||
|
import { toUserAPIResponse } from "common/api/user-types";
|
||||||
|
import * as cleanUsernameModules from "common/util/clean-username";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as objectUtils from "common/util/object";
|
||||||
|
import * as lodashModules from "lodash";
|
||||||
|
import * as sharedUtils from "shared/utils";
|
||||||
|
import * as supabaseUsers from "shared/supabase/users";
|
||||||
|
import * as websocketHelperModules from "shared/websockets/helpers";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('updateMe', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should update user information', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUpdate = {
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUsername",
|
||||||
|
avatarUrl: "mockAvatarUrl",
|
||||||
|
bio: "mockBio",
|
||||||
|
link: {"mockLink" : "mockLinkValue"},
|
||||||
|
optOutBetWarnings:true,
|
||||||
|
website: "mockWebsite",
|
||||||
|
twitterHandle: "mockTwitterHandle",
|
||||||
|
discordHandle: "mockDiscordHandle",
|
||||||
|
};
|
||||||
|
const mockStripped = {
|
||||||
|
bio: "mockBio"
|
||||||
|
};
|
||||||
|
const mockData = {link: "mockNewLinks"};
|
||||||
|
const arrySpy = jest.spyOn(Array.prototype, 'includes');
|
||||||
|
|
||||||
|
(lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(true);
|
||||||
|
(cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name);
|
||||||
|
(cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(mockUpdate.username);
|
||||||
|
arrySpy.mockReturnValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(false);
|
||||||
|
(supabaseUsers.updateUser as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(lodashModules.mapValues as jest.Mock).mockReturnValue(mockStripped);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockData);
|
||||||
|
(mockPg.none as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
(objectUtils.removeUndefinedProps as jest.Mock).mockReturnValue("mockRemoveUndefinedProps");
|
||||||
|
(websocketHelperModules.broadcastUpdatedUser as jest.Mock).mockReturnValue(null);
|
||||||
|
(toUserAPIResponse as jest.Mock).mockReturnValue(null);
|
||||||
|
|
||||||
|
await updateMe(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(lodashModules.cloneDeep).toBeCalledTimes(1);
|
||||||
|
expect(lodashModules.cloneDeep).toBeCalledWith(mockProps);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(cleanUsernameModules.cleanDisplayName).toBeCalledTimes(1);
|
||||||
|
expect(cleanUsernameModules.cleanDisplayName).toBeCalledWith(mockUpdate.name);
|
||||||
|
expect(cleanUsernameModules.cleanUsername).toBeCalledTimes(1);
|
||||||
|
expect(cleanUsernameModules.cleanUsername).toBeCalledWith(mockUpdate.username);
|
||||||
|
expect(arrySpy).toBeCalledTimes(1);
|
||||||
|
expect(arrySpy).toBeCalledWith(mockUpdate.username);
|
||||||
|
expect(sharedUtils.getUserByUsername).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUserByUsername).toBeCalledWith(mockUpdate.username);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledTimes(2);
|
||||||
|
expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.any(Object),
|
||||||
|
mockAuth.uid,
|
||||||
|
'mockRemoveUndefinedProps'
|
||||||
|
);
|
||||||
|
expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.any(Object),
|
||||||
|
mockAuth.uid,
|
||||||
|
{avatarUrl: mockUpdate.avatarUrl}
|
||||||
|
);
|
||||||
|
expect(lodashModules.mapValues).toBeCalledTimes(1);
|
||||||
|
expect(lodashModules.mapValues).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('update users'),
|
||||||
|
{
|
||||||
|
adds: expect.any(Object),
|
||||||
|
removes: expect.any(Array),
|
||||||
|
id: mockAuth.uid
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(2);
|
||||||
|
expect(mockPg.none).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining(`update users set name = $1 where id = $2`),
|
||||||
|
[mockUpdate.name, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining(`update users set username = $1 where id = $2`),
|
||||||
|
[mockUpdate.username, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(objectUtils.removeUndefinedProps).toBeCalledTimes(2);
|
||||||
|
expect(objectUtils.removeUndefinedProps).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
id: mockAuth.uid,
|
||||||
|
name: mockUpdate.name,
|
||||||
|
username: mockUpdate.username,
|
||||||
|
avatarUrl: mockUpdate.avatarUrl,
|
||||||
|
link: mockData.link
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledTimes(1);
|
||||||
|
expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledWith('mockRemoveUndefinedProps');
|
||||||
|
expect(toUserAPIResponse).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if no account was found', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUpdate = {
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUsername",
|
||||||
|
avatarUrl: "mockAvatarUrl",
|
||||||
|
bio: "mockBio",
|
||||||
|
link: {"mockLink" : "mockLinkValue"},
|
||||||
|
optOutBetWarnings:true,
|
||||||
|
website: "mockWebsite",
|
||||||
|
twitterHandle: "mockTwitterHandle",
|
||||||
|
discordHandle: "mockDiscordHandle",
|
||||||
|
};
|
||||||
|
|
||||||
|
(lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(updateMe(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Your account was not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the username is invalid', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUpdate = {
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUsername",
|
||||||
|
avatarUrl: "mockAvatarUrl",
|
||||||
|
bio: "mockBio",
|
||||||
|
link: {"mockLink" : "mockLinkValue"},
|
||||||
|
optOutBetWarnings:true,
|
||||||
|
website: "mockWebsite",
|
||||||
|
twitterHandle: "mockTwitterHandle",
|
||||||
|
discordHandle: "mockDiscordHandle",
|
||||||
|
};
|
||||||
|
|
||||||
|
(lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(true);
|
||||||
|
(cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name);
|
||||||
|
(cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(updateMe(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Invalid username');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the username is reserved', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUpdate = {
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUsername",
|
||||||
|
avatarUrl: "mockAvatarUrl",
|
||||||
|
bio: "mockBio",
|
||||||
|
link: {"mockLink" : "mockLinkValue"},
|
||||||
|
optOutBetWarnings:true,
|
||||||
|
website: "mockWebsite",
|
||||||
|
twitterHandle: "mockTwitterHandle",
|
||||||
|
discordHandle: "mockDiscordHandle",
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrySpy = jest.spyOn(Array.prototype, 'includes');
|
||||||
|
|
||||||
|
(lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(true);
|
||||||
|
(cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name);
|
||||||
|
(cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(mockUpdate.username);
|
||||||
|
arrySpy.mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(updateMe(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('This username is reserved');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the username is taken', async () => {
|
||||||
|
const mockProps = {} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUpdate = {
|
||||||
|
name: "mockName",
|
||||||
|
username: "mockUsername",
|
||||||
|
avatarUrl: "mockAvatarUrl",
|
||||||
|
bio: "mockBio",
|
||||||
|
link: {"mockLink" : "mockLinkValue"},
|
||||||
|
optOutBetWarnings:true,
|
||||||
|
website: "mockWebsite",
|
||||||
|
twitterHandle: "mockTwitterHandle",
|
||||||
|
discordHandle: "mockDiscordHandle",
|
||||||
|
};
|
||||||
|
const arrySpy = jest.spyOn(Array.prototype, 'includes');
|
||||||
|
|
||||||
|
(lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate);
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(true);
|
||||||
|
(cleanUsernameModules.cleanDisplayName as jest.Mock).mockReturnValue(mockUpdate.name);
|
||||||
|
(cleanUsernameModules.cleanUsername as jest.Mock).mockReturnValue(mockUpdate.username);
|
||||||
|
arrySpy.mockReturnValue(false);
|
||||||
|
(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(updateMe(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Username already taken');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
71
backend/api/tests/unit/update-notif-setting.unit.test.ts
Normal file
71
backend/api/tests/unit/update-notif-setting.unit.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/supabase/users');
|
||||||
|
jest.mock('shared/websockets/helpers');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import { updateNotifSettings } from "api/update-notif-setting";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as supabaseUsers from "shared/supabase/users";
|
||||||
|
import * as websocketHelpers from "shared/websockets/helpers";
|
||||||
|
|
||||||
|
describe('updateNotifSettings', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should update notification settings', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
type: "new_match" as const,
|
||||||
|
medium: "email" as const,
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(mockPg.none as jest.Mock).mockResolvedValue(null);
|
||||||
|
(websocketHelpers.broadcastUpdatedPrivateUser as jest.Mock).mockReturnValue(null);
|
||||||
|
|
||||||
|
await updateNotifSettings(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('update private_users'),
|
||||||
|
[mockProps.type, mockProps.medium, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(websocketHelpers.broadcastUpdatedPrivateUser).toBeCalledTimes(1);
|
||||||
|
expect(websocketHelpers.broadcastUpdatedPrivateUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should turn off notifications', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
type: "opt_out_all" as const,
|
||||||
|
medium: "mobile" as const,
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await updateNotifSettings(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUsers.updatePrivateUser).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
mockAuth.uid,
|
||||||
|
{interestedInPushNotifications: !mockProps.enabled}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
170
backend/api/tests/unit/update-options.unit.test.ts
Normal file
170
backend/api/tests/unit/update-options.unit.test.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
jest.mock('common/util/try-catch');
|
||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import { updateOptions } from "api/update-options";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
|
||||||
|
describe('updateOptions', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
let mockTx = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockTx = {
|
||||||
|
one: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
tx: jest.fn(async (cb) => await cb(mockTx))
|
||||||
|
};
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should update user', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
table: 'causes' as const,
|
||||||
|
names: ["mockNamesOne", "mockNamesTwo"]
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockProfileIdResult = {id: 123};
|
||||||
|
const mockRow1 = {
|
||||||
|
id: 1234,
|
||||||
|
};
|
||||||
|
const mockRow2 = {
|
||||||
|
id: 12345,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockProfileIdResult);
|
||||||
|
(tryCatch as jest.Mock).mockImplementation(async (fn: any) => {
|
||||||
|
try {
|
||||||
|
const data = await fn;
|
||||||
|
return {data, error: null};
|
||||||
|
} catch (error) {
|
||||||
|
return {data:null, error};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(mockTx.one as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockRow1)
|
||||||
|
.mockResolvedValueOnce(mockRow2);
|
||||||
|
|
||||||
|
const result: any = await updateOptions(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.updatedIds).toStrictEqual([mockRow1.id, mockRow2.id]);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('SELECT id FROM profiles WHERE user_id = $1'),
|
||||||
|
[mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(tryCatch).toBeCalledTimes(1);
|
||||||
|
expect(mockTx.one).toBeCalledTimes(2);
|
||||||
|
expect(mockTx.one).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining(`INSERT INTO ${mockProps.table} (name, creator_id)`),
|
||||||
|
[mockProps.names[0], mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockTx.one).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining(`INSERT INTO ${mockProps.table} (name, creator_id)`),
|
||||||
|
[mockProps.names[1], mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockTx.none).toBeCalledTimes(2);
|
||||||
|
expect(mockTx.none).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining(`DELETE FROM profile_${mockProps.table} WHERE profile_id = $1`),
|
||||||
|
[mockProfileIdResult.id]
|
||||||
|
);
|
||||||
|
expect(mockTx.none).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining(`INSERT INTO profile_${mockProps.table} (profile_id, option_id) VALUES`),
|
||||||
|
[mockProfileIdResult.id, mockRow1.id, mockRow2.id]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the table param is invalid', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
table: 'causes' as const,
|
||||||
|
names: ["mockNamesOne", "mockNamesTwo"]
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(false);
|
||||||
|
|
||||||
|
expect(updateOptions(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Invalid table');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the names param is not provided', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
table: 'causes' as const,
|
||||||
|
names: []
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(updateOptions(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('No names provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to find profile', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
table: 'causes' as const,
|
||||||
|
names: ["mockNamesOne", "mockNamesTwo"]
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(updateOptions(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Profile not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update user', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
table: 'causes' as const,
|
||||||
|
names: ["mockNamesOne", "mockNamesTwo"]
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockProfileIdResult = {id: 123};
|
||||||
|
const mockRow1 = {
|
||||||
|
id: 1234,
|
||||||
|
};
|
||||||
|
const mockRow2 = {
|
||||||
|
id: 12345,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockProfileIdResult);
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error});
|
||||||
|
(mockPg.tx as jest.Mock).mockResolvedValue(null);
|
||||||
|
(mockTx.one as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(mockRow1)
|
||||||
|
.mockResolvedValueOnce(mockRow2);
|
||||||
|
(mockTx.none as jest.Mock)
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
expect(updateOptions(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Error updating profile options');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/utils');
|
||||||
|
jest.mock('common/supabase/utils');
|
||||||
|
|
||||||
|
import {updatePrivateUserMessageChannel} from "api/update-private-user-message-channel";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as sharedUtils from "shared/utils";
|
||||||
|
import * as supabaseUtils from "common/supabase/utils";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
|
||||||
|
describe('updatePrivateUserMessageChannel', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
oneOrNone: jest.fn(),
|
||||||
|
none: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should return success after updating the users private message channel', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
channelId: 123,
|
||||||
|
notifyAfterTime: 10
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(true);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(true);
|
||||||
|
(supabaseUtils.millisToTs as jest.Mock).mockReturnValue('mockMillisToTs');
|
||||||
|
|
||||||
|
const results = await updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(results.status).toBe('success');
|
||||||
|
expect(results.channelId).toBe(mockBody.channelId);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select status from private_user_message_channel_members'),
|
||||||
|
[mockBody.channelId, mockAuth.uid]
|
||||||
|
);
|
||||||
|
expect(mockPg.none).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.none).toBeCalledWith(
|
||||||
|
expect.stringContaining('update private_user_message_channel_members'),
|
||||||
|
[mockBody.channelId, mockAuth.uid, 'mockMillisToTs']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if the user account does not exist', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
channelId: 123,
|
||||||
|
notifyAfterTime: 10
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Your account was not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the user is not authorized in the channel', async () => {
|
||||||
|
const mockBody = {
|
||||||
|
channelId: 123,
|
||||||
|
notifyAfterTime: 10
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(true);
|
||||||
|
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('You are not authorized to this channel');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,86 +1,100 @@
|
|||||||
jest.mock("shared/supabase/init");
|
jest.mock("shared/supabase/init");
|
||||||
jest.mock("shared/supabase/utils");
|
jest.mock("shared/supabase/utils");
|
||||||
|
jest.mock("common/util/try-catch");
|
||||||
|
jest.mock("shared/profiles/parse-photos");
|
||||||
|
jest.mock("shared/supabase/users");
|
||||||
|
|
||||||
import { AuthedUser } from "api/helpers/endpoint";
|
|
||||||
import { updateProfile } from "api/update-profile";
|
import { updateProfile } from "api/update-profile";
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
import * as supabaseInit from "shared/supabase/init";
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
import * as supabaseUtils from "shared/supabase/utils";
|
import * as supabaseUtils from "shared/supabase/utils";
|
||||||
|
import * as supabaseUsers from "shared/supabase/users";
|
||||||
|
import { tryCatch } from "common/util/try-catch";
|
||||||
|
import { removePinnedUrlFromPhotoUrls } from "shared/profiles/parse-photos";
|
||||||
|
|
||||||
describe('updateProfiles', () => {
|
describe('updateProfiles', () => {
|
||||||
let mockPg: any;
|
let mockPg: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
mockPg = {
|
mockPg = {
|
||||||
oneOrNone: jest.fn(),
|
oneOrNone: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
.mockReturnValue(mockPg);
|
.mockReturnValue(mockPg);
|
||||||
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
});
|
||||||
describe('should', () => {
|
afterEach(() => {
|
||||||
it('update an existing profile when provided the user id', async () => {
|
jest.restoreAllMocks();
|
||||||
const mockUserProfile = {
|
});
|
||||||
user_id: '234',
|
|
||||||
diet: 'Nothing',
|
|
||||||
gender: 'female',
|
|
||||||
is_smoker: true,
|
|
||||||
}
|
|
||||||
const mockUpdateMade = {
|
|
||||||
gender: 'male'
|
|
||||||
}
|
|
||||||
const mockUpdatedProfile = {
|
|
||||||
user_id: '234',
|
|
||||||
diet: 'Nothing',
|
|
||||||
gender: 'male',
|
|
||||||
is_smoker: true,
|
|
||||||
}
|
|
||||||
const mockParams = {} as any;
|
|
||||||
const mockAuth = {
|
|
||||||
uid: '234'
|
|
||||||
}
|
|
||||||
|
|
||||||
mockPg.oneOrNone.mockResolvedValue(mockUserProfile);
|
describe('when given valid input', () => {
|
||||||
(supabaseUtils.update as jest.Mock).mockResolvedValue(mockUpdatedProfile);
|
it('should update an existing profile when provided the user id', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
avatar_url: "mockAvatarUrl"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockData = "success";
|
||||||
|
|
||||||
const result = await updateProfile(
|
(tryCatch as jest.Mock)
|
||||||
mockUpdateMade,
|
.mockResolvedValueOnce({data: true})
|
||||||
mockAuth as AuthedUser,
|
.mockResolvedValueOnce({data: mockData, error: null});
|
||||||
mockParams
|
|
||||||
|
const result = await updateProfile(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result).toBe(mockData);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||||
|
expect.stringContaining('select * from profiles where user_id = $1'),
|
||||||
|
[mockAuth.uid]
|
||||||
);
|
);
|
||||||
|
expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1);
|
||||||
|
expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockProps);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUsers.updateUser).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
mockAuth.uid,
|
||||||
|
{avatarUrl: mockProps.avatar_url}
|
||||||
|
);
|
||||||
|
expect(supabaseUtils.update).toBeCalledTimes(1);
|
||||||
|
expect(supabaseUtils.update).toBeCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
'profiles',
|
||||||
|
'user_id',
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockPg.oneOrNone.mock.calls.length).toBe(1);
|
describe('when an error occurs', () => {
|
||||||
expect(mockPg.oneOrNone.mock.calls[0][1]).toEqual([mockAuth.uid]);
|
it('should throw if the profile does not exist', async () => {
|
||||||
expect(result).toEqual(mockUpdatedProfile);
|
const mockProps = {
|
||||||
|
avatar_url: "mockAvatarUrl"
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(tryCatch as jest.Mock).mockResolvedValue({data: false});
|
||||||
|
|
||||||
|
expect(updateProfile(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Profile not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throw an error if a profile is not found', async () => {
|
it('should throw if unable to update the profile', async () => {
|
||||||
mockPg.oneOrNone.mockResolvedValue(null);
|
const mockProps = {
|
||||||
expect(updateProfile({} as any, {} as any, {} as any,))
|
avatar_url: "mockAvatarUrl"
|
||||||
.rejects
|
};
|
||||||
.toThrowError('Profile not found');
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
});
|
const mockReq = {} as any;
|
||||||
|
|
||||||
it('throw an error if unable to update the profile', async () => {
|
(tryCatch as jest.Mock)
|
||||||
const mockUserProfile = {
|
.mockResolvedValueOnce({data: true})
|
||||||
user_id: '234',
|
.mockResolvedValueOnce({data: null, error: Error});
|
||||||
diet: 'Nothing',
|
|
||||||
gender: 'female',
|
expect(updateProfile(mockProps, mockAuth, mockReq))
|
||||||
is_smoker: true,
|
|
||||||
}
|
|
||||||
const data = null;
|
|
||||||
const error = true;
|
|
||||||
const mockError = {
|
|
||||||
data,
|
|
||||||
error
|
|
||||||
}
|
|
||||||
mockPg.oneOrNone.mockResolvedValue(mockUserProfile);
|
|
||||||
(supabaseUtils.update as jest.Mock).mockRejectedValue(mockError);
|
|
||||||
expect(updateProfile({} as any, {} as any, {} as any,))
|
|
||||||
.rejects
|
.rejects
|
||||||
.toThrowError('Error updating profile');
|
.toThrow('Error updating profile');
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
101
backend/api/tests/unit/vote-unit.test.ts
Normal file
101
backend/api/tests/unit/vote-unit.test.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
jest.mock('shared/supabase/init');
|
||||||
|
jest.mock('shared/utils');
|
||||||
|
|
||||||
|
import { AuthedUser } from "api/helpers/endpoint";
|
||||||
|
import { vote } from "api/vote";
|
||||||
|
import * as supabaseInit from "shared/supabase/init";
|
||||||
|
import * as sharedUtils from "shared/utils";
|
||||||
|
|
||||||
|
describe('vote', () => {
|
||||||
|
let mockPg = {} as any;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockPg = {
|
||||||
|
one: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(supabaseInit.createSupabaseDirectClient as jest.Mock)
|
||||||
|
.mockReturnValue(mockPg);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
describe('when given valid input', () => {
|
||||||
|
it('should vote successfully', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
voteId: 1,
|
||||||
|
choice: 'for' as const,
|
||||||
|
priority: 10
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUser = {id: "mockUserId"};
|
||||||
|
const mockResult = "success";
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(mockPg.one as jest.Mock).mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
const result = await vote(mockProps, mockAuth, mockReq);
|
||||||
|
|
||||||
|
expect(result.data).toBe(mockResult);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledTimes(1);
|
||||||
|
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
|
||||||
|
expect(mockPg.one).toBeCalledTimes(1);
|
||||||
|
expect(mockPg.one).toBeCalledWith(
|
||||||
|
expect.stringContaining('insert into vote_results (user_id, vote_id, choice, priority)'),
|
||||||
|
[mockUser.id, mockProps.voteId, 1, mockProps.priority]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when an error occurs', () => {
|
||||||
|
it('should throw if unable to find the account', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
voteId: 1,
|
||||||
|
choice: 'for' as const,
|
||||||
|
priority: 10
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
|
||||||
|
|
||||||
|
expect(vote(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Your account was not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the choice is invalid', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
voteId: 1,
|
||||||
|
priority: 10
|
||||||
|
} as any;
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUser = {id: "mockUserId"};
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
|
||||||
|
expect(vote(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Invalid choice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if unable to record vote', async () => {
|
||||||
|
const mockProps = {
|
||||||
|
voteId: 1,
|
||||||
|
choice: 'for' as const,
|
||||||
|
priority: 10
|
||||||
|
};
|
||||||
|
const mockAuth = { uid: '321' } as AuthedUser;
|
||||||
|
const mockReq = {} as any;
|
||||||
|
const mockUser = {id: "mockUserId"};
|
||||||
|
|
||||||
|
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser);
|
||||||
|
(mockPg.one as jest.Mock).mockRejectedValue(new Error('Result error'));
|
||||||
|
|
||||||
|
expect(vote(mockProps, mockAuth, mockReq))
|
||||||
|
.rejects
|
||||||
|
.toThrow('Error recording vote');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
(
|
(
|
||||||
# Set PGPASSWORD
|
# Set PGPASSWORD
|
||||||
source ../../.env
|
source .env.df
|
||||||
|
|
||||||
# Target database connection info - replace with your target DB
|
# Target database connection info - replace with your target DB
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ export const API = (_apiTypeCheck = {
|
|||||||
authed: false,
|
authed: false,
|
||||||
rateLimited: false,
|
rateLimited: false,
|
||||||
props: z.object({}),
|
props: z.object({}),
|
||||||
returns: {} as { message: 'Server is working.'; uid?: string },
|
returns: {} as {
|
||||||
|
message: 'Server is working.'
|
||||||
|
uid?: string
|
||||||
|
version?: string
|
||||||
|
git?: { revision?: string; commitDate?: string; author?: string, message?: string }
|
||||||
|
},
|
||||||
summary: 'Check whether the API server is running',
|
summary: 'Check whether the API server is running',
|
||||||
tag: 'General',
|
tag: 'General',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const stoatLink = "https://stt.gg/YKQp81yA"
|
|||||||
export const redditLink = "https://www.reddit.com/r/CompassConnect"
|
export const redditLink = "https://www.reddit.com/r/CompassConnect"
|
||||||
export const xLink = "https://x.com/compassmeet"
|
export const xLink = "https://x.com/compassmeet"
|
||||||
export const formLink = "https://forms.gle/tKnXUMAbEreMK6FC6"
|
export const formLink = "https://forms.gle/tKnXUMAbEreMK6FC6"
|
||||||
|
export const ANDROID_APP_URL = 'https://play.google.com/store/apps/details?id=com.compassconnections.app'
|
||||||
|
|
||||||
export const IS_MAINTENANCE = false // set to true to enable the maintenance mode banner
|
export const IS_MAINTENANCE = false // set to true to enable the maintenance mode banner
|
||||||
|
|
||||||
@@ -26,3 +27,12 @@ export const WEB_GOOGLE_CLIENT_ID = '253367029065-khkj31qt22l0vc3v754h09vhpg6t33
|
|||||||
// export const ANDROID_GOOGLE_CLIENT_ID = '253367029065-s9sr5vqgkhc8f7p5s6ti6a4chqsrqgc4.apps.googleusercontent.com'
|
// export const ANDROID_GOOGLE_CLIENT_ID = '253367029065-s9sr5vqgkhc8f7p5s6ti6a4chqsrqgc4.apps.googleusercontent.com'
|
||||||
export const GOOGLE_CLIENT_ID = WEB_GOOGLE_CLIENT_ID
|
export const GOOGLE_CLIENT_ID = WEB_GOOGLE_CLIENT_ID
|
||||||
|
|
||||||
|
export const defaultLocale = 'en'
|
||||||
|
export const LOCALES = {
|
||||||
|
en: "English",
|
||||||
|
fr: "Français",
|
||||||
|
de: "Deutsch",
|
||||||
|
// es: "Español",
|
||||||
|
}
|
||||||
|
export const supportedLocales = Object.keys(LOCALES)
|
||||||
|
export type Locale = typeof supportedLocales[number]
|
||||||
3
common/src/parsing.ts
Normal file
3
common/src/parsing.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const toKey = (str: string | number | boolean) => {
|
||||||
|
return String(str).replace(/ /g, '_').toLowerCase()
|
||||||
|
}
|
||||||
@@ -20,7 +20,3 @@ export const STATUS_CHOICES: Record<string, string> = {
|
|||||||
expired: "Expired ⌛",
|
expired: "Expired ⌛",
|
||||||
archived: "Archived",
|
archived: "Archived",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const REVERSED_STATUS_CHOICES: Record<string, string> = Object.fromEntries(
|
|
||||||
Object.entries(STATUS_CHOICES).map(([key, value]) => [value, key])
|
|
||||||
)
|
|
||||||
@@ -32,6 +32,16 @@ yarn regen-types dev
|
|||||||
|
|
||||||
That's it!
|
That's it!
|
||||||
|
|
||||||
|
### Adding a new language
|
||||||
|
|
||||||
|
Adding a new language is very easy, especially with translating tools like large language models (ChatGPT, etc.) which you can use as first draft.
|
||||||
|
|
||||||
|
- Add the language to the LOCALES dictionary in [constants.ts](../common/src/constants.ts) (the key is the locale code, the value is the original language name (not in English)).
|
||||||
|
- Duplicate [fr.json](../web/messages/fr.json) and rename it to the locale code (e.g., `de.json` for German). Translate all the strings in the new file (keep the keys identical). In order to fit the bottom navigation bar on mobile, make sure the values for those keys are less than 10 characters: "nav.home", "nav.messages", "nav.more", "nav.notifs", "nav.people".
|
||||||
|
- Duplicate the [fr](../web/public/md/fr) folder and rename it to the locale code (e.g., `de` for German). Translate all the markdown files in the new folder.
|
||||||
|
|
||||||
|
That's all, no code needed!
|
||||||
|
|
||||||
### Cover with tests
|
### Cover with tests
|
||||||
|
|
||||||
Best Practices
|
Best Practices
|
||||||
@@ -43,4 +53,4 @@ Best Practices
|
|||||||
* Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves—test what they render.
|
* Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves—test what they render.
|
||||||
* Use jest.spyOn() for Internal Utilities . Avoid reaching into modules you don’t own.
|
* Use jest.spyOn() for Internal Utilities . Avoid reaching into modules you don’t own.
|
||||||
* Don't test just for coverage. Test to prevent regressions, document intent, and handle edge cases.
|
* Don't test just for coverage. Test to prevent regressions, document intent, and handle edge cases.
|
||||||
* Don't write end-to-end tests for features that change frequently unless absolutely necessary.
|
* Don't write end-to-end tests for features that change frequently unless absolutely necessary.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "compass",
|
"name": "compass",
|
||||||
"version": "1.8.0",
|
"version": "1.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"common",
|
"common",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"clean-install": "./scripts/install.sh",
|
"clean-install": "./scripts/install.sh",
|
||||||
"build-web-view": "./scripts/build_web_view.sh",
|
"build-web-view": "./scripts/build_web_view.sh",
|
||||||
"build-sync-android": "./scripts/build_sync_android.sh",
|
"build-sync-android": "./scripts/build_sync_android.sh",
|
||||||
|
"android-live-update": "./scripts/android_live_update.sh",
|
||||||
"sync-android": "./scripts/sync_android.sh",
|
"sync-android": "./scripts/sync_android.sh",
|
||||||
"migrate": "./scripts/migrate.sh",
|
"migrate": "./scripts/migrate.sh",
|
||||||
"test": "yarn workspaces run test",
|
"test": "yarn workspaces run test",
|
||||||
|
|||||||
41
scripts/android_live_update.sh
Executable file
41
scripts/android_live_update.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"/..
|
||||||
|
|
||||||
|
COMMIT_SHA=$(git rev-parse HEAD)
|
||||||
|
COMMIT_REF=$(git branch --show-current)
|
||||||
|
COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s")
|
||||||
|
COMMIT_DATE=$(git log -1 --pretty=format:"%cI")
|
||||||
|
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
cat <<EOF > web/public/live-update.json
|
||||||
|
{
|
||||||
|
"commitSha": "$COMMIT_SHA",
|
||||||
|
"commitRef": "$COMMIT_REF",
|
||||||
|
"commitMessage": "$COMMIT_MESSAGE",
|
||||||
|
"commitDate": "$COMMIT_DATE",
|
||||||
|
"buildDate": "$BUILD_DATE"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat web/public/live-update.json
|
||||||
|
|
||||||
|
yarn build-web-view
|
||||||
|
|
||||||
|
echo npx @capawesome/cli apps:bundles:create \
|
||||||
|
--app-id 969bc540-8077-492f-8403-b554bee5de50 \
|
||||||
|
--channel default \
|
||||||
|
--commitMessage "$COMMIT_MESSAGE" \
|
||||||
|
--commitRef $COMMIT_REF \
|
||||||
|
--commitSha $COMMIT_SHA \
|
||||||
|
--path web/out
|
||||||
|
|
||||||
|
npx @capawesome/cli apps:bundles:create \
|
||||||
|
--app-id 969bc540-8077-492f-8403-b554bee5de50 \
|
||||||
|
--channel default \
|
||||||
|
--commitMessage "$COMMIT_MESSAGE" \
|
||||||
|
--commitRef $COMMIT_REF \
|
||||||
|
--commitSha $COMMIT_SHA \
|
||||||
|
--path web/out
|
||||||
@@ -4,8 +4,6 @@ set -e
|
|||||||
|
|
||||||
cd "$(dirname "$0")"/..
|
cd "$(dirname "$0")"/..
|
||||||
|
|
||||||
export NEXT_PUBLIC_WEBVIEW=1
|
|
||||||
|
|
||||||
# Paths
|
# Paths
|
||||||
ROOT_ENV=".env" # your root .env
|
ROOT_ENV=".env" # your root .env
|
||||||
WEB_ENV="web/.env" # target for frontend
|
WEB_ENV="web/.env" # target for frontend
|
||||||
@@ -17,7 +15,13 @@ if [ -f "$WEB_ENV" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Filter NEXT_PUBLIC_* lines
|
# Filter NEXT_PUBLIC_* lines
|
||||||
grep '^NEXT_PUBLIC_' "$ROOT_ENV" > "$WEB_ENV"
|
if [ -f "$ROOT_ENV" ]; then
|
||||||
|
set -a
|
||||||
|
source "$ROOT_ENV"
|
||||||
|
set +a
|
||||||
|
echo "Sourced variables from $ROOT_ENV"
|
||||||
|
fi
|
||||||
|
env | grep '^NEXT_PUBLIC_' > "$WEB_ENV" || true
|
||||||
|
|
||||||
echo "Copied NEXT_PUBLIC_ variables to $WEB_ENV:"
|
echo "Copied NEXT_PUBLIC_ variables to $WEB_ENV:"
|
||||||
|
|
||||||
@@ -27,6 +31,8 @@ cat "$WEB_ENV"
|
|||||||
|
|
||||||
cd web
|
cd web
|
||||||
|
|
||||||
|
export NEXT_PUBLIC_WEBVIEW=1
|
||||||
|
|
||||||
rm -rf .next
|
rm -rf .next
|
||||||
|
|
||||||
# Hack to ignore getServerSideProps, getStaticProps and getStaticPaths for mobile webview build
|
# Hack to ignore getServerSideProps, getStaticProps and getStaticPaths for mobile webview build
|
||||||
|
|||||||
3
web/.gitignore
vendored
3
web/.gitignore
vendored
@@ -8,4 +8,5 @@ tsconfig.tsbuildinfo
|
|||||||
testing
|
testing
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
/public/live-update.json
|
||||||
|
|||||||
13
web/components/MarkdownPageLoader.tsx
Normal file
13
web/components/MarkdownPageLoader.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import MarkdownPage, {MD_PATHS} from 'web/components/markdown'
|
||||||
|
import {useMarkdown} from "web/hooks/use-markdown"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filename: typeof MD_PATHS[number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownPageLoader({filename}: Props) {
|
||||||
|
const content = useMarkdown(filename)
|
||||||
|
return <MarkdownPage content={content} filename={filename}/>
|
||||||
|
}
|
||||||
225
web/components/about-settings.tsx
Normal file
225
web/components/about-settings.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import {WithPrivateUser} from "web/components/user/with-user"
|
||||||
|
import {PrivateUser} from "common/user"
|
||||||
|
import {Col} from "web/components/layout/col"
|
||||||
|
import {HOSTING_ENV, IS_VERCEL} from "common/hosting/constants"
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {LiveUpdate} from "@capawesome/capacitor-live-update"
|
||||||
|
import {useEffect, useState} from "react"
|
||||||
|
import {App} from "@capacitor/app"
|
||||||
|
import {api} from "web/lib/api"
|
||||||
|
import {githubRepo} from "common/constants"
|
||||||
|
import {CustomLink} from "web/components/links"
|
||||||
|
import {Button} from "web/components/buttons/button"
|
||||||
|
import {getLiveUpdateInfo} from "web/lib/live-update";
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
|
export type WebBuild = {
|
||||||
|
gitSha?: string
|
||||||
|
gitMessage?: string
|
||||||
|
deploymentId?: string
|
||||||
|
environment?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LiveUpdateInfo = {
|
||||||
|
bundleId?: string | null
|
||||||
|
commitSha?: string
|
||||||
|
commitMessage?: string
|
||||||
|
commitDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Android = {
|
||||||
|
appVersion?: string
|
||||||
|
buildNumber?: string
|
||||||
|
liveUpdate?: LiveUpdateInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Backend = {
|
||||||
|
version?: string
|
||||||
|
gitSha?: string
|
||||||
|
gitMessage?: string
|
||||||
|
commitDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Runtime = {
|
||||||
|
platform: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Diagnostics = {
|
||||||
|
web?: WebBuild,
|
||||||
|
android?: Android
|
||||||
|
backend?: Backend
|
||||||
|
runtime: Runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDiagnostics() {
|
||||||
|
const [data, setData] = useState<Diagnostics | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
const diagnostics: Diagnostics = {
|
||||||
|
runtime: {
|
||||||
|
platform: IS_VERCEL
|
||||||
|
? 'web'
|
||||||
|
: Capacitor.isNativePlatform()
|
||||||
|
? 'android'
|
||||||
|
: HOSTING_ENV
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_VERCEL) {
|
||||||
|
diagnostics.web = {
|
||||||
|
environment: process.env.NEXT_PUBLIC_VERCEL_ENV,
|
||||||
|
gitSha: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
||||||
|
gitMessage: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE,
|
||||||
|
deploymentId: process.env.NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
const appInfo = await App.getInfo()
|
||||||
|
const bundle = await LiveUpdate.getCurrentBundle().catch(() => {return {bundleId: null}})
|
||||||
|
const buildInfo = await getLiveUpdateInfo().catch(() => null)
|
||||||
|
diagnostics.android = {
|
||||||
|
appVersion: appInfo.version,
|
||||||
|
buildNumber: appInfo.build,
|
||||||
|
liveUpdate: {
|
||||||
|
bundleId: bundle?.bundleId,
|
||||||
|
commitSha: buildInfo?.commitSha,
|
||||||
|
commitMessage: buildInfo?.commitMessage,
|
||||||
|
commitDate: buildInfo?.commitDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backend = await api('health').catch(() => null)
|
||||||
|
if (backend) {
|
||||||
|
diagnostics.backend = {
|
||||||
|
version: backend.version,
|
||||||
|
gitSha: backend.git?.revision,
|
||||||
|
gitMessage: backend.git?.message,
|
||||||
|
commitDate: backend.git?.commitDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnosticsToText(d: Diagnostics): string {
|
||||||
|
const replacer = (key: string, value: any) => {
|
||||||
|
if (value === null) return 'null'
|
||||||
|
if (value === undefined) return 'undefined'
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(d, replacer, 2)
|
||||||
|
.replace(/ {2}"/g, '')
|
||||||
|
.replace(/["{}\[\]]/g, '')
|
||||||
|
.replace(/^[ \t]*\n/gm, '')
|
||||||
|
.replace(/,\n/g, '\n')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const AboutSettings = () => (
|
||||||
|
<WithPrivateUser>
|
||||||
|
{user => <LoadedAboutSettings privateUser={user}/>}
|
||||||
|
</WithPrivateUser>
|
||||||
|
)
|
||||||
|
|
||||||
|
const LoadedAboutSettings = (props: {
|
||||||
|
privateUser: PrivateUser,
|
||||||
|
}) => {
|
||||||
|
const {} = props
|
||||||
|
|
||||||
|
const [copyFeedback, setCopyFeedback] = useState('')
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
|
const diagnostics = useDiagnostics()
|
||||||
|
if (!diagnostics) return null
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!diagnostics) return
|
||||||
|
await navigator.clipboard.writeText(diagnosticsToText(diagnostics))
|
||||||
|
setCopyFeedback(t('about.settings.copied', 'Copied!'))
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopyFeedback('')
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Col className={''}>
|
||||||
|
<RuntimeInfo info={diagnostics.runtime}/>
|
||||||
|
<WebBuildInfo info={diagnostics.web}/>
|
||||||
|
<AndroidInfo info={diagnostics.android}/>
|
||||||
|
<BackendInfo info={diagnostics.backend}/>
|
||||||
|
<Button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="w-fit mt-4"
|
||||||
|
>
|
||||||
|
{copyFeedback || t('about.settings.copy_info', 'Copy Info')}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
|
||||||
|
const WebBuildInfo = (props: {info?: WebBuild}) => {
|
||||||
|
const {info} = props
|
||||||
|
if (!info) return
|
||||||
|
const env = info.environment
|
||||||
|
const gitMessage = info.gitMessage
|
||||||
|
const sha = info.gitSha
|
||||||
|
const deploymentId = info.deploymentId
|
||||||
|
const url = `${githubRepo}/commit/${sha}`
|
||||||
|
return <Col className={'custom-link'}>
|
||||||
|
<h3>Web build (Vercel)</h3>
|
||||||
|
<p>Commit SHA: <CustomLink href={url}>{sha}</CustomLink></p>
|
||||||
|
<p>Commit message: {gitMessage}</p>
|
||||||
|
<p>Vercel deployment ID: {deploymentId}</p>
|
||||||
|
<p>Environment: {env}</p>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AndroidInfo = (props: {info?: Android}) => {
|
||||||
|
const {info} = props
|
||||||
|
if (!info) return
|
||||||
|
const sha = info.liveUpdate?.commitSha
|
||||||
|
const url = `${githubRepo}/commit/${sha}`
|
||||||
|
return <Col className={'custom-link'}>
|
||||||
|
<h3>Android (Capacitor / Capawesome)</h3>
|
||||||
|
<p>App version (Android): {info.appVersion}</p>
|
||||||
|
<p>Native build number (Android): {info.buildNumber}</p>
|
||||||
|
<p>Live update build ID (Capawesome): {info.liveUpdate?.bundleId}</p>
|
||||||
|
<p>Live update commit SHA (Capawesome): <CustomLink href={url}>{sha}</CustomLink></p>
|
||||||
|
<p>Live update commit message (Capawesome): {info.liveUpdate?.commitMessage}</p>
|
||||||
|
<p>Live update commit date (Capawesome): {info.liveUpdate?.commitDate}</p>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
|
||||||
|
const BackendInfo = (props: {info?: Backend}) => {
|
||||||
|
const {info} = props
|
||||||
|
if (!info) return
|
||||||
|
const sha = info.gitSha
|
||||||
|
const commitDate = info.commitDate
|
||||||
|
const commitMessage = info.gitMessage
|
||||||
|
const url = `${githubRepo}/commit/${sha}`
|
||||||
|
return <Col className={'custom-link'}>
|
||||||
|
<h3>Backend</h3>
|
||||||
|
<p>API version: {info.version}</p>
|
||||||
|
{sha && <p>API commit SHA: <CustomLink href={url}>{sha}</CustomLink></p>}
|
||||||
|
{commitMessage && <p>API commit message: {commitMessage}</p>}
|
||||||
|
{commitDate && <p>API commit date: {commitDate}</p>}
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuntimeInfo = (props: {info?: Runtime}) => {
|
||||||
|
const {info} = props
|
||||||
|
if (!info) return
|
||||||
|
return <Col className={'custom-link'}>
|
||||||
|
<h3>Runtime</h3>
|
||||||
|
<p>Platform: {info.platform}</p>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
@@ -4,21 +4,22 @@ import {Col} from "web/components/layout/col";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {colClassName, labelClassName} from "web/pages/signup";
|
import {colClassName, labelClassName} from "web/pages/signup";
|
||||||
import {MultiCheckbox} from "web/components/multi-checkbox";
|
import {MultiCheckbox} from "web/components/multi-checkbox";
|
||||||
import {capitalize} from "lodash";
|
|
||||||
|
|
||||||
export function AddOptionEntry(props: {
|
export function AddOptionEntry(props: {
|
||||||
|
title: string
|
||||||
choices: { [key: string]: string }
|
choices: { [key: string]: string }
|
||||||
setChoices: (choices: any) => void
|
setChoices: (choices: any) => void
|
||||||
profile: ProfileWithoutUser,
|
profile: ProfileWithoutUser,
|
||||||
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void
|
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void
|
||||||
label: OptionTableKey,
|
label: OptionTableKey,
|
||||||
}) {
|
}) {
|
||||||
const {profile, setProfile, label, choices, setChoices} = props
|
const {profile, setProfile, label, choices, setChoices, title} = props
|
||||||
return <Col className={clsx(colClassName)}>
|
return <Col className={clsx(colClassName)}>
|
||||||
<label className={clsx(labelClassName)}>{capitalize(label)}</label>
|
<label className={clsx(labelClassName)}>{title}</label>
|
||||||
<MultiCheckbox
|
<MultiCheckbox
|
||||||
choices={choices}
|
choices={choices}
|
||||||
selected={profile[label] ?? []}
|
selected={profile[label] ?? []}
|
||||||
|
translationPrefix={`profile.${label}`}
|
||||||
onChange={(selected) => setProfile(label, selected)}
|
onChange={(selected) => setProfile(label, selected)}
|
||||||
addOption={(v: string) => {
|
addOption={(v: string) => {
|
||||||
console.log(`Adding ${label}:`, v)
|
console.log(`Adding ${label}:`, v)
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ import {AnswerCompatibilityQuestionContent} from './answer-compatibility-questio
|
|||||||
import {uniq} from 'lodash'
|
import {uniq} from 'lodash'
|
||||||
import {QuestionWithCountType} from 'web/hooks/use-questions'
|
import {QuestionWithCountType} from 'web/hooks/use-questions'
|
||||||
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants'
|
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants'
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export function AddCompatibilityQuestionButton(props: {
|
export function AddCompatibilityQuestionButton(props: {
|
||||||
refreshCompatibilityAll: () => void
|
refreshCompatibilityAll: () => void
|
||||||
}) {
|
}) {
|
||||||
const { refreshCompatibilityAll } = props
|
const { refreshCompatibilityAll } = props
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const t = useT()
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +34,7 @@ export function AddCompatibilityQuestionButton(props: {
|
|||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
submit your own!
|
{t('answers.add.submit_own', 'submit your own!')}
|
||||||
</button>
|
</button>
|
||||||
<AddCompatibilityQuestionModal
|
<AddCompatibilityQuestionModal
|
||||||
open={open}
|
open={open}
|
||||||
@@ -94,6 +96,7 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const { afterAddQuestion, setOpen } = props
|
const { afterAddQuestion, setOpen } = props
|
||||||
|
const t = useT()
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
const [options, setOptions] = useState<string[]>(['', ''])
|
const [options, setOptions] = useState<string[]>(['', ''])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -144,7 +147,7 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
}
|
}
|
||||||
track('create compatibility question')
|
track('create compatibility question')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('Error creating compatibility question. Try again?')
|
toast.error(t('answers.add.error_create', 'Error creating compatibility question. Try again?'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -152,7 +155,8 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
<Col className="w-full gap-4 main-font">
|
<Col className="w-full gap-4 main-font">
|
||||||
<Col className="gap-1">
|
<Col className="gap-1">
|
||||||
<label>
|
<label>
|
||||||
Question<span className={'text-scarlet-500'}>*</span>
|
{t('answers.add.question_label', 'Question')}
|
||||||
|
<span className={'text-scarlet-500'}>*</span>
|
||||||
</label>
|
</label>
|
||||||
<ExpandingInput
|
<ExpandingInput
|
||||||
maxLength={MAX_COMPATIBILITY_QUESTION_LENGTH}
|
maxLength={MAX_COMPATIBILITY_QUESTION_LENGTH}
|
||||||
@@ -162,7 +166,8 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-1">
|
<Col className="gap-1">
|
||||||
<label>
|
<label>
|
||||||
Options<span className={'text-scarlet-500'}>*</span>
|
{t('answers.add.options_label', 'Options')}
|
||||||
|
<span className={'text-scarlet-500'}>*</span>
|
||||||
</label>
|
</label>
|
||||||
<Col className="w-full gap-1">
|
<Col className="w-full gap-1">
|
||||||
{options.map((o, index) => (
|
{options.map((o, index) => (
|
||||||
@@ -171,7 +176,7 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
value={options[index]}
|
value={options[index]}
|
||||||
onChange={(e) => onOptionChange(index, e.target.value)}
|
onChange={(e) => onOptionChange(index, e.target.value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder={`Option ${index + 1}`}
|
placeholder={t('answers.add.option_placeholder', 'Option {n}', { n: String(index + 1) })}
|
||||||
rows={1}
|
rows={1}
|
||||||
maxLength={MAX_ANSWER_LENGTH}
|
maxLength={MAX_ANSWER_LENGTH}
|
||||||
/>
|
/>
|
||||||
@@ -188,7 +193,7 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
<Button onClick={addOption} color="gray-outline">
|
<Button onClick={addOption} color="gray-outline">
|
||||||
<Row className="items-center gap-1">
|
<Row className="items-center gap-1">
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Option
|
{t('answers.add.add_option', 'Add Option')}
|
||||||
</Row>
|
</Row>
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -201,7 +206,7 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
{t('settings.action.cancel', 'Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -211,7 +216,7 @@ function CreateCompatibilityModalContent(props: {
|
|||||||
}}
|
}}
|
||||||
disabled={!optionsAreValid || !questionIsValid || !noRepeatOptions}
|
disabled={!optionsAreValid || !questionIsValid || !noRepeatOptions}
|
||||||
>
|
>
|
||||||
Submit & Answer
|
{t('answers.add.submit_and_answer', 'Submit & Answer')}
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {Modal, MODAL_CLASS} from 'web/components/layout/modal'
|
|||||||
import {AnswerCompatibilityQuestionContent} from './answer-compatibility-question-content'
|
import {AnswerCompatibilityQuestionContent} from './answer-compatibility-question-content'
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export function AnswerCompatibilityQuestionButton(props: {
|
export function AnswerCompatibilityQuestionButton(props: {
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
@@ -23,6 +24,7 @@ export function AnswerCompatibilityQuestionButton(props: {
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
} = props
|
} = props
|
||||||
const [open, setOpen] = useState(fromSignup ?? false)
|
const [open, setOpen] = useState(fromSignup ?? false)
|
||||||
|
const t = useT()
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
if (otherQuestions.length === 0) return null
|
if (otherQuestions.length === 0) return null
|
||||||
const isCore = otherQuestions.some((q) => q.importance_score === 0)
|
const isCore = otherQuestions.some((q) => q.importance_score === 0)
|
||||||
@@ -31,7 +33,7 @@ export function AnswerCompatibilityQuestionButton(props: {
|
|||||||
<>
|
<>
|
||||||
{size === 'md' ? (
|
{size === 'md' ? (
|
||||||
<Button onClick={() => setOpen(true)} color="none" className={'px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 hover:text-ink-900'}>
|
<Button onClick={() => setOpen(true)} color="none" className={'px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 hover:text-ink-900'}>
|
||||||
Answer{isCore && ' Core'} Questions{' '}
|
{t('answers.answer.cta', 'Answer{core} Questions', { core: isCore ? ' Core' : '' })}{' '}
|
||||||
<span className="text-primary-600 ml-2">
|
<span className="text-primary-600 ml-2">
|
||||||
+{questionsToAnswer.length}
|
+{questionsToAnswer.length}
|
||||||
</span>
|
</span>
|
||||||
@@ -41,7 +43,7 @@ export function AnswerCompatibilityQuestionButton(props: {
|
|||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className="bg-ink-100 dark:bg-ink-300 text-ink-1000 hover:bg-ink-200 hover:dark:bg-ink-400 w-28 rounded-full px-2 py-0.5 text-xs transition-colors"
|
className="bg-ink-100 dark:bg-ink-300 text-ink-1000 hover:bg-ink-200 hover:dark:bg-ink-400 w-28 rounded-full px-2 py-0.5 text-xs transition-colors"
|
||||||
>
|
>
|
||||||
Answer yourself
|
{t('answers.answer.answer_yourself', 'Answer yourself')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<AnswerCompatibilityQuestionModal
|
<AnswerCompatibilityQuestionModal
|
||||||
@@ -59,11 +61,12 @@ export function AnswerCompatibilityQuestionButton(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CompatibilityPageButton() {
|
export function CompatibilityPageButton() {
|
||||||
|
const t = useT()
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href="/compatibility"
|
href="/compatibility"
|
||||||
className="px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 flex items-center justify-center text-center"
|
className="px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 flex items-center justify-center text-center"
|
||||||
>View List of Questions</Link>
|
>{t('answers.answer.view_list', 'View List of Questions')}</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +77,7 @@ export function AnswerSkippedCompatibilityQuestionsButton(props: {
|
|||||||
}) {
|
}) {
|
||||||
const {user, skippedQuestions, refreshCompatibilityAll} = props
|
const {user, skippedQuestions, refreshCompatibilityAll} = props
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const t = useT()
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -81,7 +85,7 @@ export function AnswerSkippedCompatibilityQuestionsButton(props: {
|
|||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className="text-ink-500 text-sm hover:underline"
|
className="text-ink-500 text-sm hover:underline"
|
||||||
>
|
>
|
||||||
Answer {skippedQuestions.length} skipped questions{' '}
|
{t('answers.answer.answer_skipped', 'Answer {n} skipped questions', { n: String(skippedQuestions.length) })}{' '}
|
||||||
</button>
|
</button>
|
||||||
<AnswerCompatibilityQuestionModal
|
<AnswerCompatibilityQuestionModal
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {track} from 'web/lib/service/analytics'
|
|||||||
import {api} from 'web/lib/api'
|
import {api} from 'web/lib/api'
|
||||||
import {filterKeys} from '../questions-form'
|
import {filterKeys} from '../questions-form'
|
||||||
import toast from "react-hot-toast"
|
import toast from "react-hot-toast"
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export type CompatibilityAnswerSubmitType = Omit<
|
export type CompatibilityAnswerSubmitType = Omit<
|
||||||
rowFor<'compatibility_answers'>,
|
rowFor<'compatibility_answers'>,
|
||||||
@@ -72,6 +73,7 @@ export const submitCompatibilityAnswer = async (
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set compatibility answer:', error)
|
console.error('Failed to set compatibility answer:', error)
|
||||||
|
// Note: toast not localized here due to lack of hook; callers may handle UI feedback
|
||||||
toast.error('Error submitting. Try again?')
|
toast.error('Error submitting. Try again?')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +88,7 @@ export const deleteCompatibilityAnswer = async (
|
|||||||
await track('delete compatibility question', {id})
|
await track('delete compatibility question', {id})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete prompt answer:', error)
|
console.error('Failed to delete prompt answer:', error)
|
||||||
|
// Note: toast not localized here due to lack of hook; callers may handle UI feedback
|
||||||
toast.error('Error deleting. Try again?')
|
toast.error('Error deleting. Try again?')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +125,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
index,
|
index,
|
||||||
total,
|
total,
|
||||||
} = props
|
} = props
|
||||||
|
const t = useT()
|
||||||
const [answer, setAnswer] = useState<CompatibilityAnswerSubmitType>(
|
const [answer, setAnswer] = useState<CompatibilityAnswerSubmitType>(
|
||||||
(props.answer as CompatibilityAnswerSubmitType) ??
|
(props.answer as CompatibilityAnswerSubmitType) ??
|
||||||
getEmptyAnswer(user.id, compatibilityQuestion.id)
|
getEmptyAnswer(user.id, compatibilityQuestion.id)
|
||||||
@@ -175,7 +179,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
{shortenedPopularity && (
|
{shortenedPopularity && (
|
||||||
<Row className="text-ink-500 select-none items-center text-sm">
|
<Row className="text-ink-500 select-none items-center text-sm">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={`${shortenedPopularity} people have answered this question`}
|
text={t('answers.content.people_answered', '{count} people have answered this question', { count: String(shortenedPopularity) })}
|
||||||
>
|
>
|
||||||
{shortenedPopularity}
|
{shortenedPopularity}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -190,7 +194,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<span className="text-ink-500 text-sm">Your answer</span>
|
<span className="text-ink-500 text-sm">{t('answers.preferred.your_answer', 'Your answer')}</span>
|
||||||
<SelectAnswer
|
<SelectAnswer
|
||||||
value={answer.multiple_choice}
|
value={answer.multiple_choice}
|
||||||
setValue={(choice) =>
|
setValue={(choice) =>
|
||||||
@@ -200,7 +204,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<span className="text-ink-500 text-sm">Answers you'll accept</span>
|
<span className="text-ink-500 text-sm">{t('answers.content.answers_you_accept', "Answers you'll accept")}</span>
|
||||||
<MultiSelectAnswers
|
<MultiSelectAnswers
|
||||||
values={answer.pref_choices ?? []}
|
values={answer.pref_choices ?? []}
|
||||||
setValue={(choice) =>
|
setValue={(choice) =>
|
||||||
@@ -210,10 +214,10 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<span className="text-ink-500 text-sm">Importance</span>
|
<span className="text-ink-500 text-sm">{t('answers.content.importance', 'Importance')}</span>
|
||||||
<RadioToggleGroup
|
<RadioToggleGroup
|
||||||
currentChoice={answer.importance ?? -1}
|
currentChoice={answer.importance ?? -1}
|
||||||
choicesMap={IMPORTANCE_CHOICES}
|
choicesMap={Object.fromEntries(Object.entries(IMPORTANCE_CHOICES).map(([k, v]) => [t(`answers.importance.${v}`, k), v]))}
|
||||||
setChoice={(choice: number) =>
|
setChoice={(choice: number) =>
|
||||||
setAnswer({...answer, importance: choice})
|
setAnswer({...answer, importance: choice})
|
||||||
}
|
}
|
||||||
@@ -222,7 +226,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col className="-mt-6 gap-2">
|
<Col className="-mt-6 gap-2">
|
||||||
<span className="text-ink-500 text-sm">
|
<span className="text-ink-500 text-sm">
|
||||||
Your thoughts (optional, but recommended)
|
{t('answers.content.your_thoughts', 'Your thoughts (optional, but recommended)')}
|
||||||
</span>
|
</span>
|
||||||
<ExpandingInput
|
<ExpandingInput
|
||||||
className={'w-full'}
|
className={'w-full'}
|
||||||
@@ -259,7 +263,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
skipLoading && 'animate-pulse'
|
skipLoading && 'animate-pulse'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Skip
|
{t('answers.menu.skip', 'Skip')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -284,7 +288,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
|||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLastQuestion ? 'Finish' : 'Next'}
|
{isLastQuestion ? t('answers.finish', 'Finish') : t('answers.next', 'Next')}
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import clsx from 'clsx'
|
|||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { shortenName } from 'web/components/widgets/user-link'
|
import { shortenName } from 'web/components/widgets/user-link'
|
||||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/outline'
|
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/outline'
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export function PreferredList(props: {
|
export function PreferredList(props: {
|
||||||
question: QuestionWithCountType
|
question: QuestionWithCountType
|
||||||
@@ -16,6 +17,7 @@ export function PreferredList(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { question, answer, comparedAnswer, comparedUser, isComparedUser } =
|
const { question, answer, comparedAnswer, comparedUser, isComparedUser } =
|
||||||
props
|
props
|
||||||
|
const t = useT()
|
||||||
const { multiple_choice_options } = question
|
const { multiple_choice_options } = question
|
||||||
if (!multiple_choice_options) return null
|
if (!multiple_choice_options) return null
|
||||||
const sortedEntries = Object.entries(multiple_choice_options).sort(
|
const sortedEntries = Object.entries(multiple_choice_options).sort(
|
||||||
@@ -54,8 +56,9 @@ export function PreferredList(props: {
|
|||||||
) : (
|
) : (
|
||||||
<XCircleIcon className="h-4 w-4" />
|
<XCircleIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{isComparedUser ? 'Your' : shortenName(comparedUser.name) + "'s"}{' '}
|
{isComparedUser
|
||||||
answer
|
? t('answers.preferred.your_answer', 'Your answer')
|
||||||
|
: t('answers.preferred.user_answer', "{name}'s answer", { name: shortenName(comparedUser.name) })}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {buildArray} from 'common/util/array'
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import {useCompatibleProfiles} from "web/hooks/use-profiles";
|
import {useCompatibleProfiles} from "web/hooks/use-profiles";
|
||||||
import {CompatibleBadge} from "web/components/widgets/compatible-badge";
|
import {CompatibleBadge} from "web/components/widgets/compatible-badge";
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
const NUM_QUESTIONS_TO_SHOW = 8
|
const NUM_QUESTIONS_TO_SHOW = 8
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ export function CompatibilityQuestionsDisplay(props: {
|
|||||||
fromProfilePage?: Profile
|
fromProfilePage?: Profile
|
||||||
}) {
|
}) {
|
||||||
const {isCurrentUser, user, fromSignup, fromProfilePage, profile} = props
|
const {isCurrentUser, user, fromSignup, fromProfilePage, profile} = props
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
const currentUser = useUser()
|
const currentUser = useUser()
|
||||||
const compatibleProfiles = useCompatibleProfiles(currentUser?.id)
|
const compatibleProfiles = useCompatibleProfiles(currentUser?.id)
|
||||||
@@ -175,9 +177,11 @@ export function CompatibilityQuestionsDisplay(props: {
|
|||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
<Row className="flex-wrap items-center justify-between gap-x-6 gap-y-4">
|
<Row className="flex-wrap items-center justify-between gap-x-6 gap-y-4">
|
||||||
<Row className={'gap-8'}>
|
<Row className={'gap-8'}>
|
||||||
<Subtitle>{`${
|
<Subtitle>
|
||||||
isCurrentUser ? 'Your' : shortenName(user.name) + `'s`
|
{isCurrentUser
|
||||||
} Compatibility Prompts`}</Subtitle>
|
? t('answers.display.your_prompts', 'Your Compatibility Prompts')
|
||||||
|
: t('answers.display.user_prompts', "{name}'s Compatibility Prompts", { name: shortenName(user.name) })}
|
||||||
|
</Subtitle>
|
||||||
{compatibilityScore &&
|
{compatibilityScore &&
|
||||||
<CompatibleBadge compatibility={compatibilityScore} className={'mt-4 mr-4'}/>
|
<CompatibleBadge compatibility={compatibilityScore} className={'mt-4 mr-4'}/>
|
||||||
}
|
}
|
||||||
@@ -194,10 +198,11 @@ export function CompatibilityQuestionsDisplay(props: {
|
|||||||
</Row>
|
</Row>
|
||||||
{answeredQuestions.length <= 0 ? (
|
{answeredQuestions.length <= 0 ? (
|
||||||
<span className="text-ink-600 text-sm">
|
<span className="text-ink-600 text-sm">
|
||||||
{isCurrentUser ? "You haven't" : `${user.name} hasn't`} answered any
|
{isCurrentUser
|
||||||
compatibility questions yet!{' '}
|
? t('answers.display.none_answered_you', "You haven't answered any compatibility questions yet!")
|
||||||
|
: t('answers.display.none_answered_user', "{name} hasn't answered any compatibility questions yet!", { name: user.name })}{' '}
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<>Add some to better see who you'd be most compatible with.</>
|
<>{t('answers.display.add_some', "Add some to better see who you'd be most compatible with.")}</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -206,11 +211,11 @@ export function CompatibilityQuestionsDisplay(props: {
|
|||||||
<span className='custom-link'>
|
<span className='custom-link'>
|
||||||
{otherQuestions.length < 1 ? (
|
{otherQuestions.length < 1 ? (
|
||||||
<span className="text-ink-600 text-sm">
|
<span className="text-ink-600 text-sm">
|
||||||
You've already answered all the compatibility questions—
|
{t('answers.display.already_answered_all', "You've already answered all the compatibility questions—")}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-ink-600 text-sm">
|
<span className="text-ink-600 text-sm">
|
||||||
Answer more questions to increase your compatibility scores—or{' '}
|
{t('answers.display.answer_more', 'Answer more questions to increase your compatibility scores—or ')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<AddCompatibilityQuestionButton
|
<AddCompatibilityQuestionButton
|
||||||
@@ -233,7 +238,7 @@ export function CompatibilityQuestionsDisplay(props: {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{shownAnswers.length === 0 && (
|
{shownAnswers.length === 0 && (
|
||||||
<div className="text-ink-500">None</div>
|
<div className="text-ink-500">{t('answers.display.none', 'None')}</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -279,13 +284,14 @@ function CompatibilitySortWidget(props: {
|
|||||||
const {sort, setSort, user, fromProfilePage, className} = props
|
const {sort, setSort, user, fromProfilePage, className} = props
|
||||||
const currentUser = useUser()
|
const currentUser = useUser()
|
||||||
|
|
||||||
|
const t = useT()
|
||||||
const sortToDisplay = {
|
const sortToDisplay = {
|
||||||
'your-important': fromProfilePage
|
'your-important': fromProfilePage
|
||||||
? `Important to ${fromProfilePage.user.name}`
|
? t('answers.sort.important_to_user', 'Important to {name}', { name: fromProfilePage.user.name })
|
||||||
: 'Important to you',
|
: t('answers.sort.important_to_you', 'Important to you'),
|
||||||
'their-important': `Important to ${user.name}`,
|
'their-important': t('answers.sort.important_to_them', 'Important to {name}', { name: user.name }),
|
||||||
disagree: 'Incompatible',
|
disagree: t('answers.sort.incompatible', 'Incompatible'),
|
||||||
'your-unanswered': 'Unanswered by you',
|
'your-unanswered': t('answers.sort.unanswered_by_you', 'Unanswered by you'),
|
||||||
}
|
}
|
||||||
|
|
||||||
const shownSorts = buildArray(
|
const shownSorts = buildArray(
|
||||||
@@ -339,6 +345,7 @@ export function CompatibilityAnswerBlock(props: {
|
|||||||
const [editOpen, setEditOpen] = useState<boolean>(false)
|
const [editOpen, setEditOpen] = useState<boolean>(false)
|
||||||
const currentUser = useUser()
|
const currentUser = useUser()
|
||||||
const currentProfile = useProfile()
|
const currentProfile = useProfile()
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
const [newAnswer, setNewAnswer] = useState<CompatibilityAnswerSubmitType | undefined>(props.answer)
|
const [newAnswer, setNewAnswer] = useState<CompatibilityAnswerSubmitType | undefined>(props.answer)
|
||||||
|
|
||||||
@@ -408,12 +415,12 @@ export function CompatibilityAnswerBlock(props: {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
name: 'Edit',
|
name: t('answers.menu.edit', 'Edit'),
|
||||||
icon: <PencilIcon className="h-5 w-5"/>,
|
icon: <PencilIcon className="h-5 w-5"/>,
|
||||||
onClick: () => setEditOpen(true),
|
onClick: () => setEditOpen(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Delete',
|
name: t('answers.menu.delete', 'Delete'),
|
||||||
icon: <TrashIcon className="h-5 w-5"/>,
|
icon: <TrashIcon className="h-5 w-5"/>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
deleteCompatibilityAnswer(answer.id, user.id)
|
deleteCompatibilityAnswer(answer.id, user.id)
|
||||||
@@ -436,7 +443,7 @@ export function CompatibilityAnswerBlock(props: {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
name: 'Skip',
|
name: t('answers.menu.skip', 'Skip'),
|
||||||
icon: <TrashIcon className="h-5 w-5"/>,
|
icon: <TrashIcon className="h-5 w-5"/>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
submitCompatibilityAnswer(getEmptyAnswer(user.id, question.id))
|
submitCompatibilityAnswer(getEmptyAnswer(user.id, question.id))
|
||||||
@@ -469,9 +476,9 @@ export function CompatibilityAnswerBlock(props: {
|
|||||||
{distinctPreferredAnswersText.length > 0 && (
|
{distinctPreferredAnswersText.length > 0 && (
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<div className="text-ink-800 text-sm">
|
<div className="text-ink-800 text-sm">
|
||||||
{preferredDoesNotIncludeAnswerText
|
{preferredDoesNotIncludeAnswerText
|
||||||
? 'Acceptable'
|
? t('answers.display.acceptable', 'Acceptable')
|
||||||
: 'Also acceptable'}
|
: t('answers.display.also_acceptable', 'Also acceptable')}
|
||||||
</div>
|
</div>
|
||||||
<Row className="flex-wrap gap-2 mt-0">
|
<Row className="flex-wrap gap-2 mt-0">
|
||||||
{distinctPreferredAnswersText.map((text) => (
|
{distinctPreferredAnswersText.map((text) => (
|
||||||
@@ -568,6 +575,8 @@ function CompatibilityDisplay(props: {
|
|||||||
currentUser,
|
currentUser,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
const [answer2, setAnswer2] = useState<
|
const [answer2, setAnswer2] = useState<
|
||||||
rowFor<'compatibility_answers'> | null | undefined
|
rowFor<'compatibility_answers'> | null | undefined
|
||||||
>(undefined)
|
>(undefined)
|
||||||
@@ -635,7 +644,7 @@ function CompatibilityDisplay(props: {
|
|||||||
: 'bg-red-500/20 hover:bg-red-500/30'
|
: 'bg-red-500/20 hover:bg-red-500/30'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{answerCompatibility ? 'Compatible' : 'Incompatible'}
|
{answerCompatibility ? t('answers.compatible', 'Compatible') : t('answers.incompatible', 'Incompatible')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -644,10 +653,10 @@ function CompatibilityDisplay(props: {
|
|||||||
<Subtitle>{question.question}</Subtitle>
|
<Subtitle>{question.question}</Subtitle>
|
||||||
<Col className={clsx('w-full gap-1', SCROLLABLE_MODAL_CLASS)}>
|
<Col className={clsx('w-full gap-1', SCROLLABLE_MODAL_CLASS)}>
|
||||||
<div className="text-ink-600 items-center gap-2">
|
<div className="text-ink-600 items-center gap-2">
|
||||||
{`${shortenName(user1.name)}'s preferred answers`}
|
{t('answers.modal.preferred_of_user', "{name}'s preferred answers", { name: shortenName(user1.name) })}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ink-500 text-sm">
|
<div className="text-ink-500 text-sm">
|
||||||
{shortenName(user1.name)} marked this as{' '}
|
{t('answers.modal.user_marked', '{name} marked this as ', { name: shortenName(user1.name) })}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
<ImportanceDisplay importance={answer1.importance}/>
|
<ImportanceDisplay importance={answer1.importance}/>
|
||||||
</span>
|
</span>
|
||||||
@@ -666,13 +675,14 @@ function CompatibilityDisplay(props: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="text-ink-600 mt-6 items-center gap-2">
|
<div className="text-ink-600 mt-6 items-center gap-2">
|
||||||
{`${
|
{isCurrentUser
|
||||||
isCurrentUser ? 'Your' : shortenName(user2.name) + `'s`
|
? t('answers.modal.your_preferred', 'Your preferred answers')
|
||||||
} preferred answers`}
|
: t('answers.modal.preferred_of_user', "{name}'s preferred answers", { name: shortenName(user2.name) })}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ink-500 text-sm">
|
<div className="text-ink-500 text-sm">
|
||||||
{isCurrentUser ? 'You' : shortenName(user2.name)} marked this
|
{isCurrentUser
|
||||||
as{' '}
|
? t('answers.modal.you_marked', 'You marked this as ')
|
||||||
|
: t('answers.modal.user_marked', '{name} marked this as ', { name: shortenName(user2.name) })}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
<ImportanceDisplay importance={answer2.importance}/>
|
<ImportanceDisplay importance={answer2.importance}/>
|
||||||
</span>
|
</span>
|
||||||
@@ -694,9 +704,10 @@ function CompatibilityDisplay(props: {
|
|||||||
|
|
||||||
function ImportanceDisplay(props: { importance: number }) {
|
function ImportanceDisplay(props: { importance: number }) {
|
||||||
const {importance} = props
|
const {importance} = props
|
||||||
|
const t = useT()
|
||||||
return (
|
return (
|
||||||
<span className={clsx('w-fit')}>
|
<span className={clsx('w-fit')}>
|
||||||
{getStringKeyFromNumValue(importance, IMPORTANCE_CHOICES)}
|
{t(`answers.importance.${importance}`, getStringKeyFromNumValue(importance, IMPORTANCE_CHOICES) as string)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {IndividualQuestionRow} from '../questions-form'
|
|||||||
import {TbMessage} from 'react-icons/tb'
|
import {TbMessage} from 'react-icons/tb'
|
||||||
import {OtherProfileAnswers} from './other-profile-answers'
|
import {OtherProfileAnswers} from './other-profile-answers'
|
||||||
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export function AddQuestionButton(props: {
|
export function AddQuestionButton(props: {
|
||||||
isFirstQuestion?: boolean
|
isFirstQuestion?: boolean
|
||||||
@@ -22,12 +23,13 @@ export function AddQuestionButton(props: {
|
|||||||
false,
|
false,
|
||||||
`add-question-${user.id}`
|
`add-question-${user.id}`
|
||||||
)
|
)
|
||||||
|
const t = useT()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button color={'gray-outline'} onClick={() => setOpenModal(true)}>
|
<Button color={'gray-outline'} onClick={() => setOpenModal(true)}>
|
||||||
<Row className="items-center gap-1">
|
<Row className="items-center gap-1">
|
||||||
<PlusIcon className="h-4 w-4"/>
|
<PlusIcon className="h-4 w-4"/>
|
||||||
Add Free Response
|
{t('answers.free.add_free_response', 'Add Free Response')}
|
||||||
</Row>
|
</Row>
|
||||||
</Button>
|
</Button>
|
||||||
<AddQuestionModal
|
<AddQuestionModal
|
||||||
@@ -63,6 +65,7 @@ function AddQuestionModal(props: {
|
|||||||
null,
|
null,
|
||||||
`selected-expanded-question-${user.id}}`
|
`selected-expanded-question-${user.id}}`
|
||||||
)
|
)
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} setOpen={setOpen}>
|
<Modal open={open} setOpen={setOpen}>
|
||||||
@@ -91,7 +94,7 @@ function AddQuestionModal(props: {
|
|||||||
) : selectedQuestion == null ? (
|
) : selectedQuestion == null ? (
|
||||||
<>
|
<>
|
||||||
<div className="text-primary-600 w-full font-semibold">
|
<div className="text-primary-600 w-full font-semibold">
|
||||||
Choose a question to answer
|
{t('answers.free.choose_question', 'Choose a question to answer')}
|
||||||
</div>
|
</div>
|
||||||
<Col className={SCROLLABLE_MODAL_CLASS}>
|
<Col className={SCROLLABLE_MODAL_CLASS}>
|
||||||
{addableQuestions.map((question) => {
|
{addableQuestions.map((question) => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { partition } from 'lodash'
|
|||||||
import { shortenName } from 'web/components/widgets/user-link'
|
import { shortenName } from 'web/components/widgets/user-link'
|
||||||
import { AddQuestionButton } from './free-response-add-question'
|
import { AddQuestionButton } from './free-response-add-question'
|
||||||
import { Profile } from 'common/profiles/profile'
|
import { Profile } from 'common/profiles/profile'
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export function FreeResponseDisplay(props: {
|
export function FreeResponseDisplay(props: {
|
||||||
isCurrentUser: boolean
|
isCurrentUser: boolean
|
||||||
@@ -34,6 +35,7 @@ export function FreeResponseDisplay(props: {
|
|||||||
fromProfilePage: Profile | undefined
|
fromProfilePage: Profile | undefined
|
||||||
}) {
|
}) {
|
||||||
const { isCurrentUser, user, fromProfilePage } = props
|
const { isCurrentUser, user, fromProfilePage } = props
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
const { refreshAnswers, answers: allAnswers } = useUserAnswers(user?.id)
|
const { refreshAnswers, answers: allAnswers } = useUserAnswers(user?.id)
|
||||||
|
|
||||||
@@ -59,9 +61,11 @@ export function FreeResponseDisplay(props: {
|
|||||||
return (
|
return (
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<Row className={'w-full items-center justify-between gap-2'}>
|
<Row className={'w-full items-center justify-between gap-2'}>
|
||||||
<Subtitle>{`${
|
<Subtitle>
|
||||||
isCurrentUser ? 'Your' : shortenName(user.name) + `'s`
|
{isCurrentUser
|
||||||
} Free Response`}</Subtitle>
|
? t('answers.free.your_title', 'Your Free Response')
|
||||||
|
: t('answers.free.user_title', "{name}'s Free Response", { name: shortenName(user.name) })}
|
||||||
|
</Subtitle>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
@@ -101,6 +105,7 @@ function AnswerBlock(props: {
|
|||||||
const { answer, questions, isCurrentUser, user, refreshAnswers } = props
|
const { answer, questions, isCurrentUser, user, refreshAnswers } = props
|
||||||
const question = questions.find((q) => q.id === answer.question_id)
|
const question = questions.find((q) => q.id === answer.question_id)
|
||||||
const [edit, setEdit] = useState(false)
|
const [edit, setEdit] = useState(false)
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
const [otherAnswerModal, setOtherAnswerModal] = useState<boolean>(false)
|
const [otherAnswerModal, setOtherAnswerModal] = useState<boolean>(false)
|
||||||
|
|
||||||
@@ -119,18 +124,18 @@ function AnswerBlock(props: {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
name: 'Edit',
|
name: t('answers.menu.edit', 'Edit'),
|
||||||
icon: <PencilIcon className="h-5 w-5" />,
|
icon: <PencilIcon className="h-5 w-5" />,
|
||||||
onClick: () => setEdit(true),
|
onClick: () => setEdit(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Delete',
|
name: t('answers.menu.delete', 'Delete'),
|
||||||
icon: <XIcon className="h-5 w-5" />,
|
icon: <XIcon className="h-5 w-5" />,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
deleteAnswer(answer, user.id).then(() => refreshAnswers()),
|
deleteAnswer(answer, user.id).then(() => refreshAnswers()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `See ${question.answer_count} other answers`,
|
name: t('answers.free.see_others', 'See {count} other answers', { count: String(question.answer_count) }),
|
||||||
icon: <TbMessage className="h-5 w-5" />,
|
icon: <TbMessage className="h-5 w-5" />,
|
||||||
onClick: () => setOtherAnswerModal(true),
|
onClick: () => setOtherAnswerModal(true),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Col } from 'web/components/layout/col'
|
|||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Subtitle } from '../widgets/profile-subtitle'
|
import { Subtitle } from '../widgets/profile-subtitle'
|
||||||
import { BiTachometer } from 'react-icons/bi'
|
import { BiTachometer } from 'react-icons/bi'
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export function OpinionScale(props: {
|
export function OpinionScale(props: {
|
||||||
multiChoiceAnswers: rowFor<'compatibility_answers_free'>[]
|
multiChoiceAnswers: rowFor<'compatibility_answers_free'>[]
|
||||||
@@ -17,6 +18,7 @@ export function OpinionScale(props: {
|
|||||||
isCurrentUser: boolean
|
isCurrentUser: boolean
|
||||||
}) {
|
}) {
|
||||||
const { multiChoiceAnswers, questions, isCurrentUser } = props
|
const { multiChoiceAnswers, questions, isCurrentUser } = props
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
const answeredMultiChoice = multiChoiceAnswers.filter(
|
const answeredMultiChoice = multiChoiceAnswers.filter(
|
||||||
(a) => a.multiple_choice != null && a.multiple_choice != -1
|
(a) => a.multiple_choice != null && a.multiple_choice != -1
|
||||||
@@ -28,7 +30,7 @@ export function OpinionScale(props: {
|
|||||||
<Button color="indigo" onClick={() => Router.push('opinion-scale')}>
|
<Button color="indigo" onClick={() => Router.push('opinion-scale')}>
|
||||||
<Row className="items-center gap-1">
|
<Row className="items-center gap-1">
|
||||||
<BiTachometer className="h-5 w-5" />
|
<BiTachometer className="h-5 w-5" />
|
||||||
Fill Opinion Scale
|
{t('answers.opinion.fill', 'Fill Opinion Scale')}
|
||||||
</Row>
|
</Row>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@@ -39,7 +41,7 @@ export function OpinionScale(props: {
|
|||||||
return (
|
return (
|
||||||
<Col className="gap-2">
|
<Col className="gap-2">
|
||||||
<Row className={'w-full items-center justify-between gap-2'}>
|
<Row className={'w-full items-center justify-between gap-2'}>
|
||||||
<Subtitle>Opinion Scale</Subtitle>
|
<Subtitle>{t('answers.opinion.title', 'Opinion Scale')}</Subtitle>
|
||||||
|
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<Button
|
<Button
|
||||||
@@ -52,7 +54,7 @@ export function OpinionScale(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="mr-2 h-4 w-4" />
|
<PencilIcon className="mr-2 h-4 w-4" />
|
||||||
Edit
|
{t('answers.opinion.edit', 'Edit')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -13,24 +13,28 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import {MIN_BIO_LENGTH} from "common/constants";
|
import {MIN_BIO_LENGTH} from "common/constants";
|
||||||
import {ShowMore} from 'web/components/widgets/show-more'
|
import {ShowMore} from 'web/components/widgets/show-more'
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
const placeHolder = "Tell us about yourself — and what you're looking for!";
|
export function BioTips() {
|
||||||
|
const t = useT();
|
||||||
const tips = `
|
const tips = t('profile.bio.tips_list', `
|
||||||
- Your core values, interests, and activities
|
- Your core values, interests, and activities
|
||||||
- Personality traits, what makes you unique and what you care about
|
- Personality traits, what makes you unique and what you care about
|
||||||
- Connection goals (collaborative, friendship, romantic)
|
- Connection goals (collaborative, friendship, romantic)
|
||||||
- Expectations and boundaries
|
- Expectations and boundaries
|
||||||
- Availability, how to contact you or start a conversation (email, social media, etc.)
|
- Availability, how to contact you or start a conversation (email, social media, etc.)
|
||||||
- Optional: romantic preferences, lifestyle habits, and conversation starters
|
- Optional: romantic preferences, lifestyle habits, and conversation starters
|
||||||
`
|
`);
|
||||||
|
|
||||||
export function BioTips() {
|
|
||||||
return (
|
return (
|
||||||
<ShowMore labelClosed="Tips" labelOpen="Hide info" className={'custom-link text-sm'}>
|
<ShowMore
|
||||||
<p>Write a clear and engaging bio to help others understand who you are and the connections you seek. Include:</p>
|
labelClosed={t('profile.bio.tips', 'Tips')}
|
||||||
|
labelOpen={t('profile.bio.hide_info', 'Hide info')}
|
||||||
|
className={'custom-link text-sm'}
|
||||||
|
>
|
||||||
|
<p>{t('profile.bio.tips_intro', "Write a clear and engaging bio to help others understand who you are and the connections you seek. Include:")}</p>
|
||||||
<ReactMarkdown>{tips}</ReactMarkdown>
|
<ReactMarkdown>{tips}</ReactMarkdown>
|
||||||
<Link href="/tips-bio" target="_blank">Read full tips for writing a high-quality bio</Link>
|
<Link href="/tips-bio" target="_blank">{t('profile.bio.tips_link', 'Read full tips for writing a high-quality bio')}</Link>
|
||||||
</ShowMore>
|
</ShowMore>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -43,6 +47,7 @@ export function EditableBio(props: {
|
|||||||
const {profile, onCancel, onSave} = props
|
const {profile, onCancel, onSave} = props
|
||||||
const [editor, setEditor] = useState<any>(null)
|
const [editor, setEditor] = useState<any>(null)
|
||||||
const [textLength, setTextLength] = useState(0);
|
const [textLength, setTextLength] = useState(0);
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
const hideButtons = (textLength === 0) && !profile.bio
|
const hideButtons = (textLength === 0) && !profile.bio
|
||||||
|
|
||||||
@@ -74,7 +79,7 @@ export function EditableBio(props: {
|
|||||||
<Row className="absolute bottom-1 right-1 justify-between gap-2">
|
<Row className="absolute bottom-1 right-1 justify-between gap-2">
|
||||||
{onCancel && (
|
{onCancel && (
|
||||||
<Button size="xs" color="gray-outline" onClick={onCancel}>
|
<Button size="xs" color="gray-outline" onClick={onCancel}>
|
||||||
Cancel
|
{t('profile.bio.cancel', 'Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -84,7 +89,7 @@ export function EditableBio(props: {
|
|||||||
onSave()
|
onSave()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{t('profile.bio.save', 'Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
@@ -115,13 +120,15 @@ interface BaseBioProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BaseBio({defaultValue, onBlur, onEditor}: BaseBioProps) {
|
export function BaseBio({defaultValue, onBlur, onEditor}: BaseBioProps) {
|
||||||
|
const t = useT();
|
||||||
const editor = useTextEditor({
|
const editor = useTextEditor({
|
||||||
// extensions: [StarterKit],
|
// extensions: [StarterKit],
|
||||||
max: MAX_DESCRIPTION_LENGTH,
|
max: MAX_DESCRIPTION_LENGTH,
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
placeholder: placeHolder,
|
placeholder: t('profile.bio.placeholder', "Tell us about yourself — and what you're looking for!"),
|
||||||
})
|
})
|
||||||
const textLength = editor?.getText().length ?? 0
|
const textLength = editor?.getText().length ?? 0
|
||||||
|
const remainingChars = MIN_BIO_LENGTH - textLength
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onEditor?.(editor)
|
onEditor?.(editor)
|
||||||
@@ -131,8 +138,10 @@ export function BaseBio({defaultValue, onBlur, onEditor}: BaseBioProps) {
|
|||||||
<div>
|
<div>
|
||||||
{textLength < MIN_BIO_LENGTH &&
|
{textLength < MIN_BIO_LENGTH &&
|
||||||
<p>
|
<p>
|
||||||
Add {MIN_BIO_LENGTH - textLength} more {MIN_BIO_LENGTH - textLength === 1 ? 'character' : 'characters'} so
|
{remainingChars === 1
|
||||||
you can appear in search results—or take your time and start by exploring others.
|
? t('profile.bio.add_characters_one', 'Add {count} more character so you can appear in search results—or take your time and start by exploring others.', {count: remainingChars})
|
||||||
|
: t('profile.bio.add_characters_many', 'Add {count} more characters so you can appear in search results—or take your time and start by exploring others.', {count: remainingChars})
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<BioTips/>
|
<BioTips/>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Content } from 'web/components/widgets/editor'
|
|||||||
import { updateProfile } from 'web/lib/api'
|
import { updateProfile } from 'web/lib/api'
|
||||||
import { EditableBio } from './editable-bio'
|
import { EditableBio } from './editable-bio'
|
||||||
import { tryCatch } from 'common/util/try-catch'
|
import { tryCatch } from 'common/util/try-catch'
|
||||||
|
import { useT } from 'web/lib/locale'
|
||||||
|
|
||||||
export function BioBlock(props: {
|
export function BioBlock(props: {
|
||||||
isCurrentUser: boolean
|
isCurrentUser: boolean
|
||||||
@@ -19,6 +20,7 @@ export function BioBlock(props: {
|
|||||||
setEdit: (edit: boolean) => void
|
setEdit: (edit: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const { isCurrentUser, refreshProfile, profile, edit, setEdit } = props
|
const { isCurrentUser, refreshProfile, profile, edit, setEdit } = props
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
@@ -47,12 +49,12 @@ export function BioBlock(props: {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
name: 'Edit',
|
name: t('profile.bio.edit', 'Edit'),
|
||||||
icon: <PencilIcon className="h-5 w-5" />,
|
icon: <PencilIcon className="h-5 w-5" />,
|
||||||
onClick: () => setEdit(true),
|
onClick: () => setEdit(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Delete',
|
name: t('profile.bio.delete', 'Delete'),
|
||||||
icon: <XIcon className="h-5 w-5" />,
|
icon: <XIcon className="h-5 w-5" />,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const { error } = await tryCatch(updateProfile({ bio: null }))
|
const { error } = await tryCatch(updateProfile({ bio: null }))
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {MAX_INT, MIN_BIO_LENGTH} from "common/constants";
|
|||||||
import {useTextEditor} from "web/components/widgets/editor";
|
import {useTextEditor} from "web/components/widgets/editor";
|
||||||
import {JSONContent} from "@tiptap/core"
|
import {JSONContent} from "@tiptap/core"
|
||||||
import {flip, offset, shift, useFloating} from "@floating-ui/react-dom";
|
import {flip, offset, shift, useFloating} from "@floating-ui/react-dom";
|
||||||
|
import {useT} from "web/lib/locale";
|
||||||
|
|
||||||
export default function TooShortBio() {
|
export default function TooShortBio() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const t = useT();
|
||||||
const {y, refs, strategy} = useFloating({
|
const {y, refs, strategy} = useFloating({
|
||||||
placement: "bottom", // place below the trigger
|
placement: "bottom", // place below the trigger
|
||||||
middleware: [
|
middleware: [
|
||||||
@@ -21,7 +23,7 @@ export default function TooShortBio() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="text-red-600">
|
<p className="text-red-600">
|
||||||
Bio too short. Profile may be filtered from search results.{" "}
|
{t('profile.bio.too_short', "Bio too short. Profile may be filtered from search results.")}{" "}
|
||||||
<span
|
<span
|
||||||
className="inline-flex align-middle"
|
className="inline-flex align-middle"
|
||||||
onMouseEnter={() => setOpen(true)}
|
onMouseEnter={() => setOpen(true)}
|
||||||
@@ -46,9 +48,7 @@ export default function TooShortBio() {
|
|||||||
className="p-3 bg-canvas-50 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50 transition-opacity w-72 max-w-[calc(100vw-1rem)] whitespace-normal break-words"
|
className="p-3 bg-canvas-50 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50 transition-opacity w-72 max-w-[calc(100vw-1rem)] whitespace-normal break-words"
|
||||||
>
|
>
|
||||||
<p className="text-sm text-gray-800 dark:text-gray-100">
|
<p className="text-sm text-gray-800 dark:text-gray-100">
|
||||||
Since your bio is too short, Compass' algorithm filters out your
|
{t('profile.bio.too_short_tooltip', "Since your bio is too short, Compass' algorithm filters out your profile from search results (unless \"Include short bios\" is selected). This ensures searches show meaningful profiles.")}
|
||||||
profile from search results (unless "Include short bios" is
|
|
||||||
selected). This ensures searches show meaningful profiles.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -67,6 +67,7 @@ export function ProfileBio(props: {
|
|||||||
const [edit, setEdit] = useState(false)
|
const [edit, setEdit] = useState(false)
|
||||||
const editor = useTextEditor({defaultValue: ''})
|
const editor = useTextEditor({defaultValue: ''})
|
||||||
const [textLength, setTextLength] = useState(MAX_INT)
|
const [textLength, setTextLength] = useState(MAX_INT)
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
@@ -80,7 +81,7 @@ export function ProfileBio(props: {
|
|||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
{textLength < MIN_BIO_LENGTH && !edit && isCurrentUser && <TooShortBio/>}
|
{textLength < MIN_BIO_LENGTH && !edit && isCurrentUser && <TooShortBio/>}
|
||||||
<Subtitle className="mb-4">About Me</Subtitle>
|
<Subtitle className="mb-4">{t('profile.bio.about_me', 'About Me')}</Subtitle>
|
||||||
<BioBlock
|
<BioBlock
|
||||||
isCurrentUser={isCurrentUser}
|
isCurrentUser={isCurrentUser}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {useClickOutside} from "web/hooks/use-click-outside"
|
|||||||
import {PrivateChatMessage} from "common/chat-message";
|
import {PrivateChatMessage} from "common/chat-message";
|
||||||
import {updateReactionUI} from "web/lib/supabase/chat-messages";
|
import {updateReactionUI} from "web/lib/supabase/chat-messages";
|
||||||
import {useIsMobile} from "web/hooks/use-is-mobile";
|
import {useIsMobile} from "web/hooks/use-is-mobile";
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
const REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '👎']
|
const REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '👎']
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export function MessageActions(props: {
|
|||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isOwner = user?.id === message.userId
|
const isOwner = user?.id === message.userId
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
useClickOutside(emojiPickerRef, () => {
|
useClickOutside(emojiPickerRef, () => {
|
||||||
setShowEmojiPicker(false)
|
setShowEmojiPicker(false)
|
||||||
@@ -50,17 +52,17 @@ export function MessageActions(props: {
|
|||||||
}, [openEmojiPickerKey])
|
}, [openEmojiPickerKey])
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!confirm('Are you sure you want to delete this message?')) return
|
if (!confirm(t('messages.delete_confirm', 'Are you sure you want to delete this message?'))) return
|
||||||
const messageId = message.id
|
const messageId = message.id
|
||||||
try {
|
try {
|
||||||
await api('delete-message', {messageId})
|
await api('delete-message', {messageId})
|
||||||
toast.success('Message deleted')
|
toast.success(t('messages.deleted', 'Message deleted'))
|
||||||
setMessages?.((prevMessages) => {
|
setMessages?.((prevMessages) => {
|
||||||
if (!prevMessages) return prevMessages
|
if (!prevMessages) return prevMessages
|
||||||
return prevMessages.filter((m) => m.id !== messageId)
|
return prevMessages.filter((m) => m.id !== messageId)
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to delete message')
|
toast.error(t('messages.delete_failed', 'Failed to delete message'))
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,17 +99,17 @@ export function MessageActions(props: {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={[
|
items={[
|
||||||
isOwner && {
|
isOwner && {
|
||||||
name: 'Edit',
|
name: t('messages.action.edit', 'Edit'),
|
||||||
icon: <PencilIcon className="h-4 w-4"/>,
|
icon: <PencilIcon className="h-4 w-4"/>,
|
||||||
onClick: onRequestEdit,
|
onClick: onRequestEdit,
|
||||||
},
|
},
|
||||||
isOwner && {
|
isOwner && {
|
||||||
name: 'Delete',
|
name: t('messages.action.delete', 'Delete'),
|
||||||
icon: <TrashIcon className="h-4 w-4"/>,
|
icon: <TrashIcon className="h-4 w-4"/>,
|
||||||
onClick: handleDelete,
|
onClick: handleDelete,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Add Reaction',
|
name: t('messages.action.add_reaction', 'Add Reaction'),
|
||||||
icon: <EmojiHappyIcon className="h-4 w-4"/>,
|
icon: <EmojiHappyIcon className="h-4 w-4"/>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setShowEmojiPicker(!showEmojiPicker)
|
setShowEmojiPicker(!showEmojiPicker)
|
||||||
@@ -121,7 +123,7 @@ export function MessageActions(props: {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/*{message.isEdited && (*/}
|
{/*{message.isEdited && (*/}
|
||||||
{/* <span className="text-xs text-gray-400">edited</span>*/}
|
{/* <span className="text-xs text-gray-400">{t('messages.edited', 'edited')}</span>*/}
|
||||||
{/*)}*/}
|
{/*)}*/}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Tooltip } from 'web/components/widgets/tooltip'
|
|||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
import { useEvent } from 'web/hooks/use-event'
|
import { useEvent } from 'web/hooks/use-event'
|
||||||
|
import {useT} from "web/lib/locale";
|
||||||
|
|
||||||
export function CommentInput(props: {
|
export function CommentInput(props: {
|
||||||
replyToUserInfo?: ReplyToUserInfo
|
replyToUserInfo?: ReplyToUserInfo
|
||||||
@@ -144,6 +145,8 @@ export function CommentInputTextArea(props: {
|
|||||||
commentTypes = ['comment'],
|
commentTypes = ['comment'],
|
||||||
cancelEditing,
|
cancelEditing,
|
||||||
} = props
|
} = props
|
||||||
|
const t = useT()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editor?.setEditable(!isSubmitting)
|
editor?.setEditable(!isSubmitting)
|
||||||
}, [isSubmitting, editor])
|
}, [isSubmitting, editor])
|
||||||
@@ -216,7 +219,7 @@ export function CommentInputTextArea(props: {
|
|||||||
className="text-primary-600 hover:underline"
|
className="text-primary-600 hover:underline"
|
||||||
onClick={cancelEditing}
|
onClick={cancelEditing}
|
||||||
>
|
>
|
||||||
Cancel
|
{t("comment.cancel", "Cancel")}
|
||||||
</button>
|
</button>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,30 +10,29 @@ import {Title} from "web/components/widgets/title";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {formLink} from "common/constants";
|
import {formLink} from "common/constants";
|
||||||
|
import {useT} from 'web/lib/locale' // added
|
||||||
|
|
||||||
export function ContactComponent() {
|
export function ContactComponent() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const t = useT() // use translations
|
||||||
|
|
||||||
const editor = useTextEditor({
|
const editor = useTextEditor({
|
||||||
max: MAX_DESCRIPTION_LENGTH,
|
max: MAX_DESCRIPTION_LENGTH,
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
placeholder: 'Contact us here...',
|
placeholder: t('contact.editor.placeholder', 'Contact us here...'), // localized placeholder
|
||||||
})
|
})
|
||||||
|
|
||||||
const showButton = !!editor?.getText().length
|
const showButton = !!editor?.getText().length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="mx-2">
|
<Col className="mx-2">
|
||||||
<Title className="!mb-2 text-3xl">Contact</Title>
|
<Title className="!mb-2 text-3xl">{t('contact.title', 'Contact')}</Title>
|
||||||
<p className={'custom-link mb-4'}>
|
<p className={'custom-link mb-4'}>
|
||||||
You can also contact us through this <Link href={formLink}>feedback form</Link> or any of our <Link
|
{t('contact.intro_prefix', "You can also contact us through this ")}
|
||||||
href={'/social'}>socials</Link>. Feel free to give your contact information if you'd like us to get back to you.
|
<Link href={formLink}>{t('contact.form_link', 'feedback form')}</Link>
|
||||||
</p>
|
{t('contact.intro_middle', ' or any of our ')}
|
||||||
<h4 className="">Android App</h4>
|
<Link href={'/social'}>{t('contact.socials', 'socials')}</Link>
|
||||||
<p className={'custom-link mb-4'}>
|
{t('contact.intro_suffix', ". Feel free to give your contact information if you'd like us to get back to you.")}
|
||||||
To release our app, Google requires a closed test with at least 12 testers for 14 days. Please share your Google Play–registered email address so we can add you as a tester.
|
|
||||||
You'll be able to download the app from the Play Store and use it right away.
|
|
||||||
Your email address will NOT be shared with anyone else and will be used solely for the purpose of the review process.
|
|
||||||
</p>
|
</p>
|
||||||
<Col>
|
<Col>
|
||||||
<div className={'mb-2'}>
|
<div className={'mb-2'}>
|
||||||
@@ -52,14 +51,14 @@ export function ContactComponent() {
|
|||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
};
|
};
|
||||||
const result = await api('contact', data).catch(() => {
|
const result = await api('contact', data).catch(() => {
|
||||||
toast.error('Failed to contact — try again or contact us...')
|
toast.error(t('contact.toast.failed', 'Failed to contact — try again or contact us...'))
|
||||||
})
|
})
|
||||||
if (!result) return
|
if (!result) return
|
||||||
editor.commands.clearContent()
|
editor.commands.clearContent()
|
||||||
toast.success('Thank you for your message!')
|
toast.success(t('contact.toast.success', 'Thank you for your message!'))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Submit
|
{t('contact.submit', 'Submit')}
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { RangeSlider } from 'web/components/widgets/slider'
|
import {RangeSlider} from 'web/components/widgets/slider'
|
||||||
import {FilterFields} from "common/filters";
|
import {FilterFields} from "common/filters"
|
||||||
|
import {useT} from 'web/lib/locale'
|
||||||
|
|
||||||
export const PREF_AGE_MIN = 18
|
export const PREF_AGE_MIN = 18
|
||||||
export const PREF_AGE_MAX = 100
|
export const PREF_AGE_MAX = 100
|
||||||
@@ -22,11 +23,12 @@ export function AgeFilterText(props: {
|
|||||||
const { pref_age_min, pref_age_max, highlightedClass } = props
|
const { pref_age_min, pref_age_max, highlightedClass } = props
|
||||||
const [noMinAge, noMaxAge] = getNoMinMaxAge(pref_age_min, pref_age_max)
|
const [noMinAge, noMaxAge] = getNoMinMaxAge(pref_age_min, pref_age_max)
|
||||||
|
|
||||||
|
const t = useT()
|
||||||
if (noMinAge && noMaxAge) {
|
if (noMinAge && noMaxAge) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span className={clsx('text-semibold', highlightedClass)}>Any</span>{' '}
|
<span className={clsx('text-semibold', highlightedClass)}>{t('filter.age.any', 'Any')}</span>{' '}
|
||||||
<span className="hidden sm:inline">age</span>
|
<span className="hidden sm:inline">{t('filter.age.age', 'age')}</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -37,7 +39,7 @@ export function AgeFilterText(props: {
|
|||||||
{'<'}
|
{'<'}
|
||||||
{pref_age_max}
|
{pref_age_max}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
years
|
{t('filter.age.years', 'years')}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,7 @@ export function AgeFilterText(props: {
|
|||||||
{'>'}
|
{'>'}
|
||||||
{pref_age_min}
|
{pref_age_min}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
years
|
{t('filter.age.years', 'years')}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -59,7 +61,7 @@ export function AgeFilterText(props: {
|
|||||||
{' - '}
|
{' - '}
|
||||||
{pref_age_max}
|
{pref_age_max}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
years
|
{t('filter.age.years', 'years')}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user