129 Commits
1.8.0 ... 1.9.0

Author SHA1 Message Date
MartinBraquet
26d9851c9e Merge remote-tracking branch 'origin/main' 2026-01-15 16:57:38 +01:00
MartinBraquet
fbfb959de2 Release 2026-01-15 16:57:26 +01:00
Okechi Jones-Williams
449b32d4bc Adding/Updating API unit tests (#26) 2026-01-15 16:54:29 +01:00
MartinBraquet
a3c479ff92 Release live update: Fix profile not found upon creation 2026-01-15 16:42:55 +01:00
MartinBraquet
51c97adce4 Fix profile not found upon creation 2026-01-15 16:32:21 +01:00
MartinBraquet
cde8a0d97f Add /press page 2026-01-15 16:07:39 +01:00
MartinBraquet
e257a10fdb Add bottom margin 2026-01-15 16:06:59 +01:00
MartinBraquet
6df9c5519a Translate 404 2026-01-15 16:06:42 +01:00
MartinBraquet
33a50b7e7e Update FAQ about android app 2026-01-12 11:36:59 +01:00
MartinBraquet
5029af9c5f Android release 2026-01-08 12:53:59 +02:00
Okechi Jones-Williams
7b61c70f6d Add more API endoints unit tests (#25)
* setting up test structure

* .

* added playwright config file, deleted original playwright folder and moved "some.test" file

* continued test structure setup

* Updating test folder structure

* Added database seeding script and backend testing folder structure

* removed the database test

* Replaced db seeding script

* Updated userInformation.ts to use values from choices.tsx

* merge prep

* removing extra unit test, moving api test to correct folder

* Pushing to get help with sql Unit test

* Updating get-profiles unit tests

* Added more unit tests

* .

* Added more unit tests

* Added getSupabaseToken unit test

* .

* excluding supabase token test so ci can pass

* .

* Seperated the seedDatabase func into its own file so it can be accessed seperatly

* Fixed failing test

* .

* .

* Fix tests

* Fix lint

* Clean

* Fixed module paths in compute-score unit test

* Updated root tsconfig to recognise backend/shared

* Added create comment unit test

* Added some unit tests

* Working on createProfile return issue

* .

* Fixes

* Updated Create profile unit test

* Updating create user unit test

* Add create-user unit tests

* .

* Added more unit tests

* Added more unit tests

* .

* Apply suggestion from @MartinBraquet

* .

* Added unit tests

* Added unit tests

---------

Co-authored-by: MartinBraquet <martin.braquet@gmail.com>
2026-01-08 12:52:49 +02:00
Okechi Jones-Williams
87059494a3 Add unit tests for continue in /create-profile (#24)
* setting up test structure

* .

* added playwright config file, deleted original playwright folder and moved "some.test" file

* continued test structure setup

* Updating test folder structure

* Added database seeding script and backend testing folder structure

* removed the database test

* Replaced db seeding script

* Updated userInformation.ts to use values from choices.tsx

* merge prep

* removing extra unit test, moving api test to correct folder

* Pushing to get help with sql Unit test

* Updating get-profiles unit tests

* Added more unit tests

* .

* Added more unit tests

* Added getSupabaseToken unit test

* .

* excluding supabase token test so ci can pass

* .

* Seperated the seedDatabase func into its own file so it can be accessed seperatly

* Fixed failing test

* .

* .

* Fix tests

* Fix lint

* Clean

* Fixed module paths in compute-score unit test

* Updated root tsconfig to recognise backend/shared

* Added create comment unit test

* Added some unit tests

* Working on createProfile return issue

* .

* Fixes

* Updated Create profile unit test

* Apply suggestion from @MartinBraquet

---------

Co-authored-by: MartinBraquet <martin.braquet@gmail.com>
2026-01-06 13:04:07 +02:00
Martin Braquet
e7f348e34e Update capawesome.json 2026-01-05 10:17:47 +01:00
Martin Braquet
f953b5c10b Update development.md 2026-01-05 10:10:26 +01:00
Martin Braquet
d75f179a46 Update de.json 2026-01-05 09:43:53 +01:00
Martin Braquet
87713a7803 Update README.md 2026-01-05 09:38:43 +01:00
MartinBraquet
ac7091ae06 Fix e2e 2026-01-04 13:58:18 +02:00
MartinBraquet
f4f3aa80f1 Fix missing translation for has kids 2026-01-04 13:58:11 +02:00
MartinBraquet
fa82a30907 Fix e2e tests 2026-01-04 13:48:13 +02:00
MartinBraquet
2e81ef25f1 Check that all keys are translated in german 2026-01-04 13:28:10 +02:00
MartinBraquet
12ef6a891b FInish german translation 2026-01-04 13:27:32 +02:00
MartinBraquet
fb3f5e5ace Add german translation 2026-01-04 13:03:23 +02:00
MartinBraquet
9537500fe1 Fix conditional hooks 2026-01-04 12:51:23 +02:00
MartinBraquet
bb423ba0e4 Translate filters 2026-01-04 12:42:53 +02:00
MartinBraquet
21db5cb481 Add docs for translation 2026-01-03 14:09:52 +02:00
MartinBraquet
078425f1b2 Translate search and save buttons 2026-01-03 13:50:15 +02:00
MartinBraquet
77e3b56b65 Translate search 2026-01-03 13:48:24 +02:00
MartinBraquet
2ed23e05fa Translate profile grid 2026-01-03 13:38:09 +02:00
MartinBraquet
486a4a81f7 Fix compat translation 2026-01-03 13:25:34 +02:00
MartinBraquet
9a0f0c0892 Translate profile forms 2026-01-03 13:18:21 +02:00
MartinBraquet
4abed529d3 Translate compat prompts 2026-01-02 19:23:12 +02:00
MartinBraquet
4293a8c24b Translate endorsements 2026-01-02 18:42:24 +02:00
MartinBraquet
60721eefb2 Translate profile bio 2026-01-02 18:19:28 +02:00
MartinBraquet
e5c8650df0 Translate profile-about.tsx to french 2026-01-02 18:11:29 +02:00
MartinBraquet
8e6d6584ea Add profile about translation features 2026-01-02 17:42:56 +02:00
MartinBraquet
077f3ac1a3 Fix 2026-01-02 15:44:37 +02:00
MartinBraquet
04b8e21769 Translate compat questions 2026-01-02 15:38:34 +02:00
MartinBraquet
bc672db79a Translate some profile blocks 2026-01-02 15:13:51 +02:00
MartinBraquet
dc7be2d334 Translate comment input 2026-01-02 15:10:58 +02:00
MartinBraquet
08c9f60010 Translate profiles 2026-01-02 15:05:27 +02:00
MartinBraquet
0bb52e72f7 Translate messages 2026-01-02 14:59:53 +02:00
MartinBraquet
593c2ac024 Translate about settings 2026-01-02 14:46:38 +02:00
MartinBraquet
8712424b89 Translate theme and notif settings 2026-01-02 14:42:16 +02:00
MartinBraquet
c408d895b1 Translate vote statuses 2026-01-02 14:25:10 +02:00
MartinBraquet
33c2121c8d Translate /referrals 2026-01-02 14:22:46 +02:00
MartinBraquet
fa151e79d3 Translate /stats charts 2026-01-02 14:15:19 +02:00
MartinBraquet
58e65baa47 Fix hydration 2026-01-02 14:15:02 +02:00
MartinBraquet
7bc57d8380 Fix sign in translation 2026-01-02 14:08:25 +02:00
MartinBraquet
eed3e71113 Translate vote sorting 2026-01-02 13:55:49 +02:00
MartinBraquet
08d98468c5 Translate vote components 2026-01-02 13:45:53 +02:00
MartinBraquet
0ee66a264e Translate /news 2026-01-02 13:37:11 +02:00
MartinBraquet
c10f8ceb91 Fix 2025-12-29 13:22:01 +02:00
MartinBraquet
e3cb85271c Translate /contact 2025-12-28 11:56:58 +02:00
MartinBraquet
2c43f3e1e7 Translate /security, /terms, /social, and /help pages 2025-12-28 11:54:40 +02:00
MartinBraquet
29ea3a600a Translate /stats 2025-12-28 11:43:27 +02:00
MartinBraquet
3ace0e80e8 Translate /profile, /privacy, /organization, and /notifications pages 2025-12-28 11:31:38 +02:00
MartinBraquet
79f460b9a2 Translate /compatibility, /delete-account and /donate 2025-12-28 11:24:48 +02:00
MartinBraquet
7d41846b0a Translate /register 2025-12-28 11:20:38 +02:00
MartinBraquet
d8997f64cb Translate /signin and /register 2025-12-28 11:13:33 +02:00
MartinBraquet
8a9139633d Translate /vote 2025-12-27 13:06:49 +02:00
MartinBraquet
a5191b440e Remove /md 2025-12-27 13:06:22 +02:00
MartinBraquet
c7d58905b5 Translate .md files 2025-12-27 12:51:47 +02:00
MartinBraquet
b6df79b836 Translate contact 2025-12-27 12:50:48 +02:00
MartinBraquet
9975113eff Translate about 2025-12-27 12:50:40 +02:00
MartinBraquet
e8431845b1 Fix translation bug in nav bars 2025-12-27 11:00:55 +02:00
MartinBraquet
a3e51f06e3 Add language picker when signed out and new badge 2025-12-27 10:00:17 +02:00
MartinBraquet
ce447db6b5 Translate nav bars 2025-12-26 19:56:23 +02:00
MartinBraquet
8c14212e10 Live update locale 2025-12-26 19:23:09 +02:00
MartinBraquet
9976e085c1 Add locale mem cache 2025-12-26 19:20:45 +02:00
MartinBraquet
6da973dd0c Clean 2025-12-26 19:16:34 +02:00
MartinBraquet
1dcf86e5ba Translate /home 2025-12-26 19:16:04 +02:00
MartinBraquet
3232e783e3 Remove forced server side rendering 2025-12-26 19:15:44 +02:00
MartinBraquet
c66d82a06d Fix 2025-12-26 18:53:30 +02:00
MartinBraquet
a46ff44f99 Add multi-lingual support 2025-12-26 18:45:53 +02:00
MartinBraquet
669a95bfa9 Fix sidebar swipe not working on webview 2025-12-26 16:13:52 +02:00
MartinBraquet
4287fbae85 Android live update 2025-12-26 14:17:28 +02:00
MartinBraquet
f2845eab91 Add open / close mobile sidebar on left / right swipe 2025-12-26 14:09:37 +02:00
MartinBraquet
40702e7832 Add android app link to google play 2025-12-21 01:06:01 +02:00
MartinBraquet
9909bd41cb Android prod release (finally) 2025-12-21 00:38:06 +02:00
MartinBraquet
0dec7e1987 Add Swahili 2025-12-16 12:06:40 +02:00
MartinBraquet
c5965fff89 Fix 2025-12-15 21:44:28 +02:00
MartinBraquet
53a1b4c415 Fix 2025-12-15 21:43:42 +02:00
MartinBraquet
773551e41d Fix 2025-12-15 21:40:03 +02:00
MartinBraquet
2df0e3df88 Fix 2025-12-15 21:38:58 +02:00
MartinBraquet
df39c2ee70 Fix 2025-12-15 21:38:07 +02:00
MartinBraquet
0bfd4d47b9 Fix 2025-12-15 21:34:49 +02:00
MartinBraquet
097617cfa0 Fix 2025-12-15 21:33:31 +02:00
MartinBraquet
b2968166df Fix 2025-12-15 21:31:55 +02:00
MartinBraquet
9116144de4 Fix 2025-12-15 21:30:58 +02:00
MartinBraquet
47e5e8bb28 Fix 2025-12-15 21:28:37 +02:00
MartinBraquet
32cae16045 Fix 2025-12-15 21:26:04 +02:00
MartinBraquet
4e4c946acf Improve issue templates 2025-12-15 21:24:04 +02:00
MartinBraquet
3e06b61eba Failed attempt to increase api docs font size on mobile 2025-12-15 18:39:31 +02:00
MartinBraquet
04e2376829 Increase version 2025-12-15 16:58:57 +02:00
MartinBraquet
709ef3f4ad Clean live update info 2025-12-15 16:58:42 +02:00
MartinBraquet
2e7de541aa Fix live update 2025-12-15 16:51:57 +02:00
MartinBraquet
dc073ef0f9 Fix live update 2025-12-15 16:51:47 +02:00
MartinBraquet
a677df2fdd Add live update info 2025-12-15 16:49:15 +02:00
MartinBraquet
58c005194c Clean 2025-12-15 15:59:00 +02:00
MartinBraquet
16a5c3e408 Clean 2025-12-15 15:58:23 +02:00
MartinBraquet
8a4fb040e1 OTA 2025-12-15 14:52:18 +02:00
MartinBraquet
eab140e51d Fix 2025-12-15 14:51:27 +02:00
MartinBraquet
009d4bf91f Fix 2025-12-15 14:44:40 +02:00
MartinBraquet
ed094bbeca Add backend commit message 2025-12-15 13:45:07 +02:00
MartinBraquet
704bcb4619 Increase API docs font size on mobile 2025-12-15 13:38:09 +02:00
MartinBraquet
fbdc594fa9 Expose version in /health 2025-12-15 12:41:58 +02:00
MartinBraquet
6a511adcf5 Fix cond hook 2025-12-15 12:33:39 +02:00
MartinBraquet
d55b04d22d Update about settings 2025-12-15 12:29:09 +02:00
MartinBraquet
96d0c90f8c Increase version 2025-12-15 12:28:59 +02:00
MartinBraquet
2253a734b1 Fix 2025-12-15 10:52:39 +02:00
MartinBraquet
dca08a6d81 Clean 2025-12-15 10:49:21 +02:00
MartinBraquet
8a4dc44fbc Show build info 2025-12-15 10:22:05 +02:00
MartinBraquet
e231f016d6 Clean 2025-12-15 00:16:04 +02:00
MartinBraquet
05250285c0 Clean 2025-12-15 00:14:49 +02:00
MartinBraquet
ce9ac99894 Update FAQ 2025-12-15 00:11:05 +02:00
MartinBraquet
f76c39bd1c Fix 2025-12-15 00:05:41 +02:00
MartinBraquet
9ccb8d002b Fix 2025-12-15 00:01:13 +02:00
MartinBraquet
d112d4e739 Fix 2025-12-15 00:01:00 +02:00
MartinBraquet
7dfec75ac0 Fix 2025-12-14 23:54:41 +02:00
MartinBraquet
3fd27131e2 Fix 2025-12-14 23:50:24 +02:00
MartinBraquet
355fd7e6d1 Fix 2025-12-14 23:43:46 +02:00
MartinBraquet
c0b8df4ef9 Fix 2025-12-14 23:39:48 +02:00
MartinBraquet
cc3af74676 Fix 2025-12-14 23:27:45 +02:00
MartinBraquet
40a1e079a1 Fix 2025-12-14 23:27:19 +02:00
MartinBraquet
764769366c Fix 2025-12-14 23:25:33 +02:00
MartinBraquet
b433668628 Fix 2025-12-14 23:24:55 +02:00
MartinBraquet
cf27794776 Fix 2025-12-14 23:20:46 +02:00
MartinBraquet
238fed617f Fix 2025-12-14 23:19:02 +02:00
MartinBraquet
0d026d36d1 Add CD for android live update 2025-12-14 23:05:23 +02:00
215 changed files with 11625 additions and 2048 deletions

View File

@@ -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
View 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!

View File

@@ -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.

View 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!

View File

@@ -1,15 +1,25 @@
name: Other
description: Any other question or issue
description: Use this only if no other issue type fits.
body:
- type: textarea
attributes:
label: Issue
description: >
A clear and concise description of the question or issue
validations:
required: true
- type: markdown
attributes:
value: >
Thanks for contributing!
- type: textarea
attributes:
label: Issue
description: |
A clear and concise description of the question or issue
validations:
required: true
- 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: false
- type: markdown
attributes:
value: |
Thanks for contributing!

View 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 commits 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
View File

@@ -5,12 +5,13 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests",
"name": "Debug Current Test",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"${fileBasename}",
"--runInBand"
],
"console": "integratedTerminal",

View File

@@ -4,7 +4,7 @@
[![CD API](https://github.com/CompassConnections/Compass/actions/workflows/cd-api.yml/badge.svg)](https://github.com/CompassConnections/Compass/actions/workflows/cd-api.yml)
[![CI](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml/badge.svg)](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/CompassConnections/Compass/branch/main/graph/badge.svg)](https://codecov.io/gh/CompassConnections/Compass)
[![Users](https://img.shields.io/badge/Users-300%2B-blue?logo=myspace)](https://www.compassmeet.com/stats)
[![Users](https://img.shields.io/badge/Users-400%2B-blue?logo=myspace)](https://www.compassmeet.com/stats)
# 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.)
- [ ] Add profile fields: conflict style
- [ ] 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.)
- [ ] Make the app more user-friendly and appealing (UI/UX)
- [ ] 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:
- [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.
- [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.
- [backend/api](backend/api) for the backend API.
- [android](android) for the Android app.

View File

@@ -267,9 +267,9 @@ yarn build-sync-android
## 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
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.
```
yarn build-web-view
npx @capawesome/cli apps:bundles:create --path web/out
yarn android:live-update
```
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.
- ∞ Live Updates

View File

@@ -8,8 +8,8 @@ android {
applicationId "com.compassconnections.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 14
versionName "1.1.3"
versionCode 18
versionName "1.1.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

3
android/capawesome.json Normal file
View File

@@ -0,0 +1,3 @@
{
"version": 23
}

View File

@@ -27,13 +27,16 @@ SERVICE_NAME="api"
GIT_REVISION=$(git rev-parse --short HEAD)
GIT_COMMIT_DATE=$(git log -1 --format=%ci)
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
{
"git": {
"revision": "${GIT_REVISION}",
"commitDate": "${GIT_COMMIT_DATE}",
"author": "${GIT_COMMIT_AUTHOR}"
"author": "${GIT_COMMIT_AUTHOR}",
"message": "${GIT_COMMIT_MESSAGE}"
}
}
EOF

View File

@@ -1,7 +1,8 @@
{
"git": {
"revision": "91f69ed",
"commitDate": "2025-12-04 20:51:09+0100",
"author": "MartinBraquet"
"revision": "704bcb4",
"commitDate": "2025-12-15 13:38:09 +0200",
"author": "MartinBraquet",
"message": "Increase API docs font size on mobile"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@compass/api",
"description": "Backend API endpoints",
"version": "1.0.11",
"version": "1.0.14",
"private": true,
"scripts": {
"watch:serve": "tsx watch src/serve.ts",

View File

@@ -518,6 +518,12 @@ app.get(
swaggerUi.setup(swaggerDocument, {
customSiteTitle: 'Compass API Docs',
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)

View File

@@ -3,6 +3,25 @@ import {Notification} from 'common/notifications'
import {insertNotificationToSupabase} from 'shared/supabase/notifications'
import {tryCatch} from "common/util/try-catch";
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 () => {
const createdTime = Date.now();

View File

@@ -1,10 +1,12 @@
import { APIHandler } from './helpers/endpoint'
import {git} from './../metadata.json'
import {version as pkgVersion} from './../package.json'
export const health: APIHandler<'health'> = async (_, auth) => {
return {
message: 'Server is working.',
uid: auth?.uid,
git: git,
version: pkgVersion,
}
}

View File

@@ -2,6 +2,7 @@
body {
background-color: #1e1e1e !important;
color: #ffffff !important;
}
.swagger-ui p,
h1,
@@ -62,3 +63,44 @@
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; }
}

View File

@@ -11,8 +11,7 @@ import { throwErrorIfNotMod } from "shared/helpers/auth";
import * as constants from "common/envs/constants";
import * as supabaseUsers from "shared/supabase/users";
import * as sharedAnalytics from "shared/analytics";
import { } from "shared/helpers/auth";
import { APIError, AuthedUser } from "api/helpers/endpoint"
import { AuthedUser } from "api/helpers/endpoint"
describe('banUser', () => {
@@ -24,13 +23,13 @@ describe('banUser', () => {
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('ban a user successfully', async () => {
describe('when given valid input', () => {
it('should ban a user successfully', async () => {
const mockUser = {
userId: '123',
unban: false
@@ -42,15 +41,25 @@ describe('banUser', () => {
await banUser(mockUser, mockAuth, mockReq);
expect(throwErrorIfNotMod).toBeCalledTimes(1);
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
expect(constants.isAdminId).toBeCalledTimes(1);
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
expect(sharedAnalytics.trackPublicEvent)
.toBeCalledWith(mockAuth.uid, 'ban user', {userId: mockUser.userId});
expect(supabaseUsers.updateUser)
.toBeCalledWith(mockPg, mockUser.userId, {isBannedFromPosting: true});
expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(1);
expect(sharedAnalytics.trackPublicEvent).toBeCalledWith(
mockAuth.uid,
'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 = {
userId: '123',
unban: true
@@ -64,13 +73,20 @@ describe('banUser', () => {
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
expect(sharedAnalytics.trackPublicEvent)
.toBeCalledWith(mockAuth.uid, 'ban user', {userId: mockUser.userId});
expect(supabaseUsers.updateUser)
.toBeCalledWith(mockPg, mockUser.userId, {isBannedFromPosting: false});
expect(sharedAnalytics.trackPublicEvent).toBeCalledWith(
mockAuth.uid,
'ban user',
{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 = {
userId: '123',
unban: false
@@ -79,21 +95,16 @@ describe('banUser', () => {
const mockReq = {} as any;
(throwErrorIfNotMod as jest.Mock).mockRejectedValue(
new APIError(
403,
`User ${mockAuth.uid} must be an admin or trusted to perform this action.`
)
new Error(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`)
);
await expect(banUser(mockUser, mockAuth, mockReq))
.rejects
.toThrowError(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`);
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 = {
userId: '123',
unban: false
@@ -108,8 +119,6 @@ describe('banUser', () => {
.toThrowError('Cannot ban admin');
expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid);
expect(constants.isAdminId).toBeCalledWith(mockUser.userId);
expect(sharedAnalytics.trackPublicEvent).toBeCalledTimes(0);
expect(supabaseUsers.updateUser).toBeCalledTimes(0);
});
});
});

View File

@@ -23,37 +23,35 @@ describe('blockUser', () => {
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg)
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
describe('when given valid input', () => {
it('block the user successfully', async () => {
const mockParams = { id: '123' }
const mockAuth = {uid: '321'} as AuthedUser;
const mockReq = {} as any;
(supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null);
await blockUserModule.blockUser(mockParams, mockAuth, mockReq)
expect(mockPg.tx).toHaveBeenCalledTimes(1)
expect(supabaseUsers.updatePrivateUser)
.toHaveBeenCalledWith(
expect.any(Object),
mockAuth.uid,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
);
expect(supabaseUsers.updatePrivateUser)
.toHaveBeenCalledWith(
expect.any(Object),
mockParams.id,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
);
expect(mockPg.tx).toHaveBeenCalledTimes(1);
expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(2);
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
1,
expect.any(Object),
mockAuth.uid,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
);
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
2,
expect.any(Object),
mockParams.id,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
);
});
});
describe('when an error occurs', () => {
it('throw an error if the user tries to block themselves', async () => {
const mockParams = { id: '123' }
const mockAuth = {uid: '123'} as AuthedUser;
@@ -61,12 +59,9 @@ describe('blockUser', () => {
expect(blockUserModule.blockUser(mockParams, mockAuth, mockReq))
.rejects
.toThrowError('You cannot block yourself')
expect(mockPg.tx).toHaveBeenCalledTimes(0)
.toThrowError('You cannot block yourself');
});
});
});
describe('unblockUser', () => {
@@ -84,35 +79,32 @@ describe('unblockUser', () => {
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg)
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('block the user successfully', async () => {
describe('when given valid input', () => {
it('should block the user successfully', async () => {
const mockParams = { id: '123' }
const mockAuth = {uid: '321'} as AuthedUser;
const mockReq = {} as any;
(supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null);
await blockUserModule.unblockUser(mockParams, mockAuth, mockReq)
expect(mockPg.tx).toHaveBeenCalledTimes(1)
expect(supabaseUsers.updatePrivateUser)
.toHaveBeenCalledWith(
expect.any(Object),
mockAuth.uid,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
);
expect(supabaseUsers.updatePrivateUser)
.toHaveBeenCalledWith(
expect.any(Object),
mockParams.id,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
);
expect(mockPg.tx).toHaveBeenCalledTimes(1);
expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(2);
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
1,
expect.any(Object),
mockAuth.uid,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockParams.id)}
);
expect(supabaseUsers.updatePrivateUser).toHaveBeenNthCalledWith(
2,
expect.any(Object),
mockParams.id,
{ blockedByUserIds: supabaseUtils.FieldVal.arrayConcat(mockAuth.uid)}
);
});
});

View File

@@ -1,32 +1,41 @@
import * as supabaseInit from "shared/supabase/init";
import {getCompatibleProfiles} from "api/compatible-profiles";
jest.mock('shared/supabase/init');
import {getCompatibleProfiles} from "api/compatible-profiles";
import * as supabaseInit from "shared/supabase/init";
jest.mock('shared/supabase/init')
describe('getCompatibleProfiles', () => {
let mockPg = {} as any;
beforeEach(() => {
jest.resetAllMocks();
const mockPg = {
none: jest.fn().mockResolvedValue(null),
one: jest.fn().mockResolvedValue(null),
oneOrNone: jest.fn().mockResolvedValue(null),
any: jest.fn().mockResolvedValue([]),
map: jest.fn().mockResolvedValue([["abc", {score: 0.69}]]),
} as any;
mockPg = {
map: jest.fn().mockResolvedValue([]),
};
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('successfully get compatible profiles when supplied with a valid user Id', async () => {
const results = await getCompatibleProfiles("123");
expect(results.status).toEqual('success');
expect(results.profileCompatibilityScores).toEqual({"abc": {score: 0.69}});
});
describe('when given valid input', () => {
it('should successfully get compatible profiles', async () => {
const mockProps = '123';
const mockScores = ["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));
});
});
});

View File

@@ -14,7 +14,6 @@ describe('contact', () => {
let mockPg: any;
beforeEach(() => {
jest.resetAllMocks();
mockPg = {
oneOrNone: jest.fn(),
};
@@ -22,13 +21,12 @@ describe('contact', () => {
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('send a discord message to the user', async () => {
describe('when given valid input', () => {
it('should send a discord message to the user', async () => {
const mockProps = {
content: {
type: 'doc',
@@ -52,29 +50,42 @@ describe('contact', () => {
const mockReturnData = {} as any;
(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);
expect(results.success).toBe(true);
expect(results.result).toStrictEqual({});
expect(tryCatch).toBeCalledTimes(1);
expect(supabaseUtils.insert).toBeCalledTimes(1)
expect(supabaseUtils.insert).toBeCalledWith(
mockPg,
expect.any(Object),
'contact',
{
user_id: mockProps.userId,
content: JSON.stringify(mockProps.content)
}
);
expect(results.success).toBe(true);
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser);
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.stringContaining(`New message from ${mockDbUser.name}`),
'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 = {
content: {
type: 'doc',
@@ -100,15 +111,59 @@ describe('contact', () => {
expect(contact(mockProps, mockAuth, mockReq))
.rejects
.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).toBeCalledWith(
mockPg,
expect.any(Object),
'contact',
{
user_id: mockProps.userId,
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'})
);
});
});
});

View File

@@ -8,7 +8,6 @@ describe('createBookmarkedSearch', () => {
let mockPg: any;
beforeEach(() => {
jest.resetAllMocks();
mockPg = {
one: jest.fn(),
};
@@ -16,13 +15,12 @@ describe('createBookmarkedSearch', () => {
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('insert a bookmarked search into the database', async () => {
describe('when given valid input', () => {
it('should insert a bookmarked search into the database', async () => {
const mockProps = {
search_filters: 'mock_search_filters',
location: 'mock_location',
@@ -30,9 +28,14 @@ describe('createBookmarkedSearch', () => {
};
const mockAuth = { uid: '321' } as AuthedUser;
const mockReq = {} as any;
const mockInserted = "mockInsertedReturn";
await createBookmarkedSearch(mockProps, mockAuth, mockReq)
expect(mockPg.one).toBeCalledTimes(1)
(mockPg.one as jest.Mock).mockResolvedValue(mockInserted);
const result = await createBookmarkedSearch(mockProps, mockAuth, mockReq);
expect(result).toBe(mockInserted);
expect(mockPg.one).toBeCalledTimes(1);
expect(mockPg.one).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO bookmarked_searches'),
[

View File

@@ -15,32 +15,26 @@ import * as supabaseNotifications from "shared/supabase/notifications";
import * as emailHelpers from "email/functions/helpers";
import * as websocketHelpers from "shared/websockets/helpers";
import { convertComment } from "common/supabase/comment";
import { richTextToString } from "common/util/parse";
describe('createComment', () => {
let mockPg: any;
beforeEach(() => {
jest.resetAllMocks();
mockPg = {
one: jest.fn()
};
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
(supabaseNotifications.insertNotificationToSupabase as jest.Mock)
.mockResolvedValue(null);
(emailHelpers.sendNewEndorsementEmail as jest.Mock)
.mockResolvedValue(null);
(convertComment as jest.Mock)
.mockResolvedValue(null);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('successfully create a comment with information provided', async () => {
describe('when given valid input', () => {
it('should successfully create a comment', async () => {
const mockUserId = {
userId: '123',
blockedUserIds: ['111']
@@ -74,12 +68,17 @@ describe('createComment', () => {
const mockReq = {} as any;
const mockReplyToCommentId = {} as any;
const mockComment = {id: 12};
const mockNotificationDestination = {} as any;
const mockNotificationDestination = {
sendToBrowser: true,
sendToMobile: false,
sendToEmail: true
};
const mockProps = {
userId: mockUserId.userId,
content: mockContent.content,
replyToCommentId: mockReplyToCommentId
};
const mockConvertCommentReturn = 'mockConverComment';
(sharedUtils.getUser as jest.Mock)
.mockResolvedValueOnce(mockCreator)
@@ -90,24 +89,51 @@ describe('createComment', () => {
(mockPg.one as jest.Mock).mockResolvedValue(mockComment);
(notificationPrefs.getNotificationDestinationsForUser as jest.Mock)
.mockReturnValue(mockNotificationDestination);
(convertComment as jest.Mock).mockReturnValue(mockConvertCommentReturn);
const results = await createComment(mockProps, mockAuth, mockReq);
expect(results.status).toBe('success');
expect(sharedUtils.getUser).toBeCalledTimes(2);
expect(sharedUtils.getUser).toBeCalledWith(mockUserId.userId);
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
expect(sharedUtils.getUser).toHaveBeenNthCalledWith(1, mockAuth.uid);
expect(sharedUtils.getUser).toHaveBeenNthCalledWith(2, mockUserId.userId);
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).toBeCalledWith(
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 mockReq = {} as any;
const mockReplyToCommentId = {} as any;
@@ -147,14 +173,16 @@ describe('createComment', () => {
(sharedUtils.getUser as jest.Mock)
.mockResolvedValueOnce(mockCreator)
.mockResolvedValueOnce(null);
.mockResolvedValueOnce(false);
(sharedUtils.getPrivateUser as jest.Mock)
.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 mockReq = {} as any;
const mockReplyToCommentId = {} as any;
@@ -188,10 +216,12 @@ describe('createComment', () => {
(sharedUtils.getUser as jest.Mock)
.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 mockReq = {} as any;
const mockReplyToCommentId = {} as any;
@@ -232,10 +262,12 @@ describe('createComment', () => {
(sharedUtils.getUser as jest.Mock)
.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 mockReq = {} as any;
const mockReplyToCommentId = {} as any;
@@ -278,10 +310,12 @@ describe('createComment', () => {
(sharedUtils.getPrivateUser as jest.Mock)
.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 mockReq = {} as any;
const mockReplyToCommentId = {} as any;
@@ -324,10 +358,12 @@ describe('createComment', () => {
(sharedUtils.getPrivateUser as jest.Mock)
.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 mockReq = {} as any;
const mockReplyToCommentId = {} as any;
@@ -369,9 +405,10 @@ describe('createComment', () => {
.mockResolvedValueOnce(mockCreator);
(sharedUtils.getPrivateUser as jest.Mock)
.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');
});
});
});

View File

@@ -18,12 +18,12 @@ describe('createCompatibilityQuestion', () => {
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
});
afterEach(() => {
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 mockOptions = {} as any;
const mockProps = {options:mockOptions, question:mockQuestion};
@@ -41,31 +41,45 @@ describe('createCompatibilityQuestion', () => {
multiple_choice_options: {"first_choice":"first_answer"},
question: "mockQuestion"
};
(shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
(supabaseUtils.insert as jest.Mock).mockResolvedValue(mockData);
(tryCatch as jest.Mock).mockResolvedValue({data:mockData, error: null});
const results = await createCompatibilityQuestion(mockProps, mockAuth, mockReq);
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 mockOptions = {} as any;
const mockProps = {options:mockOptions, question:mockQuestion};
const mockAuth = {uid: '321'} as AuthedUser;
const mockReq = {} as any;
(shareUtils.getUser as jest.Mock).mockResolvedValue(null);
(shareUtils.getUser as jest.Mock).mockResolvedValue(false);
expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq))
.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 mockOptions = {} as any;
const mockProps = {options:mockOptions, question:mockQuestion};
@@ -74,23 +88,13 @@ describe('createCompatibilityQuestion', () => {
const mockCreator = {
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);
(supabaseUtils.insert as jest.Mock).mockResolvedValue(mockData);
(tryCatch as jest.Mock).mockResolvedValue({data:null, error: Error});
expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq))
.rejects
.toThrowError('Error creating question')
.toThrowError('Error creating question');
});
});
});

View File

@@ -8,25 +8,23 @@ import { tryCatch } from "common/util/try-catch";
import * as supabaseNotifications from "shared/supabase/notifications";
import { Notification } from "common/notifications";
type MockNotificationUser = Pick<Notification, 'userId'>;
describe('createNotifications', () => {
let mockPg = {} as any;
beforeEach(() => {
jest.resetAllMocks();
const mockPg = {
mockPg = {
many: jest.fn().mockReturnValue(null)
} as any;
};
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('sucessfully create a notification', async () => {
describe('when given valid input', () => {
it('should sucessfully create a notification', async () => {
const mockUsers = [
{
created_time: "mockCreatedTime",
@@ -39,60 +37,89 @@ describe('createNotifications', () => {
];
const mockNotification = {
userId: "mockUserId"
} as MockNotificationUser;
} as Notification;
(tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null});
(supabaseNotifications.insertNotificationToSupabase as jest.Mock)
.mockResolvedValue(null);
jest.spyOn(createNotificationModules, 'createNotification');
const results = await createNotificationModules.createNotifications(mockNotification);
const results = await createNotificationModules.createNotifications(mockNotification as Notification);
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 () => {
const mockUsers = [
{
created_time: "mockCreatedTime",
data: {"mockData": "mockDataJson"},
id: "mockId",
name: "mockName",
name_user_vector: "mockNUV",
username: "mockUsername"
},
];
describe('when an error occurs', () => {
it('should throw if its unable to fetch users', async () => {
const mockNotification = {
userId: "mockUserId"
} as MockNotificationUser;
} as Notification;
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)
expect(errorSpy).toHaveBeenCalledWith('Error fetching users', expect.objectContaining({name: 'Error'}))
await createNotificationModules.createNotifications(mockNotification);
expect(errorSpy).toBeCalledWith(
'Error fetching users',
expect.objectContaining({name: 'Error'})
);
});
it('throws an error if there are no users', async () => {
const mockUsers = [
{
created_time: "mockCreatedTime",
data: {"mockData": "mockDataJson"},
id: "mockId",
name: "mockName",
name_user_vector: "mockNUV",
username: "mockUsername"
},
];
it('should throw if there are no users', async () => {
const mockNotification = {
userId: "mockUserId"
} as MockNotificationUser;
} as Notification;
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
(tryCatch as jest.Mock).mockResolvedValue({data: null, error:null});
await createNotificationModules.createNotifications(mockNotification as Notification)
expect(errorSpy).toHaveBeenCalledWith('No users found')
await createNotificationModules.createNotifications(mockNotification);
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]
);
});
});
});

View File

@@ -23,13 +23,12 @@ describe('createPrivateUserMessageChannel', () => {
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg)
});
afterEach(() => {
jest.restoreAllMocks()
});
describe('should', () => {
it('successfully create a private user message channel (currentChannel)', async () => {
describe('when given valid input', () => {
it('should successfully create a private user message channel (currentChannel)', async () => {
const mockBody = {
userIds: ["123"]
};
@@ -55,29 +54,27 @@ describe('createPrivateUserMessageChannel', () => {
isBannedFromPosting: false
};
(sharedUtils.getUser as jest.Mock)
.mockResolvedValue(mockCreator);
(sharedUtils.getPrivateUser as jest.Mock)
.mockResolvedValue(mockUserIds);
(utilArrayModules.filterDefined as jest.Mock)
.mockReturnValue(mockPrivateUsers);
(mockPg.oneOrNone as jest.Mock)
.mockResolvedValue(mockCurrentChannel);
(privateMessageModules.addUsersToPrivateMessageChannel as jest.Mock)
.mockResolvedValue(null);
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
(utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers);
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockCurrentChannel);
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).toBeCalledWith(mockAuth.uid);
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]);
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]);
expect(results.status).toBe('success');
expect(results.channelId).toBe(444)
expect(mockPg.oneOrNone).toBeCalledTimes(1);
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 = {
userIds: ["123"]
};
@@ -103,45 +100,54 @@ describe('createPrivateUserMessageChannel', () => {
isBannedFromPosting: false
};
(sharedUtils.getUser as jest.Mock)
.mockResolvedValue(mockCreator);
(sharedUtils.getPrivateUser as jest.Mock)
.mockResolvedValue(mockUserIds);
(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);
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
(utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers);
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false);
(mockPg.one as jest.Mock).mockResolvedValue(mockChannel);
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).toBeCalledWith(mockAuth.uid);
expect(sharedUtils.getPrivateUser).toBeCalledTimes(2);
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[0]);
expect(sharedUtils.getPrivateUser).toBeCalledWith(mockUserIds[1]);
expect(results.status).toBe('success');
expect(results.channelId).toBe(333)
expect(mockPg.one).toBeCalledTimes(1);
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 = {
userIds: ["123"]
};
const mockAuth = {uid: '321'} as AuthedUser;
const mockReq = {} as any;
(sharedUtils.getUser as jest.Mock)
.mockResolvedValue(null);
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq))
.rejects
.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 = {
userIds: ["123"]
};
@@ -151,21 +157,17 @@ describe('createPrivateUserMessageChannel', () => {
isBannedFromPosting: true
};
(sharedUtils.getUser as jest.Mock)
.mockResolvedValue(mockCreator);
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq))
.rejects
.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 = {
userIds: ["123"]
};
const mockUserIds = ['123'];
const mockPrivateUsers = [
{
id: '123',
@@ -181,8 +183,6 @@ describe('createPrivateUserMessageChannel', () => {
(sharedUtils.getUser as jest.Mock)
.mockResolvedValue(mockCreator);
(sharedUtils.getPrivateUser as jest.Mock)
.mockResolvedValue(mockUserIds);
(utilArrayModules.filterDefined as jest.Mock)
.mockReturnValue(mockPrivateUsers);
@@ -191,11 +191,10 @@ describe('createPrivateUserMessageChannel', () => {
.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 = {
userIds: ["123"]
};
const mockUserIds = ['321'];
const mockPrivateUsers = [
{
id: '123',
@@ -216,8 +215,6 @@ describe('createPrivateUserMessageChannel', () => {
(sharedUtils.getUser as jest.Mock)
.mockResolvedValue(mockCreator);
(sharedUtils.getPrivateUser as jest.Mock)
.mockResolvedValue(mockUserIds);
(utilArrayModules.filterDefined as jest.Mock)
.mockReturnValue(mockPrivateUsers);

View File

@@ -23,7 +23,7 @@ describe('createPrivateUserMessage', () => {
jest.restoreAllMocks();
});
describe('should', () => {
describe('when given valid input', () => {
it('successfully create a private user message', async () => {
const mockBody = {
content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH-8))},
@@ -36,10 +36,12 @@ describe('createPrivateUserMessage', () => {
};
(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator);
(helpersPrivateMessagesModules.createPrivateUserMessageMain as jest.Mock)
.mockResolvedValue(null);
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(
mockCreator,
mockBody.channelId,
@@ -48,8 +50,9 @@ describe('createPrivateUserMessage', () => {
'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 = {
content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH))},
channelId: 123
@@ -62,7 +65,7 @@ describe('createPrivateUserMessage', () => {
.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 = {
content: {"mockJson": "mockJsonContent"},
channelId: 123
@@ -70,14 +73,14 @@ describe('createPrivateUserMessage', () => {
const mockAuth = {uid: '321'} as AuthedUser;
const mockReq = {} as any;
(sharedUtils.getUser as jest.Mock).mockResolvedValue(null);
(sharedUtils.getUser as jest.Mock).mockResolvedValue(false);
expect(createPrivateUserMessage(mockBody, mockAuth, mockReq))
.rejects
.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 = {
content: {"mockJson": "mockJsonContent"},
channelId: 123

View File

@@ -6,6 +6,7 @@ jest.mock('shared/supabase/utils');
jest.mock('common/util/try-catch');
jest.mock('shared/analytics');
jest.mock('common/discord/core');
jest.mock('common/util/time');
import { createProfile } from "api/create-profile";
import * as supabaseInit from "shared/supabase/init";
@@ -20,25 +21,189 @@ import { AuthedUser } from "api/helpers/endpoint";
describe('createProfile', () => {
let mockPg = {} as any;
beforeEach(() => {
jest.resetAllMocks();
mockPg = {
oneOrNone: jest.fn().mockReturnValue(null),
oneOrNone: jest.fn(),
one: jest.fn()
};
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('should', () => {
it('successfully create a profile', async () => {
describe('when given valid input', () => {
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 = {
city: "mockCity",
gender: "mockGender",
@@ -52,7 +217,6 @@ describe('createProfile', () => {
};
const mockAuth = {uid: '321'} as AuthedUser;
const mockReq = {} as any;
const mockExistingUser = {id: "mockExistingUserId"};
const mockData = {
age: 30,
city: "mockCity"
@@ -65,18 +229,25 @@ describe('createProfile', () => {
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
(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});
(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);
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 = {
city: "mockCity",
gender: "mockGender",
@@ -90,16 +261,69 @@ describe('createProfile', () => {
};
const mockAuth = {uid: '321'} as AuthedUser;
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))
.rejects
.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 = {
city: "mockCity",
gender: "mockGender",
@@ -115,16 +339,14 @@ describe('createProfile', () => {
const mockReq = {} as any;
(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))
.rejects
.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 = {
city: "mockCity",
gender: "mockGender",
@@ -146,15 +368,11 @@ describe('createProfile', () => {
(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null});
(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});
await expect(createProfile(mockBody, mockAuth, mockReq))
.rejects
.toThrowError('Error creating user');
expect(sharedUtils.getUser).toBeCalledTimes(1);
expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid);
});
});
});

View File

@@ -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('should', () => {
it('', async () => {
const originalIsLocal = (hostingConstants as any).IS_LOCAL;
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));
});
});
});

View 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');
});
});
});

View 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
]
);
});
});
});

View File

@@ -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');
});
});
});

View 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)
);
});
});
});

View 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)
);
});
});
});

View 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)
);
});
});
});

View File

@@ -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')
);
});
});
});

View File

@@ -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');
});
});
});

View 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)
);
});
});
});

View 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});
});
});
});

View 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)
);
});
});
});

View 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)
);
});
});
});

View 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');
});
});
});

View 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]
);
});
});
});

View 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]
);
});
});
});

View File

@@ -1,6 +1,7 @@
import * as profilesModule from "api/get-profiles";
import { Profile } from "common/profiles/profile";
import * as supabaseInit from "shared/supabase/init";
import * as sqlBuilder from "shared/supabase/sql-builder";
describe('getProfiles', () => {
beforeEach(() => {
@@ -11,8 +12,8 @@ describe('getProfiles', () => {
jest.restoreAllMocks();
});
describe('should fetch the user profiles', () => {
it('successfully', async ()=> {
describe('when given valid input', () => {
it('should successfully return profile information and count', async ()=> {
const mockProfiles = [
{
diet: ['Jonathon Hammon'],
@@ -27,19 +28,15 @@ describe('getProfiles', () => {
has_kids: 2,
}
] as Profile [];
jest.spyOn(profilesModule, 'loadProfiles').mockResolvedValue({profiles: mockProfiles, count: 3});
const props = {
limit: 2,
orderBy: "last_online_time" as const,
};
const mockReq = {} as any;
const results = await profilesModule.getProfiles(props, mockReq, mockReq);
if('continue' in results) {
throw new Error('Expected direct response')
};
jest.spyOn(profilesModule, 'loadProfiles').mockResolvedValue({profiles: mockProfiles, count: 3});
const results: any = await profilesModule.getProfiles(props, mockReq, mockReq);
expect(results.status).toEqual('success');
expect(results.profiles).toEqual(mockProfiles);
@@ -47,8 +44,10 @@ describe('getProfiles', () => {
expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props);
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);
const props = {
@@ -56,278 +55,274 @@ describe('getProfiles', () => {
orderBy: "last_online_time" as const,
};
const mockReq = {} as any;
const results = await profilesModule.getProfiles(props, mockReq, mockReq);
if('continue' in results) {
throw new Error('Expected direct response')
};
const results: any = await profilesModule.getProfiles(props, mockReq, mockReq);
expect(results.status).toEqual('fail');
expect(results.profiles).toEqual([]);
expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props);
expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1);
});
});
});
describe('loadProfiles', () => {
let mockPg: any;
describe('should call pg.map with an SQL query', () => {
beforeEach(() => {
jest.clearAllMocks();
mockPg = {
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`);
});
beforeEach(() => {
jest.clearAllMocks();
mockPg = {
map: jest.fn(),
one: jest.fn()
};
it('that contains a gender filter', async () => {
await profilesModule.loadProfiles({
genders: ['Electrical_gender'],
jest.spyOn(supabaseInit, 'createSupabaseDirectClient')
.mockReturnValue(mockPg);
});
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]
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'],
it('that contains a prefer gender filter', async () => {
await profilesModule.loadProfiles({
pref_gender: ['female'],
});
const [query, values, cb] = mockPg.map.mock.calls[0]
console.log(query);
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`pref_gender`);
expect(query).toContain(`female`);
});
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`);
});
it('that contains a prefer gender filter', async () => {
await profilesModule.loadProfiles({
pref_gender: ['female'],
it('that contains a minimum age filter', async () => {
await profilesModule.loadProfiles({
pref_age_min: 20,
});
const [query, values, cb] = mockPg.map.mock.calls[0]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`age`);
expect(query).toContain(`>= 20`);
});
const [query, values, cb] = mockPg.map.mock.calls[0]
console.log(query);
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`pref_gender`);
expect(query).toContain(`female`);
});
it('that contains a maximum age filter', async () => {
await profilesModule.loadProfiles({
pref_age_max: 40,
});
it('that contains a minimum age filter', async () => {
await profilesModule.loadProfiles({
pref_age_min: 20,
const [query, values, cb] = mockPg.map.mock.calls[0]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`age`);
expect(query).toContain(`<= 40`);
});
const [query, values, cb] = mockPg.map.mock.calls[0]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`age`);
expect(query).toContain(`>= 20`);
});
it('that contains a minimum drinks per month filter', async () => {
await profilesModule.loadProfiles({
drinks_min: 4,
});
it('that contains a maximum age filter', async () => {
await profilesModule.loadProfiles({
pref_age_max: 40,
const [query, values, cb] = mockPg.map.mock.calls[0]
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]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`age`);
expect(query).toContain(`<= 40`);
});
it('that contains a maximum drinks per month filter', async () => {
await profilesModule.loadProfiles({
drinks_max: 20,
});
it('that contains a minimum drinks per month filter', async () => {
await profilesModule.loadProfiles({
drinks_min: 4,
const [query, values, cb] = mockPg.map.mock.calls[0]
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]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`drinks_per_month`);
expect(query).toContain('4');
});
it('that contains a relationship style filter', async () => {
await profilesModule.loadProfiles({
pref_relation_styles: ['Chill and relaxing'],
});
it('that contains a maximum drinks per month filter', async () => {
await profilesModule.loadProfiles({
drinks_max: 20,
const [query, values, cb] = mockPg.map.mock.calls[0]
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]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`drinks_per_month`);
expect(query).toContain('20');
});
it('that contains a romantic style filter', async () => {
await profilesModule.loadProfiles({
pref_romantic_styles: ['Sexy'],
});
it('that contains a relationship style filter', async () => {
await profilesModule.loadProfiles({
pref_relation_styles: ['Chill and relaxing'],
const [query, values, cb] = mockPg.map.mock.calls[0]
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]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`pref_relation_styles`);
expect(query).toContain('Chill and relaxing');
});
it('that contains a diet filter', async () => {
await profilesModule.loadProfiles({
diet: ['Glutton'],
});
it('that contains a romantic style filter', async () => {
await profilesModule.loadProfiles({
pref_romantic_styles: ['Sexy'],
const [query, values, cb] = mockPg.map.mock.calls[0]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`diet`);
expect(query).toContain('Glutton');
});
const [query, values, cb] = mockPg.map.mock.calls[0]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`pref_romantic_styles`);
expect(query).toContain('Sexy');
});
it('that contains a political beliefs filter', async () => {
await profilesModule.loadProfiles({
political_beliefs: ['For the people'],
});
it('that contains a diet filter', async () => {
await profilesModule.loadProfiles({
diet: ['Glutton'],
const [query, values, cb] = mockPg.map.mock.calls[0]
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]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`diet`);
expect(query).toContain('Glutton');
});
it('that contains a religion filter', async () => {
await profilesModule.loadProfiles({
religion: ['The blood god'],
});
it('that contains a political beliefs filter', async () => {
await profilesModule.loadProfiles({
political_beliefs: ['For the people'],
const [query, values, cb] = mockPg.map.mock.calls[0]
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]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`political_beliefs`);
expect(query).toContain('For the people');
});
it('that contains a has kids filter', async () => {
await profilesModule.loadProfiles({
has_kids: 3,
});
it('that contains a religion filter', async () => {
await profilesModule.loadProfiles({
religion: ['The blood god'],
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');
});
const [query, values, cb] = mockPg.map.mock.calls[0]
expect(mockPg.map.mock.calls).toHaveLength(1)
expect(query).toContain(`religion`);
expect(query).toContain('The blood god');
});
it('should 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 [];
const props = {} as any;
it('that contains a has kids filter', async () => {
await profilesModule.loadProfiles({
has_kids: 3,
(mockPg.map as jest.Mock).mockResolvedValue(mockProfiles);
(mockPg.one as jest.Mock).mockResolvedValue(1);
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', () => {
beforeEach(() => {
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 () => {
describe('when an error occurs', () => {
it('throw if there is no compatability', async () => {
const props = {
orderBy: 'compatibility_score'
}
expect(profilesModule.loadProfiles(props))
.rejects
.toThrowError('Incompatible with user ID')
});
})
})
});
});

View File

@@ -1,163 +1,90 @@
jest.mock("shared/supabase/init");
jest.mock("common/supabase/users");
jest.mock("common/api/user-types");
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 { 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', () =>{
let mockPg: any;
beforeEach(() => {
jest.resetAllMocks();
mockPg = {
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', () => {
it('should fetch user successfully by id', async () => {
const mockDbUser = {
created_time: '2025-11-11T16:42:05.188Z',
data: { link: {}, avatarUrl: "", isBannedFromPosting: false },
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);
});
describe('when given valid input', () => {
describe('and fetching by id', () => {
it('should fetch user successfully by id', async () => {
const mockProps = {id: "mockId"};
const mockUser = {} as any;
it('should throw 404 when user is not found by id', async () => {
mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => {
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')
}
})
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser);
(toUserAPIResponse as jest.Mock).mockReturnValue('mockApiResponse');
})
const result = await getUser(mockProps);
describe('when fetching by username', () => {
it('should fetch user successfully by username', async () => {
const mockDbUser = {
created_time: '2025-11-11T16:42:05.188Z',
data: { link: {}, avatarUrl: "", isBannedFromPosting: false },
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);
expect(result).toBe('mockApiResponse');
expect(mockPg.oneOrNone).toBeCalledTimes(1);
expect(mockPg.oneOrNone).toBeCalledWith(
expect.stringContaining('select * from users'),
[mockProps.id],
expect.any(Function)
);
expect(toUserAPIResponse).toBeCalledTimes(1);
expect(toUserAPIResponse).toBeCalledWith(mockUser);
});
(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 () => {
mockPg.oneOrNone.mockImplementation((query: string, values: any[], cb: (value: any) => any) => {
return Promise.resolve(null);
});
describe('when fetching by username', () => {
it('should fetch user successfully by username', async () => {
const mockProps = {username: "mockUsername"};
const mockUser = {} as any;
(convertUser as jest.Mock).mockReturnValue(null)
try {
await getUser({username: '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')
}
})
})
})
(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser);
await getUser(mockProps)
expect(mockPg.oneOrNone).toHaveBeenCalledWith(
expect.stringContaining('where username = $1'),
[mockProps.username],
expect.any(Function)
);
});
});
});
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');
});
});
});
});

View 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();
});
});
});

View 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);
});
});
});

View 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');
});
});
});

View File

@@ -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');
});
});
});

View 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: ');
});
});
});

View File

@@ -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]
);
});
});
});

View 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)
);
});
});
});

View 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');
});
});
});

View 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)
);
});
});
});

View 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)
// );
});
});
});

View 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)
// );
});
});
});

View 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`)
);
});
});
});

View 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`)
);
});
});
});

View 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)
);
});
});
});

View 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({});
});
});
});

View File

@@ -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));
});
});
});

View File

@@ -1,34 +1,56 @@
jest.mock('shared/supabase/init');
import { AuthedUser } from "api/helpers/endpoint";
import * as setLastTimeOnlineModule from "api/set-last-online-time";
import * as supabaseInit from "shared/supabase/init";
describe('Should', () => {
describe('setLastOnlineTimeUser', () => {
let mockPg: any;
beforeEach(() => {
jest.resetAllMocks();
mockPg = {
none: jest.fn(),
};
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
jest.clearAllMocks();
.mockReturnValue(mockPg);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('change the users last online time', async () => {
const mockProfile = {user_id: 'Jonathon'};
await setLastTimeOnlineModule.setLastOnlineTimeUser(mockProfile.user_id);
describe('when given valid input', () => {
it('should change the users last online time', async () => {
const mockAuth = { uid: '321' } as AuthedUser;
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];
expect(userId).toContain(mockProfile.user_id);
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'")
it('should return if there is no auth', async () => {
const mockAuth = { } as any;
const mockReq = {} as any;
const mockProps = {} as any;
(mockPg.none as jest.Mock).mockResolvedValue(null);
jest.spyOn(setLastTimeOnlineModule, 'setLastOnlineTimeUser');
await setLastTimeOnlineModule.setLastOnlineTime(mockProps, mockAuth, mockReq);
expect(setLastTimeOnlineModule.setLastOnlineTimeUser).not.toBeCalled();
});
});
})
});

View 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: ');
});
});
});

View 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');
});
});
});

View 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');
});
});
});

View 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}
);
});
});
});

View 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');
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -1,86 +1,100 @@
jest.mock("shared/supabase/init");
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 { AuthedUser } from "api/helpers/endpoint";
import * as supabaseInit from "shared/supabase/init";
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', () => {
let mockPg: any;
beforeEach(() => {
jest.resetAllMocks();
mockPg = {
oneOrNone: jest.fn(),
};
(supabaseInit.createSupabaseDirectClient as jest.Mock)
.mockReturnValue(mockPg);
jest.clearAllMocks();
});
describe('should', () => {
it('update an existing profile when provided the user id', async () => {
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'
}
afterEach(() => {
jest.restoreAllMocks();
});
mockPg.oneOrNone.mockResolvedValue(mockUserProfile);
(supabaseUtils.update as jest.Mock).mockResolvedValue(mockUpdatedProfile);
describe('when given valid input', () => {
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(
mockUpdateMade,
mockAuth as AuthedUser,
mockParams
(tryCatch as jest.Mock)
.mockResolvedValueOnce({data: true})
.mockResolvedValueOnce({data: mockData, error: null});
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);
expect(mockPg.oneOrNone.mock.calls[0][1]).toEqual([mockAuth.uid]);
expect(result).toEqual(mockUpdatedProfile);
describe('when an error occurs', () => {
it('should throw if the profile does not exist', async () => {
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 () => {
mockPg.oneOrNone.mockResolvedValue(null);
expect(updateProfile({} as any, {} as any, {} as any,))
.rejects
.toThrowError('Profile not found');
});
it('should throw if unable to update the profile', async () => {
const mockProps = {
avatar_url: "mockAvatarUrl"
};
const mockAuth = { uid: '321' } as AuthedUser;
const mockReq = {} as any;
it('throw an error if unable to update the profile', async () => {
const mockUserProfile = {
user_id: '234',
diet: 'Nothing',
gender: 'female',
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,))
(tryCatch as jest.Mock)
.mockResolvedValueOnce({data: true})
.mockResolvedValueOnce({data: null, error: Error});
expect(updateProfile(mockProps, mockAuth, mockReq))
.rejects
.toThrowError('Error updating profile');
.toThrow('Error updating profile');
});
});
});

View 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');
});
});
});

View File

@@ -2,7 +2,7 @@
(
# Set PGPASSWORD
source ../../.env
source .env.df
# Target database connection info - replace with your target DB

View File

@@ -45,7 +45,12 @@ export const API = (_apiTypeCheck = {
authed: false,
rateLimited: false,
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',
tag: 'General',
},

View File

@@ -17,6 +17,7 @@ export const stoatLink = "https://stt.gg/YKQp81yA"
export const redditLink = "https://www.reddit.com/r/CompassConnect"
export const xLink = "https://x.com/compassmeet"
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
@@ -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 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
View File

@@ -0,0 +1,3 @@
export const toKey = (str: string | number | boolean) => {
return String(str).replace(/ /g, '_').toLowerCase()
}

View File

@@ -20,7 +20,3 @@ export const STATUS_CHOICES: Record<string, string> = {
expired: "Expired ⌛",
archived: "Archived",
}
export const REVERSED_STATUS_CHOICES: Record<string, string> = Object.fromEntries(
Object.entries(STATUS_CHOICES).map(([key, value]) => [value, key])
)

View File

@@ -32,6 +32,16 @@ yarn regen-types dev
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
Best Practices
@@ -43,4 +53,4 @@ Best Practices
* Avoid Testing Next.js Internals . You dont need to test getStaticProps, getServerSideProps themselves—test what they render.
* Use jest.spyOn() for Internal Utilities . Avoid reaching into modules you dont own.
* 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.

View File

@@ -1,6 +1,6 @@
{
"name": "compass",
"version": "1.8.0",
"version": "1.9.0",
"private": true,
"workspaces": [
"common",
@@ -18,6 +18,7 @@
"clean-install": "./scripts/install.sh",
"build-web-view": "./scripts/build_web_view.sh",
"build-sync-android": "./scripts/build_sync_android.sh",
"android-live-update": "./scripts/android_live_update.sh",
"sync-android": "./scripts/sync_android.sh",
"migrate": "./scripts/migrate.sh",
"test": "yarn workspaces run test",

41
scripts/android_live_update.sh Executable file
View 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

View File

@@ -4,8 +4,6 @@ set -e
cd "$(dirname "$0")"/..
export NEXT_PUBLIC_WEBVIEW=1
# Paths
ROOT_ENV=".env" # your root .env
WEB_ENV="web/.env" # target for frontend
@@ -17,7 +15,13 @@ if [ -f "$WEB_ENV" ]; then
fi
# 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:"
@@ -27,6 +31,8 @@ cat "$WEB_ENV"
cd web
export NEXT_PUBLIC_WEBVIEW=1
rm -rf .next
# Hack to ignore getServerSideProps, getStaticProps and getStaticPaths for mobile webview build

3
web/.gitignore vendored
View File

@@ -8,4 +8,5 @@ tsconfig.tsbuildinfo
testing
.env
.env.local
.env.local
/public/live-update.json

View 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}/>
}

View 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>
}

View File

@@ -4,21 +4,22 @@ import {Col} from "web/components/layout/col";
import clsx from "clsx";
import {colClassName, labelClassName} from "web/pages/signup";
import {MultiCheckbox} from "web/components/multi-checkbox";
import {capitalize} from "lodash";
export function AddOptionEntry(props: {
title: string
choices: { [key: string]: string }
setChoices: (choices: any) => void
profile: ProfileWithoutUser,
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void
label: OptionTableKey,
}) {
const {profile, setProfile, label, choices, setChoices} = props
const {profile, setProfile, label, choices, setChoices, title} = props
return <Col className={clsx(colClassName)}>
<label className={clsx(labelClassName)}>{capitalize(label)}</label>
<label className={clsx(labelClassName)}>{title}</label>
<MultiCheckbox
choices={choices}
selected={profile[label] ?? []}
translationPrefix={`profile.${label}`}
onChange={(selected) => setProfile(label, selected)}
addOption={(v: string) => {
console.log(`Adding ${label}:`, v)

View File

@@ -17,12 +17,14 @@ import {AnswerCompatibilityQuestionContent} from './answer-compatibility-questio
import {uniq} from 'lodash'
import {QuestionWithCountType} from 'web/hooks/use-questions'
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants'
import {useT} from 'web/lib/locale'
export function AddCompatibilityQuestionButton(props: {
refreshCompatibilityAll: () => void
}) {
const { refreshCompatibilityAll } = props
const [open, setOpen] = useState(false)
const t = useT()
const user = useUser()
if (!user) return null
return (
@@ -32,7 +34,7 @@ export function AddCompatibilityQuestionButton(props: {
onClick={() => setOpen(true)}
className="text-sm"
>
submit your own!
{t('answers.add.submit_own', 'submit your own!')}
</button>
<AddCompatibilityQuestionModal
open={open}
@@ -94,6 +96,7 @@ function CreateCompatibilityModalContent(props: {
setOpen: (open: boolean) => void
}) {
const { afterAddQuestion, setOpen } = props
const t = useT()
const [question, setQuestion] = useState('')
const [options, setOptions] = useState<string[]>(['', ''])
const [loading, setLoading] = useState(false)
@@ -144,7 +147,7 @@ function CreateCompatibilityModalContent(props: {
}
track('create compatibility question')
} 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="gap-1">
<label>
Question<span className={'text-scarlet-500'}>*</span>
{t('answers.add.question_label', 'Question')}
<span className={'text-scarlet-500'}>*</span>
</label>
<ExpandingInput
maxLength={MAX_COMPATIBILITY_QUESTION_LENGTH}
@@ -162,7 +166,8 @@ function CreateCompatibilityModalContent(props: {
</Col>
<Col className="gap-1">
<label>
Options<span className={'text-scarlet-500'}>*</span>
{t('answers.add.options_label', 'Options')}
<span className={'text-scarlet-500'}>*</span>
</label>
<Col className="w-full gap-1">
{options.map((o, index) => (
@@ -171,7 +176,7 @@ function CreateCompatibilityModalContent(props: {
value={options[index]}
onChange={(e) => onOptionChange(index, e.target.value)}
className="w-full"
placeholder={`Option ${index + 1}`}
placeholder={t('answers.add.option_placeholder', 'Option {n}', { n: String(index + 1) })}
rows={1}
maxLength={MAX_ANSWER_LENGTH}
/>
@@ -188,7 +193,7 @@ function CreateCompatibilityModalContent(props: {
<Button onClick={addOption} color="gray-outline">
<Row className="items-center gap-1">
<PlusIcon className="h-4 w-4" />
Add Option
{t('answers.add.add_option', 'Add Option')}
</Row>
</Button>
</Col>
@@ -201,7 +206,7 @@ function CreateCompatibilityModalContent(props: {
setOpen(false)
}}
>
Cancel
{t('settings.action.cancel', 'Cancel')}
</Button>
<Button
loading={loading}
@@ -211,7 +216,7 @@ function CreateCompatibilityModalContent(props: {
}}
disabled={!optionsAreValid || !questionIsValid || !noRepeatOptions}
>
Submit & Answer
{t('answers.add.submit_and_answer', 'Submit & Answer')}
</Button>
</Row>
</Col>

View File

@@ -7,6 +7,7 @@ import {Modal, MODAL_CLASS} from 'web/components/layout/modal'
import {AnswerCompatibilityQuestionContent} from './answer-compatibility-question-content'
import router from "next/router";
import Link from "next/link";
import {useT} from 'web/lib/locale'
export function AnswerCompatibilityQuestionButton(props: {
user: User | null | undefined
@@ -23,6 +24,7 @@ export function AnswerCompatibilityQuestionButton(props: {
size = 'md',
} = props
const [open, setOpen] = useState(fromSignup ?? false)
const t = useT()
if (!user) return null
if (otherQuestions.length === 0) return null
const isCore = otherQuestions.some((q) => q.importance_score === 0)
@@ -31,7 +33,7 @@ export function AnswerCompatibilityQuestionButton(props: {
<>
{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'}>
Answer{isCore && ' Core'} Questions{' '}
{t('answers.answer.cta', 'Answer{core} Questions', { core: isCore ? ' Core' : '' })}{' '}
<span className="text-primary-600 ml-2">
+{questionsToAnswer.length}
</span>
@@ -41,7 +43,7 @@ export function AnswerCompatibilityQuestionButton(props: {
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"
>
Answer yourself
{t('answers.answer.answer_yourself', 'Answer yourself')}
</button>
)}
<AnswerCompatibilityQuestionModal
@@ -59,11 +61,12 @@ export function AnswerCompatibilityQuestionButton(props: {
}
export function CompatibilityPageButton() {
const t = useT()
return (
<Link
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"
>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 [open, setOpen] = useState(false)
const t = useT()
if (!user) return null
return (
<>
@@ -81,7 +85,7 @@ export function AnswerSkippedCompatibilityQuestionsButton(props: {
onClick={() => setOpen(true)}
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>
<AnswerCompatibilityQuestionModal
open={open}

View File

@@ -18,6 +18,7 @@ import {track} from 'web/lib/service/analytics'
import {api} from 'web/lib/api'
import {filterKeys} from '../questions-form'
import toast from "react-hot-toast"
import {useT} from 'web/lib/locale'
export type CompatibilityAnswerSubmitType = Omit<
rowFor<'compatibility_answers'>,
@@ -72,6 +73,7 @@ export const submitCompatibilityAnswer = async (
})
} catch (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?')
}
}
@@ -86,6 +88,7 @@ export const deleteCompatibilityAnswer = async (
await track('delete compatibility question', {id})
} catch (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?')
}
}
@@ -122,6 +125,7 @@ export function AnswerCompatibilityQuestionContent(props: {
index,
total,
} = props
const t = useT()
const [answer, setAnswer] = useState<CompatibilityAnswerSubmitType>(
(props.answer as CompatibilityAnswerSubmitType) ??
getEmptyAnswer(user.id, compatibilityQuestion.id)
@@ -175,7 +179,7 @@ export function AnswerCompatibilityQuestionContent(props: {
{shortenedPopularity && (
<Row className="text-ink-500 select-none items-center text-sm">
<Tooltip
text={`${shortenedPopularity} people have answered this question`}
text={t('answers.content.people_answered', '{count} people have answered this question', { count: String(shortenedPopularity) })}
>
{shortenedPopularity}
</Tooltip>
@@ -190,7 +194,7 @@ export function AnswerCompatibilityQuestionContent(props: {
)}
>
<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
value={answer.multiple_choice}
setValue={(choice) =>
@@ -200,7 +204,7 @@ export function AnswerCompatibilityQuestionContent(props: {
/>
</Col>
<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
values={answer.pref_choices ?? []}
setValue={(choice) =>
@@ -210,10 +214,10 @@ export function AnswerCompatibilityQuestionContent(props: {
/>
</Col>
<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
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) =>
setAnswer({...answer, importance: choice})
}
@@ -222,7 +226,7 @@ export function AnswerCompatibilityQuestionContent(props: {
</Col>
<Col className="-mt-6 gap-2">
<span className="text-ink-500 text-sm">
Your thoughts (optional, but recommended)
{t('answers.content.your_thoughts', 'Your thoughts (optional, but recommended)')}
</span>
<ExpandingInput
className={'w-full'}
@@ -259,7 +263,7 @@ export function AnswerCompatibilityQuestionContent(props: {
skipLoading && 'animate-pulse'
)}
>
Skip
{t('answers.menu.skip', 'Skip')}
</button>
)}
<Button
@@ -284,7 +288,7 @@ export function AnswerCompatibilityQuestionContent(props: {
.finally(() => setLoading(false))
}}
>
{isLastQuestion ? 'Finish' : 'Next'}
{isLastQuestion ? t('answers.finish', 'Finish') : t('answers.next', 'Next')}
</Button>
</Row>
</Col>

View File

@@ -6,6 +6,7 @@ import clsx from 'clsx'
import { User } from 'common/user'
import { shortenName } from 'web/components/widgets/user-link'
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/outline'
import {useT} from 'web/lib/locale'
export function PreferredList(props: {
question: QuestionWithCountType
@@ -16,6 +17,7 @@ export function PreferredList(props: {
}) {
const { question, answer, comparedAnswer, comparedUser, isComparedUser } =
props
const t = useT()
const { multiple_choice_options } = question
if (!multiple_choice_options) return null
const sortedEntries = Object.entries(multiple_choice_options).sort(
@@ -54,8 +56,9 @@ export function PreferredList(props: {
) : (
<XCircleIcon className="h-4 w-4" />
)}
{isComparedUser ? 'Your' : shortenName(comparedUser.name) + "'s"}{' '}
answer
{isComparedUser
? t('answers.preferred.your_answer', 'Your answer')
: t('answers.preferred.user_answer', "{name}'s answer", { name: shortenName(comparedUser.name) })}
</Row>
)}
</Row>

View File

@@ -45,6 +45,7 @@ import {buildArray} from 'common/util/array'
import toast from "react-hot-toast";
import {useCompatibleProfiles} from "web/hooks/use-profiles";
import {CompatibleBadge} from "web/components/widgets/compatible-badge";
import {useT} from 'web/lib/locale'
const NUM_QUESTIONS_TO_SHOW = 8
@@ -84,6 +85,7 @@ export function CompatibilityQuestionsDisplay(props: {
fromProfilePage?: Profile
}) {
const {isCurrentUser, user, fromSignup, fromProfilePage, profile} = props
const t = useT()
const currentUser = useUser()
const compatibleProfiles = useCompatibleProfiles(currentUser?.id)
@@ -175,9 +177,11 @@ export function CompatibilityQuestionsDisplay(props: {
<Col className="gap-4">
<Row className="flex-wrap items-center justify-between gap-x-6 gap-y-4">
<Row className={'gap-8'}>
<Subtitle>{`${
isCurrentUser ? 'Your' : shortenName(user.name) + `'s`
} Compatibility Prompts`}</Subtitle>
<Subtitle>
{isCurrentUser
? t('answers.display.your_prompts', 'Your Compatibility Prompts')
: t('answers.display.user_prompts', "{name}'s Compatibility Prompts", { name: shortenName(user.name) })}
</Subtitle>
{compatibilityScore &&
<CompatibleBadge compatibility={compatibilityScore} className={'mt-4 mr-4'}/>
}
@@ -194,10 +198,11 @@ export function CompatibilityQuestionsDisplay(props: {
</Row>
{answeredQuestions.length <= 0 ? (
<span className="text-ink-600 text-sm">
{isCurrentUser ? "You haven't" : `${user.name} hasn't`} answered any
compatibility questions yet!{' '}
{isCurrentUser
? 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 && (
<>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>
) : (
@@ -206,11 +211,11 @@ export function CompatibilityQuestionsDisplay(props: {
<span className='custom-link'>
{otherQuestions.length < 1 ? (
<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 className="text-ink-600 text-sm">
Answer more questions to increase your compatibility scoresor{' '}
{t('answers.display.answer_more', 'Answer more questions to increase your compatibility scores—or ')}
</span>
)}
<AddCompatibilityQuestionButton
@@ -233,7 +238,7 @@ export function CompatibilityQuestionsDisplay(props: {
)
})}
{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 currentUser = useUser()
const t = useT()
const sortToDisplay = {
'your-important': fromProfilePage
? `Important to ${fromProfilePage.user.name}`
: 'Important to you',
'their-important': `Important to ${user.name}`,
disagree: 'Incompatible',
'your-unanswered': 'Unanswered by you',
? t('answers.sort.important_to_user', 'Important to {name}', { name: fromProfilePage.user.name })
: t('answers.sort.important_to_you', 'Important to you'),
'their-important': t('answers.sort.important_to_them', 'Important to {name}', { name: user.name }),
disagree: t('answers.sort.incompatible', 'Incompatible'),
'your-unanswered': t('answers.sort.unanswered_by_you', 'Unanswered by you'),
}
const shownSorts = buildArray(
@@ -339,6 +345,7 @@ export function CompatibilityAnswerBlock(props: {
const [editOpen, setEditOpen] = useState<boolean>(false)
const currentUser = useUser()
const currentProfile = useProfile()
const t = useT()
const [newAnswer, setNewAnswer] = useState<CompatibilityAnswerSubmitType | undefined>(props.answer)
@@ -408,12 +415,12 @@ export function CompatibilityAnswerBlock(props: {
<DropdownMenu
items={[
{
name: 'Edit',
name: t('answers.menu.edit', 'Edit'),
icon: <PencilIcon className="h-5 w-5"/>,
onClick: () => setEditOpen(true),
},
{
name: 'Delete',
name: t('answers.menu.delete', 'Delete'),
icon: <TrashIcon className="h-5 w-5"/>,
onClick: () => {
deleteCompatibilityAnswer(answer.id, user.id)
@@ -436,7 +443,7 @@ export function CompatibilityAnswerBlock(props: {
<DropdownMenu
items={[
{
name: 'Skip',
name: t('answers.menu.skip', 'Skip'),
icon: <TrashIcon className="h-5 w-5"/>,
onClick: () => {
submitCompatibilityAnswer(getEmptyAnswer(user.id, question.id))
@@ -469,9 +476,9 @@ export function CompatibilityAnswerBlock(props: {
{distinctPreferredAnswersText.length > 0 && (
<Col className="gap-2">
<div className="text-ink-800 text-sm">
{preferredDoesNotIncludeAnswerText
? 'Acceptable'
: 'Also acceptable'}
{preferredDoesNotIncludeAnswerText
? t('answers.display.acceptable', 'Acceptable')
: t('answers.display.also_acceptable', 'Also acceptable')}
</div>
<Row className="flex-wrap gap-2 mt-0">
{distinctPreferredAnswersText.map((text) => (
@@ -568,6 +575,8 @@ function CompatibilityDisplay(props: {
currentUser,
} = props
const t = useT()
const [answer2, setAnswer2] = useState<
rowFor<'compatibility_answers'> | null | undefined
>(undefined)
@@ -635,7 +644,7 @@ function CompatibilityDisplay(props: {
: 'bg-red-500/20 hover:bg-red-500/30'
)}
>
{answerCompatibility ? 'Compatible' : 'Incompatible'}
{answerCompatibility ? t('answers.compatible', 'Compatible') : t('answers.incompatible', 'Incompatible')}
</button>
</>
)}
@@ -644,10 +653,10 @@ function CompatibilityDisplay(props: {
<Subtitle>{question.question}</Subtitle>
<Col className={clsx('w-full gap-1', SCROLLABLE_MODAL_CLASS)}>
<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 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">
<ImportanceDisplay importance={answer1.importance}/>
</span>
@@ -666,13 +675,14 @@ function CompatibilityDisplay(props: {
/>
<div className="text-ink-600 mt-6 items-center gap-2">
{`${
isCurrentUser ? 'Your' : shortenName(user2.name) + `'s`
} preferred answers`}
{isCurrentUser
? t('answers.modal.your_preferred', 'Your preferred answers')
: t('answers.modal.preferred_of_user', "{name}'s preferred answers", { name: shortenName(user2.name) })}
</div>
<div className="text-ink-500 text-sm">
{isCurrentUser ? 'You' : shortenName(user2.name)} marked this
as{' '}
{isCurrentUser
? 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">
<ImportanceDisplay importance={answer2.importance}/>
</span>
@@ -694,9 +704,10 @@ function CompatibilityDisplay(props: {
function ImportanceDisplay(props: { importance: number }) {
const {importance} = props
const t = useT()
return (
<span className={clsx('w-fit')}>
{getStringKeyFromNumValue(importance, IMPORTANCE_CHOICES)}
{t(`answers.importance.${importance}`, getStringKeyFromNumValue(importance, IMPORTANCE_CHOICES) as string)}
</span>
)
}

View File

@@ -10,6 +10,7 @@ import {IndividualQuestionRow} from '../questions-form'
import {TbMessage} from 'react-icons/tb'
import {OtherProfileAnswers} from './other-profile-answers'
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
import {useT} from 'web/lib/locale'
export function AddQuestionButton(props: {
isFirstQuestion?: boolean
@@ -22,12 +23,13 @@ export function AddQuestionButton(props: {
false,
`add-question-${user.id}`
)
const t = useT()
return (
<>
<Button color={'gray-outline'} onClick={() => setOpenModal(true)}>
<Row className="items-center gap-1">
<PlusIcon className="h-4 w-4"/>
Add Free Response
{t('answers.free.add_free_response', 'Add Free Response')}
</Row>
</Button>
<AddQuestionModal
@@ -63,6 +65,7 @@ function AddQuestionModal(props: {
null,
`selected-expanded-question-${user.id}}`
)
const t = useT()
return (
<Modal open={open} setOpen={setOpen}>
@@ -91,7 +94,7 @@ function AddQuestionModal(props: {
) : selectedQuestion == null ? (
<>
<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>
<Col className={SCROLLABLE_MODAL_CLASS}>
{addableQuestions.map((question) => {

View File

@@ -27,6 +27,7 @@ import { partition } from 'lodash'
import { shortenName } from 'web/components/widgets/user-link'
import { AddQuestionButton } from './free-response-add-question'
import { Profile } from 'common/profiles/profile'
import {useT} from 'web/lib/locale'
export function FreeResponseDisplay(props: {
isCurrentUser: boolean
@@ -34,6 +35,7 @@ export function FreeResponseDisplay(props: {
fromProfilePage: Profile | undefined
}) {
const { isCurrentUser, user, fromProfilePage } = props
const t = useT()
const { refreshAnswers, answers: allAnswers } = useUserAnswers(user?.id)
@@ -59,9 +61,11 @@ export function FreeResponseDisplay(props: {
return (
<Col className="gap-2">
<Row className={'w-full items-center justify-between gap-2'}>
<Subtitle>{`${
isCurrentUser ? 'Your' : shortenName(user.name) + `'s`
} Free Response`}</Subtitle>
<Subtitle>
{isCurrentUser
? t('answers.free.your_title', 'Your Free Response')
: t('answers.free.user_title', "{name}'s Free Response", { name: shortenName(user.name) })}
</Subtitle>
</Row>
<Col className="gap-2">
@@ -101,6 +105,7 @@ function AnswerBlock(props: {
const { answer, questions, isCurrentUser, user, refreshAnswers } = props
const question = questions.find((q) => q.id === answer.question_id)
const [edit, setEdit] = useState(false)
const t = useT()
const [otherAnswerModal, setOtherAnswerModal] = useState<boolean>(false)
@@ -119,18 +124,18 @@ function AnswerBlock(props: {
<DropdownMenu
items={[
{
name: 'Edit',
name: t('answers.menu.edit', 'Edit'),
icon: <PencilIcon className="h-5 w-5" />,
onClick: () => setEdit(true),
},
{
name: 'Delete',
name: t('answers.menu.delete', 'Delete'),
icon: <XIcon className="h-5 w-5" />,
onClick: () =>
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" />,
onClick: () => setOtherAnswerModal(true),
},

View File

@@ -10,6 +10,7 @@ import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { Subtitle } from '../widgets/profile-subtitle'
import { BiTachometer } from 'react-icons/bi'
import {useT} from 'web/lib/locale'
export function OpinionScale(props: {
multiChoiceAnswers: rowFor<'compatibility_answers_free'>[]
@@ -17,6 +18,7 @@ export function OpinionScale(props: {
isCurrentUser: boolean
}) {
const { multiChoiceAnswers, questions, isCurrentUser } = props
const t = useT()
const answeredMultiChoice = multiChoiceAnswers.filter(
(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')}>
<Row className="items-center gap-1">
<BiTachometer className="h-5 w-5" />
Fill Opinion Scale
{t('answers.opinion.fill', 'Fill Opinion Scale')}
</Row>
</Button>
)
@@ -39,7 +41,7 @@ export function OpinionScale(props: {
return (
<Col className="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 && (
<Button
@@ -52,7 +54,7 @@ export function OpinionScale(props: {
}}
>
<PencilIcon className="mr-2 h-4 w-4" />
Edit
{t('answers.opinion.edit', 'Edit')}
</Button>
)}
</Row>

View File

@@ -13,24 +13,28 @@ import ReactMarkdown from "react-markdown";
import Link from "next/link"
import {MIN_BIO_LENGTH} from "common/constants";
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!";
const tips = `
export function BioTips() {
const t = useT();
const tips = t('profile.bio.tips_list', `
- Your core values, interests, and activities
- Personality traits, what makes you unique and what you care about
- Connection goals (collaborative, friendship, romantic)
- Expectations and boundaries
- Availability, how to contact you or start a conversation (email, social media, etc.)
- Optional: romantic preferences, lifestyle habits, and conversation starters
`
export function BioTips() {
`);
return (
<ShowMore labelClosed="Tips" labelOpen="Hide info" className={'custom-link text-sm'}>
<p>Write a clear and engaging bio to help others understand who you are and the connections you seek. Include:</p>
<ShowMore
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>
<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>
)
}
@@ -43,6 +47,7 @@ export function EditableBio(props: {
const {profile, onCancel, onSave} = props
const [editor, setEditor] = useState<any>(null)
const [textLength, setTextLength] = useState(0);
const t = useT();
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">
{onCancel && (
<Button size="xs" color="gray-outline" onClick={onCancel}>
Cancel
{t('profile.bio.cancel', 'Cancel')}
</Button>
)}
<Button
@@ -84,7 +89,7 @@ export function EditableBio(props: {
onSave()
}}
>
Save
{t('profile.bio.save', 'Save')}
</Button>
</Row>
)}
@@ -115,13 +120,15 @@ interface BaseBioProps {
}
export function BaseBio({defaultValue, onBlur, onEditor}: BaseBioProps) {
const t = useT();
const editor = useTextEditor({
// extensions: [StarterKit],
max: MAX_DESCRIPTION_LENGTH,
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 remainingChars = MIN_BIO_LENGTH - textLength
useEffect(() => {
onEditor?.(editor)
@@ -131,8 +138,10 @@ export function BaseBio({defaultValue, onBlur, onEditor}: BaseBioProps) {
<div>
{textLength < MIN_BIO_LENGTH &&
<p>
Add {MIN_BIO_LENGTH - textLength} more {MIN_BIO_LENGTH - textLength === 1 ? 'character' : 'characters'} so
you can appear in search resultsor take your time and start by exploring others.
{remainingChars === 1
? 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>
}
<BioTips/>

View File

@@ -10,6 +10,7 @@ import { Content } from 'web/components/widgets/editor'
import { updateProfile } from 'web/lib/api'
import { EditableBio } from './editable-bio'
import { tryCatch } from 'common/util/try-catch'
import { useT } from 'web/lib/locale'
export function BioBlock(props: {
isCurrentUser: boolean
@@ -19,6 +20,7 @@ export function BioBlock(props: {
setEdit: (edit: boolean) => void
}) {
const { isCurrentUser, refreshProfile, profile, edit, setEdit } = props
const t = useT()
return (
<Col
@@ -47,12 +49,12 @@ export function BioBlock(props: {
<DropdownMenu
items={[
{
name: 'Edit',
name: t('profile.bio.edit', 'Edit'),
icon: <PencilIcon className="h-5 w-5" />,
onClick: () => setEdit(true),
},
{
name: 'Delete',
name: t('profile.bio.delete', 'Delete'),
icon: <XIcon className="h-5 w-5" />,
onClick: async () => {
const { error } = await tryCatch(updateProfile({ bio: null }))

View File

@@ -7,9 +7,11 @@ import {MAX_INT, MIN_BIO_LENGTH} from "common/constants";
import {useTextEditor} from "web/components/widgets/editor";
import {JSONContent} from "@tiptap/core"
import {flip, offset, shift, useFloating} from "@floating-ui/react-dom";
import {useT} from "web/lib/locale";
export default function TooShortBio() {
const [open, setOpen] = useState(false);
const t = useT();
const {y, refs, strategy} = useFloating({
placement: "bottom", // place below the trigger
middleware: [
@@ -21,7 +23,7 @@ export default function TooShortBio() {
return (
<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
className="inline-flex align-middle"
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"
>
<p className="text-sm text-gray-800 dark:text-gray-100">
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.
{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.")}
</p>
</div>
)}
@@ -67,6 +67,7 @@ export function ProfileBio(props: {
const [edit, setEdit] = useState(false)
const editor = useTextEditor({defaultValue: ''})
const [textLength, setTextLength] = useState(MAX_INT)
const t = useT();
useEffect(() => {
if (!editor) return
@@ -80,7 +81,7 @@ export function ProfileBio(props: {
return (
<Col>
{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
isCurrentUser={isCurrentUser}
profile={profile}

View File

@@ -12,6 +12,7 @@ import {useClickOutside} from "web/hooks/use-click-outside"
import {PrivateChatMessage} from "common/chat-message";
import {updateReactionUI} from "web/lib/supabase/chat-messages";
import {useIsMobile} from "web/hooks/use-is-mobile";
import {useT} from 'web/lib/locale'
const REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '👎']
@@ -37,6 +38,7 @@ export function MessageActions(props: {
const user = useUser()
const isOwner = user?.id === message.userId
const isMobile = useIsMobile()
const t = useT()
useClickOutside(emojiPickerRef, () => {
setShowEmojiPicker(false)
@@ -50,17 +52,17 @@ export function MessageActions(props: {
}, [openEmojiPickerKey])
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
try {
await api('delete-message', {messageId})
toast.success('Message deleted')
toast.success(t('messages.deleted', 'Message deleted'))
setMessages?.((prevMessages) => {
if (!prevMessages) return prevMessages
return prevMessages.filter((m) => m.id !== messageId)
})
} catch (error) {
toast.error('Failed to delete message')
toast.error(t('messages.delete_failed', 'Failed to delete message'))
console.error(error)
}
}
@@ -97,17 +99,17 @@ export function MessageActions(props: {
<DropdownMenu
items={[
isOwner && {
name: 'Edit',
name: t('messages.action.edit', 'Edit'),
icon: <PencilIcon className="h-4 w-4"/>,
onClick: onRequestEdit,
},
isOwner && {
name: 'Delete',
name: t('messages.action.delete', 'Delete'),
icon: <TrashIcon className="h-4 w-4"/>,
onClick: handleDelete,
},
{
name: 'Add Reaction',
name: t('messages.action.add_reaction', 'Add Reaction'),
icon: <EmojiHappyIcon className="h-4 w-4"/>,
onClick: () => {
setShowEmojiPicker(!showEmojiPicker)
@@ -121,7 +123,7 @@ export function MessageActions(props: {
/>
)}
{/*{message.isEdited && (*/}
{/* <span className="text-xs text-gray-400">edited</span>*/}
{/* <span className="text-xs text-gray-400">{t('messages.edited', 'edited')}</span>*/}
{/*)}*/}
</div>
)

View File

@@ -17,6 +17,7 @@ import { Tooltip } from 'web/components/widgets/tooltip'
import { track } from 'web/lib/service/analytics'
import { firebaseLogin } from 'web/lib/firebase/users'
import { useEvent } from 'web/hooks/use-event'
import {useT} from "web/lib/locale";
export function CommentInput(props: {
replyToUserInfo?: ReplyToUserInfo
@@ -144,6 +145,8 @@ export function CommentInputTextArea(props: {
commentTypes = ['comment'],
cancelEditing,
} = props
const t = useT()
useEffect(() => {
editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor])
@@ -216,7 +219,7 @@ export function CommentInputTextArea(props: {
className="text-primary-600 hover:underline"
onClick={cancelEditing}
>
Cancel
{t("comment.cancel", "Cancel")}
</button>
</Row>
)}

View File

@@ -10,30 +10,29 @@ import {Title} from "web/components/widgets/title";
import toast from "react-hot-toast";
import Link from "next/link";
import {formLink} from "common/constants";
import {useT} from 'web/lib/locale' // added
export function ContactComponent() {
const user = useUser()
const t = useT() // use translations
const editor = useTextEditor({
max: MAX_DESCRIPTION_LENGTH,
defaultValue: '',
placeholder: 'Contact us here...',
placeholder: t('contact.editor.placeholder', 'Contact us here...'), // localized placeholder
})
const showButton = !!editor?.getText().length
return (
<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'}>
You can also contact us through this <Link href={formLink}>feedback form</Link> or any of our <Link
href={'/social'}>socials</Link>. Feel free to give your contact information if you'd like us to get back to you.
</p>
<h4 className="">Android App</h4>
<p className={'custom-link mb-4'}>
To release our app, Google requires a closed test with at least 12 testers for 14 days. Please share your Google Playregistered 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.
{t('contact.intro_prefix', "You can also contact us through this ")}
<Link href={formLink}>{t('contact.form_link', 'feedback form')}</Link>
{t('contact.intro_middle', ' or any of our ')}
<Link href={'/social'}>{t('contact.socials', 'socials')}</Link>
{t('contact.intro_suffix', ". Feel free to give your contact information if you'd like us to get back to you.")}
</p>
<Col>
<div className={'mb-2'}>
@@ -52,14 +51,14 @@ export function ContactComponent() {
userId: user?.id,
};
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
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>
</Row>
)}

View File

@@ -1,6 +1,7 @@
import clsx from 'clsx'
import { RangeSlider } from 'web/components/widgets/slider'
import {FilterFields} from "common/filters";
import {RangeSlider} from 'web/components/widgets/slider'
import {FilterFields} from "common/filters"
import {useT} from 'web/lib/locale'
export const PREF_AGE_MIN = 18
export const PREF_AGE_MAX = 100
@@ -22,11 +23,12 @@ export function AgeFilterText(props: {
const { pref_age_min, pref_age_max, highlightedClass } = props
const [noMinAge, noMaxAge] = getNoMinMaxAge(pref_age_min, pref_age_max)
const t = useT()
if (noMinAge && noMaxAge) {
return (
<span>
<span className={clsx('text-semibold', highlightedClass)}>Any</span>{' '}
<span className="hidden sm:inline">age</span>
<span className={clsx('text-semibold', highlightedClass)}>{t('filter.age.any', 'Any')}</span>{' '}
<span className="hidden sm:inline">{t('filter.age.age', 'age')}</span>
</span>
)
}
@@ -37,7 +39,7 @@ export function AgeFilterText(props: {
{'<'}
{pref_age_max}
</span>{' '}
years
{t('filter.age.years', 'years')}
</span>
)
}
@@ -48,7 +50,7 @@ export function AgeFilterText(props: {
{'>'}
{pref_age_min}
</span>{' '}
years
{t('filter.age.years', 'years')}
</span>
)
}
@@ -59,7 +61,7 @@ export function AgeFilterText(props: {
{' - '}
{pref_age_max}
</span>{' '}
years
{t('filter.age.years', 'years')}
</span>
)
}

Some files were not shown because too many files have changed in this diff Show More