diff --git a/.aiassistant/rules/guidelines.md b/.aiassistant/rules/guidelines.md index 884c6125..4ef3b760 100644 --- a/.aiassistant/rules/guidelines.md +++ b/.aiassistant/rules/guidelines.md @@ -3,25 +3,27 @@ apply: by model decision --- --- + trigger: always_on description: globs: + --- ## Project Structure - next.js react tailwind frontend `/web` - - broken down into pages, components, hooks, lib + - broken down into pages, components, hooks, lib - express node api server `/backend/api` - one off scripts, like migrations `/backend/scripts` - supabase postgres. schema in `/backend/supabase` - - supabase-generated types in `/backend/supabase/schema.ts` + - supabase-generated types in `/backend/supabase/schema.ts` - files shared between backend directories `/backend/shared` - - anything in `/backend` can import from `shared`, but not vice versa + - anything in `/backend` can import from `shared`, but not vice versa - files shared between the frontend and backend in `/common` - - `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility - functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from - `/common`, but not vice versa. + - `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility + functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from + `/common`, but not vice versa. ## Deployment @@ -56,18 +58,11 @@ export function HeadlineTabs(props: { notSticky?: boolean className?: string }) { - const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = - props + const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = props const user = useUser() return ( -
+
{headlines.map(({id, slug, title}) => ( ))} - {user && } + {user && } {user && (isAdminId(user.id) || isModId(user.id)) && ( - + )}
@@ -150,9 +145,7 @@ Here's the definition of usePersistentInMemoryState: ```ts export const usePersistentInMemoryState = (initialValue: T, key: string) => { - const [state, setState] = useStateCheckEquality( - safeJsonParse(store[key]) ?? initialValue - ) + const [state, setState] = useStateCheckEquality(safeJsonParse(store[key]) ?? initialValue) useEffect(() => { const storedValue = safeJsonParse(store[key]) ?? initialValue @@ -196,7 +189,7 @@ In `use-bets`, we have this hook to get live updates with useApiSubscription: ```ts export const useContractBets = ( contractId: string, - opts?: APIParams<'bets'> & { enabled?: boolean } + opts?: APIParams<'bets'> & {enabled?: boolean}, ) => { const {enabled = true, ...apiOptions} = { contractId, @@ -204,17 +197,11 @@ export const useContractBets = ( } const optionsKey = JSON.stringify(apiOptions) - const [newBets, setNewBets] = usePersistentInMemoryState( - [], - `${optionsKey}-bets` - ) + const [newBets, setNewBets] = usePersistentInMemoryState([], `${optionsKey}-bets`) const addBets = (bets: Bet[]) => { setNewBets((currentBets) => { - const uniqueBets = sortBy( - uniqBy([...currentBets, ...bets], 'id'), - 'createdTime' - ) + const uniqueBets = sortBy(uniqBy([...currentBets, ...bets], 'id'), 'createdTime') return uniqueBets.filter((b) => !betShouldBeFiltered(b, apiOptions)) }) } @@ -249,7 +236,7 @@ export function broadcastUpdatedPrivateUser(userId: string) { broadcast(`private-user/${userId}`, {}) } -export function broadcastUpdatedUser(user: Partial & { id: string }) { +export function broadcastUpdatedUser(user: Partial & {id: string}) { broadcast(`user/${user.id}`, {user}) } @@ -334,7 +321,7 @@ export const placeBet: APIHandler<'bet'> = async (props, auth) => { const isApi = auth.creds.kind === 'key' return await betsQueue.enqueueFn( () => placeBetMain(props, auth.uid, isApi), - [props.contractId, auth.uid] + [props.contractId, auth.uid], ) } ``` @@ -380,13 +367,10 @@ using the pg-promise library. The client (code in web) does not have permission Another example using the direct client: ```ts -export const getUniqueBettorIds = async ( - contractId: string, - pg: SupabaseDirectClient -) => { +export const getUniqueBettorIds = async (contractId: string, pg: SupabaseDirectClient) => { const res = await pg.manyOrNone( 'select distinct user_id from contract_bets where contract_id = $1', - [contractId] + [contractId], ) return res.map((r) => r.user_id as string) } @@ -441,7 +425,7 @@ const query = renderSql( from('contract_bets'), where('contract_id = ${id}', {id}), orderBy('created_time desc'), - limitValue != null && limit(limitValue) + limitValue != null && limit(limitValue), ) const res = await pg.manyOrNone(query) @@ -480,7 +464,7 @@ can do so via this SQL command (change the type if not `TEXT`): ```sql ALTER TABLE profiles - ADD COLUMN profile_field TEXT; +ADD COLUMN profile_field TEXT; ``` Store it in `add_profile_field.sql` in the [migrations](../backend/supabase/migrations) folder and @@ -544,28 +528,28 @@ How we apply it here This project uses three complementary test types. Use the right level for the job: - Unit tests - - Purpose: Verify a single function/module in isolation; fast, deterministic. - - Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`, - etc.). - - Runner: Jest (configured via root `jest.config.js`). - - Naming: `*.unit.test.ts` (or `.tsx` for React in `web`). - - When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies. + - Purpose: Verify a single function/module in isolation; fast, deterministic. + - Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`, + etc.). + - Runner: Jest (configured via root `jest.config.js`). + - Naming: `*.unit.test.ts` (or `.tsx` for React in `web`). + - When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies. - Integration tests - - Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without - spinning up the full app. - - Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`). - - Runner: Jest (configured via root `jest.config.js`). - - Naming: `*.integration.test.ts` (or `.tsx` for React in `web`). - - When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB, - component trees with providers. + - Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without + spinning up the full app. + - Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`). + - Runner: Jest (configured via root `jest.config.js`). + - Naming: `*.integration.test.ts` (or `.tsx` for React in `web`). + - When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB, + component trees with providers. - End-to-End (E2E) tests - - Purpose: Validate real user flows across the full stack. - - Where: Top-level `tests/e2e` with separate areas for `web` and `backend`. - - Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`). - - Naming: `*.e2e.spec.ts`. - - When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments. + - Purpose: Validate real user flows across the full stack. + - Where: Top-level `tests/e2e` with separate areas for `web` and `backend`. + - Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`). + - Naming: `*.e2e.spec.ts`. + - When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments. Quick commands @@ -635,23 +619,23 @@ web/ - Unit and integration tests live in each package’s `tests` folder and are executed by Jest via the root `jest.config.js` projects array. - Naming: - - Unit: `*.unit.test.ts` (or `.tsx` for React in `web`) - - Integration: `*.integration.test.ts` - - E2E (Playwright): `*.e2e.spec.ts` + - Unit: `*.unit.test.ts` (or `.tsx` for React in `web`) + - Integration: `*.integration.test.ts` + - E2E (Playwright): `*.e2e.spec.ts` ### Best Practices -* Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very +- Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very critical behavior. -* Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and +- Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and errors. -* Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response +- Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response schemas). -* Prefer userEvent Over fireEvent. It simulates real user interactions more accurately. -* Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they +- Prefer userEvent Over fireEvent. It simulates real user interactions more accurately. +- Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they render. -* 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 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. ### Jest Unit Testing Guide @@ -679,14 +663,14 @@ yarn test path/to/test.unit.test.ts #### Test Standards - Test file names should convey what to expect - - Follow the pattern: `.[unit,integration].test.ts`. Examples: - - filename.unit.test.ts - - filename.integration.test.ts + - Follow the pattern: `.[unit,integration].test.ts`. Examples: + - filename.unit.test.ts + - filename.integration.test.ts - Group related tests using describe blocks - Use descriptive test names that explain the expected behavior. - - Follow the pattern: "should `expected behavior` [relevant modifier]". Examples: - - should `ban user` [with matching user id] - - should `ban user` [with matching user name] + - Follow the pattern: "should `expected behavior` [relevant modifier]". Examples: + - should `ban user` [with matching user id] + - should `ban user` [with matching user name] #### Mocking @@ -730,15 +714,15 @@ When writing mocks, assert both outcome and interaction: Why mocking is important? -- *Isolation* - Test your code independently of databases, APIs, and external systems. Tests only fail when your code +- _Isolation_ - Test your code independently of databases, APIs, and external systems. Tests only fail when your code breaks, not when a server is down. -- *Speed* - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly +- _Speed_ - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly without waiting. -- *Control* - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce +- _Control_ - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce with real systems. -- *Reliability* - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same +- _Reliability_ - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same inputs = same results, every time. -- *Focus* - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to +- _Focus_ - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to actually work yet. ###### Use `jest.mock()` @@ -751,40 +735,40 @@ function’s return value isn’t used, there’s no need to mock it further. ```tsx //Function and module mocks -jest.mock('path/to/module'); +jest.mock('path/to/module') //Function and module imports -import {functionUnderTest} from "path/to/function" -import {module} from "path/to/module" +import {functionUnderTest} from 'path/to/function' +import {module} from 'path/to/module' describe('functionUnderTest', () => { //Setup beforeEach(() => { //Run before each test - jest.resetAllMocks(); // Resets any mocks from previous tests - }); + jest.resetAllMocks() // Resets any mocks from previous tests + }) afterEach(() => { //Run after each test - jest.restoreAllMocks(); // Cleans up between tests - }); + jest.restoreAllMocks() // Cleans up between tests + }) describe('when given valid input', () => { it('should describe what is being tested', async () => { //Arrange: Setup test data - const mockData = 'test'; + const mockData = 'test' //Act: Execute the function under test - const result = myFunction(mockData); + const result = myFunction(mockData) //Assert: Verify the result - expect(result).toBe('expected'); - }); - }); + expect(result).toBe('expected') + }) + }) describe('when an error occurs', () => { //Test cases for errors - }); -}); + }) +}) ``` ###### Modules @@ -794,27 +778,27 @@ called and what it was called with. ```tsx //functionFile.ts -import {module as mockedDep} from "path/to/module" +import {module as mockedDep} from 'path/to/module' export const functionUnderTest = async (param) => { - return await mockedDep(param); -}; + return await mockedDep(param) +} ``` ```tsx //testFile.unit.test.ts -import {functionUnderTest} from "path/to/function"; -import {module as mockedDep} from "path/to/module"; +import {functionUnderTest} from 'path/to/function' +import {module as mockedDep} from 'path/to/module' -jest.mock('path/to/module'); +jest.mock('path/to/module') /** * Inside the test case * We create a mock for any information passed into the function that is being tested * and if the function returns a result we create a mock to test the result */ -const mockParam = "mockParam"; -const mockReturnValue = "mockModuleValue"; +const mockParam = 'mockParam' +const mockReturnValue = 'mockModuleValue' /** * use .mockResolvedValue when handling async/await modules that return values @@ -822,15 +806,15 @@ const mockReturnValue = "mockModuleValue"; */ describe('functionUnderTest', () => { it('returns mocked module value and calls dependency correctly', async () => { - (mockedDep as jest.Mock).mockResolvedValue(mockReturnValue); + ;(mockedDep as jest.Mock).mockResolvedValue(mockReturnValue) - const result = await functionUnderTest(mockParam); + const result = await functionUnderTest(mockParam) - expect(result).toBe(mockReturnValue); - expect(mockedDep).toHaveBeenCalledTimes(1); - expect(mockedDep).toHaveBeenCalledWith(mockParam); - }); -}); + expect(result).toBe(mockReturnValue) + expect(mockedDep).toHaveBeenCalledTimes(1) + expect(mockedDep).toHaveBeenCalledWith(mockParam) + }) +}) ``` Use namespace imports when you want to import everything a module exports under a single name. @@ -838,37 +822,36 @@ Use namespace imports when you want to import everything a module exports under ```tsx //moduleFile.ts export const module = async (param) => { - const value = "module" + const value = 'module' return value -}; +} export const moduleTwo = async (param) => { - const value = "moduleTwo" + const value = 'moduleTwo' return value -}; +} ``` ```tsx //functionFile.ts -import {module, moduleTwo} from "path/to/module" +import {module, moduleTwo} from 'path/to/module' export const functionUnderTest = async (param) => { const mockValue = await moduleTwo(param) const returnValue = await module(mockValue) - return returnValue; -}; + return returnValue +} ``` ```tsx //testFile.unit.test.ts -jest.mock('path/to/module'); +jest.mock('path/to/module') /** * This creates an object containing all named exports from ./path/to/module */ -import * as mockModule from "path/to/module" - -(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue); +import * as mockModule from 'path/to/module' +;(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue) ``` When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`. @@ -876,21 +859,21 @@ When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`. - `jest.mock()` mocks the entire module, which is ideal for external dependencies like Axios or database clients. - `jest.spyOn()` mocks specific methods while keeping the real implementation for others. It can also be used to observe how a real method is called without changing its behavior. - - also replaces the need to have `jest.mock()` at the top of the file. + - also replaces the need to have `jest.mock()` at the top of the file. ```tsx //testFile.unit.test.ts -import * as mockModule from "path/to/module" +import * as mockModule from 'path/to/module' //Mocking the return value of the module -jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue); +jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue) //Spying on the module to check functionality -jest.spyOn(mockModule, 'module'); +jest.spyOn(mockModule, 'module') //You can assert the module functionality with both of the above exactly like you would if you used jest.mock() -expect(mockModule.module).toBeCalledTimes(1); -expect(mockModule.module).toBeCalledWith(mockParam); +expect(mockModule.module).toBeCalledTimes(1) +expect(mockModule.module).toBeCalledWith(mockParam) ``` ###### Dependencies @@ -900,119 +883,114 @@ external functionality. ```tsx //functionFile.ts -import {dependency} from "path/to/dependency" +import {dependency} from 'path/to/dependency' export const functionUnderTest = async (param) => { - const depen = await dependency(); - const value = depen.module(); + const depen = await dependency() + const value = depen.module() - return value; -}; + return value +} ``` ```tsx //testFile.unit.test.ts -jest.mock('path/to/dependency'); +jest.mock('path/to/dependency') -import {dependency} from "path/to/dependency" +import {dependency} from 'path/to/dependency' describe('functionUnderTest', () => { /** * Because the dependency has modules that are used we need to * create a variable outside of scope that can be asserted on */ - let mockDependency = {} as any; + let mockDependency = {} as any beforeEach(() => { mockDependency = { module: jest.fn(), - }; - jest.resetAllMocks(); // Resets any mocks from previous tests - }); + } + jest.resetAllMocks() // Resets any mocks from previous tests + }) afterEach(() => { //Run after each test - jest.restoreAllMocks(); // Cleans up between tests - }); + jest.restoreAllMocks() // Cleans up between tests + }) //Inside the test case - (mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue); + ;(mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue) - expect(mockDependency.module).toBeCalledTimes(1); - expect(mockDependency.module).toBeCalledWith(mockParam); -}); + expect(mockDependency.module).toBeCalledTimes(1) + expect(mockDependency.module).toBeCalledWith(mockParam) +}) ``` ###### Error checking ```tsx //function.ts -const result = await functionName(param); +const result = await functionName(param) if (!result) { - throw new Error(403, 'Error text', error); + throw new Error(403, 'Error text', error) } -; ``` ```tsx //testFile.unit.test.ts -const mockParam = {} as any; +const mockParam = {} as any //This will check only the error message -expect(functionName(mockParam)) - .rejects - .toThrowError('Error text'); +expect(functionName(mockParam)).rejects.toThrowError('Error text') //This will check the complete error try { - await functionName(mockParam); - fail('Should have thrown'); + await functionName(mockParam) + fail('Should have thrown') } catch (error) { - const functionError = error as Error; - expect(functionError.code).toBe(403); - expect(functionError.message).toBe('Error text'); - expect(functionError.details).toBe(mockParam); - expect(functionError.name).toBe('Error'); + const functionError = error as Error + expect(functionError.code).toBe(403) + expect(functionError.message).toBe('Error text') + expect(functionError.details).toBe(mockParam) + expect(functionError.name).toBe('Error') } ``` ```tsx //For console.error types -console.error('Error message', error); +console.error('Error message', error) //Use spyOn to mock -const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { -}); +const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) expect(errorSpy).toHaveBeenCalledWith( 'Error message', - expect.objectContaining({name: 'Error'}) //The error 'name' refers to the error type -); - + expect.objectContaining({name: 'Error'}), //The error 'name' refers to the error type +) ``` ###### Mocking array return value ```tsx //arrayFile.ts -const exampleArray = [1, 2, 3, 4, 5]; +const exampleArray = [1, 2, 3, 4, 5] -const arrayResult = exampleArray.includes(2); +const arrayResult = exampleArray.includes(2) ``` ```tsx //testFile.unit.test.ts //This will mock 'includes' for all arrays and force the return value to be true -jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); +jest.spyOn(Array.prototype, 'includes').mockReturnValue(true) // --- //This will specify which 'includes' array to mock based on the args passed into the .includes() jest.spyOn(Array.prototype, 'includes').mockImplementation(function (value) { if (value === 2) { - return true; + return true } - return false; -}); + return false +}) ``` ### Playwright (E2E) Testing Guide @@ -1038,23 +1016,26 @@ yarn test:db:reset Use this priority order for selecting elements in Playwright tests: 1. Prefer `getByRole()` — use semantic roles that reflect how users interact + ```typescript - await page.getByRole('button', { name: 'Submit' }).click(); + await page.getByRole('button', {name: 'Submit'}).click() ``` + If a meaningful ARIA role is not available, fall back to accessible text selectors (next point). 2. Use accessible text selectors — when roles don't apply, target user-facing text + ```typescript - await page.getByLabel('Email').fill('user@example.com'); - await page.getByPlaceholder('Enter your name').fill('John'); - await page.getByText('Welcome back').isVisible(); + await page.getByLabel('Email').fill('user@example.com') + await page.getByPlaceholder('Enter your name').fill('John') + await page.getByText('Welcome back').isVisible() ``` 3. Only use `data-testid` — when elements have no stable user-facing text ```typescript // For icons, toggles, or dynamic content without text - await page.getByTestId('menu-toggle').click(); - await page.getByTestId('loading-spinner').isVisible(); + await page.getByTestId('menu-toggle').click() + await page.getByTestId('loading-spinner').isVisible() ``` This hierarchy mirrors how users actually interact with your application, making tests more reliable and meaningful. diff --git a/.github/ISSUE_TEMPLATE/other.yml b/.github/ISSUE_TEMPLATE/other.yml index 72d05b0a..61d5b1e9 100644 --- a/.github/ISSUE_TEMPLATE/other.yml +++ b/.github/ISSUE_TEMPLATE/other.yml @@ -12,9 +12,9 @@ body: attributes: label: Info description: | - - Browser: [e.g. chrome, safari] - - Device (if mobile): [e.g. iPhone6] - - Build info + - Browser: [e.g. chrome, safari] + - Device (if mobile): [e.g. iPhone6] + - Build info placeholder: | Build info from `Settings` -> `About` validations: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aa447661..047fd57d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,5 +4,5 @@ - [ ] Tests added and passed if fixing a bug or adding a new feature. ### Description - + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c939b721..e6f75f32 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,17 +7,17 @@ globs: ## Project Structure - next.js react tailwind frontend `/web` - - broken down into pages, components, hooks, lib + - broken down into pages, components, hooks, lib - express node api server `/backend/api` - one off scripts, like migrations `/backend/scripts` - supabase postgres. schema in `/backend/supabase` - - supabase-generated types in `/backend/supabase/schema.ts` + - supabase-generated types in `/backend/supabase/schema.ts` - files shared between backend directories `/backend/shared` - - anything in `/backend` can import from `shared`, but not vice versa + - anything in `/backend` can import from `shared`, but not vice versa - files shared between the frontend and backend in `/common` - - `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility - functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from - `/common`, but not vice versa. + - `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility + functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from + `/common`, but not vice versa. ## Deployment @@ -52,18 +52,11 @@ export function HeadlineTabs(props: { notSticky?: boolean className?: string }) { - const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = - props + const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = props const user = useUser() return ( -
+
{headlines.map(({id, slug, title}) => ( ))} - {user && } + {user && } {user && (isAdminId(user.id) || isModId(user.id)) && ( - + )}
@@ -146,9 +139,7 @@ Here's the definition of usePersistentInMemoryState: ```ts export const usePersistentInMemoryState = (initialValue: T, key: string) => { - const [state, setState] = useStateCheckEquality( - safeJsonParse(store[key]) ?? initialValue - ) + const [state, setState] = useStateCheckEquality(safeJsonParse(store[key]) ?? initialValue) useEffect(() => { const storedValue = safeJsonParse(store[key]) ?? initialValue @@ -192,7 +183,7 @@ In `use-bets`, we have this hook to get live updates with useApiSubscription: ```ts export const useContractBets = ( contractId: string, - opts?: APIParams<'bets'> & { enabled?: boolean } + opts?: APIParams<'bets'> & {enabled?: boolean}, ) => { const {enabled = true, ...apiOptions} = { contractId, @@ -200,17 +191,11 @@ export const useContractBets = ( } const optionsKey = JSON.stringify(apiOptions) - const [newBets, setNewBets] = usePersistentInMemoryState( - [], - `${optionsKey}-bets` - ) + const [newBets, setNewBets] = usePersistentInMemoryState([], `${optionsKey}-bets`) const addBets = (bets: Bet[]) => { setNewBets((currentBets) => { - const uniqueBets = sortBy( - uniqBy([...currentBets, ...bets], 'id'), - 'createdTime' - ) + const uniqueBets = sortBy(uniqBy([...currentBets, ...bets], 'id'), 'createdTime') return uniqueBets.filter((b) => !betShouldBeFiltered(b, apiOptions)) }) } @@ -245,7 +230,7 @@ export function broadcastUpdatedPrivateUser(userId: string) { broadcast(`private-user/${userId}`, {}) } -export function broadcastUpdatedUser(user: Partial & { id: string }) { +export function broadcastUpdatedUser(user: Partial & {id: string}) { broadcast(`user/${user.id}`, {user}) } @@ -330,7 +315,7 @@ export const placeBet: APIHandler<'bet'> = async (props, auth) => { const isApi = auth.creds.kind === 'key' return await betsQueue.enqueueFn( () => placeBetMain(props, auth.uid, isApi), - [props.contractId, auth.uid] + [props.contractId, auth.uid], ) } ``` @@ -376,13 +361,10 @@ using the pg-promise library. The client (code in web) does not have permission Another example using the direct client: ```ts -export const getUniqueBettorIds = async ( - contractId: string, - pg: SupabaseDirectClient -) => { +export const getUniqueBettorIds = async (contractId: string, pg: SupabaseDirectClient) => { const res = await pg.manyOrNone( 'select distinct user_id from contract_bets where contract_id = $1', - [contractId] + [contractId], ) return res.map((r) => r.user_id as string) } @@ -437,7 +419,7 @@ const query = renderSql( from('contract_bets'), where('contract_id = ${id}', {id}), orderBy('created_time desc'), - limitValue != null && limit(limitValue) + limitValue != null && limit(limitValue), ) const res = await pg.manyOrNone(query) @@ -476,7 +458,7 @@ can do so via this SQL command (change the type if not `TEXT`): ```sql ALTER TABLE profiles - ADD COLUMN profile_field TEXT; +ADD COLUMN profile_field TEXT; ``` Store it in `add_profile_field.sql` in the [migrations](../backend/supabase/migrations) folder and @@ -540,28 +522,28 @@ How we apply it here This project uses three complementary test types. Use the right level for the job: - Unit tests - - Purpose: Verify a single function/module in isolation; fast, deterministic. - - Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`, - etc.). - - Runner: Jest (configured via root `jest.config.js`). - - Naming: `*.unit.test.ts` (or `.tsx` for React in `web`). - - When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies. + - Purpose: Verify a single function/module in isolation; fast, deterministic. + - Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`, + etc.). + - Runner: Jest (configured via root `jest.config.js`). + - Naming: `*.unit.test.ts` (or `.tsx` for React in `web`). + - When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies. - Integration tests - - Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without - spinning up the full app. - - Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`). - - Runner: Jest (configured via root `jest.config.js`). - - Naming: `*.integration.test.ts` (or `.tsx` for React in `web`). - - When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB, - component trees with providers. + - Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without + spinning up the full app. + - Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`). + - Runner: Jest (configured via root `jest.config.js`). + - Naming: `*.integration.test.ts` (or `.tsx` for React in `web`). + - When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB, + component trees with providers. - End-to-End (E2E) tests - - Purpose: Validate real user flows across the full stack. - - Where: Top-level `tests/e2e` with separate areas for `web` and `backend`. - - Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`). - - Naming: `*.e2e.spec.ts`. - - When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments. + - Purpose: Validate real user flows across the full stack. + - Where: Top-level `tests/e2e` with separate areas for `web` and `backend`. + - Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`). + - Naming: `*.e2e.spec.ts`. + - When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments. Quick commands @@ -631,23 +613,23 @@ web/ - Unit and integration tests live in each package’s `tests` folder and are executed by Jest via the root `jest.config.js` projects array. - Naming: - - Unit: `*.unit.test.ts` (or `.tsx` for React in `web`) - - Integration: `*.integration.test.ts` - - E2E (Playwright): `*.e2e.spec.ts` + - Unit: `*.unit.test.ts` (or `.tsx` for React in `web`) + - Integration: `*.integration.test.ts` + - E2E (Playwright): `*.e2e.spec.ts` ### Best Practices -* Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very +- Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very critical behavior. -* Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and +- Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and errors. -* Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response +- Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response schemas). -* Prefer userEvent Over fireEvent. It simulates real user interactions more accurately. -* Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they +- Prefer userEvent Over fireEvent. It simulates real user interactions more accurately. +- Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they render. -* 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 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. ### Jest Unit Testing Guide @@ -675,14 +657,14 @@ yarn test path/to/test.unit.test.ts #### Test Standards - Test file names should convey what to expect - - Follow the pattern: `.[unit,integration].test.ts`. Examples: - - filename.unit.test.ts - - filename.integration.test.ts + - Follow the pattern: `.[unit,integration].test.ts`. Examples: + - filename.unit.test.ts + - filename.integration.test.ts - Group related tests using describe blocks - Use descriptive test names that explain the expected behavior. - - Follow the pattern: "should `expected behavior` [relevant modifier]". Examples: - - should `ban user` [with matching user id] - - should `ban user` [with matching user name] + - Follow the pattern: "should `expected behavior` [relevant modifier]". Examples: + - should `ban user` [with matching user id] + - should `ban user` [with matching user name] #### Mocking @@ -726,15 +708,15 @@ When writing mocks, assert both outcome and interaction: Why mocking is important? -- *Isolation* - Test your code independently of databases, APIs, and external systems. Tests only fail when your code +- _Isolation_ - Test your code independently of databases, APIs, and external systems. Tests only fail when your code breaks, not when a server is down. -- *Speed* - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly +- _Speed_ - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly without waiting. -- *Control* - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce +- _Control_ - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce with real systems. -- *Reliability* - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same +- _Reliability_ - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same inputs = same results, every time. -- *Focus* - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to +- _Focus_ - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to actually work yet. ###### Use `jest.mock()` @@ -747,40 +729,40 @@ function’s return value isn’t used, there’s no need to mock it further. ```tsx //Function and module mocks -jest.mock('path/to/module'); +jest.mock('path/to/module') //Function and module imports -import {functionUnderTest} from "path/to/function" -import {module} from "path/to/module" +import {functionUnderTest} from 'path/to/function' +import {module} from 'path/to/module' describe('functionUnderTest', () => { //Setup beforeEach(() => { //Run before each test - jest.resetAllMocks(); // Resets any mocks from previous tests - }); + jest.resetAllMocks() // Resets any mocks from previous tests + }) afterEach(() => { //Run after each test - jest.restoreAllMocks(); // Cleans up between tests - }); + jest.restoreAllMocks() // Cleans up between tests + }) describe('when given valid input', () => { it('should describe what is being tested', async () => { //Arrange: Setup test data - const mockData = 'test'; + const mockData = 'test' //Act: Execute the function under test - const result = myFunction(mockData); + const result = myFunction(mockData) //Assert: Verify the result - expect(result).toBe('expected'); - }); - }); + expect(result).toBe('expected') + }) + }) describe('when an error occurs', () => { //Test cases for errors - }); -}); + }) +}) ``` ###### Modules @@ -790,27 +772,27 @@ called and what it was called with. ```tsx //functionFile.ts -import {module as mockedDep} from "path/to/module" +import {module as mockedDep} from 'path/to/module' export const functionUnderTest = async (param) => { - return await mockedDep(param); -}; + return await mockedDep(param) +} ``` ```tsx //testFile.unit.test.ts -import {functionUnderTest} from "path/to/function"; -import {module as mockedDep} from "path/to/module"; +import {functionUnderTest} from 'path/to/function' +import {module as mockedDep} from 'path/to/module' -jest.mock('path/to/module'); +jest.mock('path/to/module') /** * Inside the test case * We create a mock for any information passed into the function that is being tested * and if the function returns a result we create a mock to test the result */ -const mockParam = "mockParam"; -const mockReturnValue = "mockModuleValue"; +const mockParam = 'mockParam' +const mockReturnValue = 'mockModuleValue' /** * use .mockResolvedValue when handling async/await modules that return values @@ -818,15 +800,15 @@ const mockReturnValue = "mockModuleValue"; */ describe('functionUnderTest', () => { it('returns mocked module value and calls dependency correctly', async () => { - (mockedDep as jest.Mock).mockResolvedValue(mockReturnValue); + ;(mockedDep as jest.Mock).mockResolvedValue(mockReturnValue) - const result = await functionUnderTest(mockParam); + const result = await functionUnderTest(mockParam) - expect(result).toBe(mockReturnValue); - expect(mockedDep).toHaveBeenCalledTimes(1); - expect(mockedDep).toHaveBeenCalledWith(mockParam); - }); -}); + expect(result).toBe(mockReturnValue) + expect(mockedDep).toHaveBeenCalledTimes(1) + expect(mockedDep).toHaveBeenCalledWith(mockParam) + }) +}) ``` Use namespace imports when you want to import everything a module exports under a single name. @@ -834,37 +816,36 @@ Use namespace imports when you want to import everything a module exports under ```tsx //moduleFile.ts export const module = async (param) => { - const value = "module" + const value = 'module' return value -}; +} export const moduleTwo = async (param) => { - const value = "moduleTwo" + const value = 'moduleTwo' return value -}; +} ``` ```tsx //functionFile.ts -import {module, moduleTwo} from "path/to/module" +import {module, moduleTwo} from 'path/to/module' export const functionUnderTest = async (param) => { const mockValue = await moduleTwo(param) const returnValue = await module(mockValue) - return returnValue; -}; + return returnValue +} ``` ```tsx //testFile.unit.test.ts -jest.mock('path/to/module'); +jest.mock('path/to/module') /** * This creates an object containing all named exports from ./path/to/module */ -import * as mockModule from "path/to/module" - -(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue); +import * as mockModule from 'path/to/module' +;(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue) ``` When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`. @@ -872,21 +853,21 @@ When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`. - `jest.mock()` mocks the entire module, which is ideal for external dependencies like Axios or database clients. - `jest.spyOn()` mocks specific methods while keeping the real implementation for others. It can also be used to observe how a real method is called without changing its behavior. - - also replaces the need to have `jest.mock()` at the top of the file. + - also replaces the need to have `jest.mock()` at the top of the file. ```tsx //testFile.unit.test.ts -import * as mockModule from "path/to/module" +import * as mockModule from 'path/to/module' //Mocking the return value of the module -jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue); +jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue) //Spying on the module to check functionality -jest.spyOn(mockModule, 'module'); +jest.spyOn(mockModule, 'module') //You can assert the module functionality with both of the above exactly like you would if you used jest.mock() -expect(mockModule.module).toBeCalledTimes(1); -expect(mockModule.module).toBeCalledWith(mockParam); +expect(mockModule.module).toBeCalledTimes(1) +expect(mockModule.module).toBeCalledWith(mockParam) ``` ###### Dependencies @@ -896,119 +877,114 @@ external functionality. ```tsx //functionFile.ts -import {dependency} from "path/to/dependency" +import {dependency} from 'path/to/dependency' export const functionUnderTest = async (param) => { - const depen = await dependency(); - const value = depen.module(); + const depen = await dependency() + const value = depen.module() - return value; -}; + return value +} ``` ```tsx //testFile.unit.test.ts -jest.mock('path/to/dependency'); +jest.mock('path/to/dependency') -import {dependency} from "path/to/dependency" +import {dependency} from 'path/to/dependency' describe('functionUnderTest', () => { /** * Because the dependency has modules that are used we need to * create a variable outside of scope that can be asserted on */ - let mockDependency = {} as any; + let mockDependency = {} as any beforeEach(() => { mockDependency = { module: jest.fn(), - }; - jest.resetAllMocks(); // Resets any mocks from previous tests - }); + } + jest.resetAllMocks() // Resets any mocks from previous tests + }) afterEach(() => { //Run after each test - jest.restoreAllMocks(); // Cleans up between tests - }); + jest.restoreAllMocks() // Cleans up between tests + }) //Inside the test case - (mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue); + ;(mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue) - expect(mockDependency.module).toBeCalledTimes(1); - expect(mockDependency.module).toBeCalledWith(mockParam); -}); + expect(mockDependency.module).toBeCalledTimes(1) + expect(mockDependency.module).toBeCalledWith(mockParam) +}) ``` ###### Error checking ```tsx //function.ts -const result = await functionName(param); +const result = await functionName(param) if (!result) { - throw new Error(403, 'Error text', error); + throw new Error(403, 'Error text', error) } -; ``` ```tsx //testFile.unit.test.ts -const mockParam = {} as any; +const mockParam = {} as any //This will check only the error message -expect(functionName(mockParam)) - .rejects - .toThrowError('Error text'); +expect(functionName(mockParam)).rejects.toThrowError('Error text') //This will check the complete error try { - await functionName(mockParam); - fail('Should have thrown'); + await functionName(mockParam) + fail('Should have thrown') } catch (error) { - const functionError = error as Error; - expect(functionError.code).toBe(403); - expect(functionError.message).toBe('Error text'); - expect(functionError.details).toBe(mockParam); - expect(functionError.name).toBe('Error'); + const functionError = error as Error + expect(functionError.code).toBe(403) + expect(functionError.message).toBe('Error text') + expect(functionError.details).toBe(mockParam) + expect(functionError.name).toBe('Error') } ``` ```tsx //For console.error types -console.error('Error message', error); +console.error('Error message', error) //Use spyOn to mock -const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { -}); +const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) expect(errorSpy).toHaveBeenCalledWith( 'Error message', - expect.objectContaining({name: 'Error'}) //The error 'name' refers to the error type -); - + expect.objectContaining({name: 'Error'}), //The error 'name' refers to the error type +) ``` ###### Mocking array return value ```tsx //arrayFile.ts -const exampleArray = [1, 2, 3, 4, 5]; +const exampleArray = [1, 2, 3, 4, 5] -const arrayResult = exampleArray.includes(2); +const arrayResult = exampleArray.includes(2) ``` ```tsx //testFile.unit.test.ts //This will mock 'includes' for all arrays and force the return value to be true -jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); +jest.spyOn(Array.prototype, 'includes').mockReturnValue(true) // --- //This will specify which 'includes' array to mock based on the args passed into the .includes() jest.spyOn(Array.prototype, 'includes').mockImplementation(function (value) { if (value === 2) { - return true; + return true } - return false; -}); + return false +}) ``` ### Playwright (E2E) Testing Guide @@ -1034,23 +1010,26 @@ yarn test:db:reset Use this priority order for selecting elements in Playwright tests: 1. Prefer `getByRole()` — use semantic roles that reflect how users interact + ```typescript - await page.getByRole('button', { name: 'Submit' }).click(); + await page.getByRole('button', {name: 'Submit'}).click() ``` + If a meaningful ARIA role is not available, fall back to accessible text selectors (next point). 2. Use accessible text selectors — when roles don't apply, target user-facing text + ```typescript - await page.getByLabel('Email').fill('user@example.com'); - await page.getByPlaceholder('Enter your name').fill('John'); - await page.getByText('Welcome back').isVisible(); + await page.getByLabel('Email').fill('user@example.com') + await page.getByPlaceholder('Enter your name').fill('John') + await page.getByText('Welcome back').isVisible() ``` 3. Only use `data-testid` — when elements have no stable user-facing text ```typescript // For icons, toggles, or dynamic content without text - await page.getByTestId('menu-toggle').click(); - await page.getByTestId('loading-spinner').isVisible(); + await page.getByTestId('menu-toggle').click() + await page.getByTestId('loading-spinner').isVisible() ``` This hierarchy mirrors how users actually interact with your application, making tests more reliable and meaningful. diff --git a/.github/workflows/cd-android-live-update.yml b/.github/workflows/cd-android-live-update.yml index 0cc3b209..8d0df364 100644 --- a/.github/workflows/cd-android-live-update.yml +++ b/.github/workflows/cd-android-live-update.yml @@ -1,10 +1,10 @@ name: CD Android Live Update on: push: - branches: [ main, master ] + branches: [main, master] paths: - - "android/capawesome.json" - - ".github/workflows/cd-android-live-update.yml" + - 'android/capawesome.json' + - '.github/workflows/cd-android-live-update.yml' jobs: deploy: @@ -15,7 +15,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # we need full history for git log + fetch-depth: 0 # we need full history for git log - name: Install jq run: sudo apt-get install -y jq diff --git a/.github/workflows/cd-api.yml b/.github/workflows/cd-api.yml index b975d49d..e05ab407 100644 --- a/.github/workflows/cd-api.yml +++ b/.github/workflows/cd-api.yml @@ -1,10 +1,10 @@ name: CD API on: push: - branches: [ main, master ] + branches: [main, master] paths: - - "backend/api/package.json" - - ".github/workflows/cd-api.yml" + - 'backend/api/package.json' + - '.github/workflows/cd-api.yml' jobs: deploy: @@ -15,7 +15,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # we need full history for git log + fetch-depth: 0 # we need full history for git log - name: Install jq run: sudo apt-get install -y jq diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a10f524a..85403916 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,13 +2,12 @@ name: CD # Must select "Read and write permissions" in GitHub → Repo → Settings → Actions → General → Workflow permissions - on: push: - branches: [ main, master ] + branches: [main, master] paths: - - "package.json" - - ".github/workflows/cd.yml" + - 'package.json' + - '.github/workflows/cd.yml' jobs: release: @@ -18,7 +17,7 @@ jobs: - name: Checkout repo uses: actions/checkout@master with: - fetch-depth: 0 # To fetch all history for tags + fetch-depth: 0 # To fetch all history for tags - name: Setup Node.js uses: actions/setup-node@v4 @@ -32,4 +31,4 @@ jobs: git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} - ./scripts/release.sh \ No newline at end of file + ./scripts/release.sh diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index 2ef53776..1bb783f6 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -2,9 +2,9 @@ name: E2E Tests on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: e2e: @@ -26,7 +26,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '21' # Required for firebase-tools@15+ + java-version: '21' # Required for firebase-tools@15+ - name: Setup Supabase CLI uses: supabase/setup-cli@v1 @@ -41,8 +41,8 @@ jobs: - name: Run E2E tests env: - SKIP_DB_CLEANUP: true # Don't try to stop Docker in CI - FIREBASE_TOKEN: "dummy" # Suppresses auth warning + SKIP_DB_CLEANUP: true # Don't try to stop Docker in CI + FIREBASE_TOKEN: 'dummy' # Suppresses auth warning # or run: | yarn test:e2e @@ -61,4 +61,4 @@ jobs: with: name: test-results path: test-results/ - retention-days: 7 \ No newline at end of file + retention-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18631f48..79d38322 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Jest Tests on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: ci: @@ -29,22 +29,22 @@ jobs: run: yarn lint - name: Type check - run: npx tsc --noEmit + run: yarn typecheck - name: Run Jest tests env: NEXT_PUBLIC_FIREBASE_ENV: DEV run: | yarn test:coverage -# npm install -g lcov-result-merger -# mkdir coverage -# lcov-result-merger \ -# "backend/api/coverage/lcov.info" \ -# "backend/shared/coverage/lcov.info" \ -# "backend/email/coverage/lcov.info" \ -# "common/coverage/lcov.info" \ -# "web/coverage/lcov.info" \ -# > coverage/lcov.info + # npm install -g lcov-result-merger + # mkdir coverage + # lcov-result-merger \ + # "backend/api/coverage/lcov.info" \ + # "backend/shared/coverage/lcov.info" \ + # "backend/email/coverage/lcov.info" \ + # "common/coverage/lcov.info" \ + # "web/coverage/lcov.info" \ + # > coverage/lcov.info - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 91c504e9..e6f75f32 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -1,23 +1,23 @@ --- trigger: always_on description: -globs: +globs: --- ## Project Structure - next.js react tailwind frontend `/web` - - broken down into pages, components, hooks, lib + - broken down into pages, components, hooks, lib - express node api server `/backend/api` - one off scripts, like migrations `/backend/scripts` - supabase postgres. schema in `/backend/supabase` - - supabase-generated types in `/backend/supabase/schema.ts` + - supabase-generated types in `/backend/supabase/schema.ts` - files shared between backend directories `/backend/shared` - - anything in `/backend` can import from `shared`, but not vice versa + - anything in `/backend` can import from `shared`, but not vice versa - files shared between the frontend and backend in `/common` - - `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility - functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from - `/common`, but not vice versa. + - `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility + functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from + `/common`, but not vice versa. ## Deployment @@ -52,18 +52,11 @@ export function HeadlineTabs(props: { notSticky?: boolean className?: string }) { - const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = - props + const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = props const user = useUser() return ( -
+
{headlines.map(({id, slug, title}) => ( ))} - {user && } + {user && } {user && (isAdminId(user.id) || isModId(user.id)) && ( - + )}
@@ -146,9 +139,7 @@ Here's the definition of usePersistentInMemoryState: ```ts export const usePersistentInMemoryState = (initialValue: T, key: string) => { - const [state, setState] = useStateCheckEquality( - safeJsonParse(store[key]) ?? initialValue - ) + const [state, setState] = useStateCheckEquality(safeJsonParse(store[key]) ?? initialValue) useEffect(() => { const storedValue = safeJsonParse(store[key]) ?? initialValue @@ -192,7 +183,7 @@ In `use-bets`, we have this hook to get live updates with useApiSubscription: ```ts export const useContractBets = ( contractId: string, - opts?: APIParams<'bets'> & { enabled?: boolean } + opts?: APIParams<'bets'> & {enabled?: boolean}, ) => { const {enabled = true, ...apiOptions} = { contractId, @@ -200,17 +191,11 @@ export const useContractBets = ( } const optionsKey = JSON.stringify(apiOptions) - const [newBets, setNewBets] = usePersistentInMemoryState( - [], - `${optionsKey}-bets` - ) + const [newBets, setNewBets] = usePersistentInMemoryState([], `${optionsKey}-bets`) const addBets = (bets: Bet[]) => { setNewBets((currentBets) => { - const uniqueBets = sortBy( - uniqBy([...currentBets, ...bets], 'id'), - 'createdTime' - ) + const uniqueBets = sortBy(uniqBy([...currentBets, ...bets], 'id'), 'createdTime') return uniqueBets.filter((b) => !betShouldBeFiltered(b, apiOptions)) }) } @@ -245,7 +230,7 @@ export function broadcastUpdatedPrivateUser(userId: string) { broadcast(`private-user/${userId}`, {}) } -export function broadcastUpdatedUser(user: Partial & { id: string }) { +export function broadcastUpdatedUser(user: Partial & {id: string}) { broadcast(`user/${user.id}`, {user}) } @@ -330,7 +315,7 @@ export const placeBet: APIHandler<'bet'> = async (props, auth) => { const isApi = auth.creds.kind === 'key' return await betsQueue.enqueueFn( () => placeBetMain(props, auth.uid, isApi), - [props.contractId, auth.uid] + [props.contractId, auth.uid], ) } ``` @@ -376,13 +361,10 @@ using the pg-promise library. The client (code in web) does not have permission Another example using the direct client: ```ts -export const getUniqueBettorIds = async ( - contractId: string, - pg: SupabaseDirectClient -) => { +export const getUniqueBettorIds = async (contractId: string, pg: SupabaseDirectClient) => { const res = await pg.manyOrNone( 'select distinct user_id from contract_bets where contract_id = $1', - [contractId] + [contractId], ) return res.map((r) => r.user_id as string) } @@ -437,7 +419,7 @@ const query = renderSql( from('contract_bets'), where('contract_id = ${id}', {id}), orderBy('created_time desc'), - limitValue != null && limit(limitValue) + limitValue != null && limit(limitValue), ) const res = await pg.manyOrNone(query) @@ -476,7 +458,7 @@ can do so via this SQL command (change the type if not `TEXT`): ```sql ALTER TABLE profiles - ADD COLUMN profile_field TEXT; +ADD COLUMN profile_field TEXT; ``` Store it in `add_profile_field.sql` in the [migrations](../backend/supabase/migrations) folder and @@ -540,28 +522,28 @@ How we apply it here This project uses three complementary test types. Use the right level for the job: - Unit tests - - Purpose: Verify a single function/module in isolation; fast, deterministic. - - Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`, - etc.). - - Runner: Jest (configured via root `jest.config.js`). - - Naming: `*.unit.test.ts` (or `.tsx` for React in `web`). - - When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies. + - Purpose: Verify a single function/module in isolation; fast, deterministic. + - Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`, + etc.). + - Runner: Jest (configured via root `jest.config.js`). + - Naming: `*.unit.test.ts` (or `.tsx` for React in `web`). + - When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies. - Integration tests - - Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without - spinning up the full app. - - Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`). - - Runner: Jest (configured via root `jest.config.js`). - - Naming: `*.integration.test.ts` (or `.tsx` for React in `web`). - - When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB, - component trees with providers. + - Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without + spinning up the full app. + - Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`). + - Runner: Jest (configured via root `jest.config.js`). + - Naming: `*.integration.test.ts` (or `.tsx` for React in `web`). + - When to use: Boundaries between modules, real serialization/parsing, API handlers with mocked network/DB, + component trees with providers. - End-to-End (E2E) tests - - Purpose: Validate real user flows across the full stack. - - Where: Top-level `tests/e2e` with separate areas for `web` and `backend`. - - Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`). - - Naming: `*.e2e.spec.ts`. - - When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments. + - Purpose: Validate real user flows across the full stack. + - Where: Top-level `tests/e2e` with separate areas for `web` and `backend`. + - Runner: Playwright (see root `playwright.config.ts`, `testDir: ./tests/e2e`). + - Naming: `*.e2e.spec.ts`. + - When to use: Critical journeys (signup, login, checkout), cross-service interactions, smoke tests for deployments. Quick commands @@ -631,23 +613,23 @@ web/ - Unit and integration tests live in each package’s `tests` folder and are executed by Jest via the root `jest.config.js` projects array. - Naming: - - Unit: `*.unit.test.ts` (or `.tsx` for React in `web`) - - Integration: `*.integration.test.ts` - - E2E (Playwright): `*.e2e.spec.ts` + - Unit: `*.unit.test.ts` (or `.tsx` for React in `web`) + - Integration: `*.integration.test.ts` + - E2E (Playwright): `*.e2e.spec.ts` ### Best Practices -* Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very +- Test Behavior, Not Implementation. Don’t test internal state or function calls unless you’re testing utilities or very critical behavior. -* Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and +- Use msw to Mock APIs. Don't manually mock fetch—use msw to simulate realistic behavior, including network delays and errors. -* Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response +- Don’t Overuse Snapshots. Snapshots are fragile and often meaningless unless used sparingly (e.g., for JSON response schemas). -* Prefer userEvent Over fireEvent. It simulates real user interactions more accurately. -* Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they +- Prefer userEvent Over fireEvent. It simulates real user interactions more accurately. +- Avoid Testing Next.js Internals . You don’t need to test getStaticProps, getServerSideProps themselves-test what they render. -* 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 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. ### Jest Unit Testing Guide @@ -675,14 +657,14 @@ yarn test path/to/test.unit.test.ts #### Test Standards - Test file names should convey what to expect - - Follow the pattern: `.[unit,integration].test.ts`. Examples: - - filename.unit.test.ts - - filename.integration.test.ts + - Follow the pattern: `.[unit,integration].test.ts`. Examples: + - filename.unit.test.ts + - filename.integration.test.ts - Group related tests using describe blocks - Use descriptive test names that explain the expected behavior. - - Follow the pattern: "should `expected behavior` [relevant modifier]". Examples: - - should `ban user` [with matching user id] - - should `ban user` [with matching user name] + - Follow the pattern: "should `expected behavior` [relevant modifier]". Examples: + - should `ban user` [with matching user id] + - should `ban user` [with matching user name] #### Mocking @@ -726,15 +708,15 @@ When writing mocks, assert both outcome and interaction: Why mocking is important? -- *Isolation* - Test your code independently of databases, APIs, and external systems. Tests only fail when your code +- _Isolation_ - Test your code independently of databases, APIs, and external systems. Tests only fail when your code breaks, not when a server is down. -- *Speed* - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly +- _Speed_ - Mocked tests run in milliseconds vs. seconds for real network/database calls. Run your suite constantly without waiting. -- *Control* - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce +- _Control_ - Easily simulate edge cases like API errors, timeouts, or rare conditions that are difficult to reproduce with real systems. -- *Reliability* - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same +- _Reliability_ - Eliminate unpredictable failures from network issues, rate limits, or changing external data. Same inputs = same results, every time. -- *Focus* - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to +- _Focus_ - Verify your function's logic and how it uses its dependencies, without requiring those dependencies to actually work yet. ###### Use `jest.mock()` @@ -747,40 +729,40 @@ function’s return value isn’t used, there’s no need to mock it further. ```tsx //Function and module mocks -jest.mock('path/to/module'); +jest.mock('path/to/module') //Function and module imports -import {functionUnderTest} from "path/to/function" -import {module} from "path/to/module" +import {functionUnderTest} from 'path/to/function' +import {module} from 'path/to/module' describe('functionUnderTest', () => { //Setup beforeEach(() => { //Run before each test - jest.resetAllMocks(); // Resets any mocks from previous tests - }); + jest.resetAllMocks() // Resets any mocks from previous tests + }) afterEach(() => { //Run after each test - jest.restoreAllMocks(); // Cleans up between tests - }); + jest.restoreAllMocks() // Cleans up between tests + }) describe('when given valid input', () => { it('should describe what is being tested', async () => { //Arrange: Setup test data - const mockData = 'test'; + const mockData = 'test' //Act: Execute the function under test - const result = myFunction(mockData); + const result = myFunction(mockData) //Assert: Verify the result - expect(result).toBe('expected'); - }); - }); + expect(result).toBe('expected') + }) + }) describe('when an error occurs', () => { //Test cases for errors - }); -}); + }) +}) ``` ###### Modules @@ -790,27 +772,27 @@ called and what it was called with. ```tsx //functionFile.ts -import {module as mockedDep} from "path/to/module" +import {module as mockedDep} from 'path/to/module' export const functionUnderTest = async (param) => { - return await mockedDep(param); -}; + return await mockedDep(param) +} ``` ```tsx //testFile.unit.test.ts -import {functionUnderTest} from "path/to/function"; -import {module as mockedDep} from "path/to/module"; +import {functionUnderTest} from 'path/to/function' +import {module as mockedDep} from 'path/to/module' -jest.mock('path/to/module'); +jest.mock('path/to/module') /** * Inside the test case * We create a mock for any information passed into the function that is being tested * and if the function returns a result we create a mock to test the result */ -const mockParam = "mockParam"; -const mockReturnValue = "mockModuleValue"; +const mockParam = 'mockParam' +const mockReturnValue = 'mockModuleValue' /** * use .mockResolvedValue when handling async/await modules that return values @@ -818,15 +800,15 @@ const mockReturnValue = "mockModuleValue"; */ describe('functionUnderTest', () => { it('returns mocked module value and calls dependency correctly', async () => { - (mockedDep as jest.Mock).mockResolvedValue(mockReturnValue); + ;(mockedDep as jest.Mock).mockResolvedValue(mockReturnValue) - const result = await functionUnderTest(mockParam); + const result = await functionUnderTest(mockParam) - expect(result).toBe(mockReturnValue); - expect(mockedDep).toHaveBeenCalledTimes(1); - expect(mockedDep).toHaveBeenCalledWith(mockParam); - }); -}); + expect(result).toBe(mockReturnValue) + expect(mockedDep).toHaveBeenCalledTimes(1) + expect(mockedDep).toHaveBeenCalledWith(mockParam) + }) +}) ``` Use namespace imports when you want to import everything a module exports under a single name. @@ -834,37 +816,36 @@ Use namespace imports when you want to import everything a module exports under ```tsx //moduleFile.ts export const module = async (param) => { - const value = "module" + const value = 'module' return value -}; +} export const moduleTwo = async (param) => { - const value = "moduleTwo" + const value = 'moduleTwo' return value -}; +} ``` ```tsx //functionFile.ts -import {module, moduleTwo} from "path/to/module" +import {module, moduleTwo} from 'path/to/module' export const functionUnderTest = async (param) => { const mockValue = await moduleTwo(param) const returnValue = await module(mockValue) - return returnValue; -}; + return returnValue +} ``` ```tsx //testFile.unit.test.ts -jest.mock('path/to/module'); +jest.mock('path/to/module') /** * This creates an object containing all named exports from ./path/to/module */ -import * as mockModule from "path/to/module" - -(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue); +import * as mockModule from 'path/to/module' +;(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue) ``` When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`. @@ -872,21 +853,21 @@ When mocking modules, you can use `jest.spyOn()` instead of `jest.mock()`. - `jest.mock()` mocks the entire module, which is ideal for external dependencies like Axios or database clients. - `jest.spyOn()` mocks specific methods while keeping the real implementation for others. It can also be used to observe how a real method is called without changing its behavior. - - also replaces the need to have `jest.mock()` at the top of the file. + - also replaces the need to have `jest.mock()` at the top of the file. ```tsx //testFile.unit.test.ts -import * as mockModule from "path/to/module" +import * as mockModule from 'path/to/module' //Mocking the return value of the module -jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue); +jest.spyOn(mockModule, 'module').mockResolvedValue(mockReturnValue) //Spying on the module to check functionality -jest.spyOn(mockModule, 'module'); +jest.spyOn(mockModule, 'module') //You can assert the module functionality with both of the above exactly like you would if you used jest.mock() -expect(mockModule.module).toBeCalledTimes(1); -expect(mockModule.module).toBeCalledWith(mockParam); +expect(mockModule.module).toBeCalledTimes(1) +expect(mockModule.module).toBeCalledWith(mockParam) ``` ###### Dependencies @@ -896,119 +877,114 @@ external functionality. ```tsx //functionFile.ts -import {dependency} from "path/to/dependency" +import {dependency} from 'path/to/dependency' export const functionUnderTest = async (param) => { - const depen = await dependency(); - const value = depen.module(); + const depen = await dependency() + const value = depen.module() - return value; -}; + return value +} ``` ```tsx //testFile.unit.test.ts -jest.mock('path/to/dependency'); +jest.mock('path/to/dependency') -import {dependency} from "path/to/dependency" +import {dependency} from 'path/to/dependency' describe('functionUnderTest', () => { /** * Because the dependency has modules that are used we need to * create a variable outside of scope that can be asserted on */ - let mockDependency = {} as any; + let mockDependency = {} as any beforeEach(() => { mockDependency = { module: jest.fn(), - }; - jest.resetAllMocks(); // Resets any mocks from previous tests - }); + } + jest.resetAllMocks() // Resets any mocks from previous tests + }) afterEach(() => { //Run after each test - jest.restoreAllMocks(); // Cleans up between tests - }); + jest.restoreAllMocks() // Cleans up between tests + }) //Inside the test case - (mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue); + ;(mockDependency.module as jest.Mock).mockResolvedValue(mockReturnValue) - expect(mockDependency.module).toBeCalledTimes(1); - expect(mockDependency.module).toBeCalledWith(mockParam); -}); + expect(mockDependency.module).toBeCalledTimes(1) + expect(mockDependency.module).toBeCalledWith(mockParam) +}) ``` ###### Error checking ```tsx //function.ts -const result = await functionName(param); +const result = await functionName(param) if (!result) { - throw new Error(403, 'Error text', error); + throw new Error(403, 'Error text', error) } -; ``` ```tsx //testFile.unit.test.ts -const mockParam = {} as any; +const mockParam = {} as any //This will check only the error message -expect(functionName(mockParam)) - .rejects - .toThrowError('Error text'); +expect(functionName(mockParam)).rejects.toThrowError('Error text') //This will check the complete error try { - await functionName(mockParam); - fail('Should have thrown'); + await functionName(mockParam) + fail('Should have thrown') } catch (error) { - const functionError = error as Error; - expect(functionError.code).toBe(403); - expect(functionError.message).toBe('Error text'); - expect(functionError.details).toBe(mockParam); - expect(functionError.name).toBe('Error'); + const functionError = error as Error + expect(functionError.code).toBe(403) + expect(functionError.message).toBe('Error text') + expect(functionError.details).toBe(mockParam) + expect(functionError.name).toBe('Error') } ``` ```tsx //For console.error types -console.error('Error message', error); +console.error('Error message', error) //Use spyOn to mock -const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { -}); +const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) expect(errorSpy).toHaveBeenCalledWith( 'Error message', - expect.objectContaining({name: 'Error'}) //The error 'name' refers to the error type -); - + expect.objectContaining({name: 'Error'}), //The error 'name' refers to the error type +) ``` ###### Mocking array return value ```tsx //arrayFile.ts -const exampleArray = [1, 2, 3, 4, 5]; +const exampleArray = [1, 2, 3, 4, 5] -const arrayResult = exampleArray.includes(2); +const arrayResult = exampleArray.includes(2) ``` ```tsx //testFile.unit.test.ts //This will mock 'includes' for all arrays and force the return value to be true -jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); +jest.spyOn(Array.prototype, 'includes').mockReturnValue(true) // --- //This will specify which 'includes' array to mock based on the args passed into the .includes() jest.spyOn(Array.prototype, 'includes').mockImplementation(function (value) { if (value === 2) { - return true; + return true } - return false; -}); + return false +}) ``` ### Playwright (E2E) Testing Guide @@ -1034,23 +1010,26 @@ yarn test:db:reset Use this priority order for selecting elements in Playwright tests: 1. Prefer `getByRole()` — use semantic roles that reflect how users interact + ```typescript - await page.getByRole('button', { name: 'Submit' }).click(); + await page.getByRole('button', {name: 'Submit'}).click() ``` + If a meaningful ARIA role is not available, fall back to accessible text selectors (next point). 2. Use accessible text selectors — when roles don't apply, target user-facing text + ```typescript - await page.getByLabel('Email').fill('user@example.com'); - await page.getByPlaceholder('Enter your name').fill('John'); - await page.getByText('Welcome back').isVisible(); + await page.getByLabel('Email').fill('user@example.com') + await page.getByPlaceholder('Enter your name').fill('John') + await page.getByText('Welcome back').isVisible() ``` 3. Only use `data-testid` — when elements have no stable user-facing text ```typescript // For icons, toggles, or dynamic content without text - await page.getByTestId('menu-toggle').click(); - await page.getByTestId('loading-spinner').isVisible(); + await page.getByTestId('menu-toggle').click() + await page.getByTestId('loading-spinner').isVisible() ``` This hierarchy mirrors how users actually interact with your application, making tests more reliable and meaningful. diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..ad83ab78 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules +.yarn + +# Build outputs +dist +build +.next +out +lib + +# Generated files +coverage +*.min.js +*.min.css + +# Database / migrations +**/*.sql + +# Config / lock files +yarn.lock +package-lock.json +pnpm-lock.yaml + +# Android / iOS +android +ios +capacitor.config.ts + +# Playwright +tests/reports +playwright-report + +coverage +.vscode \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 8f448141..cfbc2803 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,9 +2,11 @@ "tabWidth": 2, "useTabs": false, "semi": false, - "trailingComma": "es5", "singleQuote": true, + "singleAttributePerLine": false, "bracketSpacing": false, + "printWidth": 100, + "trailingComma": "all", "plugins": ["prettier-plugin-sql"], "overrides": [ { diff --git a/.windsurf/rules/compass.md b/.windsurf/rules/compass.md index a0e5318b..154ffc55 100644 --- a/.windsurf/rules/compass.md +++ b/.windsurf/rules/compass.md @@ -1,7 +1,7 @@ --- trigger: always_on -description: -globs: +description: +globs: --- ## Project Structure @@ -33,14 +33,14 @@ Here's an example component from web in our style: import clsx from 'clsx' import Link from 'next/link' -import { isAdminId, isModId } from 'common/envs/constants' -import { type Headline } from 'common/news' -import { EditNewsButton } from 'web/components/news/edit-news-button' -import { Carousel } from 'web/components/widgets/carousel' -import { useUser } from 'web/hooks/use-user' -import { track } from 'web/lib/service/analytics' -import { DashboardEndpoints } from 'web/components/dashboard/dashboard-page' -import { removeEmojis } from 'common/util/string' +import {isAdminId, isModId} from 'common/envs/constants' +import {type Headline} from 'common/news' +import {EditNewsButton} from 'web/components/news/edit-news-button' +import {Carousel} from 'web/components/widgets/carousel' +import {useUser} from 'web/hooks/use-user' +import {track} from 'web/lib/service/analytics' +import {DashboardEndpoints} from 'web/components/dashboard/dashboard-page' +import {removeEmojis} from 'common/util/string' export function HeadlineTabs(props: { headlines: Headline[] @@ -50,20 +50,13 @@ export function HeadlineTabs(props: { notSticky?: boolean className?: string }) { - const { headlines, endpoint, currentSlug, hideEmoji, notSticky, className } = - props + const {headlines, endpoint, currentSlug, hideEmoji, notSticky, className} = props const user = useUser() return ( -
+
- {headlines.map(({ id, slug, title }) => ( + {headlines.map(({id, slug, title}) => ( (initialValue: T, key: string) => { - const [state, setState] = useStateCheckEquality( - safeJsonParse(store[key]) ?? initialValue - ) + const [state, setState] = useStateCheckEquality(safeJsonParse(store[key]) ?? initialValue) useEffect(() => { const storedValue = safeJsonParse(store[key]) ?? initialValue @@ -183,25 +174,19 @@ In `use-bets`, we have this hook to get live updates with useApiSubscription: ```ts export const useContractBets = ( contractId: string, - opts?: APIParams<'bets'> & { enabled?: boolean } + opts?: APIParams<'bets'> & {enabled?: boolean}, ) => { - const { enabled = true, ...apiOptions } = { + const {enabled = true, ...apiOptions} = { contractId, ...opts, } const optionsKey = JSON.stringify(apiOptions) - const [newBets, setNewBets] = usePersistentInMemoryState( - [], - `${optionsKey}-bets` - ) + const [newBets, setNewBets] = usePersistentInMemoryState([], `${optionsKey}-bets`) const addBets = (bets: Bet[]) => { setNewBets((currentBets) => { - const uniqueBets = sortBy( - uniqBy([...currentBets, ...bets], 'id'), - 'createdTime' - ) + const uniqueBets = sortBy(uniqBy([...currentBets, ...bets], 'id'), 'createdTime') return uniqueBets.filter((b) => !betShouldBeFiltered(b, apiOptions)) }) } @@ -236,12 +221,12 @@ export function broadcastUpdatedPrivateUser(userId: string) { broadcast(`private-user/${userId}`, {}) } -export function broadcastUpdatedUser(user: Partial & { id: string }) { - broadcast(`user/${user.id}`, { user }) +export function broadcastUpdatedUser(user: Partial & {id: string}) { + broadcast(`user/${user.id}`, {user}) } export function broadcastUpdatedComment(comment: Comment) { - broadcast(`user/${comment.onUserId}/comment`, { comment }) + broadcast(`user/${comment.onUserId}/comment`, {comment}) } ``` @@ -310,7 +295,7 @@ export const placeBet: APIHandler<'bet'> = async (props, auth) => { const isApi = auth.creds.kind === 'key' return await betsQueue.enqueueFn( () => placeBetMain(props, auth.uid, isApi), - [props.contractId, auth.uid] + [props.contractId, auth.uid], ) } ``` @@ -332,7 +317,7 @@ const handlers = { We have two ways to access our postgres database. ```ts -import { db } from 'web/lib/supabase/db' +import {db} from 'web/lib/supabase/db' db.from('profiles').select('*').eq('user_id', userId) ``` @@ -340,7 +325,7 @@ db.from('profiles').select('*').eq('user_id', userId) and ```ts -import { createSupabaseDirectClient } from 'shared/supabase/init' +import {createSupabaseDirectClient} from 'shared/supabase/init' const pg = createSupabaseDirectClient() pg.oneOrNone>('select * from profiles where user_id = $1', [userId]) @@ -353,13 +338,10 @@ The supabase client just uses the supabase client library, which is a wrapper ar Another example using the direct client: ```ts -export const getUniqueBettorIds = async ( - contractId: string, - pg: SupabaseDirectClient -) => { +export const getUniqueBettorIds = async (contractId: string, pg: SupabaseDirectClient) => { const res = await pg.manyOrNone( 'select distinct user_id from contract_bets where contract_id = $1', - [contractId] + [contractId], ) return res.map((r) => r.user_id as string) } @@ -411,12 +393,12 @@ Example usage: const query = renderSql( select('distinct user_id'), from('contract_bets'), - where('contract_id = ${id}', { id }), + where('contract_id = ${id}', {id}), orderBy('created_time desc'), - limitValue != null && limit(limitValue) + limitValue != null && limit(limitValue), ) const res = await pg.manyOrNone(query) ``` -Use these functions instead of string concatenation. \ No newline at end of file +Use these functions instead of string concatenation. diff --git a/.windsurf/rules/next.md b/.windsurf/rules/next.md index fa377ded..66bd0fd4 100644 --- a/.windsurf/rules/next.md +++ b/.windsurf/rules/next.md @@ -1,16 +1,16 @@ --- trigger: manual description: -globs: +globs: --- ### Translations ```typescript -import {useT} from "web/lib/locale"; +import {useT} from 'web/lib/locale' const t = useT() -t("common.key", "English translations") +t('common.key', 'English translations') ``` Translations should go to the JSON files in `web/messages` (`de.json` and `fr.json`, as of now). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 10ae4644..177f11ac 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbb46b0b..b33229d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,10 +14,13 @@ We welcome pull requests, but only if they meet the project's quality and design 1. **Fork the repository** using the GitHub UI. 2. **Clone your fork** locally: + ```bash git clone https://github.com/your-username/Compass.git cd your-fork + ``` + 3. **Add the upstream remote**: ```bash @@ -95,27 +98,26 @@ Or whatever command is defined in the repo. When opening a pull request: -* **Title**: Describe what the PR does, clearly and specifically. -* **Description**: Explain the context. Link related issues (use `Fixes #123` if applicable). -* **Checklist**: - - * [ ] My code is clean and follows the style guide - * [ ] I’ve added or updated tests - * [ ] I’ve run all tests and they pass - * [ ] I’ve documented my changes (if necessary) +- **Title**: Describe what the PR does, clearly and specifically. +- **Description**: Explain the context. Link related issues (use `Fixes #123` if applicable). +- **Checklist**: + - [ ] My code is clean and follows the style guide + - [ ] I’ve added or updated tests + - [ ] I’ve run all tests and they pass + - [ ] I’ve documented my changes (if necessary) ## Code Review Process -* PRs are reviewed by maintainers or core contributors. -* If feedback is given, respond and push updates. Do **not** open new PRs for changes to an existing one. -* PRs that are incomplete, sloppy, or violate the above will be closed. +- PRs are reviewed by maintainers or core contributors. +- If feedback is given, respond and push updates. Do **not** open new PRs for changes to an existing one. +- PRs that are incomplete, sloppy, or violate the above will be closed. ## Don't Do This -* Don’t commit directly to `main` -* Don’t submit multiple unrelated changes in a single PR -* Don’t ignore CI/test failures -* Don’t expect hand-holding—read the docs and the source first +- Don’t commit directly to `main` +- Don’t submit multiple unrelated changes in a single PR +- Don’t ignore CI/test failures +- Don’t expect hand-holding—read the docs and the source first ## Security Issues @@ -124,4 +126,3 @@ Do **not** open public issues for security vulnerabilities. Email the developmen ## License By contributing, you agree that your code will be licensed under the same license as the rest of the project. - diff --git a/README.md b/README.md index ec71b5d8..447c4f52 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - ![Vercel](https://deploy-badge.vercel.app/vercel/compass) [![CD](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml/badge.svg)](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml) [![CD API](https://github.com/CompassConnections/Compass/actions/workflows/cd-api.yml/badge.svg)](https://github.com/CompassConnections/Compass/actions/workflows/cd-api.yml) @@ -34,14 +33,17 @@ No contribution is too small—whether it’s changing a color, resizing a butto The complete, official list of tasks is available [here on ClickUp](https://sharing.clickup.com/90181043445/l/h/6-901810339879-1/bbfd32f4f4bf64b). If you are working on one task, just assign it to yourself and move its status to "in progress". If there is also a GitHub issue for that task, assign it to yourself as well. To have edit access to the ClickUp workspace, you need an admin to manually give you permission (one time thing). To do so, use your preferred option: + - Ask or DM an admin on [Discord](https://discord.gg/8Vd7jzqjun) - Email hello@compassmeet.com - Raise an issue on GitHub If you want to add tasks without creating an account, you can simply email + ``` a.t.901810339879.u-276866260.b847aba1-2709-4f17-b4dc-565a6967c234@tasks.clickup.com ``` + Put the task title in the email subject and the task description in the email content. Here is a tailored selection of things that would be very useful. If you want to help but don’t know where to start, just ask us on [Discord](https://discord.gg/8Vd7jzqjun). @@ -105,17 +107,20 @@ Below are the steps to contribute. If you have any trouble or questions, please ### Installation Fork the [repo](https://github.com/CompassConnections/Compass) on GitHub (button in top right). Then, clone your repo and navigating into it: + ```bash git clone https://github.com//Compass.git cd Compass ``` Install `yarn` (if not already installed): + ```bash npm install --global yarn ``` Then, install the dependencies for this project: + ```bash yarn install --frozen-lockfile ``` @@ -123,14 +128,17 @@ yarn install --frozen-lockfile ### Tests Make sure the Jest tests pass: + ```bash yarn test ``` + If they don't and you can't find out why, simply raise an issue! Sometimes it's something on our end that we overlooked. ### Running the Development Server Start the development server: + ```bash yarn dev ``` @@ -227,6 +235,7 @@ looks and feels like the real thing. Now you can start contributing by making changes and submitting pull requests! We recommend using a good code editor (VSCode, WebStorm, Cursor, etc.) with Typescript support and a good AI assistant (GitHub Copilot, etc.) to make your life easier. To debug, you can use the browser developer tools (F12), specifically: + - Components tab to see the React component tree and props (you need to install the [React Developer Tools](https://react.dev/learn/react-developer-tools) extension) - Console tab for errors and logs - Network tab to see the requests and responses @@ -239,6 +248,7 @@ If you are new to Typescript or the open-source space, you could start with smal ##### Resources 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 or languages. @@ -252,22 +262,26 @@ There are a lot of useful scripts you can use in the [scripts](scripts) folder. ### Submission Add the original repo as upstream for syncing: + ```bash git remote add upstream https://github.com/CompassConnections/Compass.git ``` Create a new branch for your changes: + ```bash git checkout -b ``` Make changes, then stage and commit: + ```bash git add . git commit -m "Describe your changes" ``` Push branch to your fork: + ```bash git push origin ``` @@ -279,6 +293,7 @@ Finally, open a Pull Request on GitHub from your `fork/` → `Compa Almost all the features will work out of the box, so you can skip this step and come back later if you need to test the following services: email, geolocation. We can't make the following information public, for security and privacy reasons: + - Database, otherwise anyone could access all the user data (including private messages) - Firebase, otherwise anyone could remove users or modify the media files - Email, analytics, and location services, otherwise anyone could use the service plans Compass paid for and run up the bill. @@ -289,4 +304,5 @@ Contributors should use the default keys for local development. Production uses If you do need one of the few remaining services, you need to set them up and store your own secrets as environment variables. To do so, simply open `.env` and fill in the variables according to the instructions in the file. ## Acknowledgements + This project is built on top of [manifold.love](https://github.com/sipec/polylove), an open-source dating platform licensed under the MIT License. We greatly appreciate their work and contributions to open-source, which have significantly aided in the development of some core features such as direct messaging, prompts, and email notifications. We invite the community to explore and contribute to other open-source projects like manifold.love as well, especially if you're interested in functionalities that deviate from Compass' ideals of deep, intentional connections. diff --git a/SECURITY.md b/SECURITY.md index ab0c17cd..7c84828c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,4 +9,3 @@ ## Reporting a Vulnerability Contact the development team at hello@compassmeet.com to report a vulnerability. You should receive updates within a week. - diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index f4fc69cf..f4e3fa4c 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -2,8 +2,8 @@ android { compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 } } diff --git a/backend/api/.eslintrc.js b/backend/api/.eslintrc.js index 8f6f05d2..d1f83573 100644 --- a/backend/api/.eslintrc.js +++ b/backend/api/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { - plugins: ['lodash', 'unused-imports'], + plugins: ['lodash', 'unused-imports', 'simple-import-sort'], extends: ['eslint:recommended'], - ignorePatterns: ['dist', 'lib', 'tests', 'coverage'], + ignorePatterns: ['dist', 'lib', 'coverage'], env: { node: true, }, @@ -16,9 +16,9 @@ module.exports = { project: ['./tsconfig.json', './tsconfig.test.json'], }, rules: { - "@typescript-eslint/no-empty-object-type": "error", // replaces banning {} - "@typescript-eslint/no-unsafe-function-type": "error", // replaces banning Function - "@typescript-eslint/no-wrapper-object-types": "error", // replaces banning String, Number, etc. + '@typescript-eslint/no-empty-object-type': 'error', // replaces banning {} + '@typescript-eslint/no-unsafe-function-type': 'error', // replaces banning Function + '@typescript-eslint/no-wrapper-object-types': 'error', // replaces banning String, Number, etc. '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-extra-semi': 'off', '@typescript-eslint/no-unused-vars': [ @@ -35,10 +35,9 @@ module.exports = { }, ], rules: { - 'linebreak-style': [ - 'error', - process.platform === 'win32' ? 'windows' : 'unix', - ], + 'linebreak-style': ['error', process.platform === 'win32' ? 'windows' : 'unix'], 'lodash/import-scope': [2, 'member'], + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', }, } diff --git a/backend/api/.prettierignore b/backend/api/.prettierignore new file mode 100644 index 00000000..d4cf806b --- /dev/null +++ b/backend/api/.prettierignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules +.yarn + +# Build outputs +dist +build +.next +out +lib + +# Generated files +coverage +*.min.js +*.min.css + +# Database / migrations +**/*.sql + +# Config / lock files +yarn.lock +package-lock.json +pnpm-lock.yaml + +# Android / iOS +android +ios +capacitor.config.ts + +# Playwright +tests/reports +playwright-report + +coverage \ No newline at end of file diff --git a/backend/api/README.md b/backend/api/README.md index 4004e91e..c2b91dba 100644 --- a/backend/api/README.md +++ b/backend/api/README.md @@ -28,9 +28,11 @@ gcloud config set project YOUR_PROJECT_ID ``` You also need `opentofu` and `docker`. Try running this (from root) on Linux or macOS for a faster install: + ```bash ./script/setup.sh ``` + If it doesn't work, you can install them manually (google how to install `opentofu` and `docker` for your OS). ### Setup @@ -105,8 +107,8 @@ gcloud iam service-accounts keys create keyfile.json --iam-account=ci-deployer@c ##### DNS -* After deployment, Terraform assigns a static external IP to this resource. -* You can get it manually: +- After deployment, Terraform assigns a static external IP to this resource. +- You can get it manually: ```bash gcloud compute addresses describe api-lb-ip-2 --global --format="get(address)" @@ -120,11 +122,11 @@ Since Vercel manages your domain (`compassmeet.com`): 3. Add an **A record** for your API subdomain: | Type | Name | Value | TTL | -|------|------|--------------|-------| +| ---- | ---- | ------------ | ----- | | A | api | 34.123.45.67 | 600 s | -* `Name` is just the subdomain: `api` → `api.compassmeet.com`. -* `Value` is the **external IP of the LB** from step 1. +- `Name` is just the subdomain: `api` → `api.compassmeet.com`. +- `Value` is the **external IP of the LB** from step 1. Verify connectivity From your local machine: @@ -135,8 +137,8 @@ ping -c 3 api.compassmeet.com curl -I https://api.compassmeet.com ``` -* `nslookup` should return the LB IP (`34.123.45.67`). -* `curl -I` should return `200 OK` from your service. +- `nslookup` should return the LB IP (`34.123.45.67`). +- `curl -I` should return `200 OK` from your service. If SSL isn’t ready (may take 15 mins), check LB logs: @@ -167,6 +169,7 @@ In root directory, run the local api with hot reload, along with all the other b To deploy the backend code, simply increment the version number in [package.json](package.json) and push to the `main` branch. Or if you have access to the project on google cloud, run in this directory: + ```bash ./deploy-api.sh prod ``` @@ -195,4 +198,5 @@ docker rmi -f $(docker images -aq) The API doc is available at https://api.compassmeet.com. It's dynamically prepared in [app.ts](src/app.ts). ### Todo (Tests) -- [ ] Finish get-supabase-token unit test when endpoint is implemented \ No newline at end of file + +- [ ] Finish get-supabase-token unit test when endpoint is implemented diff --git a/backend/api/ecosystem.config.js b/backend/api/ecosystem.config.js index 9094d11b..de4336de 100644 --- a/backend/api/ecosystem.config.js +++ b/backend/api/ecosystem.config.js @@ -1,21 +1,21 @@ module.exports = { - apps: [ - { - name: "api", - script: "node", - args: "--dns-result-order=ipv4first backend/api/lib/serve.js", - env: { - NODE_ENV: "production", - NODE_PATH: "/usr/src/app/node_modules", // <- ensures Node finds tsconfig-paths - PORT: 80, - }, - instances: 1, - exec_mode: "fork", - autorestart: true, - watch: false, - // 4 GB on the box, give 3 GB to the JS heap - node_args: "--max-old-space-size=3072", - max_memory_restart: "3500M" - } - ] -}; + apps: [ + { + name: 'api', + script: 'node', + args: '--dns-result-order=ipv4first backend/api/lib/serve.js', + env: { + NODE_ENV: 'production', + NODE_PATH: '/usr/src/app/node_modules', // <- ensures Node finds tsconfig-paths + PORT: 80, + }, + instances: 1, + exec_mode: 'fork', + autorestart: true, + watch: false, + // 4 GB on the box, give 3 GB to the JS heap + node_args: '--max-old-space-size=3072', + max_memory_restart: '3500M', + }, + ], +} diff --git a/backend/api/jest.config.js b/backend/api/jest.config.js index b443f00d..361aa9ba 100644 --- a/backend/api/jest.config.js +++ b/backend/api/jest.config.js @@ -1,31 +1,28 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', + preset: 'ts-jest', + testEnvironment: 'node', - rootDir: '.', - testMatch: [ - "/tests/**/*.test.ts", - "/tests/**/*.spec.ts" + rootDir: '.', + testMatch: ['/tests/**/*.test.ts', '/tests/**/*.spec.ts'], + + moduleNameMapper: { + '^api/(.*)$': '/src/$1', + '^shared/(.*)$': '/../shared/src/$1', + '^common/(.*)$': '/../../common/src/$1', + '^email/(.*)$': '/../email/emails/$1', + }, + + moduleFileExtensions: ['tsx', 'ts', 'js', 'json'], + clearMocks: true, + + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.test.json', + }, ], + }, - moduleNameMapper: { - "^api/(.*)$": "/src/$1", - "^shared/(.*)$": "/../shared/src/$1", - "^common/(.*)$": "/../../common/src/$1", - "^email/(.*)$": "/../email/emails/$1" - }, - - moduleFileExtensions: ["tsx","ts", "js", "json"], - clearMocks: true, - - globals: { - 'ts-jest': { - tsconfig: "/tsconfig.test.json" - } - }, - - collectCoverageFrom: [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts" - ], -}; + collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'], +} diff --git a/backend/api/package.json b/backend/api/package.json index c090dbbf..b7249d78 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -5,22 +5,21 @@ "private": true, "scripts": { "watch:serve": "tsx watch src/serve.ts", - "watch:compile": "npx concurrently \"tsc -b --watch --preserveWatchOutput\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\" \"tsc-alias --watch\"", + "watch:compile": "npx concurrently \"(cd ../../common && tsc --watch)\" \"(cd ../shared && tsc --watch)\" \"(cd ../email && tsc --watch)\" \"tsc --watch --preserveWatchOutput\" \"tsc-alias --watch\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\"", "dev": "yarn watch:serve", "prod": "npx concurrently -n COMPILE,SERVER -c cyan,green \"yarn watch:compile\" \"yarn watch:serve\"", "build": "yarn compile && yarn dist:clean && yarn dist:copy", "build:fast": "yarn compile && yarn dist:copy", "clean": "rm -rf lib && (cd ../../common && rm -rf lib) && (cd ../shared && rm -rf lib) && (cd ../email && rm -rf lib)", - "compile": "tsc -b && tsc-alias && (cd ../../common && tsc-alias) && (cd ../shared && tsc-alias) && (cd ../email && tsc-alias) && cp -r src/public/ lib/", + "compile": "(cd ../../common && tsc) && (cd ../shared && tsc) && (cd ../email && tsc) && tsc && tsc-alias && (cd ../../common && tsc-alias) && (cd ../shared && tsc-alias) && (cd ../email && tsc-alias) && cp -r src/public/ lib/", "debug": "nodemon -r tsconfig-paths/register --watch src -e ts --watch ../../common/src --watch ../shared/src --exec \"yarn build && node --inspect-brk src/serve.ts\"", "dist": "yarn dist:clean && yarn dist:copy", "dist:clean": "rm -rf dist && mkdir -p dist/common/lib dist/backend/shared/lib dist/backend/api/lib dist/backend/email/lib", "dist:copy": "rsync -a --delete ../../common/lib/ dist/common/lib && rsync -a --delete ../shared/lib/ dist/backend/shared/lib && rsync -a --delete ../email/lib/ dist/backend/email/lib && rsync -a --delete ./lib/* dist/backend/api/lib && cp ../../yarn.lock dist && cp package.json dist && cp package.json dist/backend/api && cp metadata.json dist && cp metadata.json dist/backend/api", "watch": "tsc -w", - "verify": "yarn --cwd=../.. verify", "lint": "npx eslint . --max-warnings 0", "lint-fix": "npx eslint . --fix", - "verify:dir": "npx eslint . --max-warnings 0", + "typecheck": "npx tsc --noEmit", "regen-types": "cd ../supabase && make ENV=prod regen-types", "regen-types-dev": "cd ../supabase && make ENV=dev regen-types-dev", "test": "jest --config jest.config.js", diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index c78872f6..d78b066d 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -1,91 +1,93 @@ -import {API, type APIPath} from 'common/api/schema' -import {APIError, pathWithPrefix} from 'common/api/utils' -import cors from 'cors' -import * as crypto from 'crypto' -import express, {type ErrorRequestHandler, type RequestHandler} from 'express' +import path from 'node:path' import {hrtime} from 'node:process' -import {withMonitoringContext} from 'shared/monitoring/context' -import {log} from 'shared/monitoring/log' -import {metrics} from 'shared/monitoring/metrics' -import {banUser} from './ban-user' -import {blockUser, unblockUser} from './block-user' -import {getCompatibleProfilesHandler} from './compatible-profiles' -import {createComment} from './create-comment' -import {createCompatibilityQuestion} from './create-compatibility-question' -import {setCompatibilityAnswer} from './set-compatibility-answer' -import {deleteCompatibilityAnswer} from './delete-compatibility-answer' -import {createProfile} from './create-profile' -import {createUser} from './create-user' -import {getCompatibilityQuestions} from './get-compatibililty-questions' -import {getLikesAndShips} from './get-likes-and-ships' -import {getProfileAnswers} from './get-profile-answers' -import {getProfiles} from './get-profiles' -import {getSupabaseToken} from './get-supabase-token' -import {getMe} from './get-me' -import {hasFreeLike} from './has-free-like' -import {health} from './health' -import {type APIHandler, typedEndpoint} from './helpers/endpoint' -import {hideComment} from './hide-comment' -import {likeProfile} from './like-profile' -import {markAllNotifsRead} from './mark-all-notifications-read' -import {removePinnedPhoto} from './remove-pinned-photo' -import {report} from './report' -import {searchLocation} from './search-location' -import {searchNearCity} from './search-near-city' -import {shipProfiles} from './ship-profiles' -import {starProfile} from './star-profile' -import {updateProfile} from './update-profile' -import {updateMe} from './update-me' -import {deleteMe} from './delete-me' -import {getCurrentPrivateUser} from './get-current-private-user' -import {createPrivateUserMessage} from './create-private-user-message' + +import {contact} from 'api/contact' +import {createVote} from 'api/create-vote' +import {deleteMessage} from 'api/delete-message' +import {editMessage} from 'api/edit-message' +import {getHiddenProfiles} from 'api/get-hidden-profiles' +import {getMessagesCount} from 'api/get-messages-count' +import {getOptions} from 'api/get-options' import { getChannelMemberships, getChannelMessagesEndpoint, getLastSeenChannelTime, setChannelLastSeenTime, } from 'api/get-private-messages' -import {searchUsers} from './search-users' -import {createPrivateUserMessageChannel} from './create-private-user-message-channel' -import {leavePrivateUserMessageChannel} from './leave-private-user-message-channel' -import {updatePrivateUserMessageChannel} from './update-private-user-message-channel' -import {getNotifications} from './get-notifications' -import {updateNotifSettings} from './update-notif-setting' -import {setLastOnlineTime} from './set-last-online-time' -import swaggerUi from "swagger-ui-express" -import {sendSearchNotifications} from "api/send-search-notifications"; -import {sendDiscordMessage} from "common/discord/core"; -import {getMessagesCount} from "api/get-messages-count"; -import {createVote} from "api/create-vote"; -import {vote} from "api/vote"; -import {contact} from "api/contact"; -import {saveSubscription} from "api/save-subscription"; -import {createBookmarkedSearch} from './create-bookmarked-search' -import {deleteBookmarkedSearch} from './delete-bookmarked-search' -import {OpenAPIV3} from 'openapi-types'; -import {version as pkgVersion} from './../package.json' +import {getUser} from 'api/get-user' +import {hideProfile} from 'api/hide-profile' +import {reactToMessage} from 'api/react-to-message' +import {saveSubscription} from 'api/save-subscription' +import {saveSubscriptionMobile} from 'api/save-subscription-mobile' +import {sendSearchNotifications} from 'api/send-search-notifications' +import {localSendTestEmail} from 'api/test' +import {unhideProfile} from 'api/unhide-profile' +import {updateOptions} from 'api/update-options' +import {vote} from 'api/vote' +import {API, type APIPath} from 'common/api/schema' +import {APIError, pathWithPrefix} from 'common/api/utils' +import {sendDiscordMessage} from 'common/discord/core' +import {IS_LOCAL} from 'common/hosting/constants' +import cors from 'cors' +import * as crypto from 'crypto' +import express, {type ErrorRequestHandler, type RequestHandler} from 'express' +import {OpenAPIV3} from 'openapi-types' +import {withMonitoringContext} from 'shared/monitoring/context' +import {log} from 'shared/monitoring/log' +import {metrics} from 'shared/monitoring/metrics' +import swaggerUi from 'swagger-ui-express' +import {z, ZodFirstPartyTypeKind, ZodTypeAny} from 'zod' + import {git} from './../metadata.json' -import {z, ZodFirstPartyTypeKind, ZodTypeAny} from "zod"; -import {getUser} from "api/get-user"; -import {localSendTestEmail} from "api/test"; -import path from "node:path"; -import {saveSubscriptionMobile} from "api/save-subscription-mobile"; -import {IS_LOCAL} from "common/hosting/constants"; -import {editMessage} from "api/edit-message"; -import {reactToMessage} from "api/react-to-message"; -import {deleteMessage} from "api/delete-message"; -import {updateOptions} from "api/update-options"; -import {getOptions} from "api/get-options"; -import {hideProfile} from "api/hide-profile"; -import {unhideProfile} from "api/unhide-profile"; -import {getHiddenProfiles} from "api/get-hidden-profiles"; -import {getUserDataExport} from "./get-user-data-export"; -import {getEvents} from "./get-events"; -import {createEvent} from "./create-event"; -import {rsvpEvent} from "./rsvp-event"; -import {cancelRsvp} from "./cancel-rsvp"; -import {cancelEvent} from "./cancel-event"; -import {updateEvent} from "./update-event"; +import {version as pkgVersion} from './../package.json' +import {banUser} from './ban-user' +import {blockUser, unblockUser} from './block-user' +import {cancelEvent} from './cancel-event' +import {cancelRsvp} from './cancel-rsvp' +import {getCompatibleProfilesHandler} from './compatible-profiles' +import {createBookmarkedSearch} from './create-bookmarked-search' +import {createComment} from './create-comment' +import {createCompatibilityQuestion} from './create-compatibility-question' +import {createEvent} from './create-event' +import {createPrivateUserMessage} from './create-private-user-message' +import {createPrivateUserMessageChannel} from './create-private-user-message-channel' +import {createProfile} from './create-profile' +import {createUser} from './create-user' +import {deleteBookmarkedSearch} from './delete-bookmarked-search' +import {deleteCompatibilityAnswer} from './delete-compatibility-answer' +import {deleteMe} from './delete-me' +import {getCompatibilityQuestions} from './get-compatibililty-questions' +import {getCurrentPrivateUser} from './get-current-private-user' +import {getEvents} from './get-events' +import {getLikesAndShips} from './get-likes-and-ships' +import {getMe} from './get-me' +import {getNotifications} from './get-notifications' +import {getProfileAnswers} from './get-profile-answers' +import {getProfiles} from './get-profiles' +import {getSupabaseToken} from './get-supabase-token' +import {getUserDataExport} from './get-user-data-export' +import {hasFreeLike} from './has-free-like' +import {health} from './health' +import {type APIHandler, typedEndpoint} from './helpers/endpoint' +import {hideComment} from './hide-comment' +import {leavePrivateUserMessageChannel} from './leave-private-user-message-channel' +import {likeProfile} from './like-profile' +import {markAllNotifsRead} from './mark-all-notifications-read' +import {removePinnedPhoto} from './remove-pinned-photo' +import {report} from './report' +import {rsvpEvent} from './rsvp-event' +import {searchLocation} from './search-location' +import {searchNearCity} from './search-near-city' +import {searchUsers} from './search-users' +import {setCompatibilityAnswer} from './set-compatibility-answer' +import {setLastOnlineTime} from './set-last-online-time' +import {shipProfiles} from './ship-profiles' +import {starProfile} from './star-profile' +import {updateEvent} from './update-event' +import {updateMe} from './update-me' +import {updateNotifSettings} from './update-notif-setting' +import {updatePrivateUserMessageChannel} from './update-private-user-message-channel' +import {updateProfile} from './update-profile' // const corsOptions: CorsOptions = { // origin: ['*'], // Only allow requests from this domain @@ -104,9 +106,7 @@ function cacheController(policy?: string): RequestHandler { const requestMonitoring: RequestHandler = (req, _res, next) => { const traceContext = req.get('X-Cloud-Trace-Context') - const traceId = traceContext - ? traceContext.split('/')[0] - : crypto.randomUUID() + const traceId = traceContext ? traceContext.split('/')[0] : crypto.randomUUID() const context = {endpoint: req.path, traceId} withMonitoringContext(context, () => { const startTs = hrtime.bigint() @@ -123,7 +123,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => { if (error instanceof APIError) { log.info(error) if (!res.headersSent) { - const output: { [k: string]: unknown } = {message: error.message} + const output: {[k: string]: unknown} = {message: error.message} if (error.details != null) { output.details = error.details } @@ -140,92 +140,91 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => { export const app = express() app.use(requestMonitoring) +const schemaCache = new WeakMap() -const schemaCache = new WeakMap(); - -export function zodToOpenApiSchema(zodObj: ZodTypeAny,): any { +export function zodToOpenApiSchema(zodObj: ZodTypeAny): any { if (schemaCache.has(zodObj)) { - return schemaCache.get(zodObj); + return schemaCache.get(zodObj) } - const def: any = (zodObj as any)._def; - const typeName = def.typeName as ZodFirstPartyTypeKind; + const def: any = (zodObj as any)._def + const typeName = def.typeName as ZodFirstPartyTypeKind // Placeholder so recursive references can point here - const placeholder: any = {}; - schemaCache.set(zodObj, placeholder); + const placeholder: any = {} + schemaCache.set(zodObj, placeholder) - let schema: any; + let schema: any switch (typeName) { case 'ZodString': - schema = {type: 'string'}; - break; + schema = {type: 'string'} + break case 'ZodNumber': - schema = {type: 'number'}; - break; + schema = {type: 'number'} + break case 'ZodBoolean': - schema = {type: 'boolean'}; - break; + schema = {type: 'boolean'} + break case 'ZodEnum': - schema = {type: 'string', enum: def.values}; - break; + schema = {type: 'string', enum: def.values} + break case 'ZodArray': - schema = {type: 'array', items: zodToOpenApiSchema(def.type)}; - break; + schema = {type: 'array', items: zodToOpenApiSchema(def.type)} + break case 'ZodObject': { - const shape = def.shape(); - const properties: Record = {}; - const required: string[] = []; + const shape = def.shape() + const properties: Record = {} + const required: string[] = [] for (const key in shape) { - const child = shape[key]; - properties[key] = zodToOpenApiSchema(child); - if (!child.isOptional()) required.push(key); + const child = shape[key] + properties[key] = zodToOpenApiSchema(child) + if (!child.isOptional()) required.push(key) } schema = { type: 'object', properties, ...(required.length ? {required} : {}), - }; - break; + } + break } case 'ZodRecord': schema = { type: 'object', additionalProperties: zodToOpenApiSchema(def.valueType), - }; - break; + } + break case 'ZodIntersection': { - const left = zodToOpenApiSchema(def.left); - const right = zodToOpenApiSchema(def.right); - schema = {allOf: [left, right]}; - break; + const left = zodToOpenApiSchema(def.left) + const right = zodToOpenApiSchema(def.right) + schema = {allOf: [left, right]} + break } case 'ZodLazy': - schema = {type: 'object', description: 'Lazy schema - details omitted'}; - break; + schema = {type: 'object', description: 'Lazy schema - details omitted'} + break case 'ZodUnion': schema = { oneOf: def.options.map((opt: ZodTypeAny) => zodToOpenApiSchema(opt)), - }; - break; + } + break default: - schema = {type: 'string'}; // fallback for unhandled + schema = {type: 'string'} // fallback for unhandled } - Object.assign(placeholder, schema); - return schema; + Object.assign(placeholder, schema) + return schema } function generateSwaggerPaths(api: typeof API) { - const paths: Record = {}; + const paths: Record = {} for (const [route, config] of Object.entries(api)) { - const pathKey = '/' + route.replace(/_/g, '-'); // optional: convert underscores to dashes - const method = config.method.toLowerCase(); - const summary = (config as any).summary ?? route; + const pathKey = '/' + route.replace(/_/g, '-') // optional: convert underscores to dashes + const method = config.method.toLowerCase() + const summary = (config as any).summary ?? route // Include props in request body for POST/PUT const operation: any = { @@ -241,7 +240,7 @@ function generateSwaggerPaths(api: typeof API) { }, }, }, - }; + } // Include props in request body for POST/PUT if (config.props && ['post', 'put', 'patch'].includes(method)) { @@ -252,26 +251,26 @@ function generateSwaggerPaths(api: typeof API) { schema: zodToOpenApiSchema(config.props), }, }, - }; + } } // Include props as query parameters for GET/DELETE if (config.props && ['get', 'delete'].includes(method)) { - const shape = (config.props as z.ZodObject)._def.shape(); + const shape = (config.props as z.ZodObject)._def.shape() operation.parameters = Object.entries(shape).map(([key, zodType]) => { const typeMap: Record = { ZodString: 'string', ZodNumber: 'number', ZodBoolean: 'boolean', - }; - const t = zodType as z.ZodTypeAny; // assert type to ZodTypeAny + } + const t = zodType as z.ZodTypeAny // assert type to ZodTypeAny return { name: key, in: 'query', required: !(t.isOptional ?? false), schema: {type: typeMap[t._def.typeName] ?? 'string'}, - }; - }); + } + }) } paths[pathKey] = { @@ -279,25 +278,24 @@ function generateSwaggerPaths(api: typeof API) { } if (config.authed) { - operation.security = [{BearerAuth: []}]; + operation.security = [{BearerAuth: []}] } } - return paths; + return paths } - const swaggerDocument: OpenAPIV3.Document = { - openapi: "3.0.0", + openapi: '3.0.0', info: { - title: "Compass API", + title: 'Compass API', description: `Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. It’s made possible by contributions from the community, including code, ideas, feedback, and donations. Unlike typical apps, Compass prioritizes values, interests, and personality over swipes and ads, giving you full control over who you discover and how you connect.\n Git: ${git.commitDate} (${git.revision}).`, version: pkgVersion, contact: { - name: "Compass", - email: "hello@compassmeet.com", - url: "https://compassmeet.com" - } + name: 'Compass', + email: 'hello@compassmeet.com', + url: 'https://compassmeet.com', + }, }, paths: generateSwaggerPaths(API), components: { @@ -313,17 +311,17 @@ const swaggerDocument: OpenAPIV3.Document = { name: 'x-api-key', }, }, - } -} as OpenAPIV3.Document; + }, +} as OpenAPIV3.Document // Triggers Missing parameter name at index 3: *; visit https://git.new/pathToRegexpError for info // May not be necessary // app.options('*', allowCorsUnrestricted) -const handlers: { [k in APIPath]: APIHandler } = { +const handlers: {[k in APIPath]: APIHandler} = { 'ban-user': banUser, 'compatible-profiles': getCompatibleProfilesHandler, - 'contact': contact, + contact: contact, 'create-bookmarked-search': createBookmarkedSearch, 'create-comment': createComment, 'create-compatibility-question': createCompatibilityQuestion, @@ -378,7 +376,7 @@ const handlers: { [k in APIPath]: APIHandler } = { 'user/by-id/:id': getUser, 'user/by-id/:id/block': blockUser, 'user/by-id/:id/unblock': unblockUser, - 'vote': vote, + vote: vote, // 'user/:username': getUser, // 'user/:username/lite': getDisplayUser, // 'user/by-id/:id/lite': getDisplayUser, @@ -396,7 +394,7 @@ const handlers: { [k in APIPath]: APIHandler } = { Object.entries(handlers).forEach(([path, handler]) => { const api = API[path as APIPath] const cache = cacheController((api as any).cache) - const url = pathWithPrefix('/' + path as APIPath) + const url = pathWithPrefix(('/' + path) as APIPath) const apiRoute = [ url, @@ -419,75 +417,73 @@ Object.entries(handlers).forEach(([path, handler]) => { }) // Internal Endpoints -app.post(pathWithPrefix("/internal/send-search-notifications"), - async (req, res) => { - const apiKey = req.header("x-api-key"); - if (apiKey !== process.env.COMPASS_API_KEY) { - return res.status(401).json({error: "Unauthorized"}); - } - - try { - const result = await sendSearchNotifications() - return res.status(200).json(result) - } catch (err) { - console.error("Failed to send notifications:", err); - await sendDiscordMessage( - "Failed to send [daily notifications](https://console.cloud.google.com/cloudscheduler?project=compass-130ba) for bookmarked searches...", - "health" - ) - return res.status(500).json({error: "Internal server error"}); - } +app.post(pathWithPrefix('/internal/send-search-notifications'), async (req, res) => { + const apiKey = req.header('x-api-key') + if (apiKey !== process.env.COMPASS_API_KEY) { + return res.status(401).json({error: 'Unauthorized'}) } -); + + try { + const result = await sendSearchNotifications() + return res.status(200).json(result) + } catch (err) { + console.error('Failed to send notifications:', err) + await sendDiscordMessage( + 'Failed to send [daily notifications](https://console.cloud.google.com/cloudscheduler?project=compass-130ba) for bookmarked searches...', + 'health', + ) + return res.status(500).json({error: 'Internal server error'}) + } +}) const responses = { 200: { - description: "Request successful", + description: 'Request successful', content: { - "application/json": { + 'application/json': { schema: { - type: "object", + type: 'object', properties: { - status: {type: "string", example: "success"} + status: {type: 'string', example: 'success'}, }, }, }, }, }, 401: { - description: "Unauthorized (e.g., invalid or missing API key)", + description: 'Unauthorized (e.g., invalid or missing API key)', content: { - "application/json": { + 'application/json': { schema: { - type: "object", + type: 'object', properties: { - error: {type: "string", example: "Unauthorized"}, + error: {type: 'string', example: 'Unauthorized'}, }, }, }, }, }, 500: { - description: "Internal server error during request processing", + description: 'Internal server error during request processing', content: { - "application/json": { + 'application/json': { schema: { - type: "object", + type: 'object', properties: { - error: {type: "string", example: "Internal server error"}, + error: {type: 'string', example: 'Internal server error'}, }, }, }, }, }, -}; +} -swaggerDocument.paths["/internal/send-search-notifications"] = { +swaggerDocument.paths['/internal/send-search-notifications'] = { post: { - summary: "Trigger daily search notifications", + summary: 'Trigger daily search notifications', description: - "Internal endpoint used by Compass schedulers to send daily notifications for bookmarked searches. Requires a valid `x-api-key` header.", - tags: ["Internal"], + 'Internal endpoint used by Compass schedulers to send daily notifications for bookmarked searches. Requires a valid `x-api-key` header.', + tags: ['Internal'], security: [ { ApiKeyAuth: [], @@ -500,28 +496,25 @@ swaggerDocument.paths["/internal/send-search-notifications"] = { }, } as any - // Local Endpoints if (IS_LOCAL) { - app.post(pathWithPrefix("/local/send-test-email"), - async (req, res) => { - if (!IS_LOCAL) { - return res.status(401).json({error: "Unauthorized"}); - } - - try { - const result = await localSendTestEmail() - return res.status(200).json(result) - } catch (err) { - return res.status(500).json({error: err}); - } + app.post(pathWithPrefix('/local/send-test-email'), async (req, res) => { + if (!IS_LOCAL) { + return res.status(401).json({error: 'Unauthorized'}) } - ); - swaggerDocument.paths["/local/send-test-email"] = { + + try { + const result = await localSendTestEmail() + return res.status(200).json(result) + } catch (err) { + return res.status(500).json({error: err}) + } + }) + swaggerDocument.paths['/local/send-test-email'] = { post: { - summary: "Send a test email", - description: "Local endpoint to send a test email.", - tags: ["Local"], + summary: 'Send a test email', + description: 'Local endpoint to send a test email.', + tags: ['Local'], requestBody: { required: false, }, @@ -530,8 +523,7 @@ if (IS_LOCAL) { } as any } - -const rootPath = pathWithPrefix("/") +const rootPath = pathWithPrefix('/') app.get( rootPath, swaggerUi.setup(swaggerDocument, { @@ -547,7 +539,7 @@ app.get( ) app.use(rootPath, swaggerUi.serve) -app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.static(path.join(__dirname, 'public'))) app.use(allowCorsUnrestricted, (req, res) => { if (req.method === 'OPTIONS') { diff --git a/backend/api/src/ban-user.ts b/backend/api/src/ban-user.ts index dd55933e..423aff94 100644 --- a/backend/api/src/ban-user.ts +++ b/backend/api/src/ban-user.ts @@ -1,13 +1,13 @@ -import { APIError, APIHandler } from 'api/helpers/endpoint' -import { trackPublicEvent } from 'shared/analytics' -import { throwErrorIfNotMod } from 'shared/helpers/auth' -import { isAdminId } from 'common/envs/constants' -import { log } from 'shared/utils' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { updateUser } from 'shared/supabase/users' +import {APIError, APIHandler} from 'api/helpers/endpoint' +import {isAdminId} from 'common/envs/constants' +import {trackPublicEvent} from 'shared/analytics' +import {throwErrorIfNotMod} from 'shared/helpers/auth' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {updateUser} from 'shared/supabase/users' +import {log} from 'shared/utils' export const banUser: APIHandler<'ban-user'> = async (body, auth) => { - const { userId, unban } = body + const {userId, unban} = body const db = createSupabaseDirectClient() await throwErrorIfNotMod(auth.uid) if (isAdminId(userId)) throw new APIError(403, 'Cannot ban admin') diff --git a/backend/api/src/block-user.ts b/backend/api/src/block-user.ts index 4d776566..fc661bb2 100644 --- a/backend/api/src/block-user.ts +++ b/backend/api/src/block-user.ts @@ -1,12 +1,10 @@ -import { APIError, APIHandler } from './helpers/endpoint' -import { FieldVal } from 'shared/supabase/utils' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { updatePrivateUser } from 'shared/supabase/users' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {updatePrivateUser} from 'shared/supabase/users' +import {FieldVal} from 'shared/supabase/utils' -export const blockUser: APIHandler<'user/by-id/:id/block'> = async ( - { id }, - auth -) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const blockUser: APIHandler<'user/by-id/:id/block'> = async ({id}, auth) => { if (auth.uid === id) throw new APIError(400, 'You cannot block yourself') const pg = createSupabaseDirectClient() @@ -20,10 +18,7 @@ export const blockUser: APIHandler<'user/by-id/:id/block'> = async ( }) } -export const unblockUser: APIHandler<'user/by-id/:id/unblock'> = async ( - { id }, - auth -) => { +export const unblockUser: APIHandler<'user/by-id/:id/unblock'> = async ({id}, auth) => { const pg = createSupabaseDirectClient() await pg.tx(async (tx) => { await updatePrivateUser(tx, auth.uid, { diff --git a/backend/api/src/cancel-event.ts b/backend/api/src/cancel-event.ts index 8bae9d2d..e9268bd9 100644 --- a/backend/api/src/cancel-event.ts +++ b/backend/api/src/cancel-event.ts @@ -1,7 +1,7 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {update} from 'shared/supabase/utils' -import {tryCatch} from 'common/util/try-catch' export const cancelEvent: APIHandler<'cancel-event'> = async (body, auth) => { const pg = createSupabaseDirectClient() @@ -15,7 +15,7 @@ export const cancelEvent: APIHandler<'cancel-event'> = async (body, auth) => { `SELECT id, creator_id, status FROM events WHERE id = $1`, - [body.eventId] + [body.eventId], ) if (!event) { @@ -35,7 +35,7 @@ export const cancelEvent: APIHandler<'cancel-event'> = async (body, auth) => { update(pg, 'events', 'id', { status: 'cancelled', id: body.eventId, - }) + }), ) if (error) { diff --git a/backend/api/src/cancel-rsvp.ts b/backend/api/src/cancel-rsvp.ts index 74b7b07b..dd822d4e 100644 --- a/backend/api/src/cancel-rsvp.ts +++ b/backend/api/src/cancel-rsvp.ts @@ -1,6 +1,6 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient} from 'shared/supabase/init' export const cancelRsvp: APIHandler<'cancel-rsvp'> = async (body, auth) => { const pg = createSupabaseDirectClient() @@ -13,7 +13,7 @@ export const cancelRsvp: APIHandler<'cancel-rsvp'> = async (body, auth) => { FROM events_participants WHERE event_id = $1 AND user_id = $2`, - [body.eventId, auth.uid] + [body.eventId, auth.uid], ) if (!rsvp) { @@ -26,8 +26,8 @@ export const cancelRsvp: APIHandler<'cancel-rsvp'> = async (body, auth) => { `DELETE FROM events_participants WHERE id = $1`, - [rsvp.id] - ) + [rsvp.id], + ), ) if (error) { diff --git a/backend/api/src/compatible-profiles.ts b/backend/api/src/compatible-profiles.ts index 8e31929b..fef356d4 100644 --- a/backend/api/src/compatible-profiles.ts +++ b/backend/api/src/compatible-profiles.ts @@ -1,13 +1,11 @@ import {type APIHandler} from 'api/helpers/endpoint' -import {createSupabaseDirectClient} from "shared/supabase/init"; +import {createSupabaseDirectClient} from 'shared/supabase/init' export const getCompatibleProfilesHandler: APIHandler<'compatible-profiles'> = async (props) => { return getCompatibleProfiles(props.userId) } -export const getCompatibleProfiles = async ( - userId: string, -) => { +export const getCompatibleProfiles = async (userId: string) => { const pg = createSupabaseDirectClient() const scores = await pg.map( `select * @@ -15,7 +13,7 @@ export const getCompatibleProfiles = async ( where score is not null and (user_id_1 = $1 or user_id_2 = $1)`, [userId], - (r) => [r.user_id_1 == userId ? r.user_id_2 : r.user_id_1, {score: r.score}] as const + (r) => [r.user_id_1 == userId ? r.user_id_2 : r.user_id_1, {score: r.score}] as const, ) const profileCompatibilityScores = Object.fromEntries(scores) diff --git a/backend/api/src/contact.ts b/backend/api/src/contact.ts index af4a289c..7a92665f 100644 --- a/backend/api/src/contact.ts +++ b/backend/api/src/contact.ts @@ -1,24 +1,22 @@ -import {APIError, APIHandler} from './helpers/endpoint' +import {sendDiscordMessage} from 'common/discord/core' +import {jsonToMarkdown} from 'common/md' +import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {insert} from 'shared/supabase/utils' -import {tryCatch} from 'common/util/try-catch' -import {sendDiscordMessage} from "common/discord/core"; -import {jsonToMarkdown} from "common/md"; + +import {APIError, APIHandler} from './helpers/endpoint' // Stores a contact message into the `contact` table // Web sends TipTap JSON in `content`; we store it as string in `description`. // If optional content metadata is provided, we include it; otherwise we fall back to user-centric defaults. -export const contact: APIHandler<'contact'> = async ( - {content, userId}, - _auth -) => { +export const contact: APIHandler<'contact'> = async ({content, userId}, _auth) => { const pg = createSupabaseDirectClient() const {error} = await tryCatch( insert(pg, 'contact', { user_id: userId, content: JSON.stringify(content), - }) + }), ) if (error) throw new APIError(500, 'Failed to submit contact message') diff --git a/backend/api/src/create-bookmarked-search.ts b/backend/api/src/create-bookmarked-search.ts index 2dc0bd75..ea753710 100644 --- a/backend/api/src/create-bookmarked-search.ts +++ b/backend/api/src/create-bookmarked-search.ts @@ -1,9 +1,10 @@ -import {APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from 'shared/supabase/init' +import {APIHandler} from './helpers/endpoint' + export const createBookmarkedSearch: APIHandler<'create-bookmarked-search'> = async ( props, - auth + auth, ) => { const creator_id = auth.uid const {search_filters, location = null, search_name = null} = props @@ -16,7 +17,7 @@ export const createBookmarkedSearch: APIHandler<'create-bookmarked-search'> = as VALUES ($1, $2, $3, $4) RETURNING * `, - [creator_id, search_filters, location, search_name] + [creator_id, search_filters, location, search_name], ) return inserted diff --git a/backend/api/src/create-comment.ts b/backend/api/src/create-comment.ts index 4fcc887a..e28bf01c 100644 --- a/backend/api/src/create-comment.ts +++ b/backend/api/src/create-comment.ts @@ -1,32 +1,25 @@ -import { APIError, APIHandler } from 'api/helpers/endpoint' -import { type JSONContent } from '@tiptap/core' -import { getPrivateUser, getUser } from 'shared/utils' -import { - createSupabaseDirectClient, - SupabaseDirectClient, -} from 'shared/supabase/init' -import { getNotificationDestinationsForUser } from 'common/user-notification-preferences' -import { Notification } from 'common/notifications' -import { insertNotificationToSupabase } from 'shared/supabase/notifications' -import { User } from 'common/user' -import { richTextToString } from 'common/util/parse' +import {type JSONContent} from '@tiptap/core' +import {APIError, APIHandler} from 'api/helpers/endpoint' +import {Notification} from 'common/notifications' +import {convertComment} from 'common/supabase/comment' +import {type Row} from 'common/supabase/utils' +import {User} from 'common/user' +import {getNotificationDestinationsForUser} from 'common/user-notification-preferences' +import {richTextToString} from 'common/util/parse' import * as crypto from 'crypto' -import { sendNewEndorsementEmail } from 'email/functions/helpers' -import { type Row } from 'common/supabase/utils' -import { broadcastUpdatedComment } from 'shared/websockets/helpers' -import { convertComment } from 'common/supabase/comment' +import {sendNewEndorsementEmail} from 'email/functions/helpers' +import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init' +import {insertNotificationToSupabase} from 'shared/supabase/notifications' +import {getPrivateUser, getUser} from 'shared/utils' +import {broadcastUpdatedComment} from 'shared/websockets/helpers' export const MAX_COMMENT_JSON_LENGTH = 20000 export const createComment: APIHandler<'create-comment'> = async ( - { userId, content: submittedContent, replyToCommentId }, - auth + {userId, content: submittedContent, replyToCommentId}, + auth, ) => { - const { creator, content } = await validateComment( - userId, - auth.uid, - submittedContent - ) + const {creator, content} = await validateComment(userId, auth.uid, submittedContent) const onUser = await getUser(userId) if (!onUser) throw new APIError(404, 'User not found') @@ -43,7 +36,7 @@ export const createComment: APIHandler<'create-comment'> = async ( userId, content, replyToCommentId, - ] + ], ) if (onUser.id !== creator.id) await createNewCommentOnProfileNotification( @@ -51,19 +44,15 @@ export const createComment: APIHandler<'create-comment'> = async ( creator, richTextToString(content), comment.id, - pg + pg, ) broadcastUpdatedComment(convertComment(comment)) - return { status: 'success' } + return {status: 'success'} } -const validateComment = async ( - userId: string, - creatorId: string, - content: JSONContent -) => { +const validateComment = async (userId: string, creatorId: string, content: JSONContent) => { const creator = await getUser(creatorId) if (!creator) throw new APIError(401, 'Your account was not found') @@ -78,10 +67,10 @@ const validateComment = async ( if (JSON.stringify(content).length > MAX_COMMENT_JSON_LENGTH) { throw new APIError( 400, - `Comment is too long; should be less than ${MAX_COMMENT_JSON_LENGTH} as a JSON string.` + `Comment is too long; should be less than ${MAX_COMMENT_JSON_LENGTH} as a JSON string.`, ) } - return { content, creator } + return {content, creator} } const createNewCommentOnProfileNotification = async ( @@ -89,14 +78,16 @@ const createNewCommentOnProfileNotification = async ( creator: User, sourceText: string, commentId: number, - pg: SupabaseDirectClient + pg: SupabaseDirectClient, ) => { const privateUser = await getPrivateUser(onUser.id) if (!privateUser) return const id = crypto.randomUUID() const reason = 'new_endorsement' - const { sendToBrowser, sendToMobile, sendToEmail } = - getNotificationDestinationsForUser(privateUser, reason) + const {sendToBrowser, sendToMobile, sendToEmail} = getNotificationDestinationsForUser( + privateUser, + reason, + ) const notification: Notification = { id, userId: privateUser.id, diff --git a/backend/api/src/create-compatibility-question.ts b/backend/api/src/create-compatibility-question.ts index 8483e943..81bcf1be 100644 --- a/backend/api/src/create-compatibility-question.ts +++ b/backend/api/src/create-compatibility-question.ts @@ -1,27 +1,29 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { getUser } from 'shared/utils' -import { APIHandler, APIError } from './helpers/endpoint' -import { insert } from 'shared/supabase/utils' -import { tryCatch } from 'common/util/try-catch' +import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {insert} from 'shared/supabase/utils' +import {getUser} from 'shared/utils' -export const createCompatibilityQuestion: APIHandler< - 'create-compatibility-question' -> = async ({ question, options }, auth) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const createCompatibilityQuestion: APIHandler<'create-compatibility-question'> = async ( + {question, options}, + auth, +) => { const creator = await getUser(auth.uid) if (!creator) throw new APIError(401, 'Your account was not found') const pg = createSupabaseDirectClient() - const { data, error } = await tryCatch( + const {data, error} = await tryCatch( insert(pg, 'compatibility_prompts', { creator_id: creator.id, question, answer_type: 'compatibility_multiple_choice', multiple_choice_options: options, - }) + }), ) if (error) throw new APIError(401, 'Error creating question') - return { question: data } + return {question: data} } diff --git a/backend/api/src/create-event.ts b/backend/api/src/create-event.ts index 982503b0..8bf21f02 100644 --- a/backend/api/src/create-event.ts +++ b/backend/api/src/create-event.ts @@ -1,7 +1,7 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {insert} from 'shared/supabase/utils' -import {tryCatch} from 'common/util/try-catch' export const createEvent: APIHandler<'create-event'> = async (body, auth) => { const pg = createSupabaseDirectClient() @@ -38,7 +38,7 @@ export const createEvent: APIHandler<'create-event'> = async (body, auth) => { event_start_time: body.eventStartTime, event_end_time: body.eventEndTime, max_participants: body.maxParticipants, - }) + }), ) if (error) { diff --git a/backend/api/src/create-notification.ts b/backend/api/src/create-notification.ts index 55a7cfdd..0c960313 100644 --- a/backend/api/src/create-notification.ts +++ b/backend/api/src/create-notification.ts @@ -1,9 +1,9 @@ -import {createSupabaseDirectClient, SupabaseDirectClient,} from 'shared/supabase/init' -import {Notification} from 'common/notifications' -import {createBulkNotification, 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' +import {Notification} from 'common/notifications' +import {Row} from 'common/supabase/utils' +import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init' +import {createBulkNotification, insertNotificationToSupabase} from 'shared/supabase/notifications' export const createAndroidReleaseNotifications = async () => { const createdTime = Date.now() @@ -38,8 +38,7 @@ export const createAndroidTestNotifications = async () => { sourceSlug: '/contact', 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 Ready for Review — Help Us Unlock the Google Play Release', + title: 'Android App Ready for Review — Help Us Unlock the Google Play Release', sourceText: 'To release our app, Google requires a closed test with at least 12 testers for 14 days. Please share your Google Play–registered email address so we can add you as a tester and complete the review process.', } @@ -60,8 +59,7 @@ export const createShareNotifications = async () => { sourceUserAvatarUrl: 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Ficon-outreach-outstrip-outreach-272151502.jpg?alt=media&token=6d6fcecb-818c-4fca-a8e0-d2d0069b9445', title: 'Give us tips to reach more people', - sourceText: - '250 members already! Tell us where and how we can best share Compass.', + sourceText: '250 members already! Tell us where and how we can best share Compass.', } return await createNotifications(notification) } @@ -87,9 +85,7 @@ export const createVoteNotifications = async () => { export const createNotifications = async (notification: Notification) => { const pg = createSupabaseDirectClient() - const {data: users, error} = await tryCatch( - pg.many>('select * from users') - ) + const {data: users, error} = await tryCatch(pg.many>('select * from users')) if (error) { console.error('Error fetching users', error) @@ -117,7 +113,7 @@ export const createNotifications = async (notification: Notification) => { export const createNotification = async ( user: Row<'users'>, notification: Notification, - pg: SupabaseDirectClient + pg: SupabaseDirectClient, ) => { notification.userId = user.id console.log('notification', user.username) @@ -132,9 +128,7 @@ export const createEventsAvailableNotifications = async () => { const pg = createSupabaseDirectClient() // Fetch all users - const {data: users, error} = await tryCatch( - pg.many>('select id from users') - ) + const {data: users, error} = await tryCatch(pg.many>('select id from users')) if (error) { console.error('Error fetching users', error) @@ -161,12 +155,10 @@ export const createEventsAvailableNotifications = async () => { sourceUpdateType: 'created', }, userIds, - pg + pg, ) - console.log( - `Created events notification template ${templateId} for ${count} users` - ) + console.log(`Created events notification template ${templateId} for ${count} users`) return { success: true, diff --git a/backend/api/src/create-private-user-message-channel.ts b/backend/api/src/create-private-user-message-channel.ts index fe0fd92e..a5687642 100644 --- a/backend/api/src/create-private-user-message-channel.ts +++ b/backend/api/src/create-private-user-message-channel.ts @@ -1,10 +1,10 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {addUsersToPrivateMessageChannel} from 'api/helpers/private-messages' import {filterDefined} from 'common/util/array' +import * as admin from 'firebase-admin' import {uniq} from 'lodash' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {addUsersToPrivateMessageChannel} from 'api/helpers/private-messages' import {getPrivateUser, getUser} from 'shared/utils' -import * as admin from 'firebase-admin' export const createPrivateUserMessageChannel: APIHandler< 'create-private-user-message-channel' @@ -13,10 +13,7 @@ export const createPrivateUserMessageChannel: APIHandler< const user = await admin.auth().getUser(auth.uid) // console.log(JSON.stringify(user, null, 2)) if (!user?.emailVerified) { - throw new APIError( - 403, - 'You must verify your email to contact people.' - ) + throw new APIError(403, 'You must verify your email to contact people.') } const userIds = uniq(body.userIds.concat(auth.uid)) @@ -27,27 +24,22 @@ export const createPrivateUserMessageChannel: APIHandler< const creator = await getUser(creatorId) if (!creator) throw new APIError(401, 'Your account was not found') if (creator.isBannedFromPosting) throw new APIError(403, 'You are banned') - const toPrivateUsers = filterDefined( - await Promise.all(userIds.map((id) => getPrivateUser(id))) - ) + const toPrivateUsers = filterDefined(await Promise.all(userIds.map((id) => getPrivateUser(id)))) if (toPrivateUsers.length !== userIds.length) throw new APIError( 404, `Private user ${userIds.find( - (uid) => !toPrivateUsers.map((p) => p.id).includes(uid) - )} not found` + (uid) => !toPrivateUsers.map((p) => p.id).includes(uid), + )} not found`, ) if ( toPrivateUsers.some((user) => - user.blockedUserIds.some((blockedId) => userIds.includes(blockedId)) + user.blockedUserIds.some((blockedId) => userIds.includes(blockedId)), ) ) { - throw new APIError( - 403, - 'One of the users has blocked another user in the list' - ) + throw new APIError(403, 'One of the users has blocked another user in the list') } const currentChannel = await pg.oneOrNone( @@ -58,7 +50,7 @@ export const createPrivateUserMessageChannel: APIHandler< having array_agg(user_id::text) @> array [$1]::text[] and array_agg(user_id::text) <@ array [$1]::text[] `, - [userIds] + [userIds], ) if (currentChannel) return { @@ -69,14 +61,14 @@ export const createPrivateUserMessageChannel: APIHandler< const channel = await pg.one( `insert into private_user_message_channels default values - returning id` + returning id`, ) await pg.none( `insert into private_user_message_channel_members (channel_id, user_id, role, status) values ($1, $2, 'creator', 'joined') `, - [channel.id, creatorId] + [channel.id, creatorId], ) const memberIds = userIds.filter((id) => id !== creatorId) diff --git a/backend/api/src/create-private-user-message.ts b/backend/api/src/create-private-user-message.ts index 1759b477..b02780f9 100644 --- a/backend/api/src/create-private-user-message.ts +++ b/backend/api/src/create-private-user-message.ts @@ -1,18 +1,16 @@ -import {APIError, APIHandler} from 'api/helpers/endpoint' -import {getUser} from 'shared/utils' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {MAX_COMMENT_JSON_LENGTH} from 'api/create-comment' +import {APIError, APIHandler} from 'api/helpers/endpoint' import {createPrivateUserMessageMain} from 'api/helpers/private-messages' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {getUser} from 'shared/utils' -export const createPrivateUserMessage: APIHandler< - 'create-private-user-message' -> = async (body, auth) => { +export const createPrivateUserMessage: APIHandler<'create-private-user-message'> = async ( + body, + auth, +) => { const {content, channelId} = body if (JSON.stringify(content).length > MAX_COMMENT_JSON_LENGTH) { - throw new APIError( - 400, - `Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}` - ) + throw new APIError(400, `Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`) } const creator = await getUser(auth.uid) @@ -20,11 +18,5 @@ export const createPrivateUserMessage: APIHandler< if (creator.isBannedFromPosting) throw new APIError(403, 'You are banned') const pg = createSupabaseDirectClient() - return await createPrivateUserMessageMain( - creator, - channelId, - content, - pg, - 'private' - ) + return await createPrivateUserMessageMain(creator, channelId, content, pg, 'private') } diff --git a/backend/api/src/create-profile.ts b/backend/api/src/create-profile.ts index b9efb837..c13ec6bf 100644 --- a/backend/api/src/create-profile.ts +++ b/backend/api/src/create-profile.ts @@ -1,23 +1,21 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' -import {createSupabaseDirectClient} from 'shared/supabase/init' -import {getUser, log} from 'shared/utils' +import {sendDiscordMessage} from 'common/discord/core' +import {jsonToMarkdown} from 'common/md' +import {trimStrings} from 'common/parsing' import {HOUR_MS, MINUTE_MS, sleep} from 'common/util/time' -import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' -import {track} from 'shared/analytics' -import {updateUser} from 'shared/supabase/users' import {tryCatch} from 'common/util/try-catch' +import {track} from 'shared/analytics' +import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {updateUser} from 'shared/supabase/users' import {insert} from 'shared/supabase/utils' -import {sendDiscordMessage} from "common/discord/core"; -import {jsonToMarkdown} from "common/md"; -import {trimStrings} from "common/parsing"; +import {getUser, log} from 'shared/utils' export const createProfile: APIHandler<'create-profile'> = async (body, auth) => { const pg = createSupabaseDirectClient() - const { data: existingUser } = await tryCatch( - pg.oneOrNone<{ id: string }>('select id from profiles where user_id = $1', [ - auth.uid, - ]) + const {data: existingUser} = await tryCatch( + pg.oneOrNone<{id: string}>('select id from profiles where user_id = $1', [auth.uid]), ) if (existingUser) { throw new APIError(400, 'User already exists') @@ -30,14 +28,12 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) => if (!user) throw new APIError(401, 'Your account was not found') if (user.createdTime > Date.now() - HOUR_MS) { // If they just signed up, set their avatar to be their pinned photo - updateUser(pg, auth.uid, { avatarUrl: body.pinned_url }) + updateUser(pg, auth.uid, {avatarUrl: body.pinned_url}) } console.debug('body', body) - const { data, error } = await tryCatch( - insert(pg, 'profiles', { user_id: auth.uid, ...body }) - ) + const {data, error} = await tryCatch(insert(pg, 'profiles', {user_id: auth.uid, ...body})) if (error) { log.error('Error creating user: ' + error.message) @@ -67,10 +63,8 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) => console.error('Failed to send discord new profile', e) } try { - const nProfiles = await pg.one( - `SELECT count(*) FROM profiles`, - [], - (r) => Number(r.count) + const nProfiles = await pg.one(`SELECT count(*) FROM profiles`, [], (r) => + Number(r.count), ) const isMilestone = (n: number) => { @@ -81,12 +75,8 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) => } console.debug(nProfiles, isMilestone(nProfiles)) if (isMilestone(nProfiles)) { - await sendDiscordMessage( - `We just reached **${nProfiles}** total profiles! 🎉`, - 'general', - ) + await sendDiscordMessage(`We just reached **${nProfiles}** total profiles! 🎉`, 'general') } - } catch (e) { console.error('Failed to send discord user milestone', e) } diff --git a/backend/api/src/create-user.ts b/backend/api/src/create-user.ts index d4dd8541..082577b8 100644 --- a/backend/api/src/create-user.ts +++ b/backend/api/src/create-user.ts @@ -1,32 +1,28 @@ -import * as admin from 'firebase-admin' -import {PrivateUser} from 'common/user' -import {randomString} from 'common/util/random' -import {cleanDisplayName, cleanUsername} from 'common/util/clean-username' -import {getIp, track} from 'shared/analytics' -import {APIError, APIHandler} from './helpers/endpoint' -import {getDefaultNotificationPreferences} from 'common/user-notification-preferences' -import {removeUndefinedProps} from 'common/util/object' -import {generateAvatarUrl} from 'shared/helpers/generate-and-update-avatar-urls' +import {setLastOnlineTimeUser} from 'api/set-last-online-time' import {RESERVED_PATHS} from 'common/envs/constants' -import {getUser, getUserByUsername, log} from 'shared/utils' +import {IS_LOCAL} from 'common/hosting/constants' +import {convertPrivateUser, convertUser} from 'common/supabase/users' +import {PrivateUser} from 'common/user' +import {getDefaultNotificationPreferences} from 'common/user-notification-preferences' +import {cleanDisplayName, cleanUsername} from 'common/util/clean-username' +import {removeUndefinedProps} from 'common/util/object' +import {randomString} from 'common/util/random' +import {sendWelcomeEmail} from 'email/functions/helpers' +import * as admin from 'firebase-admin' +import {getIp, track} from 'shared/analytics' +import {getBucket} from 'shared/firebase-utils' +import {generateAvatarUrl} from 'shared/helpers/generate-and-update-avatar-urls' import {createSupabaseDirectClient} from 'shared/supabase/init' import {insert} from 'shared/supabase/utils' -import {convertPrivateUser, convertUser} from 'common/supabase/users' -import {getBucket} from "shared/firebase-utils"; -import {sendWelcomeEmail} from "email/functions/helpers"; -import {setLastOnlineTimeUser} from "api/set-last-online-time"; -import {IS_LOCAL} from "common/hosting/constants"; +import {getUser, getUserByUsername, log} from 'shared/utils' -export const createUser: APIHandler<'create-user'> = async ( - props, - auth, - req -) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const createUser: APIHandler<'create-user'> = async (props, auth, req) => { const {deviceToken: preDeviceToken} = props const firebaseUser = await admin.auth().getUser(auth.uid) - const testUserAKAEmailPasswordUser = - firebaseUser.providerData[0].providerId === 'password' + const testUserAKAEmailPasswordUser = firebaseUser.providerData[0].providerId === 'password' // if ( // testUserAKAEmailPasswordUser && @@ -68,7 +64,7 @@ export const createUser: APIHandler<'create-user'> = async ( from users where username ilike $1`, [username], - (r) => r.count + (r) => r.count, ) const usernameExists = dupes > 0 const isReservedName = RESERVED_PATHS.includes(username) @@ -83,14 +79,13 @@ export const createUser: APIHandler<'create-user'> = async ( // Check exact username to avoid problems with duplicate requests const sameNameUser = await getUserByUsername(username, tx) - if (sameNameUser) - throw new APIError(403, 'Username already taken', {username}) + if (sameNameUser) throw new APIError(403, 'Username already taken', {username}) const user = removeUndefinedProps({ avatarUrl, isBannedFromPosting: Boolean( (deviceToken && bannedDeviceTokens.includes(deviceToken)) || - (ip && bannedIpAddresses.includes(ip)) + (ip && bannedIpAddresses.includes(ip)), ), link: {}, }) diff --git a/backend/api/src/create-vote.ts b/backend/api/src/create-vote.ts index ffddee59..7b15deea 100644 --- a/backend/api/src/create-vote.ts +++ b/backend/api/src/create-vote.ts @@ -1,28 +1,30 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { getUser } from 'shared/utils' -import { APIHandler, APIError } from './helpers/endpoint' -import { insert } from 'shared/supabase/utils' -import { tryCatch } from 'common/util/try-catch' +import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {insert} from 'shared/supabase/utils' +import {getUser} from 'shared/utils' -export const createVote: APIHandler< - 'create-vote' -> = async ({ title, description, isAnonymous }, auth) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const createVote: APIHandler<'create-vote'> = async ( + {title, description, isAnonymous}, + auth, +) => { const creator = await getUser(auth.uid) if (!creator) throw new APIError(401, 'Your account was not found') const pg = createSupabaseDirectClient() - const { data, error } = await tryCatch( + const {data, error} = await tryCatch( insert(pg, 'votes', { creator_id: creator.id, title, description, is_anonymous: isAnonymous, status: 'voting_open', - }) + }), ) if (error) throw new APIError(401, 'Error creating question') - return { data } + return {data} } diff --git a/backend/api/src/delete-bookmarked-search.ts b/backend/api/src/delete-bookmarked-search.ts index 2326c3f3..9b5b95ca 100644 --- a/backend/api/src/delete-bookmarked-search.ts +++ b/backend/api/src/delete-bookmarked-search.ts @@ -1,9 +1,10 @@ -import {APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from 'shared/supabase/init' +import {APIHandler} from './helpers/endpoint' + export const deleteBookmarkedSearch: APIHandler<'delete-bookmarked-search'> = async ( props, - auth + auth, ) => { const creator_id = auth.uid const {id} = props @@ -16,7 +17,7 @@ export const deleteBookmarkedSearch: APIHandler<'delete-bookmarked-search'> = as DELETE FROM bookmarked_searches WHERE id = $1 AND creator_id = $2 `, - [id, creator_id] + [id, creator_id], ) return {} diff --git a/backend/api/src/delete-compatibility-answer.ts b/backend/api/src/delete-compatibility-answer.ts index 142594cb..3e09e8cc 100644 --- a/backend/api/src/delete-compatibility-answer.ts +++ b/backend/api/src/delete-compatibility-answer.ts @@ -1,10 +1,12 @@ import {APIHandler} from 'api/helpers/endpoint' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {APIError} from 'common/api/utils' import {recomputeCompatibilityScoresForUser} from 'shared/compatibility/compute-scores' +import {createSupabaseDirectClient} from 'shared/supabase/init' export const deleteCompatibilityAnswer: APIHandler<'delete-compatibility-answer'> = async ( - {id}, auth) => { + {id}, + auth, +) => { const pg = createSupabaseDirectClient() // Verify user is the answer author @@ -13,7 +15,7 @@ export const deleteCompatibilityAnswer: APIHandler<'delete-compatibility-answer' FROM compatibility_answers WHERE id = $1 AND creator_id = $2`, - [id, auth.uid] + [id, auth.uid], ) if (!item) { @@ -26,7 +28,7 @@ export const deleteCompatibilityAnswer: APIHandler<'delete-compatibility-answer' FROM compatibility_answers WHERE id = $1 AND creator_id = $2`, - [id, auth.uid] + [id, auth.uid], ) const continuation = async () => { diff --git a/backend/api/src/delete-me.ts b/backend/api/src/delete-me.ts index 12af1f69..17ccdec7 100644 --- a/backend/api/src/delete-me.ts +++ b/backend/api/src/delete-me.ts @@ -1,8 +1,9 @@ -import {getUser} from 'shared/utils' -import {APIError, APIHandler} from './helpers/endpoint' +import * as admin from 'firebase-admin' +import {deleteUserFiles} from 'shared/firebase-utils' import {createSupabaseDirectClient} from 'shared/supabase/init' -import * as admin from "firebase-admin"; -import {deleteUserFiles} from "shared/firebase-utils"; +import {getUser} from 'shared/utils' + +import {APIError, APIHandler} from './helpers/endpoint' export const deleteMe: APIHandler<'me/delete'> = async (_, auth) => { const user = await getUser(auth.uid) diff --git a/backend/api/src/delete-message.ts b/backend/api/src/delete-message.ts index 25151850..f90da5fc 100644 --- a/backend/api/src/delete-message.ts +++ b/backend/api/src/delete-message.ts @@ -1,6 +1,7 @@ -import {APIError, APIHandler} from './helpers/endpoint' +import {broadcastPrivateMessages} from 'api/helpers/private-messages' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {broadcastPrivateMessages} from "api/helpers/private-messages"; + +import {APIError, APIHandler} from './helpers/endpoint' // const DELETED_MESSAGE_CONTENT: JSONContent = { // type: 'doc', @@ -26,7 +27,7 @@ export const deleteMessage: APIHandler<'delete-message'> = async ({messageId}, a FROM private_user_messages WHERE id = $1 AND user_id = $2`, - [messageId, auth.uid] + [messageId, auth.uid], ) if (!message) { @@ -51,14 +52,12 @@ export const deleteMessage: APIHandler<'delete-message'> = async ({messageId}, a FROM private_user_messages WHERE id = $1 AND user_id = $2`, - [messageId, auth.uid] + [messageId, auth.uid], ) - void broadcastPrivateMessages(pg, message.channel_id, auth.uid) - .catch((err) => { - console.error('broadcastPrivateMessages failed', err) - }) + void broadcastPrivateMessages(pg, message.channel_id, auth.uid).catch((err) => { + console.error('broadcastPrivateMessages failed', err) + }) return {success: true} } - diff --git a/backend/api/src/edit-message.ts b/backend/api/src/edit-message.ts index aba2f11b..522e4ffd 100644 --- a/backend/api/src/edit-message.ts +++ b/backend/api/src/edit-message.ts @@ -1,8 +1,8 @@ -import {APIError, APIHandler} from './helpers/endpoint' +import {broadcastPrivateMessages} from 'api/helpers/private-messages' +import {encryptMessage} from 'shared/encryption' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {encryptMessage} from "shared/encryption"; -import {broadcastPrivateMessages} from "api/helpers/private-messages"; +import {APIError, APIHandler} from './helpers/endpoint' export const editMessage: APIHandler<'edit-message'> = async ({messageId, content}, auth) => { const pg = createSupabaseDirectClient() @@ -15,7 +15,7 @@ export const editMessage: APIHandler<'edit-message'> = async ({messageId, conten AND user_id = $2 -- AND created_time > NOW() - INTERVAL '1 day' AND deleted = FALSE`, - [messageId, auth.uid] + [messageId, auth.uid], ) if (!message) { @@ -32,13 +32,12 @@ export const editMessage: APIHandler<'edit-message'> = async ({messageId, conten is_edited = TRUE, edited_at = NOW() WHERE id = $4`, - [ciphertext, iv, tag, messageId] + [ciphertext, iv, tag, messageId], ) - void broadcastPrivateMessages(pg, message.channel_id, auth.uid) - .catch((err) => { - console.error('broadcastPrivateMessages failed', err) - }) + void broadcastPrivateMessages(pg, message.channel_id, auth.uid).catch((err) => { + console.error('broadcastPrivateMessages failed', err) + }) return {success: true} } diff --git a/backend/api/src/get-compatibililty-questions.ts b/backend/api/src/get-compatibililty-questions.ts index 211de7ba..bbcec3d1 100644 --- a/backend/api/src/get-compatibililty-questions.ts +++ b/backend/api/src/get-compatibililty-questions.ts @@ -1,6 +1,6 @@ import {type APIHandler} from 'api/helpers/endpoint' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {Row} from 'common/supabase/utils' +import {createSupabaseDirectClient} from 'shared/supabase/init' export function shuffle(array: T[]): T[] { const arr = [...array] // copy to avoid mutating the original @@ -11,9 +11,10 @@ export function shuffle(array: T[]): T[] { return arr } -export const getCompatibilityQuestions: APIHandler< - 'get-compatibility-questions' -> = async (props, _auth) => { +export const getCompatibilityQuestions: APIHandler<'get-compatibility-questions'> = async ( + props, + _auth, +) => { const {locale = 'en', keyword} = props const pg = createSupabaseDirectClient() @@ -40,7 +41,7 @@ export const getCompatibilityQuestions: APIHandler< } const questions = await pg.manyOrNone< - Row<'compatibility_prompts'> & { answer_count: number; score: number } + Row<'compatibility_prompts'> & {answer_count: number; score: number} >( ` SELECT cp.id, @@ -82,7 +83,7 @@ export const getCompatibilityQuestions: APIHandler< ORDER BY cp.importance_score `, - params + params, ) // console.debug({questions}) diff --git a/backend/api/src/get-current-private-user.ts b/backend/api/src/get-current-private-user.ts index 296aedfa..6d1cb9f1 100644 --- a/backend/api/src/get-current-private-user.ts +++ b/backend/api/src/get-current-private-user.ts @@ -1,27 +1,19 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { APIError, APIHandler } from './helpers/endpoint' -import { PrivateUser } from 'common/user' -import { Row } from 'common/supabase/utils' -import { tryCatch } from 'common/util/try-catch' +import {Row} from 'common/supabase/utils' +import {PrivateUser} from 'common/user' +import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient} from 'shared/supabase/init' -export const getCurrentPrivateUser: APIHandler<'me/private'> = async ( - _, - auth -) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const getCurrentPrivateUser: APIHandler<'me/private'> = async (_, auth) => { const pg = createSupabaseDirectClient() - const { data, error } = await tryCatch( - pg.oneOrNone>( - 'select * from private_users where id = $1', - [auth.uid] - ) + const {data, error} = await tryCatch( + pg.oneOrNone>('select * from private_users where id = $1', [auth.uid]), ) if (error) { - throw new APIError( - 500, - 'Error fetching private user data: ' + error.message - ) + throw new APIError(500, 'Error fetching private user data: ' + error.message) } if (!data) { diff --git a/backend/api/src/get-events.ts b/backend/api/src/get-events.ts index 31f4c720..326f1c6a 100644 --- a/backend/api/src/get-events.ts +++ b/backend/api/src/get-events.ts @@ -23,51 +23,53 @@ export const getEvents: APIHandler<'get-events'> = async () => { FROM events WHERE is_public = true AND status = 'active' - ORDER BY event_start_time` + ORDER BY event_start_time`, ) // Get participants for each event - const eventIds = events.map(e => e.id) - const participants = eventIds.length > 0 - ? await pg.manyOrNone<{ - event_id: string - user_id: string - status: 'going' | 'maybe' | 'not_going' - }>( - `SELECT event_id, user_id, status + const eventIds = events.map((e) => e.id) + const participants = + eventIds.length > 0 + ? await pg.manyOrNone<{ + event_id: string + user_id: string + status: 'going' | 'maybe' | 'not_going' + }>( + `SELECT event_id, user_id, status FROM events_participants WHERE event_id = ANY ($1)`, - [eventIds] - ) - : [] + [eventIds], + ) + : [] // Get creator info for each event - const creatorIds = [...new Set(events.map(e => e.creator_id))] - const creators = creatorIds.length > 0 - ? await pg.manyOrNone<{ - id: string - name: string - username: string - avatar_url: string | null - }>( - `SELECT id, name, username, data ->> 'avatarUrl' as avatar_url + const creatorIds = [...new Set(events.map((e) => e.creator_id))] + const creators = + creatorIds.length > 0 + ? await pg.manyOrNone<{ + id: string + name: string + username: string + avatar_url: string | null + }>( + `SELECT id, name, username, data ->> 'avatarUrl' as avatar_url FROM users WHERE id = ANY ($1)`, - [creatorIds] - ) - : [] + [creatorIds], + ) + : [] const now = new Date() - const eventsWithDetails = events.map(event => ({ + const eventsWithDetails = events.map((event) => ({ ...event, participants: participants - .filter(p => p.event_id === event.id && p.status === 'going') - .map(p => p.user_id), + .filter((p) => p.event_id === event.id && p.status === 'going') + .map((p) => p.user_id), maybe: participants - .filter(p => p.event_id === event.id && p.status === 'maybe') - .map(p => p.user_id), - creator: creators.find(c => c.id === event.creator_id), + .filter((p) => p.event_id === event.id && p.status === 'maybe') + .map((p) => p.user_id), + creator: creators.find((c) => c.id === event.creator_id), })) const upcoming: typeof eventsWithDetails = [] diff --git a/backend/api/src/get-hidden-profiles.ts b/backend/api/src/get-hidden-profiles.ts index 7ec67c0b..497b4f74 100644 --- a/backend/api/src/get-hidden-profiles.ts +++ b/backend/api/src/get-hidden-profiles.ts @@ -3,16 +3,16 @@ import {createSupabaseDirectClient} from 'shared/supabase/init' export const getHiddenProfiles: APIHandler<'get-hidden-profiles'> = async ( {limit = 100, offset = 0}, - auth + auth, ) => { const pg = createSupabaseDirectClient() // Count total hidden for pagination info - const countRes = await pg.one<{ count: string }>( + const countRes = await pg.one<{count: string}>( `select count(*)::text as count from hidden_profiles where hider_user_id = $1`, - [auth.uid] + [auth.uid], ) const count = Number(countRes.count) || 0 @@ -31,7 +31,7 @@ export const getHiddenProfiles: APIHandler<'get-hidden-profiles'> = async ( username: r.username as string, avatarUrl: r.avatarUrl as string | null | undefined, createdTime: r.createdTime as string | undefined, - }) + }), ) return {status: 'success', hidden: rows, count} diff --git a/backend/api/src/get-likes-and-ships.ts b/backend/api/src/get-likes-and-ships.ts index 4d257125..55cd8aeb 100644 --- a/backend/api/src/get-likes-and-ships.ts +++ b/backend/api/src/get-likes-and-ships.ts @@ -1,10 +1,8 @@ -import { type APIHandler } from 'api/helpers/endpoint' -import { createSupabaseDirectClient } from 'shared/supabase/init' +import {type APIHandler} from 'api/helpers/endpoint' +import {createSupabaseDirectClient} from 'shared/supabase/init' -export const getLikesAndShips: APIHandler<'get-likes-and-ships'> = async ( - props -) => { - const { userId } = props +export const getLikesAndShips: APIHandler<'get-likes-and-ships'> = async (props) => { + const {userId} = props return { status: 'success', @@ -34,7 +32,7 @@ export const getLikesAndShipsMain = async (userId: string) => { (r) => ({ user_id: r.target_id, created_time: new Date(r.created_time).getTime(), - }) + }), ) const likesReceived = await pg.map<{ @@ -56,7 +54,7 @@ export const getLikesAndShipsMain = async (userId: string) => { (r) => ({ user_id: r.creator_id, created_time: new Date(r.created_time).getTime(), - }) + }), ) const ships = await pg.map<{ @@ -95,7 +93,7 @@ export const getLikesAndShipsMain = async (userId: string) => { (r) => ({ ...r, created_time: new Date(r.created_time).getTime(), - }) + }), ) return { diff --git a/backend/api/src/get-me.ts b/backend/api/src/get-me.ts index ab58e57d..30fe1187 100644 --- a/backend/api/src/get-me.ts +++ b/backend/api/src/get-me.ts @@ -1,6 +1,7 @@ -import { type APIHandler } from './helpers/endpoint' -import { getUser } from 'api/get-user' +import {getUser} from 'api/get-user' + +import {type APIHandler} from './helpers/endpoint' export const getMe: APIHandler<'me'> = async (_, auth) => { - return getUser({ id: auth.uid }) + return getUser({id: auth.uid}) } diff --git a/backend/api/src/get-messages-count.ts b/backend/api/src/get-messages-count.ts index 9dce65b7..e27f1daa 100644 --- a/backend/api/src/get-messages-count.ts +++ b/backend/api/src/get-messages-count.ts @@ -1,5 +1,6 @@ +import {createSupabaseDirectClient} from 'shared/supabase/init' + import {APIHandler} from './helpers/endpoint' -import {createSupabaseDirectClient} from "shared/supabase/init"; export const getMessagesCount: APIHandler<'get-messages-count'> = async (_, _auth) => { const pg = createSupabaseDirectClient() @@ -8,10 +9,10 @@ export const getMessagesCount: APIHandler<'get-messages-count'> = async (_, _aut SELECT COUNT(*) AS count FROM private_user_messages; `, - [] - ); - const count = Number(result.count); - console.debug('private_user_messages count:', count); + [], + ) + const count = Number(result.count) + console.debug('private_user_messages count:', count) return { count: count, } diff --git a/backend/api/src/get-notifications.ts b/backend/api/src/get-notifications.ts index c2c84ce5..9629aae6 100644 --- a/backend/api/src/get-notifications.ts +++ b/backend/api/src/get-notifications.ts @@ -1,12 +1,9 @@ -import {createSupabaseDirectClient} from 'shared/supabase/init' import {APIHandler} from 'api/helpers/endpoint' import {Notification} from 'common/notifications' +import {createSupabaseDirectClient} from 'shared/supabase/init' -export const getNotifications: APIHandler<'get-notifications'> = async ( - props, - auth -) => { - const { limit, after } = props +export const getNotifications: APIHandler<'get-notifications'> = async (props, auth) => { + const {limit, after} = props const pg = createSupabaseDirectClient() const query = ` select case @@ -47,6 +44,6 @@ export const getNotifications: APIHandler<'get-notifications'> = async ( return await pg.map( query, [auth.uid, limit, after], - (row) => row.notification_data as Notification + (row) => row.notification_data as Notification, ) } diff --git a/backend/api/src/get-options.ts b/backend/api/src/get-options.ts index fff028de..0b39e7a1 100644 --- a/backend/api/src/get-options.ts +++ b/backend/api/src/get-options.ts @@ -1,20 +1,17 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {OPTION_TABLES} from 'common/profiles/constants' +import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {log} from 'shared/utils' -import {tryCatch} from 'common/util/try-catch' -import {OPTION_TABLES} from "common/profiles/constants"; -export const getOptions: APIHandler<'get-options'> = async ( - {table}, - _auth -) => { +export const getOptions: APIHandler<'get-options'> = async ({table}, _auth) => { if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table') const pg = createSupabaseDirectClient() const result = await tryCatch( - pg.manyOrNone<{ name: string }>(`SELECT interests.name - FROM interests`) + pg.manyOrNone<{name: string}>(`SELECT interests.name + FROM interests`), ) if (result.error) { @@ -22,7 +19,6 @@ export const getOptions: APIHandler<'get-options'> = async ( throw new APIError(500, 'Error getting profile options') } - const names = result.data.map(row => row.name) + const names = result.data.map((row) => row.name) return {names} } - diff --git a/backend/api/src/get-private-messages.ts b/backend/api/src/get-private-messages.ts index b6de9949..604b8686 100644 --- a/backend/api/src/get-private-messages.ts +++ b/backend/api/src/get-private-messages.ts @@ -1,13 +1,12 @@ -import {createSupabaseDirectClient} from 'shared/supabase/init' -import {APIError, APIHandler} from './helpers/endpoint' -import {PrivateMessageChannel,} from 'common/supabase/private-messages' +import {PrivateMessageChannel} from 'common/supabase/private-messages' +import {tryCatch} from 'common/util/try-catch' import {groupBy, mapValues} from 'lodash' -import {convertPrivateChatMessage} from "shared/supabase/messages"; -import {tryCatch} from "common/util/try-catch"; +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {convertPrivateChatMessage} from 'shared/supabase/messages' -export const getChannelMemberships: APIHandler< - 'get-channel-memberships' -> = async (props, auth) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const getChannelMemberships: APIHandler<'get-channel-memberships'> = async (props, auth) => { const pg = createSupabaseDirectClient() const {channelId, lastUpdatedTime, createdTime, limit} = props @@ -29,7 +28,7 @@ export const getChannelMemberships: APIHandler< limit $3 `, [auth.uid, channelId, limit], - convertRow + convertRow, ) } else { channels = await pg.map( @@ -59,11 +58,10 @@ export const getChannelMemberships: APIHandler< limit $3 `, [auth.uid, createdTime ?? null, limit, lastUpdatedTime ?? null], - convertRow + convertRow, ) } - if (!channels || channels.length === 0) - return {channels: [], memberIdsByChannelId: {}} + if (!channels || channels.length === 0) return {channels: [], memberIdsByChannelId: {}} const channelIds = channels.map((c) => c.channel_id) const members = await pg.map( @@ -77,12 +75,11 @@ export const getChannelMemberships: APIHandler< (r) => ({ channel_id: r.channel_id as number, user_id: r.user_id as string, - }) + }), ) - const memberIdsByChannelId = mapValues( - groupBy(members, 'channel_id'), - (members) => members.map((m) => m.user_id) + const memberIdsByChannelId = mapValues(groupBy(members, 'channel_id'), (members) => + members.map((m) => m.user_id), ) return { @@ -93,23 +90,24 @@ export const getChannelMemberships: APIHandler< export const getChannelMessagesEndpoint: APIHandler<'get-channel-messages'> = async ( props, - auth + auth, ) => { const userId = auth.uid return await getChannelMessages({...props, userId}) } export async function getChannelMessages(props: { - channelId: number; - limit?: number; - id?: number | undefined; - userId: string; + channelId: number + limit?: number + id?: number | undefined + userId: string }) { // console.log('initial message request', props) const {channelId, limit, id, userId} = props const pg = createSupabaseDirectClient() - const {data, error} = await tryCatch(pg.map( - `select *, created_time as created_time_ts + const {data, error} = await tryCatch( + pg.map( + `select *, created_time as created_time_ts from private_user_messages where channel_id = $1 and exists (select 1 @@ -121,9 +119,10 @@ export async function getChannelMessages(props: { order by created_time desc ${limit ? 'limit $3' : ''} `, - [channelId, userId, limit, id], - convertPrivateChatMessage - )) + [channelId, userId, limit, id], + convertPrivateChatMessage, + ), + ) if (error) { console.error(error) throw new APIError(401, 'Error getting messages') @@ -132,9 +131,7 @@ export async function getChannelMessages(props: { return data } -export const getLastSeenChannelTime: APIHandler< - 'get-channel-seen-time' -> = async (props, auth) => { +export const getLastSeenChannelTime: APIHandler<'get-channel-seen-time'> = async (props, auth) => { const pg = createSupabaseDirectClient() const {channelIds} = props const unseens = await pg.map( @@ -145,20 +142,18 @@ export const getLastSeenChannelTime: APIHandler< order by channel_id, created_time desc `, [channelIds, auth.uid], - (r) => [r.channel_id as number, r.created_time as string] + (r) => [r.channel_id as number, r.created_time as string], ) return unseens as [number, string][] } -export const setChannelLastSeenTime: APIHandler< - 'set-channel-seen-time' -> = async (props, auth) => { +export const setChannelLastSeenTime: APIHandler<'set-channel-seen-time'> = async (props, auth) => { const pg = createSupabaseDirectClient() const {channelId} = props await pg.none( `insert into private_user_seen_message_channels (user_id, channel_id) values ($1, $2) `, - [auth.uid, channelId] + [auth.uid, channelId], ) } diff --git a/backend/api/src/get-profile-answers.ts b/backend/api/src/get-profile-answers.ts index 7f7dcef9..1eb8ef4d 100644 --- a/backend/api/src/get-profile-answers.ts +++ b/backend/api/src/get-profile-answers.ts @@ -1,12 +1,9 @@ -import { type APIHandler } from 'api/helpers/endpoint' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { Row } from 'common/supabase/utils' +import {type APIHandler} from 'api/helpers/endpoint' +import {Row} from 'common/supabase/utils' +import {createSupabaseDirectClient} from 'shared/supabase/init' -export const getProfileAnswers: APIHandler<'get-profile-answers'> = async ( - props, - _auth -) => { - const { userId } = props +export const getProfileAnswers: APIHandler<'get-profile-answers'> = async (props, _auth) => { + const {userId} = props const pg = createSupabaseDirectClient() const answers = await pg.manyOrNone>( @@ -15,7 +12,7 @@ export const getProfileAnswers: APIHandler<'get-profile-answers'> = async ( creator_id = $1 order by created_time desc `, - [userId] + [userId], ) return { diff --git a/backend/api/src/get-profiles.ts b/backend/api/src/get-profiles.ts index f33a03cf..af990e27 100644 --- a/backend/api/src/get-profiles.ts +++ b/backend/api/src/get-profiles.ts @@ -1,60 +1,68 @@ import {type APIHandler} from 'api/helpers/endpoint' +import {OptionTableKey} from 'common/profiles/constants' +import {compact} from 'lodash' import {convertRow} from 'shared/profiles/supabase' import {createSupabaseDirectClient, pgp} from 'shared/supabase/init' -import {from, join, leftJoin, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder' -import {compact} from "lodash"; -import {OptionTableKey} from "common/profiles/constants"; +import { + from, + join, + leftJoin, + limit, + orderBy, + renderSql, + select, + where, +} from 'shared/supabase/sql-builder' export type profileQueryType = { - limit?: number | undefined, - after?: string | undefined, - userId?: string | undefined, - name?: string | undefined, - genders?: string[] | undefined, - education_levels?: string[] | undefined, - pref_gender?: string[] | undefined, - pref_age_min?: number | undefined, - pref_age_max?: number | undefined, - drinks_min?: number | undefined, - drinks_max?: number | undefined, - big5_openness_min?: number | undefined, - big5_openness_max?: number | undefined, - big5_conscientiousness_min?: number | undefined, - big5_conscientiousness_max?: number | undefined, - big5_extraversion_min?: number | undefined, - big5_extraversion_max?: number | undefined, - big5_agreeableness_min?: number | undefined, - big5_agreeableness_max?: number | undefined, - big5_neuroticism_min?: number | undefined, - big5_neuroticism_max?: number | undefined, - pref_relation_styles?: string[] | undefined, - pref_romantic_styles?: string[] | undefined, - diet?: string[] | undefined, - political_beliefs?: string[] | undefined, - mbti?: string[] | undefined, - relationship_status?: string[] | undefined, - languages?: string[] | undefined, - religion?: string[] | undefined, - wants_kids_strength?: number | undefined, - has_kids?: number | undefined, - is_smoker?: boolean | undefined, - shortBio?: boolean | undefined, - geodbCityIds?: string[] | undefined, - lat?: number | undefined, - lon?: number | undefined, - radius?: number | undefined, - compatibleWithUserId?: string | undefined, - skipId?: string | undefined, - orderBy?: string | undefined, - lastModificationWithin?: string | undefined, - locale?: string | undefined, + limit?: number | undefined + after?: string | undefined + userId?: string | undefined + name?: string | undefined + genders?: string[] | undefined + education_levels?: string[] | undefined + pref_gender?: string[] | undefined + pref_age_min?: number | undefined + pref_age_max?: number | undefined + drinks_min?: number | undefined + drinks_max?: number | undefined + big5_openness_min?: number | undefined + big5_openness_max?: number | undefined + big5_conscientiousness_min?: number | undefined + big5_conscientiousness_max?: number | undefined + big5_extraversion_min?: number | undefined + big5_extraversion_max?: number | undefined + big5_agreeableness_min?: number | undefined + big5_agreeableness_max?: number | undefined + big5_neuroticism_min?: number | undefined + big5_neuroticism_max?: number | undefined + pref_relation_styles?: string[] | undefined + pref_romantic_styles?: string[] | undefined + diet?: string[] | undefined + political_beliefs?: string[] | undefined + mbti?: string[] | undefined + relationship_status?: string[] | undefined + languages?: string[] | undefined + religion?: string[] | undefined + wants_kids_strength?: number | undefined + has_kids?: number | undefined + is_smoker?: boolean | undefined + shortBio?: boolean | undefined + geodbCityIds?: string[] | undefined + lat?: number | undefined + lon?: number | undefined + radius?: number | undefined + compatibleWithUserId?: string | undefined + skipId?: string | undefined + orderBy?: string | undefined + lastModificationWithin?: string | undefined + locale?: string | undefined } & { [K in OptionTableKey]?: string[] | undefined } // const userActivityColumns = ['last_online_time'] - export const loadProfiles = async (props: profileQueryType) => { const pg = createSupabaseDirectClient() console.debug('loadProfiles', props) @@ -108,7 +116,12 @@ export const loadProfiles = async (props: profileQueryType) => { const filterLocation = lat && lon && radius - const keywords = name ? name.split(",").map(q => q.trim()).filter(Boolean) : [] + const keywords = name + ? name + .split(',') + .map((q) => q.trim()) + .filter(Boolean) + : [] // console.debug('keywords:', keywords) if (orderByParam === 'compatibility_score' && !compatibleWithUserId) { @@ -116,11 +129,12 @@ export const loadProfiles = async (props: profileQueryType) => { throw Error('Incompatible with user ID') } - const tablePrefix = orderByParam === 'compatibility_score' - ? 'compatibility_scores' - : orderByParam === 'last_online_time' - ? 'user_activity' - : 'profiles' + const tablePrefix = + orderByParam === 'compatibility_score' + ? 'compatibility_scores' + : orderByParam === 'last_online_time' + ? 'user_activity' + : 'profiles' const userActivityJoin = 'user_activity on user_activity.user_id = profiles.user_id' @@ -144,7 +158,10 @@ export const loadProfiles = async (props: profileQueryType) => { const causesJoin = getManyToManyJoin('causes') const workJoin = getManyToManyJoin('work') - const compatibilityScoreJoin = pgp.as.format(`compatibility_scores cs on (cs.user_id_1 = LEAST(profiles.user_id, $(compatibleWithUserId)) and cs.user_id_2 = GREATEST(profiles.user_id, $(compatibleWithUserId)))`, {compatibleWithUserId}) + const compatibilityScoreJoin = pgp.as.format( + `compatibility_scores cs on (cs.user_id_1 = LEAST(profiles.user_id, $(compatibleWithUserId)) and cs.user_id_2 = GREATEST(profiles.user_id, $(compatibleWithUserId)))`, + {compatibleWithUserId}, + ) const joins = [ orderByParam === 'last_online_time' && leftJoin(userActivityJoin), @@ -154,7 +171,8 @@ export const loadProfiles = async (props: profileQueryType) => { joinWork && leftJoin(workJoin), ] - const _orderBy = orderByParam === 'compatibility_score' ? 'cs.score' : `${tablePrefix}.${orderByParam}` + const _orderBy = + orderByParam === 'compatibility_score' ? 'cs.score' : `${tablePrefix}.${orderByParam}` const afterFilter = renderSql( select(_orderBy), from('profiles'), @@ -189,125 +207,126 @@ export const loadProfiles = async (props: profileQueryType) => { )` } - const filters = [ where('looking_for_matches = true'), where(`profiles.disabled != true`), // where(`pinned_url is not null and pinned_url != ''`), - where( - `(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)` - ), + where(`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`), where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`), - ...keywords.map(word => where( - `lower(users.name) ilike '%' || lower($(word)) || '%' + ...keywords.map((word) => + where( + `lower(users.name) ilike '%' || lower($(word)) || '%' or lower(search_text) ilike '%' || lower($(word)) || '%' or search_tsv @@ phraseto_tsquery('english', $(word)) OR ${getOptionClauseKeyword('interests')} OR ${getOptionClauseKeyword('causes')} OR ${getOptionClauseKeyword('work')} `, - {word, locale} - )), + {word, locale}, + ), + ), genders?.length && where(`gender = ANY($(genders))`, {genders}), - education_levels?.length && where(`education_level = ANY($(education_levels))`, {education_levels}), + education_levels?.length && + where(`education_level = ANY($(education_levels))`, {education_levels}), mbti?.length && where(`mbti = ANY($(mbti))`, {mbti}), pref_gender?.length && - where(`pref_gender is NULL or pref_gender = '{}' OR pref_gender && $(pref_gender)`, {pref_gender}), + where(`pref_gender is NULL or pref_gender = '{}' OR pref_gender && $(pref_gender)`, { + pref_gender, + }), - pref_age_min && - where(`age >= $(pref_age_min) or age is null`, {pref_age_min}), + pref_age_min && where(`age >= $(pref_age_min) or age is null`, {pref_age_min}), - pref_age_max && - where(`age <= $(pref_age_max) or age is null`, {pref_age_max}), + pref_age_max && where(`age <= $(pref_age_max) or age is null`, {pref_age_max}), drinks_min && - where(`drinks_per_month >= $(drinks_min) or drinks_per_month is null`, {drinks_min}), + where(`drinks_per_month >= $(drinks_min) or drinks_per_month is null`, {drinks_min}), drinks_max && - where(`drinks_per_month <= $(drinks_max) or drinks_per_month is null`, {drinks_max}), + where(`drinks_per_month <= $(drinks_max) or drinks_per_month is null`, {drinks_max}), big5_openness_min && - where(`big5_openness >= $(big5_openness_min) or big5_openness is null`, {big5_openness_min}), + where(`big5_openness >= $(big5_openness_min) or big5_openness is null`, {big5_openness_min}), big5_openness_max && - where(`big5_openness <= $(big5_openness_max) or big5_openness is null`, {big5_openness_max}), + where(`big5_openness <= $(big5_openness_max) or big5_openness is null`, {big5_openness_max}), big5_conscientiousness_min && - where( - `big5_conscientiousness >= $(big5_conscientiousness_min) or big5_conscientiousness is null`, - {big5_conscientiousness_min} - ), + where( + `big5_conscientiousness >= $(big5_conscientiousness_min) or big5_conscientiousness is null`, + {big5_conscientiousness_min}, + ), big5_conscientiousness_max && - where( - `big5_conscientiousness <= $(big5_conscientiousness_max) or big5_conscientiousness is null`, - {big5_conscientiousness_max} - ), + where( + `big5_conscientiousness <= $(big5_conscientiousness_max) or big5_conscientiousness is null`, + {big5_conscientiousness_max}, + ), big5_extraversion_min && - where(`big5_extraversion >= $(big5_extraversion_min) or big5_extraversion is null`, {big5_extraversion_min}), + where(`big5_extraversion >= $(big5_extraversion_min) or big5_extraversion is null`, { + big5_extraversion_min, + }), big5_extraversion_max && - where(`big5_extraversion <= $(big5_extraversion_max) or big5_extraversion is null`, {big5_extraversion_max}), + where(`big5_extraversion <= $(big5_extraversion_max) or big5_extraversion is null`, { + big5_extraversion_max, + }), big5_agreeableness_min && - where(`big5_agreeableness >= $(big5_agreeableness_min) or big5_agreeableness is null`, {big5_agreeableness_min}), + where(`big5_agreeableness >= $(big5_agreeableness_min) or big5_agreeableness is null`, { + big5_agreeableness_min, + }), big5_agreeableness_max && - where(`big5_agreeableness <= $(big5_agreeableness_max) or big5_agreeableness is null`, {big5_agreeableness_max}), + where(`big5_agreeableness <= $(big5_agreeableness_max) or big5_agreeableness is null`, { + big5_agreeableness_max, + }), big5_neuroticism_min && - where(`big5_neuroticism >= $(big5_neuroticism_min) or big5_neuroticism is null`, {big5_neuroticism_min}), + where(`big5_neuroticism >= $(big5_neuroticism_min) or big5_neuroticism is null`, { + big5_neuroticism_min, + }), big5_neuroticism_max && - where(`big5_neuroticism <= $(big5_neuroticism_max) or big5_neuroticism is null`, {big5_neuroticism_max}), + where(`big5_neuroticism <= $(big5_neuroticism_max) or big5_neuroticism is null`, { + big5_neuroticism_max, + }), pref_relation_styles?.length && - where( - `pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`, - {pref_relation_styles} - ), + where( + `pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`, + {pref_relation_styles}, + ), pref_romantic_styles?.length && - where( - `pref_romantic_styles IS NULL OR pref_romantic_styles = '{}' OR pref_romantic_styles && $(pref_romantic_styles)`, - {pref_romantic_styles} - ), + where( + `pref_romantic_styles IS NULL OR pref_romantic_styles = '{}' OR pref_romantic_styles && $(pref_romantic_styles)`, + {pref_romantic_styles}, + ), - diet?.length && - where( - `diet IS NULL OR diet = '{}' OR diet && $(diet)`, - {diet} - ), + diet?.length && where(`diet IS NULL OR diet = '{}' OR diet && $(diet)`, {diet}), political_beliefs?.length && - where( - `political_beliefs IS NULL OR political_beliefs = '{}' OR political_beliefs && $(political_beliefs)`, - {political_beliefs} - ), + where( + `political_beliefs IS NULL OR political_beliefs = '{}' OR political_beliefs && $(political_beliefs)`, + {political_beliefs}, + ), relationship_status?.length && - where( - `relationship_status IS NULL OR relationship_status = '{}' OR relationship_status && $(relationship_status)`, - {relationship_status} - ), + where( + `relationship_status IS NULL OR relationship_status = '{}' OR relationship_status && $(relationship_status)`, + {relationship_status}, + ), - languages?.length && - where( - `languages && $(languages)`, - {languages} - ), + languages?.length && where(`languages && $(languages)`, {languages}), religion?.length && - where( - `religion IS NULL OR religion = '{}' OR religion && $(religion)`, - {religion} - ), + where(`religion IS NULL OR religion = '{}' OR religion && $(religion)`, {religion}), interests?.length && where(getManyToManyClause('interests'), {values: interests.map(Number)}), @@ -316,27 +335,31 @@ export const loadProfiles = async (props: profileQueryType) => { work?.length && where(getManyToManyClause('work'), {values: work.map(Number)}), !!wants_kids_strength && - wants_kids_strength !== -1 && - where( - 'wants_kids_strength = -1 OR wants_kids_strength IS NULL OR ' + (wants_kids_strength >= 2 ? `wants_kids_strength >= $(wants_kids_strength)` : `wants_kids_strength <= $(wants_kids_strength)`), - {wants_kids_strength} - ), + wants_kids_strength !== -1 && + where( + 'wants_kids_strength = -1 OR wants_kids_strength IS NULL OR ' + + (wants_kids_strength >= 2 + ? `wants_kids_strength >= $(wants_kids_strength)` + : `wants_kids_strength <= $(wants_kids_strength)`), + {wants_kids_strength}, + ), has_kids === 0 && where(`has_kids IS NULL OR has_kids = 0`), has_kids && has_kids > 0 && where(`has_kids > 0`), - is_smoker !== undefined && ( + is_smoker !== undefined && where( (is_smoker ? '' : 'is_smoker IS NULL OR ') + // smokers are rare, so we don't include the people who didn't answer if we're looking for smokers - `is_smoker = $(is_smoker)`, {is_smoker} - ) - ), + `is_smoker = $(is_smoker)`, + {is_smoker}, + ), - geodbCityIds?.length && - where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}), + geodbCityIds?.length && where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}), // miles par degree of lat: earth's radius (3950 miles) * pi / 180 = 69.0 - filterLocation && where(` + filterLocation && + where( + ` city_latitude BETWEEN $(target_lat) - ($(radius) / 69.0) AND $(target_lat) + ($(radius) / 69.0) AND city_longitude BETWEEN $(target_lon) - ($(radius) / (69.0 * COS(RADIANS($(target_lat))))) @@ -345,27 +368,36 @@ export const loadProfiles = async (props: profileQueryType) => { POWER(city_latitude - $(target_lat), 2) + POWER((city_longitude - $(target_lon)) * COS(RADIANS($(target_lat))), 2) ) <= $(radius) / 69.0 - `, {target_lat: lat, target_lon: lon, radius}), + `, + {target_lat: lat, target_lon: lon, radius}, + ), skipId && where(`profiles.user_id != $(skipId)`, {skipId}), - !shortBio && where( - `bio_length >= ${100} + !shortBio && + where( + `bio_length >= ${100} OR array_length(profile_work.work, 1) > 0 OR array_length(profile_interests.interests, 1) > 0 OR occupation_title IS NOT NULL - `), + `, + ), - lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}), + lastModificationWithin && + where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, { + lastModificationWithin, + }), // Exclude profiles that the requester has chosen to hide - userId && where( - `NOT EXISTS ( + userId && + where( + `NOT EXISTS ( SELECT 1 FROM hidden_profiles hp WHERE hp.hider_user_id = $(userId) AND hp.hidden_user_id = profiles.user_id - )`, {userId} - ), + )`, + {userId}, + ), ] let selectCols = 'profiles.*, users.name, users.username, users.data as user' @@ -393,11 +425,7 @@ export const loadProfiles = async (props: profileQueryType) => { // console.debug('profiles:', profiles) - const countQuery = renderSql( - select(`count(*) as count`), - ...tableSelection, - ...filters, - ) + const countQuery = renderSql(select(`count(*) as count`), ...tableSelection, ...filters) const count = await pg.one(countQuery, [], (r) => Number(r.count)) diff --git a/backend/api/src/get-supabase-token.ts b/backend/api/src/get-supabase-token.ts index 59df7fe7..2605277e 100644 --- a/backend/api/src/get-supabase-token.ts +++ b/backend/api/src/get-supabase-token.ts @@ -1,11 +1,9 @@ +import {ENV_CONFIG} from 'common/envs/constants' import {sign} from 'jsonwebtoken' -import {APIError, APIHandler} from './helpers/endpoint' -import {ENV_CONFIG} from "common/envs/constants"; -export const getSupabaseToken: APIHandler<'get-supabase-token'> = async ( - _, - auth -) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const getSupabaseToken: APIHandler<'get-supabase-token'> = async (_, auth) => { const jwtSecret = process.env.SUPABASE_JWT_SECRET if (jwtSecret == null) { throw new APIError(500, "No SUPABASE_JWT_SECRET; couldn't sign token.") diff --git a/backend/api/src/get-user-data-export.ts b/backend/api/src/get-user-data-export.ts index 8df3df34..82dfba0c 100644 --- a/backend/api/src/get-user-data-export.ts +++ b/backend/api/src/get-user-data-export.ts @@ -1,22 +1,20 @@ -import {APIHandler} from './helpers/endpoint' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {Row} from 'common/supabase/utils' -import {getLikesAndShipsMain} from './get-likes-and-ships' import {parseJsonContentToText} from 'common/util/parse' -import {parseMessageObject} from "shared/supabase/messages"; +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {parseMessageObject} from 'shared/supabase/messages' + +import {getLikesAndShipsMain} from './get-likes-and-ships' +import {APIHandler} from './helpers/endpoint' export const getUserDataExport: APIHandler<'me/data'> = async (_, auth) => { const userId = auth.uid const pg = createSupabaseDirectClient() - const user = await pg.oneOrNone>( - 'select * from users where id = $1', - [userId] - ) + const user = await pg.oneOrNone>('select * from users where id = $1', [userId]) const privateUser = await pg.oneOrNone>( 'select * from private_users where id = $1', - [userId] + [userId], ) const profile = await pg.oneOrNone( @@ -47,7 +45,7 @@ export const getUserDataExport: APIHandler<'me/data'> = async (_, auth) => { group by pw.profile_id) as profile_work on profile_work.profile_id = profiles.id where profiles.user_id = $1 `, - [userId] + [userId], ) if (profile.bio) { @@ -68,17 +66,17 @@ export const getUserDataExport: APIHandler<'me/data'> = async (_, auth) => { where a.creator_id = $1 order by a.created_time desc `, - [userId] + [userId], ) const userActivity = await pg.oneOrNone>( 'select * from user_activity where user_id = $1', - [userId] + [userId], ) const searchBookmarks = await pg.manyOrNone>( 'select * from bookmarked_searches where creator_id = $1 order by id desc', - [userId] + [userId], ) const hiddenProfiles = await pg.manyOrNone( @@ -87,41 +85,36 @@ export const getUserDataExport: APIHandler<'me/data'> = async (_, auth) => { join users u on u.id = hp.hidden_user_id where hp.hider_user_id = $1 order by hp.id desc`, - [userId] + [userId], ) const messageChannelMemberships = await pg.manyOrNone< Row<'private_user_message_channel_members'> - >( - 'select * from private_user_message_channel_members where user_id = $1', - [userId] - ) + >('select * from private_user_message_channel_members where user_id = $1', [userId]) - const channelIds = Array.from( - new Set(messageChannelMemberships.map((m) => m.channel_id)) - ) + const channelIds = Array.from(new Set(messageChannelMemberships.map((m) => m.channel_id))) const messageChannels = channelIds.length ? await pg.manyOrNone>( - 'select * from private_user_message_channels where id = any($1)', - [channelIds] - ) + 'select * from private_user_message_channels where id = any($1)', + [channelIds], + ) : [] const messages = channelIds.length ? await pg.manyOrNone>( - `select * + `select * from private_user_messages where channel_id = any ($1) order by created_time`, - [channelIds] - ) + [channelIds], + ) : [] for (const message of messages) parseMessageObject(message) const membershipsWithUsernames = channelIds.length ? await pg.manyOrNone( - ` + ` select m.*, u.username from private_user_message_channel_members m @@ -129,8 +122,8 @@ export const getUserDataExport: APIHandler<'me/data'> = async (_, auth) => { where m.channel_id = any ($1) and m.user_id != $2 `, - [channelIds, userId] - ) + [channelIds, userId], + ) : [] const endorsements = await getLikesAndShipsMain(userId) @@ -153,17 +146,17 @@ export const getUserDataExport: APIHandler<'me/data'> = async (_, auth) => { where r.user_id = $1 order by v.created_time desc `, - [userId] + [userId], ) const reports = await pg.manyOrNone>( 'select * from reports where user_id = $1 order by created_time desc nulls last', - [userId] + [userId], ) const contactMessages = await pg.manyOrNone>( 'select * from contact where user_id = $1 order by created_time desc nulls last', - [userId] + [userId], ) return { @@ -185,4 +178,3 @@ export const getUserDataExport: APIHandler<'me/data'> = async (_, auth) => { accountMetadata, } } - diff --git a/backend/api/src/get-user.ts b/backend/api/src/get-user.ts index a550268c..48a4fb3e 100644 --- a/backend/api/src/get-user.ts +++ b/backend/api/src/get-user.ts @@ -1,15 +1,15 @@ -import { toUserAPIResponse } from 'common/api/user-types' -import { convertUser } from 'common/supabase/users' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { APIError } from 'common/api/utils' +import {toUserAPIResponse} from 'common/api/user-types' +import {APIError} from 'common/api/utils' +import {convertUser} from 'common/supabase/users' +import {createSupabaseDirectClient} from 'shared/supabase/init' -export const getUser = async (props: { id: string } | { username: string }) => { +export const getUser = async (props: {id: string} | {username: string}) => { const pg = createSupabaseDirectClient() const user = await pg.oneOrNone( `select * from users where ${'id' in props ? 'id' : 'username'} = $1`, ['id' in props ? props.id : props.username], - (r) => (r ? convertUser(r) : null) + (r) => (r ? convertUser(r) : null), ) if (!user) throw new APIError(404, 'User not found') diff --git a/backend/api/src/has-free-like.ts b/backend/api/src/has-free-like.ts index df7158c7..fd7bebb1 100644 --- a/backend/api/src/has-free-like.ts +++ b/backend/api/src/has-free-like.ts @@ -1,10 +1,7 @@ -import { type APIHandler } from 'api/helpers/endpoint' -import { createSupabaseDirectClient } from 'shared/supabase/init' +import {type APIHandler} from 'api/helpers/endpoint' +import {createSupabaseDirectClient} from 'shared/supabase/init' -export const hasFreeLike: APIHandler<'has-free-like'> = async ( - _props, - auth -) => { +export const hasFreeLike: APIHandler<'has-free-like'> = async (_props, auth) => { return { status: 'success', hasFreeLike: await getHasFreeLike(auth.uid), @@ -23,7 +20,7 @@ export const getHasFreeLike = async (userId: string) => { and created_time at time zone 'UTC' at time zone 'America/Los_Angeles' < ((now() at time zone 'UTC' at time zone 'America/Los_Angeles')::date + interval '1 day') limit 1 `, - [userId] + [userId], ) return !likeGivenToday } diff --git a/backend/api/src/health.ts b/backend/api/src/health.ts index 6a69db6a..46c6db63 100644 --- a/backend/api/src/health.ts +++ b/backend/api/src/health.ts @@ -1,6 +1,6 @@ -import { APIHandler } from './helpers/endpoint' import {git} from './../metadata.json' import {version as pkgVersion} from './../package.json' +import {APIHandler} from './helpers/endpoint' export const health: APIHandler<'health'> = async (_, auth) => { return { diff --git a/backend/api/src/helpers/endpoint.ts b/backend/api/src/helpers/endpoint.ts index 88ed8f61..78047cad 100644 --- a/backend/api/src/helpers/endpoint.ts +++ b/backend/api/src/helpers/endpoint.ts @@ -1,11 +1,16 @@ -import * as admin from 'firebase-admin' -import {z} from 'zod' -import {NextFunction, Request, Response} from 'express' - -import {PrivateUser} from 'common/user' +import { + API, + APIPath, + APIResponseOptionalContinue, + APISchema, + ValidatedAPIParams, +} from 'common/api/schema' import {APIError} from 'common/api/utils' -import {API, APIPath, APIResponseOptionalContinue, APISchema, ValidatedAPIParams,} from 'common/api/schema' +import {PrivateUser} from 'common/user' +import {NextFunction, Request, Response} from 'express' +import * as admin from 'firebase-admin' import {getPrivateUserByKey, log} from 'shared/utils' +import {z} from 'zod' export {APIError} from 'common/api/utils' @@ -27,10 +32,10 @@ export {APIError} from 'common/api/utils' export type AuthedUser = { uid: string - creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser }) + creds: JwtCredentials | (KeyCredentials & {privateUser: PrivateUser}) } -type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken } -type KeyCredentials = { kind: 'key'; data: string } +type JwtCredentials = {kind: 'jwt'; data: admin.auth.DecodedIdToken} +type KeyCredentials = {kind: 'key'; data: string} type Credentials = JwtCredentials | KeyCredentials // export async function verifyIdToken(payload: string): Promise { @@ -76,8 +81,8 @@ export const parseCredentials = async (req: Request): Promise => { try { return {kind: 'jwt', data: await auth.verifyIdToken(payload)} } catch (err) { - const raw = payload.split(".")[0]; - console.log("JWT header:", JSON.parse(Buffer.from(raw, "base64").toString())); + const raw = payload.split('.')[0] + console.log('JWT header:', JSON.parse(Buffer.from(raw, 'base64').toString())) // This is somewhat suspicious, so get it into the firebase console console.error('Error verifying Firebase JWT: ', err, scheme, payload) throw new APIError(500, 'Error validating token.') @@ -170,10 +175,8 @@ export const validate = (schema: T, val: unknown) => { export type APIHandler = ( props: ValidatedAPIParams, - auth: APISchema extends { authed: true } - ? AuthedUser - : AuthedUser | undefined, - req: Request + auth: APISchema extends {authed: true} ? AuthedUser : AuthedUser | undefined, + req: Request, ) => Promise> // Simple in-memory fixed-window rate limiter keyed by auth uid (or IP if unauthenticated) @@ -182,7 +185,7 @@ export type APIHandler = ( // API_RATE_LIMIT_PER_MIN_AUTHED // API_RATE_LIMIT_PER_MIN_UNAUTHED // Endpoints can be exempted by adding their name to RATE_LIMIT_EXEMPT (comma-separated) -const __rateLimitState: Map = new Map() +const __rateLimitState: Map = new Map() function getRateLimitConfig() { const authed = Number(process.env.API_RATE_LIMIT_PER_MIN_AUTHED ?? 120) @@ -228,11 +231,13 @@ function checkRateLimit(name: string, req: Request, res: Response, auth?: Authed } } -export const typedEndpoint = ( - name: N, - handler: APIHandler -) => { - const {props: propSchema, authed: authRequired, rateLimited = false, method} = API[name] as APISchema +export const typedEndpoint = (name: N, handler: APIHandler) => { + const { + props: propSchema, + authed: authRequired, + rateLimited = false, + method, + } = API[name] as APISchema return async (req: Request, res: Response, next: NextFunction) => { let authUser: AuthedUser | undefined = undefined @@ -260,16 +265,14 @@ export const typedEndpoint = ( const resultOptionalContinue = await handler( validate(propSchema, props), authUser as AuthedUser, - req + req, ) const hasContinue = resultOptionalContinue && 'continue' in resultOptionalContinue && 'result' in resultOptionalContinue - const result = hasContinue - ? resultOptionalContinue.result - : resultOptionalContinue + const result = hasContinue ? resultOptionalContinue.result : resultOptionalContinue if (!res.headersSent) { // Convert bigint to number, b/c JSON doesn't support bigint. diff --git a/backend/api/src/helpers/private-messages.ts b/backend/api/src/helpers/private-messages.ts index ddf9c1dd..7b0b7e67 100644 --- a/backend/api/src/helpers/private-messages.ts +++ b/backend/api/src/helpers/private-messages.ts @@ -1,23 +1,23 @@ -import {Json} from 'common/supabase/schema' -import {SupabaseDirectClient} from 'shared/supabase/init' -import {ChatVisibility} from 'common/chat-message' -import {User} from 'common/user' -import {first} from 'lodash' -import {log} from 'shared/monitoring/log' -import {getPrivateUser, getUser} from 'shared/utils' import {type JSONContent} from '@tiptap/core' import {APIError} from 'common/api/utils' -import {broadcast} from 'shared/websockets/server' -import {track} from 'shared/analytics' -import {sendNewMessageEmail} from 'email/functions/helpers' +import {ChatVisibility} from 'common/chat-message' +import {Json} from 'common/supabase/schema' +import {User} from 'common/user' +import {parseJsonContentToText} from 'common/util/parse' import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' -import webPush from 'web-push' -import {parseJsonContentToText} from "common/util/parse" -import {encryptMessage} from "shared/encryption" +import utc from 'dayjs/plugin/utc' +import {sendNewMessageEmail} from 'email/functions/helpers' import * as admin from 'firebase-admin' -import {TokenMessage} from "firebase-admin/lib/messaging/messaging-api"; +import {TokenMessage} from 'firebase-admin/lib/messaging/messaging-api' +import {first} from 'lodash' +import {track} from 'shared/analytics' +import {encryptMessage} from 'shared/encryption' +import {log} from 'shared/monitoring/log' +import {SupabaseDirectClient} from 'shared/supabase/init' +import {getPrivateUser, getUser} from 'shared/utils' +import {broadcast} from 'shared/websockets/server' +import webPush from 'web-push' dayjs.extend(utc) dayjs.extend(timezone) @@ -48,7 +48,7 @@ export const insertPrivateMessage = async ( channelId: number, userId: string, visibility: ChatVisibility, - pg: SupabaseDirectClient + pg: SupabaseDirectClient, ) => { const plaintext = JSON.stringify(content) const {ciphertext, iv, tag} = encryptMessage(plaintext) @@ -56,20 +56,20 @@ export const insertPrivateMessage = async ( `insert into private_user_messages (ciphertext, iv, tag, channel_id, user_id, visibility) values ($1, $2, $3, $4, $5, $6) returning created_time`, - [ciphertext, iv, tag, channelId, userId, visibility] + [ciphertext, iv, tag, channelId, userId, visibility], ) await pg.none( `update private_user_message_channels set last_updated_time = $1 where id = $2`, - [lastMessage.created_time, channelId] + [lastMessage.created_time, channelId], ) } export const addUsersToPrivateMessageChannel = async ( userIds: string[], channelId: number, - pg: SupabaseDirectClient + pg: SupabaseDirectClient, ) => { await Promise.all( userIds.map((id) => @@ -78,15 +78,15 @@ export const addUsersToPrivateMessageChannel = async ( values ($1, $2, 'member', 'proposed') on conflict do nothing `, - [channelId, id] - ) - ) + [channelId, id], + ), + ), ) await pg.none( `update private_user_message_channels set last_updated_time = now() where id = $1`, - [channelId] + [channelId], ) } @@ -103,12 +103,12 @@ export async function broadcastPrivateMessages( and status != 'left' `, [channelId, userId], - (r) => r.user_id + (r) => r.user_id, ) otherUserIds.concat(userId).forEach((otherUserId) => { broadcast(`private-user-messages/${otherUserId}`, {}) }) - return otherUserIds; + return otherUserIds } export const createPrivateUserMessageMain = async ( @@ -116,7 +116,7 @@ export const createPrivateUserMessageMain = async ( channelId: number, content: JSONContent, pg: SupabaseDirectClient, - visibility: ChatVisibility + visibility: ChatVisibility, ) => { log('createPrivateUserMessageMain', creator, channelId, content) @@ -126,10 +126,9 @@ export const createPrivateUserMessageMain = async ( from private_user_message_channel_members where channel_id = $1 and user_id = $2`, - [channelId, creator.id] + [channelId, creator.id], ) - if (!authorized) - throw new APIError(403, 'You are not authorized to post to this channel') + if (!authorized) throw new APIError(403, 'You are not authorized to post to this channel') await insertPrivateMessage(content, channelId, creator.id, visibility, pg) @@ -138,13 +137,12 @@ export const createPrivateUserMessageMain = async ( channel_id: channelId, user_id: creator.id, } - const otherUserIds = await broadcastPrivateMessages(pg, channelId, creator.id); + const otherUserIds = await broadcastPrivateMessages(pg, channelId, creator.id) // Fire and forget safely - void notifyOtherUserInChannelIfInactive(channelId, creator, content, pg) - .catch((err) => { - console.error('notifyOtherUserInChannelIfInactive failed', err) - }) + void notifyOtherUserInChannelIfInactive(channelId, creator, content, pg).catch((err) => { + console.error('notifyOtherUserInChannelIfInactive failed', err) + }) track(creator.id, 'send private message', { channelId, @@ -158,16 +156,16 @@ const notifyOtherUserInChannelIfInactive = async ( channelId: number, creator: User, content: JSONContent, - pg: SupabaseDirectClient + pg: SupabaseDirectClient, ) => { - const otherUserIds = await pg.manyOrNone<{ user_id: string }>( + const otherUserIds = await pg.manyOrNone<{user_id: string}>( `select user_id from private_user_message_channel_members where channel_id = $1 and user_id != $2 and status != 'left' `, - [channelId, creator.id] + [channelId, creator.id], ) // We're only sending notifs for 1:1 channels if (!otherUserIds || otherUserIds.length > 1) return @@ -191,10 +189,7 @@ const notifyOtherUserInChannelIfInactive = async ( await sendWebNotifications(pg, receiverId, JSON.stringify(payload)) await sendMobileNotifications(pg, receiverId, payload) - const startOfDay = dayjs() - .tz('America/Los_Angeles') - .startOf('day') - .toISOString() + const startOfDay = dayjs().tz('America/Los_Angeles').startOf('day').toISOString() const previousMessagesThisDayBetweenTheseUsers = await pg.one( `select count(*) from private_user_messages @@ -202,7 +197,7 @@ const notifyOtherUserInChannelIfInactive = async ( and user_id = $2 and created_time > $3 `, - [channelId, creator.id, startOfDay] + [channelId, creator.id, startOfDay], ) log('previous messages this day', previousMessagesThisDayBetweenTheseUsers) if (previousMessagesThisDayBetweenTheseUsers.count > 1) return @@ -210,27 +205,18 @@ const notifyOtherUserInChannelIfInactive = async ( await createNewMessageNotification(creator, receiver, channelId) } -const createNewMessageNotification = async ( - fromUser: User, - toUser: User, - channelId: number, -) => { +const createNewMessageNotification = async (fromUser: User, toUser: User, channelId: number) => { const privateUser = await getPrivateUser(toUser.id) console.debug('privateUser:', privateUser) if (!privateUser) return await sendNewMessageEmail(privateUser, fromUser, toUser, channelId) } - -async function sendWebNotifications( - pg: SupabaseDirectClient, - userId: string, - payload: string, -) { +async function sendWebNotifications(pg: SupabaseDirectClient, userId: string, payload: string) { webPush.setVapidDetails( 'mailto:hello@compassmeet.com', process.env.VAPID_PUBLIC_KEY!, - process.env.VAPID_PRIVATE_KEY! + process.env.VAPID_PRIVATE_KEY!, ) // Retrieve subscription from the database const subscriptions = await getSubscriptionsFromDB(pg, userId) @@ -250,20 +236,18 @@ async function sendWebNotifications( } } - -export async function getSubscriptionsFromDB( - pg: SupabaseDirectClient, - userId: string, -) { +export async function getSubscriptionsFromDB(pg: SupabaseDirectClient, userId: string) { try { - const subscriptions = await pg.manyOrNone(` + const subscriptions = await pg.manyOrNone( + ` select endpoint, keys from push_subscriptions where user_id = $1 - `, [userId] + `, + [userId], ) - return subscriptions.map(sub => ({ + return subscriptions.map((sub) => ({ endpoint: sub.endpoint, keys: sub.keys, })) @@ -273,35 +257,26 @@ export async function getSubscriptionsFromDB( } } -async function removeSubscription( - pg: SupabaseDirectClient, - endpoint: any, - userId: string, -) { +async function removeSubscription(pg: SupabaseDirectClient, endpoint: any, userId: string) { await pg.none( `DELETE FROM push_subscriptions WHERE endpoint = $1 AND user_id = $2`, - [endpoint, userId] + [endpoint, userId], ) } -async function removeMobileSubscription( - pg: SupabaseDirectClient, - token: any, - userId: string, -) { +async function removeMobileSubscription(pg: SupabaseDirectClient, token: any, userId: string) { await pg.none( `DELETE FROM push_subscriptions_mobile WHERE token = $1 AND user_id = $2`, - [token, userId] + [token, userId], ) } - async function sendMobileNotifications( pg: SupabaseDirectClient, userId: string, @@ -349,13 +324,15 @@ export async function sendPushToToken( } catch (err: unknown) { // Check if it's a Firebase Messaging error if (err instanceof Error && 'code' in err) { - const firebaseError = err as { code: string; message: string } + const firebaseError = err as {code: string; message: string} console.warn('Firebase error:', firebaseError.code, firebaseError.message) // Handle specific error cases here if needed // For example, if token is no longer valid: - if (firebaseError.code === 'messaging/registration-token-not-registered' || - firebaseError.code === 'messaging/invalid-argument') { + if ( + firebaseError.code === 'messaging/registration-token-not-registered' || + firebaseError.code === 'messaging/invalid-argument' + ) { console.warn('Removing invalid FCM token') await removeMobileSubscription(pg, token, userId) } @@ -366,17 +343,15 @@ export async function sendPushToToken( return } - -export async function getMobileSubscriptionsFromDB( - pg: SupabaseDirectClient, - userId: string, -) { +export async function getMobileSubscriptionsFromDB(pg: SupabaseDirectClient, userId: string) { try { - const subscriptions = await pg.manyOrNone(` + const subscriptions = await pg.manyOrNone( + ` select token from push_subscriptions_mobile where user_id = $1 - `, [userId] + `, + [userId], ) return subscriptions diff --git a/backend/api/src/hide-comment.ts b/backend/api/src/hide-comment.ts index 3a57e542..8f19fe21 100644 --- a/backend/api/src/hide-comment.ts +++ b/backend/api/src/hide-comment.ts @@ -1,35 +1,25 @@ -import { APIError, APIHandler } from 'api/helpers/endpoint' -import { isAdminId } from 'common/envs/constants' -import { convertComment } from 'common/supabase/comment' -import { Row } from 'common/supabase/utils' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { broadcastUpdatedComment } from 'shared/websockets/helpers' +import {APIError, APIHandler} from 'api/helpers/endpoint' +import {isAdminId} from 'common/envs/constants' +import {convertComment} from 'common/supabase/comment' +import {Row} from 'common/supabase/utils' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {broadcastUpdatedComment} from 'shared/websockets/helpers' -export const hideComment: APIHandler<'hide-comment'> = async ( - { commentId, hide }, - auth -) => { +export const hideComment: APIHandler<'hide-comment'> = async ({commentId, hide}, auth) => { const pg = createSupabaseDirectClient() const comment = await pg.oneOrNone>( `select * from profile_comments where id = $1`, - [commentId] + [commentId], ) if (!comment) { throw new APIError(404, 'Comment not found') } - if ( - !isAdminId(auth.uid) && - comment.user_id !== auth.uid && - comment.on_user_id !== auth.uid - ) { + if (!isAdminId(auth.uid) && comment.user_id !== auth.uid && comment.on_user_id !== auth.uid) { throw new APIError(403, 'You are not allowed to hide this comment') } - await pg.none(`update profile_comments set hidden = $2 where id = $1`, [ - commentId, - hide, - ]) + await pg.none(`update profile_comments set hidden = $2 where id = $1`, [commentId, hide]) broadcastUpdatedComment(convertComment(comment)) } diff --git a/backend/api/src/hide-profile.ts b/backend/api/src/hide-profile.ts index d777f2b5..2e071d49 100644 --- a/backend/api/src/hide-profile.ts +++ b/backend/api/src/hide-profile.ts @@ -1,14 +1,11 @@ -import {APIError, APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from 'shared/supabase/init' +import {APIError, APIHandler} from './helpers/endpoint' + // Hide a profile for the requesting user by inserting a row into hidden_profiles. // Idempotent: if the pair already exists, succeed silently. -export const hideProfile: APIHandler<'hide-profile'> = async ( - {hiddenUserId}, - auth -) => { - if (auth.uid === hiddenUserId) - throw new APIError(400, 'You cannot hide yourself') +export const hideProfile: APIHandler<'hide-profile'> = async ({hiddenUserId}, auth) => { + if (auth.uid === hiddenUserId) throw new APIError(400, 'You cannot hide yourself') const pg = createSupabaseDirectClient() @@ -17,7 +14,7 @@ export const hideProfile: APIHandler<'hide-profile'> = async ( `insert into hidden_profiles (hider_user_id, hidden_user_id) values ($1, $2) on conflict (hider_user_id, hidden_user_id) do nothing`, - [auth.uid, hiddenUserId] + [auth.uid, hiddenUserId], ) return {status: 'success'} diff --git a/backend/api/src/leave-private-user-message-channel.ts b/backend/api/src/leave-private-user-message-channel.ts index 55e1c96e..f9862678 100644 --- a/backend/api/src/leave-private-user-message-channel.ts +++ b/backend/api/src/leave-private-user-message-channel.ts @@ -1,14 +1,11 @@ -import { APIError, APIHandler } from 'api/helpers/endpoint' -import { log, getUser } from 'shared/utils' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { - insertPrivateMessage, - leaveChatContent, -} from 'api/helpers/private-messages' +import {APIError, APIHandler} from 'api/helpers/endpoint' +import {insertPrivateMessage, leaveChatContent} from 'api/helpers/private-messages' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {getUser, log} from 'shared/utils' export const leavePrivateUserMessageChannel: APIHandler< 'leave-private-user-message-channel' -> = async ({ channelId }, auth) => { +> = async ({channelId}, auth) => { const pg = createSupabaseDirectClient() const user = await getUser(auth.uid) if (!user) throw new APIError(401, 'Your account was not found') @@ -16,10 +13,9 @@ export const leavePrivateUserMessageChannel: APIHandler< const membershipStatus = await pg.oneOrNone( `select status from private_user_message_channel_members where channel_id = $1 and user_id = $2`, - [channelId, auth.uid] + [channelId, auth.uid], ) - if (!membershipStatus) - throw new APIError(403, 'You are not authorized to post to this channel') + if (!membershipStatus) throw new APIError(403, 'You are not authorized to post to this channel') log('membershipStatus: ' + membershipStatus) // add message that the user left the channel @@ -29,15 +25,9 @@ export const leavePrivateUserMessageChannel: APIHandler< set status = 'left' where channel_id=$1 and user_id=$2; `, - [channelId, auth.uid] + [channelId, auth.uid], ) - await insertPrivateMessage( - leaveChatContent(user.name), - channelId, - auth.uid, - 'system_status', - pg - ) - return { status: 'success', channelId: Number(channelId) } + await insertPrivateMessage(leaveChatContent(user.name), channelId, auth.uid, 'system_status', pg) + return {status: 'success', channelId: Number(channelId)} } diff --git a/backend/api/src/like-profile.ts b/backend/api/src/like-profile.ts index d9d9b099..8849ab4a 100644 --- a/backend/api/src/like-profile.ts +++ b/backend/api/src/like-profile.ts @@ -1,42 +1,43 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { APIError, APIHandler } from './helpers/endpoint' -import { createProfileLikeNotification } from 'shared/create-profile-notification' -import { getHasFreeLike } from './has-free-like' -import { log } from 'shared/utils' -import { tryCatch } from 'common/util/try-catch' -import { Row } from 'common/supabase/utils' +import {Row} from 'common/supabase/utils' +import {tryCatch} from 'common/util/try-catch' +import {createProfileLikeNotification} from 'shared/create-profile-notification' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {log} from 'shared/utils' + +import {getHasFreeLike} from './has-free-like' +import {APIError, APIHandler} from './helpers/endpoint' export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => { - const { targetUserId, remove } = props + const {targetUserId, remove} = props const creatorId = auth.uid const pg = createSupabaseDirectClient() if (remove) { - const { error } = await tryCatch( - pg.none( - 'delete from profile_likes where creator_id = $1 and target_id = $2', - [creatorId, targetUserId] - ) + const {error} = await tryCatch( + pg.none('delete from profile_likes where creator_id = $1 and target_id = $2', [ + creatorId, + targetUserId, + ]), ) if (error) { throw new APIError(500, 'Failed to remove like: ' + error.message) } - return { status: 'success' } + return {status: 'success'} } // Check if like already exists - const { data: existing } = await tryCatch( + const {data: existing} = await tryCatch( pg.oneOrNone>( 'select * from profile_likes where creator_id = $1 and target_id = $2', - [creatorId, targetUserId] - ) + [creatorId, targetUserId], + ), ) if (existing) { log('Like already exists, do nothing') - return { status: 'success' } + return {status: 'success'} } const hasFreeLike = await getHasFreeLike(creatorId) @@ -47,11 +48,11 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => { } // Insert the new like - const { data, error } = await tryCatch( + const {data, error} = await tryCatch( pg.one>( 'insert into profile_likes (creator_id, target_id) values ($1, $2) returning *', - [creatorId, targetUserId] - ) + [creatorId, targetUserId], + ), ) if (error) { @@ -63,7 +64,7 @@ export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => { } return { - result: { status: 'success' }, + result: {status: 'success'}, continue: continuation, } } diff --git a/backend/api/src/mark-all-notifications-read.ts b/backend/api/src/mark-all-notifications-read.ts index 7d0c0e81..92a076a7 100644 --- a/backend/api/src/mark-all-notifications-read.ts +++ b/backend/api/src/mark-all-notifications-read.ts @@ -1,16 +1,14 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { APIHandler } from './helpers/endpoint' +import {createSupabaseDirectClient} from 'shared/supabase/init' -export const markAllNotifsRead: APIHandler<'mark-all-notifs-read'> = async ( - _, - auth -) => { +import {APIHandler} from './helpers/endpoint' + +export const markAllNotifsRead: APIHandler<'mark-all-notifs-read'> = async (_, auth) => { const pg = createSupabaseDirectClient() await pg.none( `update user_notifications SET data = jsonb_set(data, '{isSeen}', 'true'::jsonb) where user_id = $1 and data->>'isSeen' = 'false'`, - [auth.uid] + [auth.uid], ) } diff --git a/backend/api/src/public/swagger.css b/backend/api/src/public/swagger.css index 838c02a3..55adaf43 100644 --- a/backend/api/src/public/swagger.css +++ b/backend/api/src/public/swagger.css @@ -1,106 +1,135 @@ @media (prefers-color-scheme: dark) { - body { - background-color: #1e1e1e !important; - color: #ffffff !important; - - } - .swagger-ui p, - h1, - h2, - h3, - h4, - h5, - h6, - label, - .btn, - .parameter__name, - .parameter__type, - .parameter__in, - .response-control-media-type__title, - table thead tr td, - table thead tr th, - .tab li, - .response-col_links, - .opblock-summary-description { - color: #ffffff !important; - } - .swagger-ui .topbar, .opblock-body select, textarea { - background-color: #2b2b2b !important; - color: #ffffff !important; - } - .swagger-ui .opblock { - background-color: #2c2c2c !important; - border-color: #fff !important; - } - .swagger-ui .opblock .opblock-summary-method { - background-color: #1f1f1f !important; - color: #fff !important; - } - .swagger-ui .opblock .opblock-section-header { - background: #1f1f1f !important; - color: #fff !important; - } - .swagger-ui .responses-wrapper { - background-color: #1f1f1f !important; - } - .swagger-ui .response-col_status { - color: #fff !important; - } - .swagger-ui .scheme-container { - background-color: #1f1f1f !important; - } - .swagger-ui .modal-ux, input { - background-color: #1f1f1f !important; - color: #fff !important; - } - .swagger-ui svg path { - fill: white !important; - } - .swagger-ui .close-modal svg { - color: #1e90ff !important; - } - a { - color: #1e90ff !important; - } + body { + background-color: #1e1e1e !important; + color: #ffffff !important; + } + + .swagger-ui p, + h1, + h2, + h3, + h4, + h5, + h6, + label, + .btn, + .parameter__name, + .parameter__type, + .parameter__in, + .response-control-media-type__title, + table thead tr td, + table thead tr th, + .tab li, + .response-col_links, + .opblock-summary-description { + color: #ffffff !important; + } + + .swagger-ui .topbar, + .opblock-body select, + textarea { + background-color: #2b2b2b !important; + color: #ffffff !important; + } + + .swagger-ui .opblock { + background-color: #2c2c2c !important; + border-color: #fff !important; + } + + .swagger-ui .opblock .opblock-summary-method { + background-color: #1f1f1f !important; + color: #fff !important; + } + + .swagger-ui .opblock .opblock-section-header { + background: #1f1f1f !important; + color: #fff !important; + } + + .swagger-ui .responses-wrapper { + background-color: #1f1f1f !important; + } + + .swagger-ui .response-col_status { + color: #fff !important; + } + + .swagger-ui .scheme-container { + background-color: #1f1f1f !important; + } + + .swagger-ui .modal-ux, + input { + background-color: #1f1f1f !important; + color: #fff !important; + } + + .swagger-ui svg path { + fill: white !important; + } + + .swagger-ui .close-modal svg { + color: #1e90ff !important; + } + + a { + 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; - } + html, + body, + .swagger-ui { + font-size: 32px !important; + line-height: 1.5 !important; + } - .swagger-ui { - -webkit-text-size-adjust: 100%; - text-size-adjust: 100%; - } + .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; - } + /* 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; } + /* 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; + } } diff --git a/backend/api/src/react-to-message.ts b/backend/api/src/react-to-message.ts index c97ccfbd..56e1154f 100644 --- a/backend/api/src/react-to-message.ts +++ b/backend/api/src/react-to-message.ts @@ -1,9 +1,12 @@ -import {APIError, APIHandler} from './helpers/endpoint' +import {broadcastPrivateMessages} from 'api/helpers/private-messages' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {broadcastPrivateMessages} from "api/helpers/private-messages"; +import {APIError, APIHandler} from './helpers/endpoint' -export const reactToMessage: APIHandler<'react-to-message'> = async ({messageId, reaction, toDelete}, auth) => { +export const reactToMessage: APIHandler<'react-to-message'> = async ( + {messageId, reaction, toDelete}, + auth, +) => { const pg = createSupabaseDirectClient() // Verify user is a member of the channel @@ -13,7 +16,7 @@ export const reactToMessage: APIHandler<'react-to-message'> = async ({messageId, JOIN private_user_messages msg ON msg.channel_id = m.channel_id WHERE m.user_id = $1 AND msg.id = $2`, - [auth.uid, messageId] + [auth.uid, messageId], ) if (!message) { @@ -27,7 +30,7 @@ export const reactToMessage: APIHandler<'react-to-message'> = async ({messageId, SET reactions = reactions - $1 WHERE id = $2 AND reactions -> $1 ? $3`, - [reaction, messageId, auth.uid] + [reaction, messageId, auth.uid], ) } else { // Toggle reaction @@ -47,14 +50,13 @@ export const reactToMessage: APIHandler<'react-to-message'> = async ({messageId, ) END WHERE id = $3`, - [reaction, auth.uid, messageId] + [reaction, auth.uid, messageId], ) } - void broadcastPrivateMessages(pg, message.channel_id, auth.uid) - .catch((err) => { - console.error('broadcastPrivateMessages failed', err) - }) + void broadcastPrivateMessages(pg, message.channel_id, auth.uid).catch((err) => { + console.error('broadcastPrivateMessages failed', err) + }) return {success: true} } diff --git a/backend/api/src/remove-pinned-photo.ts b/backend/api/src/remove-pinned-photo.ts index 04880e9d..ee452300 100644 --- a/backend/api/src/remove-pinned-photo.ts +++ b/backend/api/src/remove-pinned-photo.ts @@ -1,23 +1,21 @@ -import { APIError } from 'api/helpers/endpoint' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { type APIHandler } from 'api/helpers/endpoint' -import { isAdminId } from 'common/envs/constants' -import { log } from 'shared/utils' -import { tryCatch } from 'common/util/try-catch' +import {APIError, type APIHandler} from 'api/helpers/endpoint' +import {isAdminId} from 'common/envs/constants' +import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {log} from 'shared/utils' export const removePinnedPhoto: APIHandler<'remove-pinned-photo'> = async ( - body: { userId: string }, - auth + body: {userId: string}, + auth, ) => { - const { userId } = body - log('remove pinned url', { userId }) + const {userId} = body + log('remove pinned url', {userId}) - if (!isAdminId(auth.uid)) - throw new APIError(403, 'Only admins can remove pinned photo') + if (!isAdminId(auth.uid)) throw new APIError(403, 'Only admins can remove pinned photo') const pg = createSupabaseDirectClient() - const { error } = await tryCatch( - pg.none('update profiles set pinned_url = null where user_id = $1', [userId]) + const {error} = await tryCatch( + pg.none('update profiles set pinned_url = null where user_id = $1', [userId]), ) if (error) { diff --git a/backend/api/src/report.ts b/backend/api/src/report.ts index 7d27148c..80ae9f99 100644 --- a/backend/api/src/report.ts +++ b/backend/api/src/report.ts @@ -1,22 +1,16 @@ -import {APIError, APIHandler} from './helpers/endpoint' -import {createSupabaseDirectClient} from 'shared/supabase/init' +import {sendDiscordMessage} from 'common/discord/core' +import {DOMAIN} from 'common/envs/constants' +import {Row} from 'common/supabase/utils' import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient} from 'shared/supabase/init' import {insert} from 'shared/supabase/utils' -import {sendDiscordMessage} from "common/discord/core"; -import {Row} from "common/supabase/utils"; -import {DOMAIN} from "common/envs/constants"; + +import {APIError, APIHandler} from './helpers/endpoint' // abusable: people can report the wrong person, that didn't write the comment // but in practice we check it manually and nothing bad happens to them automatically export const report: APIHandler<'report'> = async (body, auth) => { - const { - contentOwnerId, - contentType, - contentId, - description, - parentId, - parentType, - } = body + const {contentOwnerId, contentType, contentId, description, parentId, parentType} = body const pg = createSupabaseDirectClient() @@ -29,7 +23,7 @@ export const report: APIHandler<'report'> = async (body, auth) => { description, parent_id: parentId, parent_type: parentType, - }) + }), ) if (result.error) { @@ -39,14 +33,14 @@ export const report: APIHandler<'report'> = async (body, auth) => { const continuation = async () => { try { const {data: reporter, error} = await tryCatch( - pg.oneOrNone>('select * from users where id = $1', [auth.uid]) + pg.oneOrNone>('select * from users where id = $1', [auth.uid]), ) if (error) { console.error('Failed to get user for report', error) return } const {data: reported, error: userError} = await tryCatch( - pg.oneOrNone>('select * from users where id = $1', [contentOwnerId]) + pg.oneOrNone>('select * from users where id = $1', [contentOwnerId]), ) if (userError) { console.error('Failed to get reported user for report', userError) diff --git a/backend/api/src/rsvp-event.ts b/backend/api/src/rsvp-event.ts index 783afb58..f1d5f509 100644 --- a/backend/api/src/rsvp-event.ts +++ b/backend/api/src/rsvp-event.ts @@ -1,7 +1,7 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {insert, update} from 'shared/supabase/utils' -import {tryCatch} from 'common/util/try-catch' export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => { const pg = createSupabaseDirectClient() @@ -15,7 +15,7 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => { `SELECT id, status, max_participants FROM events WHERE id = $1`, - [body.eventId] + [body.eventId], ) if (!event) { @@ -34,7 +34,7 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => { FROM events_participants WHERE event_id = $1 AND user_id = $2`, - [body.eventId, auth.uid] + [body.eventId, auth.uid], ) if (existingRsvp) { @@ -43,7 +43,7 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => { update(pg, 'events_participants', 'id', { status: body.status, id: existingRsvp.id, - }) + }), ) if (error) { @@ -52,12 +52,12 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => { } else { // Check max participants limit if (event.max_participants && body.status === 'going') { - const count = await pg.one<{ count: number }>( + const count = await pg.one<{count: number}>( `SELECT COUNT(*) FROM events_participants WHERE event_id = $1 AND status = 'going'`, - [body.eventId] + [body.eventId], ) if (Number(count.count) >= event.max_participants) { @@ -71,7 +71,7 @@ export const rsvpEvent: APIHandler<'rsvp-event'> = async (body, auth) => { event_id: body.eventId, user_id: auth.uid, status: body.status, - }) + }), ) if (error) { diff --git a/backend/api/src/save-subscription-mobile.ts b/backend/api/src/save-subscription-mobile.ts index 68943322..717c066e 100644 --- a/backend/api/src/save-subscription-mobile.ts +++ b/backend/api/src/save-subscription-mobile.ts @@ -1,7 +1,11 @@ -import {APIError, APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from 'shared/supabase/init' -export const saveSubscriptionMobile: APIHandler<'save-subscription-mobile'> = async (body, auth) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const saveSubscriptionMobile: APIHandler<'save-subscription-mobile'> = async ( + body, + auth, +) => { const {token} = body if (!token) { @@ -12,17 +16,18 @@ export const saveSubscriptionMobile: APIHandler<'save-subscription-mobile'> = as try { const pg = createSupabaseDirectClient() - await pg.none(` + await pg.none( + ` insert into push_subscriptions_mobile(token, platform, user_id) values ($1, $2, $3) on conflict(token) do update set platform = excluded.platform, user_id = excluded.user_id `, - [token, 'android', userId] - ); - return {success: true}; + [token, 'android', userId], + ) + return {success: true} } catch (err) { - console.error('Error saving subscription', err); + console.error('Error saving subscription', err) throw new APIError(500, `Failed to save subscription`) } } diff --git a/backend/api/src/save-subscription.ts b/backend/api/src/save-subscription.ts index 0d44b694..0a67e6a9 100644 --- a/backend/api/src/save-subscription.ts +++ b/backend/api/src/save-subscription.ts @@ -1,6 +1,7 @@ -import {APIError, APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from 'shared/supabase/init' +import {APIError, APIHandler} from './helpers/endpoint' + export const saveSubscription: APIHandler<'save-subscription'> = async (body, auth) => { const {subscription} = body @@ -13,29 +14,29 @@ export const saveSubscription: APIHandler<'save-subscription'> = async (body, au try { const pg = createSupabaseDirectClient() // Check if a subscription already exists - const exists = await pg.oneOrNone( - 'select id from push_subscriptions where endpoint = $1', - [subscription.endpoint] - ); + const exists = await pg.oneOrNone('select id from push_subscriptions where endpoint = $1', [ + subscription.endpoint, + ]) if (exists) { // Already exists, optionally update keys and userId - await pg.none( - 'update push_subscriptions set keys = $1, user_id = $2 where id = $3', - [subscription.keys, userId, exists.id] - ); + await pg.none('update push_subscriptions set keys = $1, user_id = $2 where id = $3', [ + subscription.keys, + userId, + exists.id, + ]) } else { await pg.none( `insert into push_subscriptions(endpoint, keys, user_id) values($1, $2, $3) on conflict(endpoint) do update set keys = excluded.keys `, - [subscription.endpoint, subscription.keys, userId] - ); + [subscription.endpoint, subscription.keys, userId], + ) } - return {success: true}; + return {success: true} } catch (err) { - console.error('Error saving subscription', err); + console.error('Error saving subscription', err) throw new APIError(500, `Failed to save subscription`) } } diff --git a/backend/api/src/search-location.ts b/backend/api/src/search-location.ts index 9779922f..0ea37fe9 100644 --- a/backend/api/src/search-location.ts +++ b/backend/api/src/search-location.ts @@ -1,5 +1,6 @@ +import {geodbFetch} from 'common/geodb' + import {APIHandler} from './helpers/endpoint' -import {geodbFetch} from "common/geodb"; export const searchLocation: APIHandler<'search-location'> = async (body) => { const {term, limit} = body diff --git a/backend/api/src/search-near-city.ts b/backend/api/src/search-near-city.ts index 046c7f8a..14f8e525 100644 --- a/backend/api/src/search-near-city.ts +++ b/backend/api/src/search-near-city.ts @@ -1,5 +1,6 @@ +import {geodbFetch} from 'common/geodb' + import {APIHandler} from './helpers/endpoint' -import {geodbFetch} from "common/geodb"; const searchNearCityMain = async (cityId: string, radius: number) => { const endpoint = `/cities/${cityId}/nearbyCities?radius=${radius}&offset=0&sort=-population&limit=100` @@ -13,8 +14,6 @@ export const searchNearCity: APIHandler<'search-near-city'> = async (body) => { export const getNearbyCities = async (cityId: string, radius: number) => { const result = await searchNearCityMain(cityId, radius) - const cityIds = (result.data.data as any[]).map( - (city) => city.id.toString() as string - ) + const cityIds = (result.data.data as any[]).map((city) => city.id.toString() as string) return cityIds } diff --git a/backend/api/src/search-users.ts b/backend/api/src/search-users.ts index 3ecef033..00e3bc78 100644 --- a/backend/api/src/search-users.ts +++ b/backend/api/src/search-users.ts @@ -1,10 +1,11 @@ -import {constructPrefixTsQuery} from 'shared/helpers/search' -import {from, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder' -import {type APIHandler} from './helpers/endpoint' -import {convertUser} from 'common/supabase/users' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {toUserAPIResponse} from 'common/api/user-types' +import {convertUser} from 'common/supabase/users' import {uniqBy} from 'lodash' +import {constructPrefixTsQuery} from 'shared/helpers/search' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {from, limit, orderBy, renderSql, select, where} from 'shared/supabase/sql-builder' + +import {type APIHandler} from './helpers/endpoint' export const searchUsers: APIHandler<'search-users'> = async (props, _auth) => { const {term, page, limit} = props @@ -45,19 +46,19 @@ function getSearchUserSQL(props: { [select('*'), from('users')], term ? [ - where( - `name_username_vector @@ websearch_to_tsquery('english', $1) + where( + `name_username_vector @@ websearch_to_tsquery('english', $1) or name_username_vector @@ to_tsquery('english', $2)`, - [term, constructPrefixTsQuery(term)] - ), + [term, constructPrefixTsQuery(term)], + ), - orderBy( - `ts_rank(name_username_vector, websearch_to_tsquery($1)) desc, + orderBy( + `ts_rank(name_username_vector, websearch_to_tsquery($1)) desc, data->>'lastBetTime' desc nulls last`, - [term] - ), - ] + [term], + ), + ] : orderBy(`data->'creatorTraders'->'allTime' desc nulls last`), - limit(props.limit, props.offset) + limit(props.limit, props.offset), ) } diff --git a/backend/api/src/send-search-notifications.ts b/backend/api/src/send-search-notifications.ts index 7479a398..3451d75a 100644 --- a/backend/api/src/send-search-notifications.ts +++ b/backend/api/src/send-search-notifications.ts @@ -1,16 +1,15 @@ -import {createSupabaseDirectClient} from "shared/supabase/init"; -import {from, renderSql, select} from "shared/supabase/sql-builder"; -import {loadProfiles, profileQueryType} from "api/get-profiles"; -import {Row} from "common/supabase/utils"; -import {sendSearchAlertsEmail} from "email/functions/helpers"; -import {MatchesByUserType} from "common/profiles/bookmarked_searches"; -import {keyBy} from "lodash"; +import {loadProfiles, profileQueryType} from 'api/get-profiles' +import {MatchesByUserType} from 'common/profiles/bookmarked_searches' +import {Row} from 'common/supabase/utils' +import {sendSearchAlertsEmail} from 'email/functions/helpers' +import {keyBy} from 'lodash' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {from, renderSql, select} from 'shared/supabase/sql-builder' export function convertSearchRow(row: any): any { return row } - export const notifyBookmarkedSearch = async (matches: MatchesByUserType) => { for (const [_, value] of Object.entries(matches)) { await sendSearchAlertsEmail(value.user, value.privateUser, value.matches) @@ -20,40 +19,35 @@ export const notifyBookmarkedSearch = async (matches: MatchesByUserType) => { export const sendSearchNotifications = async () => { const pg = createSupabaseDirectClient() - const search_query = renderSql( - select('bookmarked_searches.*'), - from('bookmarked_searches'), - ) - const searches = await pg.map(search_query, [], convertSearchRow) as Row<'bookmarked_searches'>[] + const search_query = renderSql(select('bookmarked_searches.*'), from('bookmarked_searches')) + const searches = (await pg.map( + search_query, + [], + convertSearchRow, + )) as Row<'bookmarked_searches'>[] console.debug(`Running ${searches.length} bookmarked searches`) - const _users = await pg.map( - renderSql( - select('users.*'), - from('users'), - ), + const _users = (await pg.map( + renderSql(select('users.*'), from('users')), [], - convertSearchRow - ) as Row<'users'>[] + convertSearchRow, + )) as Row<'users'>[] const users = keyBy(_users, 'id') console.debug('users', users) - const _privateUsers = await pg.map( - renderSql( - select('private_users.*'), - from('private_users'), - ), + const _privateUsers = (await pg.map( + renderSql(select('private_users.*'), from('private_users')), [], - convertSearchRow - ) as Row<'private_users'>[] + convertSearchRow, + )) as Row<'private_users'>[] const privateUsers = keyBy(_privateUsers, 'id') console.debug('privateUsers', privateUsers) const matches: MatchesByUserType = {} for (const row of searches) { - if (typeof row.search_filters !== 'object') continue; - const { orderBy: _, ...filters } = (row.search_filters ?? {}) as Record + if (typeof row.search_filters !== 'object') continue + const {orderBy: _, ...filters} = (row.search_filters ?? {}) as Record const props = { ...filters, skipId: row.creator_id, @@ -85,4 +79,4 @@ export const sendSearchNotifications = async () => { await notifyBookmarkedSearch(matches) return {status: 'success'} -} \ No newline at end of file +} diff --git a/backend/api/src/serve.ts b/backend/api/src/serve.ts index ed08307c..4eb47cd1 100644 --- a/backend/api/src/serve.ts +++ b/backend/api/src/serve.ts @@ -1,12 +1,16 @@ -import "tsconfig-paths/register"; -import * as admin from 'firebase-admin' -import {initAdmin} from 'shared/init-admin' +import 'tsconfig-paths/register' + +import {IS_LOCAL} from 'common/hosting/constants' import {loadSecretsToEnv} from 'common/secrets' -import {log} from 'shared/utils' -import {IS_LOCAL} from "common/hosting/constants"; +import * as admin from 'firebase-admin' +import {getServiceAccountCredentials} from 'shared/firebase-utils' +import {initAdmin} from 'shared/init-admin' import {METRIC_WRITER} from 'shared/monitoring/metric-writer' +import {log} from 'shared/utils' import {listen as webSocketListen} from 'shared/websockets/server' +import {app} from './app' + log('Api server starting up....') if (IS_LOCAL) { @@ -21,13 +25,10 @@ if (IS_LOCAL) { METRIC_WRITER.start() -import {app} from './app' -import {getServiceAccountCredentials} from "shared/firebase-utils"; - const credentials = IS_LOCAL ? getServiceAccountCredentials() : // No explicit credentials needed for deployed service. - undefined + undefined const startupProcess = async () => { await loadSecretsToEnv(credentials) @@ -40,4 +41,4 @@ const startupProcess = async () => { webSocketListen(httpServer, '/ws') } -startupProcess().then(_r => log('Server started successfully')) +startupProcess().then((_r) => log('Server started successfully')) diff --git a/backend/api/src/set-compatibility-answer.ts b/backend/api/src/set-compatibility-answer.ts index 26b43475..8a1b37eb 100644 --- a/backend/api/src/set-compatibility-answer.ts +++ b/backend/api/src/set-compatibility-answer.ts @@ -1,11 +1,12 @@ -import {APIHandler} from './helpers/endpoint' -import {createSupabaseDirectClient} from 'shared/supabase/init' import {Row} from 'common/supabase/utils' import {recomputeCompatibilityScoresForUser} from 'shared/compatibility/compute-scores' +import {createSupabaseDirectClient} from 'shared/supabase/init' + +import {APIHandler} from './helpers/endpoint' export const setCompatibilityAnswer: APIHandler<'set-compatibility-answer'> = async ( {questionId, multipleChoice, prefChoices, importance, explanation}, - auth + auth, ) => { const pg = createSupabaseDirectClient() @@ -21,14 +22,7 @@ export const setCompatibilityAnswer: APIHandler<'set-compatibility-answer'> = as explanation = EXCLUDED.explanation RETURNING * `, - values: [ - auth.uid, - questionId, - multipleChoice, - prefChoices, - importance, - explanation ?? null, - ], + values: [auth.uid, questionId, multipleChoice, prefChoices, importance, explanation ?? null], }) const continuation = async () => { diff --git a/backend/api/src/set-last-online-time.ts b/backend/api/src/set-last-online-time.ts index eae4d524..b636ed1a 100644 --- a/backend/api/src/set-last-online-time.ts +++ b/backend/api/src/set-last-online-time.ts @@ -1,19 +1,17 @@ -import {APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from 'shared/supabase/init' -export const setLastOnlineTime: APIHandler<'set-last-online-time'> = async ( - _, - auth -) => { +import {APIHandler} from './helpers/endpoint' + +export const setLastOnlineTime: APIHandler<'set-last-online-time'> = async (_, auth) => { if (!auth || !auth.uid) return await setLastOnlineTimeUser(auth.uid) // console.log('setLastOnline') } - export const setLastOnlineTimeUser = async (userId: string) => { const pg = createSupabaseDirectClient() - await pg.none(` + await pg.none( + ` INSERT INTO user_activity (user_id, last_online_time) VALUES ($1, now()) ON CONFLICT (user_id) @@ -21,6 +19,6 @@ export const setLastOnlineTimeUser = async (userId: string) => { SET last_online_time = EXCLUDED.last_online_time WHERE user_activity.last_online_time < now() - interval '1 minute'; `, - [userId] + [userId], ) } diff --git a/backend/api/src/ship-profiles.ts b/backend/api/src/ship-profiles.ts index b288f879..8c981c6d 100644 --- a/backend/api/src/ship-profiles.ts +++ b/backend/api/src/ship-profiles.ts @@ -1,41 +1,36 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { APIError, APIHandler } from './helpers/endpoint' -import { createProfileShipNotification } from 'shared/create-profile-notification' -import { log } from 'shared/utils' -import { tryCatch } from 'common/util/try-catch' -import { insert } from 'shared/supabase/utils' +import {tryCatch} from 'common/util/try-catch' +import {createProfileShipNotification} from 'shared/create-profile-notification' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {insert} from 'shared/supabase/utils' +import {log} from 'shared/utils' + +import {APIError, APIHandler} from './helpers/endpoint' export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) => { - const { targetUserId1, targetUserId2, remove } = props + const {targetUserId1, targetUserId2, remove} = props const creatorId = auth.uid const pg = createSupabaseDirectClient() // Check if ship already exists or with swapped target IDs const existing = await tryCatch( - pg.oneOrNone<{ ship_id: string }>( + pg.oneOrNone<{ship_id: string}>( `select ship_id from profile_ships where creator_id = $1 and ( target1_id = $2 and target2_id = $3 or target1_id = $3 and target2_id = $2 )`, - [creatorId, targetUserId1, targetUserId2] - ) + [creatorId, targetUserId1, targetUserId2], + ), ) - if (existing.error) - throw new APIError( - 500, - 'Error when checking ship: ' + existing.error.message - ) + if (existing.error) throw new APIError(500, 'Error when checking ship: ' + existing.error.message) if (existing.data) { if (remove) { - const { error } = await tryCatch( - pg.none('delete from profile_ships where ship_id = $1', [ - existing.data.ship_id, - ]) + const {error} = await tryCatch( + pg.none('delete from profile_ships where ship_id = $1', [existing.data.ship_id]), ) if (error) { throw new APIError(500, 'Failed to remove ship: ' + error.message) @@ -43,16 +38,16 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) => } else { log('Ship already exists, do nothing') } - return { status: 'success' } + return {status: 'success'} } // Insert the new ship - const { data, error } = await tryCatch( + const {data, error} = await tryCatch( insert(pg, 'profile_ships', { creator_id: creatorId, target1_id: targetUserId1, target2_id: targetUserId2, - }) + }), ) if (error) { @@ -67,7 +62,7 @@ export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) => } return { - result: { status: 'success' }, + result: {status: 'success'}, continue: continuation, } } diff --git a/backend/api/src/star-profile.ts b/backend/api/src/star-profile.ts index facffe41..31aed8e8 100644 --- a/backend/api/src/star-profile.ts +++ b/backend/api/src/star-profile.ts @@ -1,51 +1,52 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { APIError, APIHandler } from './helpers/endpoint' -import { log } from 'shared/utils' -import { tryCatch } from 'common/util/try-catch' -import { Row } from 'common/supabase/utils' -import { insert } from 'shared/supabase/utils' +import {Row} from 'common/supabase/utils' +import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {insert} from 'shared/supabase/utils' +import {log} from 'shared/utils' + +import {APIError, APIHandler} from './helpers/endpoint' export const starProfile: APIHandler<'star-profile'> = async (props, auth) => { - const { targetUserId, remove } = props + const {targetUserId, remove} = props const creatorId = auth.uid const pg = createSupabaseDirectClient() if (remove) { - const { error } = await tryCatch( - pg.none( - 'delete from profile_stars where creator_id = $1 and target_id = $2', - [creatorId, targetUserId] - ) + const {error} = await tryCatch( + pg.none('delete from profile_stars where creator_id = $1 and target_id = $2', [ + creatorId, + targetUserId, + ]), ) if (error) { throw new APIError(500, 'Failed to remove star: ' + error.message) } - return { status: 'success' } + return {status: 'success'} } // Check if star already exists - const { data: existing } = await tryCatch( + const {data: existing} = await tryCatch( pg.oneOrNone>( 'select * from profile_stars where creator_id = $1 and target_id = $2', - [creatorId, targetUserId] - ) + [creatorId, targetUserId], + ), ) if (existing) { log('star already exists, do nothing') - return { status: 'success' } + return {status: 'success'} } // Insert the new star - const { error } = await tryCatch( - insert(pg, 'profile_stars', { creator_id: creatorId, target_id: targetUserId }) + const {error} = await tryCatch( + insert(pg, 'profile_stars', {creator_id: creatorId, target_id: targetUserId}), ) if (error) { throw new APIError(500, 'Failed to add star: ' + error.message) } - return { status: 'success' } + return {status: 'success'} } diff --git a/backend/api/src/test.ts b/backend/api/src/test.ts index b960af34..746f4766 100644 --- a/backend/api/src/test.ts +++ b/backend/api/src/test.ts @@ -1,8 +1,8 @@ -import {sendTestEmail} from "email/functions/helpers"; +import {sendTestEmail} from 'email/functions/helpers' export const localSendTestEmail = async () => { sendTestEmail('hello@compassmeet.com') .then(() => console.debug('Email sent successfully!')) .catch((error) => console.error('Failed to send email:', error)) - return { message: 'Email sent successfully!'} + return {message: 'Email sent successfully!'} } diff --git a/backend/api/src/unhide-profile.ts b/backend/api/src/unhide-profile.ts index ec57155b..68c51429 100644 --- a/backend/api/src/unhide-profile.ts +++ b/backend/api/src/unhide-profile.ts @@ -1,12 +1,10 @@ -import {APIHandler} from './helpers/endpoint' import {createSupabaseDirectClient} from 'shared/supabase/init' +import {APIHandler} from './helpers/endpoint' + // Unhide a profile for the requesting user by deleting from hidden_profiles. // Idempotent: if the pair does not exist, succeed silently. -export const unhideProfile: APIHandler<'unhide-profile'> = async ( - {hiddenUserId}, - auth -) => { +export const unhideProfile: APIHandler<'unhide-profile'> = async ({hiddenUserId}, auth) => { const pg = createSupabaseDirectClient() await pg.none( @@ -14,7 +12,7 @@ export const unhideProfile: APIHandler<'unhide-profile'> = async ( from hidden_profiles where hider_user_id = $1 and hidden_user_id = $2`, - [auth.uid, hiddenUserId] + [auth.uid, hiddenUserId], ) return {status: 'success'} diff --git a/backend/api/src/update-event.ts b/backend/api/src/update-event.ts index 4d9e0185..05becbad 100644 --- a/backend/api/src/update-event.ts +++ b/backend/api/src/update-event.ts @@ -1,7 +1,7 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {update} from 'shared/supabase/utils' -import {tryCatch} from 'common/util/try-catch' export const updateEvent: APIHandler<'update-event'> = async (body, auth) => { const pg = createSupabaseDirectClient() @@ -15,7 +15,7 @@ export const updateEvent: APIHandler<'update-event'> = async (body, auth) => { `SELECT id, creator_id, status FROM events WHERE id = $1`, - [body.eventId] + [body.eventId], ) if (!event) { @@ -42,7 +42,7 @@ export const updateEvent: APIHandler<'update-event'> = async (body, auth) => { event_end_time: body.eventEndTime, max_participants: body.maxParticipants, id: body.eventId, - }) + }), ) if (error) { diff --git a/backend/api/src/update-me.ts b/backend/api/src/update-me.ts index 498a88da..9df47b7a 100644 --- a/backend/api/src/update-me.ts +++ b/backend/api/src/update-me.ts @@ -1,14 +1,15 @@ import {toUserAPIResponse} from 'common/api/user-types' import {RESERVED_PATHS} from 'common/envs/constants' +import {strip} from 'common/socials' import {cleanDisplayName, cleanUsername} from 'common/util/clean-username' import {removeUndefinedProps} from 'common/util/object' import {cloneDeep, mapValues} from 'lodash' import {createSupabaseDirectClient} from 'shared/supabase/init' -import {getUser, getUserByUsername} from 'shared/utils' -import {APIError, APIHandler} from './helpers/endpoint' import {updateUser} from 'shared/supabase/users' +import {getUser, getUserByUsername} from 'shared/utils' import {broadcastUpdatedUser} from 'shared/websockets/helpers' -import {strip} from 'common/socials' + +import {APIError, APIHandler} from './helpers/endpoint' export const updateMe: APIHandler<'me/update'> = async (props, auth) => { const update = cloneDeep(props) @@ -35,12 +36,9 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => { const {name, username, avatarUrl, link = {}, ...rest} = update await updateUser(pg, auth.uid, removeUndefinedProps(rest)) - const stripped = mapValues( - link, - (value, site) => value && strip(site as any, value) - ) + const stripped = mapValues(link, (value, site) => value && strip(site as any, value)) - const adds = {} as { [key: string]: string } + const adds = {} as {[key: string]: string} const removes = [] for (const [key, value] of Object.entries(stripped)) { if (value === null || value === '') { @@ -60,20 +58,26 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => { ) where id = $(id) returning data -> 'link' as link`, - {adds, removes, id: auth.uid} + {adds, removes, id: auth.uid}, ) newLinks = data?.link } if (name) { - await pg.none(`update users + await pg.none( + `update users set name = $1 - where id = $2`, [name, auth.uid]) + where id = $2`, + [name, auth.uid], + ) } if (username) { - await pg.none(`update users + await pg.none( + `update users set username = $1 - where id = $2`, [username, auth.uid]) + where id = $2`, + [username, auth.uid], + ) } if (avatarUrl) { await updateUser(pg, auth.uid, {avatarUrl}) @@ -89,7 +93,7 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => { username, avatarUrl, link: newLinks ?? undefined, - }) + }), ) } diff --git a/backend/api/src/update-notif-setting.ts b/backend/api/src/update-notif-setting.ts index 6a75fb9d..8b068e76 100644 --- a/backend/api/src/update-notif-setting.ts +++ b/backend/api/src/update-notif-setting.ts @@ -1,11 +1,12 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { updatePrivateUser } from 'shared/supabase/users' -import { type APIHandler } from './helpers/endpoint' -import { broadcastUpdatedPrivateUser } from 'shared/websockets/helpers' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {updatePrivateUser} from 'shared/supabase/users' +import {broadcastUpdatedPrivateUser} from 'shared/websockets/helpers' + +import {type APIHandler} from './helpers/endpoint' export const updateNotifSettings: APIHandler<'update-notif-settings'> = async ( - { type, medium, enabled }, - auth + {type, medium, enabled}, + auth, ) => { const pg = createSupabaseDirectClient() if (type === 'opt_out_all' && medium === 'mobile') { @@ -21,7 +22,7 @@ export const updateNotifSettings: APIHandler<'update-notif-settings'> = async ( ${enabled ? `|| '[$2:name]'::jsonb` : `- $2`} ) where id = $3`, - [type, medium, auth.uid] + [type, medium, auth.uid], ) broadcastUpdatedPrivateUser(auth.uid) } diff --git a/backend/api/src/update-options.ts b/backend/api/src/update-options.ts index 5f6ebbf5..2cbcc0bd 100644 --- a/backend/api/src/update-options.ts +++ b/backend/api/src/update-options.ts @@ -1,76 +1,79 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {OPTION_TABLES} from 'common/profiles/constants' +import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {log} from 'shared/utils' -import {tryCatch} from 'common/util/try-catch' -import {OPTION_TABLES} from "common/profiles/constants"; -export const updateOptions: APIHandler<'update-options'> = async ( - {table, values}, - auth -) => { +export const updateOptions: APIHandler<'update-options'> = async ({table, values}, auth) => { if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table') if (!values || !Array.isArray(values)) { throw new APIError(400, 'No ids provided') } - const idsWithNumbers = values.map(id => { + const idsWithNumbers = values.map((id) => { const numberId = Number(id) return isNaN(numberId) ? {isNumber: false, v: id} : {isNumber: true, v: numberId} }) - const names: string[] = idsWithNumbers.filter(item => !item.isNumber).map(item => item.v) as string[] - const ids: number[] = idsWithNumbers.filter(item => item.isNumber).map(item => item.v) as number[] + const names: string[] = idsWithNumbers + .filter((item) => !item.isNumber) + .map((item) => item.v) as string[] + const ids: number[] = idsWithNumbers + .filter((item) => item.isNumber) + .map((item) => item.v) as number[] log('Updating profile options', {table, ids, names}) const pg = createSupabaseDirectClient() - const profileIdResult = await pg.oneOrNone<{ id: number }>( + const profileIdResult = await pg.oneOrNone<{id: number}>( 'SELECT id FROM profiles WHERE user_id = $1', - [auth.uid] + [auth.uid], ) if (!profileIdResult) throw new APIError(404, 'Profile not found') const profileId = profileIdResult.id - const result = await tryCatch(pg.tx(async (t) => { - const currentOptionsResult = await t.manyOrNone<{ id: string }>( - `SELECT option_id as id + const result = await tryCatch( + pg.tx(async (t) => { + const currentOptionsResult = await t.manyOrNone<{id: string}>( + `SELECT option_id as id FROM profile_${table} WHERE profile_id = $1`, - [profileId] - ) - const currentOptions = currentOptionsResult.map(row => row.id) - if (currentOptions.sort().join(',') === ids.sort().join(',') && !names?.length) { - log(`Skipping /update-${table} because they are already the same`) - return undefined - } + [profileId], + ) + const currentOptions = currentOptionsResult.map((row) => row.id) + if (currentOptions.sort().join(',') === ids.sort().join(',') && !names?.length) { + log(`Skipping /update-${table} because they are already the same`) + return undefined + } - // Add new options - for (const name of (names || [])) { - const row = await t.one<{ id: number }>( - `INSERT INTO ${table} (name, creator_id) + // Add new options + for (const name of names || []) { + const row = await t.one<{id: number}>( + `INSERT INTO ${table} (name, creator_id) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET name = ${table}.name RETURNING id`, - [name, auth.uid] - ) - ids.push(row.id) - } + [name, auth.uid], + ) + ids.push(row.id) + } - // Delete old options for this profile - await t.none(`DELETE FROM profile_${table} WHERE profile_id = $1`, [profileId]) + // Delete old options for this profile + await t.none(`DELETE FROM profile_${table} WHERE profile_id = $1`, [profileId]) - // Insert new option_ids - if (ids.length > 0) { - const values = ids.map((id, i) => `($1, $${i + 2})`).join(', ') - await t.none( - `INSERT INTO profile_${table} (profile_id, option_id) VALUES ${values}`, - [profileId, ...ids] - ) - } + // Insert new option_ids + if (ids.length > 0) { + const values = ids.map((id, i) => `($1, $${i + 2})`).join(', ') + await t.none(`INSERT INTO profile_${table} (profile_id, option_id) VALUES ${values}`, [ + profileId, + ...ids, + ]) + } - return ids - })) + return ids + }), + ) if (result.error) { log('Error updating profile options', result.error) @@ -79,4 +82,3 @@ export const updateOptions: APIHandler<'update-options'> = async ( return {updatedIds: result.data} } - diff --git a/backend/api/src/update-private-user-message-channel.ts b/backend/api/src/update-private-user-message-channel.ts index 584d7a1c..47d725ea 100644 --- a/backend/api/src/update-private-user-message-channel.ts +++ b/backend/api/src/update-private-user-message-channel.ts @@ -1,12 +1,12 @@ -import { APIError, APIHandler } from 'api/helpers/endpoint' -import { log, getUser } from 'shared/utils' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { millisToTs } from 'common/supabase/utils' +import {APIError, APIHandler} from 'api/helpers/endpoint' +import {millisToTs} from 'common/supabase/utils' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {getUser, log} from 'shared/utils' export const updatePrivateUserMessageChannel: APIHandler< 'update-private-user-message-channel' > = async (body, auth) => { - const { channelId, notifyAfterTime } = body + const {channelId, notifyAfterTime} = body const pg = createSupabaseDirectClient() const user = await getUser(auth.uid) if (!user) throw new APIError(401, 'Your account was not found') @@ -14,10 +14,9 @@ export const updatePrivateUserMessageChannel: APIHandler< const membershipStatus = await pg.oneOrNone( `select status from private_user_message_channel_members where channel_id = $1 and user_id = $2`, - [channelId, auth.uid] + [channelId, auth.uid], ) - if (!membershipStatus) - throw new APIError(403, 'You are not authorized to this channel') + if (!membershipStatus) throw new APIError(403, 'You are not authorized to this channel') log('membershipStatus ' + membershipStatus) await pg.none( @@ -26,8 +25,8 @@ export const updatePrivateUserMessageChannel: APIHandler< set notify_after_time = $3 where channel_id=$1 and user_id=$2; `, - [channelId, auth.uid, millisToTs(notifyAfterTime)] + [channelId, auth.uid, millisToTs(notifyAfterTime)], ) - return { status: 'success', channelId: Number(channelId) } + return {status: 'success', channelId: Number(channelId)} } diff --git a/backend/api/src/update-profile.ts b/backend/api/src/update-profile.ts index 7a3d78c0..7ba1568d 100644 --- a/backend/api/src/update-profile.ts +++ b/backend/api/src/update-profile.ts @@ -1,32 +1,27 @@ import {APIError, APIHandler} from 'api/helpers/endpoint' +import {trimStrings} from 'common/parsing' +import {type Row} from 'common/supabase/utils' +import {tryCatch} from 'common/util/try-catch' import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' import {createSupabaseDirectClient} from 'shared/supabase/init' import {updateUser} from 'shared/supabase/users' -import {log} from 'shared/utils' -import {tryCatch} from 'common/util/try-catch' import {update} from 'shared/supabase/utils' -import {type Row} from 'common/supabase/utils' -import {trimStrings} from "common/parsing"; +import {log} from 'shared/utils' -export const updateProfile: APIHandler<'update-profile'> = async ( - parsedBody, - auth -) => { +export const updateProfile: APIHandler<'update-profile'> = async (parsedBody, auth) => { trimStrings(parsedBody) log('Updating profile', parsedBody) const pg = createSupabaseDirectClient() - const { data: existingProfile } = await tryCatch( - pg.oneOrNone>('select * from profiles where user_id = $1', [ - auth.uid, - ]) + const {data: existingProfile} = await tryCatch( + pg.oneOrNone>('select * from profiles where user_id = $1', [auth.uid]), ) if (!existingProfile) { throw new APIError(404, 'Profile not found') } - log('Updating profile', { userId: auth.uid, parsedBody }) + log('Updating profile', {userId: auth.uid, parsedBody}) await removePinnedUrlFromPhotoUrls(parsedBody) @@ -34,8 +29,8 @@ export const updateProfile: APIHandler<'update-profile'> = async ( await updateUser(pg, auth.uid, {avatarUrl: parsedBody.pinned_url}) } - const { data, error } = await tryCatch( - update(pg, 'profiles', 'user_id', { user_id: auth.uid, ...parsedBody }) + const {data, error} = await tryCatch( + update(pg, 'profiles', 'user_id', {user_id: auth.uid, ...parsedBody}), ) if (error) { diff --git a/backend/api/src/vote.ts b/backend/api/src/vote.ts index 636b130e..51755265 100644 --- a/backend/api/src/vote.ts +++ b/backend/api/src/vote.ts @@ -1,8 +1,9 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { getUser } from 'shared/utils' -import { APIHandler, APIError } from './helpers/endpoint' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {getUser} from 'shared/utils' -export const vote: APIHandler<'vote'> = async ({ voteId, choice, priority }, auth) => { +import {APIError, APIHandler} from './helpers/endpoint' + +export const vote: APIHandler<'vote'> = async ({voteId, choice, priority}, auth) => { const user = await getUser(auth.uid) if (!user) throw new APIError(401, 'Your account was not found') @@ -10,9 +11,9 @@ export const vote: APIHandler<'vote'> = async ({ voteId, choice, priority }, aut // Map string choice to smallint (-1, 0, 1) const choiceMap: Record = { - 'for': 1, - 'abstain': 0, - 'against': -1, + for: 1, + abstain: 0, + against: -1, } const choiceVal = choiceMap[choice] if (choiceVal === undefined) { @@ -32,7 +33,7 @@ export const vote: APIHandler<'vote'> = async ({ voteId, choice, priority }, aut try { const result = await pg.one(query, [user.id, voteId, choiceVal, priority]) - return { data: result } + return {data: result} } catch (e) { throw new APIError(500, 'Error recording vote', e as any) } diff --git a/backend/api/tests/unit/ban-user.unit.test.ts b/backend/api/tests/unit/ban-user.unit.test.ts index 97324763..320747fc 100644 --- a/backend/api/tests/unit/ban-user.unit.test.ts +++ b/backend/api/tests/unit/ban-user.unit.test.ts @@ -5,120 +5,106 @@ jest.mock('shared/supabase/users') jest.mock('shared/analytics') jest.mock('shared/utils') -import { banUser } from "api/ban-user"; -import * as supabaseInit from "shared/supabase/init"; -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 { AuthedUser } from "api/helpers/endpoint" - +import {banUser} from 'api/ban-user' +import {AuthedUser} from 'api/helpers/endpoint' +import * as constants from 'common/envs/constants' +import * as sharedAnalytics from 'shared/analytics' +import {throwErrorIfNotMod} from 'shared/helpers/auth' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUsers from 'shared/supabase/users' describe('banUser', () => { - const mockPg = {} as any; + const mockPg = {} as any - beforeEach(() => { - jest.resetAllMocks(); + beforeEach(() => { + jest.resetAllMocks() + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + describe('when given valid input', () => { + it('should ban a user successfully', async () => { + const mockUser = { + userId: '123', + unban: false, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + ;(constants.isAdminId as jest.Mock).mockReturnValue(false) - describe('when given valid input', () => { - it('should ban a user successfully', async () => { - const mockUser = { - userId: '123', - unban: false - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; + await banUser(mockUser, mockAuth, mockReq) - (constants.isAdminId as jest.Mock).mockReturnValue(false); + expect(throwErrorIfNotMod).toBeCalledTimes(1) + expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid) + expect(constants.isAdminId).toBeCalledTimes(1) + expect(constants.isAdminId).toBeCalledWith(mockUser.userId) + 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, + }) + }) - 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).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('should unban a user successfully', async () => { + const mockUser = { + userId: '123', + unban: true, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - it('should unban a user successfully', async () => { - const mockUser = { - userId: '123', - unban: true - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; + ;(constants.isAdminId as jest.Mock).mockReturnValue(false) - (constants.isAdminId as jest.Mock).mockReturnValue(false); + await banUser(mockUser, mockAuth, mockReq) - await banUser(mockUser, mockAuth, mockReq); - - 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} - ); - }); - }); - describe('when an error occurs', () => { - it('throw if the ban requester is not a mod or admin', async () => { - const mockUser = { - userId: '123', - unban: false - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - - (throwErrorIfNotMod as jest.Mock).mockRejectedValue( - new Error(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`) - ); + 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, + }) + }) + }) + describe('when an error occurs', () => { + it('throw if the ban requester is not a mod or admin', async () => { + const mockUser = { + userId: '123', + unban: false, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - 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); - }); + ;(throwErrorIfNotMod as jest.Mock).mockRejectedValue( + new Error(`User ${mockAuth.uid} must be an admin or trusted to perform this action.`), + ) - it('throw if the ban target is an admin', async () => { - const mockUser = { - userId: '123', - unban: false - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; + 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) + }) - (constants.isAdminId as jest.Mock).mockReturnValue(true); + it('throw if the ban target is an admin', async () => { + const mockUser = { + userId: '123', + unban: false, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - await expect(banUser(mockUser, mockAuth, mockReq)) - .rejects - .toThrowError('Cannot ban admin'); - expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid); - expect(constants.isAdminId).toBeCalledWith(mockUser.userId); - }); - }); -}); \ No newline at end of file + ;(constants.isAdminId as jest.Mock).mockReturnValue(true) + + await expect(banUser(mockUser, mockAuth, mockReq)).rejects.toThrowError('Cannot ban admin') + expect(throwErrorIfNotMod).toBeCalledWith(mockAuth.uid) + expect(constants.isAdminId).toBeCalledWith(mockUser.userId) + }) + }) +}) diff --git a/backend/api/tests/unit/block-user.unit.test.ts b/backend/api/tests/unit/block-user.unit.test.ts index 87f19edb..a201aa36 100644 --- a/backend/api/tests/unit/block-user.unit.test.ts +++ b/backend/api/tests/unit/block-user.unit.test.ts @@ -2,110 +2,105 @@ jest.mock('shared/supabase/init') jest.mock('shared/supabase/users') jest.mock('shared/supabase/utils') -import * as blockUserModule from "api/block-user"; -import { AuthedUser } from "api/helpers/endpoint"; -import * as supabaseInit from "shared/supabase/init"; -import * as supabaseUsers from "shared/supabase/users"; -import * as supabaseUtils from "shared/supabase/utils"; +import * as blockUserModule from 'api/block-user' +import {AuthedUser} from 'api/helpers/endpoint' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUsers from 'shared/supabase/users' +import * as supabaseUtils from 'shared/supabase/utils' describe('blockUser', () => { - let mockPg: any; + let mockPg: any - beforeEach(() => { - jest.resetAllMocks() - mockPg = { - tx: jest.fn(async (cb) => { - const mockTx = {}; - await cb(mockTx); - }), - }; + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + tx: jest.fn(async (cb) => { + const mockTx = {} + await cb(mockTx) + }), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg) - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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 - 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; + await blockUserModule.blockUser(mockParams, mockAuth, mockReq) - await blockUserModule.blockUser(mockParams, mockAuth, mockReq) + 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 + const mockReq = {} as any - 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; - const mockReq = {} as any; - - expect(blockUserModule.blockUser(mockParams, mockAuth, mockReq)) - .rejects - .toThrowError('You cannot block yourself'); - }); - }); -}); + expect(blockUserModule.blockUser(mockParams, mockAuth, mockReq)).rejects.toThrowError( + 'You cannot block yourself', + ) + }) + }) +}) describe('unblockUser', () => { - let mockPg: any; + let mockPg: any - beforeEach(() => { - jest.resetAllMocks() - mockPg = { - tx: jest.fn(async (cb) => { - const mockTx = {}; - await cb(mockTx); - }), - }; + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + tx: jest.fn(async (cb) => { + const mockTx = {} + await cb(mockTx) + }), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg) - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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 - 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; + await blockUserModule.unblockUser(mockParams, mockAuth, mockReq) - await blockUserModule.unblockUser(mockParams, mockAuth, mockReq) - - 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)} - ); - }); - }); - -}); \ No newline at end of file + 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)}, + ) + }) + }) +}) diff --git a/backend/api/tests/unit/compatible-profiles.unit.test.ts b/backend/api/tests/unit/compatible-profiles.unit.test.ts index 4cfff7b7..c51cc4ad 100644 --- a/backend/api/tests/unit/compatible-profiles.unit.test.ts +++ b/backend/api/tests/unit/compatible-profiles.unit.test.ts @@ -1,41 +1,39 @@ -jest.mock('shared/supabase/init'); - -import {getCompatibleProfiles} from "api/compatible-profiles"; -import * as supabaseInit from "shared/supabase/init"; +jest.mock('shared/supabase/init') +import {getCompatibleProfiles} from 'api/compatible-profiles' +import * as supabaseInit from 'shared/supabase/init' describe('getCompatibleProfiles', () => { - let mockPg = {} as any; + let mockPg = {} as any beforeEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks() mockPg = { map: jest.fn().mockResolvedValue([]), - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) afterEach(() => { - jest.restoreAllMocks(); - }); + jest.restoreAllMocks() + }) 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 }}; + const mockProps = '123' + const mockScores = ['abc', {score: 0.69}] + const mockScoresFromEntries = {abc: {score: 0.69}} - (mockPg.map as jest.Mock).mockResolvedValue([mockScores]); + ;(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)); - }); - }); -}); + 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)) + }) + }) +}) diff --git a/backend/api/tests/unit/contact.unit.test.ts b/backend/api/tests/unit/contact.unit.test.ts index 49d3ed83..9e018250 100644 --- a/backend/api/tests/unit/contact.unit.test.ts +++ b/backend/api/tests/unit/contact.unit.test.ts @@ -1,30 +1,28 @@ -jest.mock('common/discord/core'); -jest.mock('shared/supabase/utils'); -jest.mock('shared/supabase/init'); -jest.mock('common/util/try-catch'); +jest.mock('common/discord/core') +jest.mock('shared/supabase/utils') +jest.mock('shared/supabase/init') +jest.mock('common/util/try-catch') -import {contact} from "api/contact"; -import * as supabaseInit from "shared/supabase/init"; -import * as supabaseUtils from "shared/supabase/utils"; -import {tryCatch} from "common/util/try-catch"; -import {sendDiscordMessage} from "common/discord/core"; -import {AuthedUser} from "api/helpers/endpoint"; +import {contact} from 'api/contact' +import {AuthedUser} from 'api/helpers/endpoint' +import {sendDiscordMessage} from 'common/discord/core' import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUtils from 'shared/supabase/utils' describe('contact', () => { - let mockPg: any; + let mockPg: any beforeEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks() mockPg = { oneOrNone: jest.fn(), - }; - - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) afterEach(() => { - jest.restoreAllMocks(); - }); + jest.restoreAllMocks() + }) describe('when given valid input', () => { it('should send a discord message to the user', async () => { @@ -37,53 +35,46 @@ describe('contact', () => { content: [ { type: 'text', - text: 'Error test message' - } - ] - } - ] + 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; + 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}); + ;(tryCatch as jest.Mock).mockResolvedValue({data: mockReturnData, error: null}) + const results = await contact(mockProps, mockAuth, mockReq) - const results = await contact(mockProps, mockAuth, mockReq); - - expect(results.success).toBe(true); - expect(results.result).toStrictEqual({}); - expect(tryCatch).toBeCalledTimes(1); + expect(results.success).toBe(true) + expect(results.result).toStrictEqual({}) + expect(tryCatch).toBeCalledTimes(1) expect(supabaseUtils.insert).toBeCalledTimes(1) - expect(supabaseUtils.insert).toBeCalledWith( - expect.any(Object), - 'contact', - { - user_id: mockProps.userId, - content: JSON.stringify(mockProps.content) - } - ); + expect(supabaseUtils.insert).toBeCalledWith(expect.any(Object), 'contact', { + user_id: mockProps.userId, + content: JSON.stringify(mockProps.content), + }) + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockDbUser); + await results.continue() - await results.continue(); - - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('select name from users where id = $1'), - [mockProps.userId] - ); - expect(sendDiscordMessage).toBeCalledTimes(1); + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith(sqlMatch('select name from users where id = $1'), [ + mockProps.userId, + ]) + expect(sendDiscordMessage).toBeCalledTimes(1) expect(sendDiscordMessage).toBeCalledWith( expect.stringContaining(`New message from ${mockDbUser.name}`), - 'contact' - ); - }); - }); + 'contact', + ) + }) + }) describe('when an error occurs', () => { it('should throw if the insert function fails', async () => { @@ -96,23 +87,23 @@ describe('contact', () => { content: [ { type: 'text', - text: 'Error test message' - } - ] - } - ] + text: 'Error test message', + }, + ], + }, + ], }, - userId: '123' - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; + userId: '123', + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - (tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}); + ;(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}) - expect(contact(mockProps, mockAuth, mockReq)) - .rejects - .toThrowError('Failed to submit contact message'); - }); + expect(contact(mockProps, mockAuth, mockReq)).rejects.toThrowError( + 'Failed to submit contact message', + ) + }) it('should throw if unable to send discord message', async () => { const mockProps = { @@ -124,48 +115,42 @@ describe('contact', () => { content: [ { type: 'text', - text: 'Error test message' - } - ] - } - ] + 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; + 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}); + ;(tryCatch as jest.Mock).mockResolvedValue({data: mockReturnData, error: null}) - const results = await contact(mockProps, mockAuth, mockReq); + const results = await contact(mockProps, mockAuth, mockReq) - expect(results.success).toBe(true); - expect(results.result).toStrictEqual({}); - expect(tryCatch).toBeCalledTimes(1); + expect(results.success).toBe(true) + expect(results.result).toStrictEqual({}) + expect(tryCatch).toBeCalledTimes(1) expect(supabaseUtils.insert).toBeCalledTimes(1) - expect(supabaseUtils.insert).toBeCalledWith( - expect.any(Object), - 'contact', - { - user_id: mockProps.userId, - content: JSON.stringify(mockProps.content) - } - ); + expect(supabaseUtils.insert).toBeCalledWith(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(() => {}) - (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() - await results.continue(); - - expect(errorSpy).toBeCalledTimes(1); + expect(errorSpy).toBeCalledTimes(1) expect(errorSpy).toBeCalledWith( expect.stringContaining('Failed to send discord contact'), - expect.objectContaining({name: 'Error'}) - ); - }); - }); -}); \ No newline at end of file + expect.objectContaining({name: 'Error'}), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/create-bookmarked-search.unit.test.ts b/backend/api/tests/unit/create-bookmarked-search.unit.test.ts index 33050410..8a2f45b2 100644 --- a/backend/api/tests/unit/create-bookmarked-search.unit.test.ts +++ b/backend/api/tests/unit/create-bookmarked-search.unit.test.ts @@ -1,51 +1,46 @@ -import {sqlMatch} from "common/test-utils"; -import {createBookmarkedSearch} from "api/create-bookmarked-search"; -import {AuthedUser} from "api/helpers/endpoint"; -import * as supabaseInit from "shared/supabase/init"; +import {createBookmarkedSearch} from 'api/create-bookmarked-search' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') describe('createBookmarkedSearch', () => { - let mockPg: any; + let mockPg: any beforeEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks() mockPg = { one: jest.fn(), - }; - - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) afterEach(() => { - jest.restoreAllMocks(); - }); + jest.restoreAllMocks() + }) 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', - search_name: 'mock_search_name' - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockInserted = "mockInsertedReturn"; + search_name: 'mock_search_name', + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockInserted = 'mockInsertedReturn' - (mockPg.one as jest.Mock).mockResolvedValue(mockInserted); + ;(mockPg.one as jest.Mock).mockResolvedValue(mockInserted) - const result = await createBookmarkedSearch(mockProps, mockAuth, mockReq); + const result = await createBookmarkedSearch(mockProps, mockAuth, mockReq) - expect(result).toBe(mockInserted); - expect(mockPg.one).toBeCalledTimes(1); - expect(mockPg.one).toHaveBeenCalledWith( - sqlMatch('INSERT INTO bookmarked_searches'), - [ - mockAuth.uid, - mockProps.search_filters, - mockProps.location, - mockProps.search_name - ] - ); - }); - }); -}); \ No newline at end of file + expect(result).toBe(mockInserted) + expect(mockPg.one).toBeCalledTimes(1) + expect(mockPg.one).toHaveBeenCalledWith(sqlMatch('INSERT INTO bookmarked_searches'), [ + mockAuth.uid, + mockProps.search_filters, + mockProps.location, + mockProps.search_name, + ]) + }) + }) +}) diff --git a/backend/api/tests/unit/create-comment.unit.test.ts b/backend/api/tests/unit/create-comment.unit.test.ts index e1154eba..8b2a414a 100644 --- a/backend/api/tests/unit/create-comment.unit.test.ts +++ b/backend/api/tests/unit/create-comment.unit.test.ts @@ -1,415 +1,401 @@ -import {sqlMatch} from "common/test-utils"; -import * as supabaseInit from "shared/supabase/init"; -import {AuthedUser} from "api/helpers/endpoint"; -import * as sharedUtils from "shared/utils"; -import {createComment} from "api/create-comment"; -import * as notificationPrefs from "common/user-notification-preferences"; -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"; +import {createComment} from 'api/create-comment' +import {AuthedUser} from 'api/helpers/endpoint' +import {convertComment} from 'common/supabase/comment' +import {sqlMatch} from 'common/test-utils' +import * as notificationPrefs from 'common/user-notification-preferences' +import {richTextToString} from 'common/util/parse' +import * as emailHelpers from 'email/functions/helpers' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseNotifications from 'shared/supabase/notifications' +import * as sharedUtils from 'shared/utils' +import * as websocketHelpers from 'shared/websockets/helpers' -jest.mock('shared/supabase/init'); -jest.mock('shared/supabase/notifications'); -jest.mock('email/functions/helpers'); -jest.mock('common/supabase/comment'); -jest.mock('shared/utils'); -jest.mock('common/user-notification-preferences'); -jest.mock('shared/websockets/helpers'); +jest.mock('shared/supabase/init') +jest.mock('shared/supabase/notifications') +jest.mock('email/functions/helpers') +jest.mock('common/supabase/comment') +jest.mock('shared/utils') +jest.mock('common/user-notification-preferences') +jest.mock('shared/websockets/helpers') describe('createComment', () => { - let mockPg: any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - one: jest.fn() - }; + let mockPg: any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + one: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); + afterEach(() => { + jest.restoreAllMocks() + }) - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('when given valid input', () => { - it('should successfully create a comment', async () => { - const mockUserId = { - userId: '123', - blockedUserIds: ['111'] - } - const mockOnUser = {id: '123'} - const mockCreator = { - id: '1234', - name: 'Mock Creator', - username: 'mock.creator.username', - avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false - } - const mockContent = { - content: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'This is the comment text' - } - ] - } - ] + describe('when given valid input', () => { + it('should successfully create a comment', async () => { + const mockUserId = { + userId: '123', + blockedUserIds: ['111'], + } + const mockOnUser = {id: '123'} + const mockCreator = { + id: '1234', + name: 'Mock Creator', + username: 'mock.creator.username', + avatarUrl: 'mock.creator.avatarurl', + isBannedFromPosting: false, + } + const mockContent = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'This is the comment text', }, - userId: '123' - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockReplyToCommentId = {} as any; - const mockComment = {id: 12}; - const mockNotificationDestination = { - sendToBrowser: true, - sendToMobile: false, - sendToEmail: true - }; - const mockProps = { - userId: mockUserId.userId, - content: mockContent.content, - replyToCommentId: mockReplyToCommentId - }; - const mockConvertCommentReturn = 'mockConverComment'; + ], + }, + ], + }, + userId: '123', + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockReplyToCommentId = {} as any + const mockComment = {id: 12} + 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) - .mockResolvedValueOnce(mockOnUser); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValueOnce(mockUserId) - .mockResolvedValueOnce(mockOnUser); - (mockPg.one as jest.Mock).mockResolvedValue(mockComment); - (notificationPrefs.getNotificationDestinationsForUser as jest.Mock) - .mockReturnValue(mockNotificationDestination); - (convertComment as jest.Mock).mockReturnValue(mockConvertCommentReturn); + ;(sharedUtils.getUser as jest.Mock) + .mockResolvedValueOnce(mockCreator) + .mockResolvedValueOnce(mockOnUser) + ;(sharedUtils.getPrivateUser as jest.Mock) + .mockResolvedValueOnce(mockUserId) + .mockResolvedValueOnce(mockOnUser) + ;(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).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( - sqlMatch('insert into profile_comments'), - [ - mockCreator.id, - mockCreator.name, - mockCreator.username, - mockCreator.avatarUrl, - mockProps.userId, - mockProps.content, - mockProps.replyToCommentId - ] - ); - 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); - }); - }); + const results = await createComment(mockProps, mockAuth, mockReq) - 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; - const mockUserId = { - userId: '123', - blockedUserIds: ['111'] - }; - const mockCreator = { - id: '1234', - name: 'Mock Creator', - username: 'mock.creator.username', - avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false - }; - const mockContent = { - content: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'This is the comment text' - } - ] - } - ] + expect(results.status).toBe('success') + expect(sharedUtils.getUser).toBeCalledTimes(2) + 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(sqlMatch('insert into profile_comments'), [ + mockCreator.id, + mockCreator.name, + mockCreator.username, + mockCreator.avatarUrl, + mockProps.userId, + mockProps.content, + mockProps.replyToCommentId, + ]) + 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) + }) + }) + + 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 + const mockUserId = { + userId: '123', + blockedUserIds: ['111'], + } + const mockCreator = { + id: '1234', + name: 'Mock Creator', + username: 'mock.creator.username', + avatarUrl: 'mock.creator.avatarurl', + isBannedFromPosting: false, + } + const mockContent = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'This is the comment text', }, - userId: '123' - }; - const mockProps = { - userId: mockUserId.userId, - content: mockContent.content, - replyToCommentId: mockReplyToCommentId - }; + ], + }, + ], + }, + userId: '123', + } + const mockProps = { + userId: mockUserId.userId, + content: mockContent.content, + replyToCommentId: mockReplyToCommentId, + } - (sharedUtils.getUser as jest.Mock) - .mockResolvedValueOnce(mockCreator) - .mockResolvedValueOnce(false); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(mockUserId); + ;(sharedUtils.getUser as jest.Mock) + .mockResolvedValueOnce(mockCreator) + .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 if there is no account associated with the authId', async () => { - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockReplyToCommentId = {} as any; - const mockUserId = { - userId: '123', - blockedUserIds: ['111'] - }; - const mockContent = { - content: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'This is the comment text' - } - ] - } - ] + 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 + const mockUserId = { + userId: '123', + blockedUserIds: ['111'], + } + const mockContent = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'This is the comment text', }, - userId: '123' - }; - const mockProps = { - userId: mockUserId.userId, - content: mockContent.content, - replyToCommentId: mockReplyToCommentId - }; + ], + }, + ], + }, + userId: '123', + } + const mockProps = { + userId: mockUserId.userId, + content: mockContent.content, + replyToCommentId: mockReplyToCommentId, + } - (sharedUtils.getUser as jest.Mock) - .mockResolvedValueOnce(null); + ;(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 if the account is banned from posting', async () => { - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockReplyToCommentId = {} as any; - const mockUserId = { - userId: '123', - blockedUserIds: ['111'] - }; - const mockCreator = { - id: '1234', - name: 'Mock Creator', - username: 'mock.creator.username', - avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: true - }; - const mockContent = { - content: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'This is the comment text' - } - ] - } - ] + it('throw if the account is banned from posting', async () => { + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockReplyToCommentId = {} as any + const mockUserId = { + userId: '123', + blockedUserIds: ['111'], + } + const mockCreator = { + id: '1234', + name: 'Mock Creator', + username: 'mock.creator.username', + avatarUrl: 'mock.creator.avatarurl', + isBannedFromPosting: true, + } + const mockContent = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'This is the comment text', }, - userId: '123' - }; - const mockProps = { - userId: mockUserId.userId, - content: mockContent.content, - replyToCommentId: mockReplyToCommentId - }; + ], + }, + ], + }, + userId: '123', + } + const mockProps = { + userId: mockUserId.userId, + content: mockContent.content, + replyToCommentId: mockReplyToCommentId, + } - (sharedUtils.getUser as jest.Mock) - .mockResolvedValueOnce(mockCreator); + ;(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 if the other user is not found', async () => { - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockReplyToCommentId = {} as any; - const mockUserId = { - userId: '123', - blockedUserIds: ['111'] - }; - const mockCreator = { - id: '1234', - name: 'Mock Creator', - username: 'mock.creator.username', - avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false - }; - const mockContent = { - content: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'This is the comment text' - } - ] - } - ] + it('throw if the other user is not found', async () => { + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockReplyToCommentId = {} as any + const mockUserId = { + userId: '123', + blockedUserIds: ['111'], + } + const mockCreator = { + id: '1234', + name: 'Mock Creator', + username: 'mock.creator.username', + avatarUrl: 'mock.creator.avatarurl', + isBannedFromPosting: false, + } + const mockContent = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'This is the comment text', }, - userId: '123' - }; - const mockProps = { - userId: mockUserId.userId, - content: mockContent.content, - replyToCommentId: mockReplyToCommentId - }; + ], + }, + ], + }, + userId: '123', + } + const mockProps = { + userId: mockUserId.userId, + content: mockContent.content, + replyToCommentId: mockReplyToCommentId, + } - (sharedUtils.getUser as jest.Mock) - .mockResolvedValueOnce(mockCreator); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(null); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValueOnce(mockCreator) + ;(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 if the user has blocked you', async () => { - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockReplyToCommentId = {} as any; - const mockUserId = { - userId: '123', - blockedUserIds: ['321'] - }; - const mockCreator = { - id: '1234', - name: 'Mock Creator', - username: 'mock.creator.username', - avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false - }; - const mockContent = { - content: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'This is the comment text' - } - ] - } - ] + it('throw if the user has blocked you', async () => { + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockReplyToCommentId = {} as any + const mockUserId = { + userId: '123', + blockedUserIds: ['321'], + } + const mockCreator = { + id: '1234', + name: 'Mock Creator', + username: 'mock.creator.username', + avatarUrl: 'mock.creator.avatarurl', + isBannedFromPosting: false, + } + const mockContent = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'This is the comment text', }, - userId: '123' - }; - const mockProps = { - userId: mockUserId.userId, - content: mockContent.content, - replyToCommentId: mockReplyToCommentId - }; + ], + }, + ], + }, + userId: '123', + } + const mockProps = { + userId: mockUserId.userId, + content: mockContent.content, + replyToCommentId: mockReplyToCommentId, + } - (sharedUtils.getUser as jest.Mock) - .mockResolvedValueOnce(mockCreator); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(mockUserId); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValueOnce(mockCreator) + ;(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 if the comment is too long', async () => { - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockReplyToCommentId = {} as any; - const mockUserId = { - userId: '123', - blockedUserIds: ['111'] - }; - const mockCreator = { - id: '1234', - name: 'Mock Creator', - username: 'mock.creator.username', - avatarUrl: 'mock.creator.avatarurl', - isBannedFromPosting: false - }; - const mockContent = { - content: { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'This '.repeat(30000), - } - ] - } - ] + it('throw if the comment is too long', async () => { + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockReplyToCommentId = {} as any + const mockUserId = { + userId: '123', + blockedUserIds: ['111'], + } + const mockCreator = { + id: '1234', + name: 'Mock Creator', + username: 'mock.creator.username', + avatarUrl: 'mock.creator.avatarurl', + isBannedFromPosting: false, + } + const mockContent = { + content: { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'This '.repeat(30000), }, - userId: '123' - }; - const mockProps = { - userId: mockUserId.userId, - content: mockContent.content, - replyToCommentId: mockReplyToCommentId - }; + ], + }, + ], + }, + userId: '123', + } + const mockProps = { + userId: mockUserId.userId, + content: mockContent.content, + replyToCommentId: mockReplyToCommentId, + } - (sharedUtils.getUser as jest.Mock) - .mockResolvedValueOnce(mockCreator); - (sharedUtils.getPrivateUser as jest.Mock) - .mockResolvedValue(mockUserId); - - expect(createComment( mockProps, mockAuth, mockReq )) - .rejects - .toThrowError('Comment is too long'); - }); - }); -}); \ No newline at end of file + ;(sharedUtils.getUser as jest.Mock).mockResolvedValueOnce(mockCreator) + ;(sharedUtils.getPrivateUser as jest.Mock).mockResolvedValue(mockUserId) + + expect(createComment(mockProps, mockAuth, mockReq)).rejects.toThrowError( + 'Comment is too long', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/create-compatibility-question.unit.test.ts b/backend/api/tests/unit/create-compatibility-question.unit.test.ts index 70a69dd8..fa8283a6 100644 --- a/backend/api/tests/unit/create-compatibility-question.unit.test.ts +++ b/backend/api/tests/unit/create-compatibility-question.unit.test.ts @@ -1,100 +1,93 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/utils'); -jest.mock('shared/supabase/utils'); -jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/init') +jest.mock('shared/utils') +jest.mock('shared/supabase/utils') +jest.mock('common/util/try-catch') -import { createCompatibilityQuestion } from "api/create-compatibility-question"; -import * as supabaseInit from "shared/supabase/init"; -import * as shareUtils from "shared/utils"; -import { tryCatch } from "common/util/try-catch"; -import * as supabaseUtils from "shared/supabase/utils"; -import { AuthedUser } from "api/helpers/endpoint"; +import {createCompatibilityQuestion} from 'api/create-compatibility-question' +import {AuthedUser} from 'api/helpers/endpoint' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUtils from 'shared/supabase/utils' +import * as shareUtils from 'shared/utils' describe('createCompatibilityQuestion', () => { - const mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); + const mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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} + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + 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', + } - 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}; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - 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) + ;(tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}) - (shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); - (tryCatch as jest.Mock).mockResolvedValue({data:mockData, error: null}); + const results = await createCompatibilityQuestion(mockProps, mockAuth, mockReq) - const results = await createCompatibilityQuestion(mockProps, mockAuth, mockReq); - - expect(results.question).toEqual(mockData); - expect(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 - } - ); - }); - }); - 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; + 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, + }) + }) + }) + 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(false); - - expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq)) - .rejects - .toThrowError('Your account was not found'); + ;(shareUtils.getUser as jest.Mock).mockResolvedValue(false) - }); + expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq)).rejects.toThrowError( + 'Your account was not found', + ) + }) - it('throws if unable to create the question', 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; - const mockCreator = { - id: '123', - }; + it('throws if unable to create the question', 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 + const mockCreator = { + id: '123', + } - (shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); - (tryCatch as jest.Mock).mockResolvedValue({data:null, error: Error}); - - expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq)) - .rejects - .toThrowError('Error creating question'); - }); - }); -}); \ No newline at end of file + ;(shareUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) + ;(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}) + + expect(createCompatibilityQuestion(mockProps, mockAuth, mockReq)).rejects.toThrowError( + 'Error creating question', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/create-notification.unit.test.ts b/backend/api/tests/unit/create-notification.unit.test.ts index 1418078b..728144f2 100644 --- a/backend/api/tests/unit/create-notification.unit.test.ts +++ b/backend/api/tests/unit/create-notification.unit.test.ts @@ -1,125 +1,125 @@ -jest.mock('common/util/try-catch'); -jest.mock('shared/supabase/init'); -jest.mock('shared/supabase/notifications'); +jest.mock('common/util/try-catch') +jest.mock('shared/supabase/init') +jest.mock('shared/supabase/notifications') -import * as supabaseInit from "shared/supabase/init"; -import * as createNotificationModules from "api/create-notification"; -import { tryCatch } from "common/util/try-catch"; -import * as supabaseNotifications from "shared/supabase/notifications"; -import { Notification } from "common/notifications"; +import * as createNotificationModules from 'api/create-notification' +import {Notification} from 'common/notifications' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseNotifications from 'shared/supabase/notifications' describe('createNotifications', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - many: jest.fn().mockReturnValue(null) - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + many: jest.fn().mockReturnValue(null), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + describe('when given valid input', () => { + it('should sucessfully create a 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 - describe('when given valid input', () => { - it('should sucessfully create a 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; + ;(tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error: null}) + jest.spyOn(createNotificationModules, 'createNotification') - (tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null}); - jest.spyOn(createNotificationModules, 'createNotification'); - - const results = await createNotificationModules.createNotifications(mockNotification); - - 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) - ); - }); - }); + const results = await createNotificationModules.createNotifications(mockNotification) - describe('when an error occurs', () => { - it('should throw if its unable to fetch users', async () => { - const mockNotification = { - userId: "mockUserId" - } 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), + ) + }) + }) - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + describe('when an error occurs', () => { + it('should throw if its unable to fetch users', async () => { + const mockNotification = { + userId: 'mockUserId', + } as Notification - (tryCatch as jest.Mock).mockResolvedValue({data: null, error:Error}); - - await createNotificationModules.createNotifications(mockNotification); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - expect(errorSpy).toBeCalledWith( - 'Error fetching users', - expect.objectContaining({name: 'Error'}) - ); - }); + ;(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}) - it('should throw if there are no users', async () => { - const mockNotification = { - userId: "mockUserId" - } as Notification; + await createNotificationModules.createNotifications(mockNotification) - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(errorSpy).toBeCalledWith( + 'Error fetching users', + expect.objectContaining({name: 'Error'}), + ) + }) - (tryCatch as jest.Mock).mockResolvedValue({data: null, error:null}); - - await createNotificationModules.createNotifications(mockNotification); - expect(errorSpy).toBeCalledWith('No users found'); - }); + it('should throw if there are no users', async () => { + const mockNotification = { + userId: 'mockUserId', + } as Notification - 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(() => {}) - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + ;(tryCatch as jest.Mock).mockResolvedValue({data: null, error: null}) - (tryCatch as jest.Mock).mockResolvedValue({data: mockUsers, error:null}); - jest.spyOn(createNotificationModules, 'createNotification').mockRejectedValue(new Error('Creation failure')); - - await createNotificationModules.createNotifications(mockNotification); + await createNotificationModules.createNotifications(mockNotification) + expect(errorSpy).toBeCalledWith('No users found') + }) - expect(errorSpy).toBeCalledWith( - 'Failed to create notification', - expect.objectContaining({name: 'Error'}), - mockUsers[0] - ); - }); - }); -}); \ No newline at end of file + 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], + ) + }) + }) +}) diff --git a/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts b/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts index 32e63caa..ccf2badc 100644 --- a/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts +++ b/backend/api/tests/unit/create-private-user-message-channel.unit.test.ts @@ -1,253 +1,250 @@ -import {sqlMatch} from "common/test-utils"; -import {createPrivateUserMessageChannel} from "api/create-private-user-message-channel"; -import * as supabaseInit from "shared/supabase/init"; -import * as sharedUtils from "shared/utils"; -import * as utilArrayModules from "common/util/array"; -import * as privateMessageModules from "api/helpers/private-messages"; -import * as admin from 'firebase-admin'; -import {AuthedUser} from "api/helpers/endpoint"; +import {createPrivateUserMessageChannel} from 'api/create-private-user-message-channel' +import {AuthedUser} from 'api/helpers/endpoint' +import * as privateMessageModules from 'api/helpers/private-messages' +import {sqlMatch} from 'common/test-utils' +import * as utilArrayModules from 'common/util/array' +import * as admin from 'firebase-admin' +import * as supabaseInit from 'shared/supabase/init' +import * as sharedUtils from 'shared/utils' -jest.mock('shared/supabase/init'); -jest.mock('common/util/array'); -jest.mock('api/helpers/private-messages'); -jest.mock('shared/utils'); +jest.mock('shared/supabase/init') +jest.mock('common/util/array') +jest.mock('api/helpers/private-messages') +jest.mock('shared/utils') jest.mock('firebase-admin', () => ({ - auth: jest.fn() -})); + auth: jest.fn(), +})) describe('createPrivateUserMessageChannel', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - one: jest.fn(), - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + one: jest.fn(), + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + ;(admin.auth as jest.Mock).mockReturnValue({ + getUser: jest.fn().mockResolvedValue({emailVerified: true}), + }) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); + describe('when given valid input', () => { + it('should successfully create a private user message channel (currentChannel)', async () => { + const mockBody = { + userIds: ['123'], + } + const mockUserIds = ['123', '321'] + const mockPrivateUsers = [ + { + id: '123', + blockedUserIds: ['111'], + blockedByUserIds: [], + }, + { + id: '321', + blockedUserIds: ['111'], + blockedByUserIds: [], + }, + ] + const mockCurrentChannel = { + channel_id: '444', + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockCreator = { + isBannedFromPosting: false, + } - (admin.auth as jest.Mock).mockReturnValue({ - getUser: jest.fn().mockResolvedValue({emailVerified: true}) - }); - }); - afterEach(() => { - jest.restoreAllMocks() - }); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) + ;(utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers) + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockCurrentChannel) - describe('when given valid input', () => { - it('should successfully create a private user message channel (currentChannel)', async () => { - const mockBody = { - userIds: ["123"] - }; - const mockUserIds = ['123', '321']; - const mockPrivateUsers = [ - { - id: '123', - blockedUserIds: ['111'], - blockedByUserIds: [], - }, - { - id: '321', - blockedUserIds: ['111'], - blockedByUserIds: [], - }, - ]; - const mockCurrentChannel = { - channel_id: "444" - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockCreator = { - isBannedFromPosting: false - }; + const results = await createPrivateUserMessageChannel(mockBody, mockAuth, mockReq) - (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(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('select channel_id\n from private_user_message_channel_members'), + [mockUserIds], + ) + }) - 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(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('select channel_id\n from private_user_message_channel_members'), - [mockUserIds] - ); - }); + it('should successfully create a private user message channel (channel)', async () => { + const mockBody = { + userIds: ['123'], + } + const mockUserIds = ['123', '321'] + const mockPrivateUsers = [ + { + id: '123', + blockedUserIds: ['111'], + blockedByUserIds: [], + }, + { + id: '321', + blockedUserIds: ['111'], + blockedByUserIds: [], + }, + ] + const mockChannel = { + id: '333', + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockCreator = { + isBannedFromPosting: false, + } - it('should successfully create a private user message channel (channel)', async () => { - const mockBody = { - userIds: ["123"] - }; - const mockUserIds = ['123', '321']; - const mockPrivateUsers = [ - { - id: '123', - blockedUserIds: ['111'], - blockedByUserIds: [], - }, - { - id: '321', - blockedUserIds: ['111'], - blockedByUserIds: [], - }, - ]; - const mockChannel = { - id: "333" - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockCreator = { - isBannedFromPosting: false - }; + ;(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) - (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); + 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(mockPg.one).toBeCalledTimes(1); - expect(mockPg.one).toBeCalledWith( - sqlMatch('insert into private_user_message_channels default\n values\n returning id') - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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) - ); - }); - }); - - describe('when an error occurs', () => { - it('should throw if user email is not verified', async () => { - const mockBody = { - userIds: ["123"] - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; + 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(mockPg.one).toBeCalledTimes(1) + expect(mockPg.one).toBeCalledWith( + sqlMatch( + 'insert into private_user_message_channels default\n values\n returning id', + ), + ) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith( + sqlMatch( + '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), + ) + }) + }) - (admin.auth as jest.Mock).mockReturnValue({ - getUser: jest.fn().mockResolvedValue({emailVerified: false}) - }); + describe('when an error occurs', () => { + it('should throw if user email is not verified', async () => { + const mockBody = { + userIds: ['123'], + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError('You must verify your email to contact people.'); + ;(admin.auth as jest.Mock).mockReturnValue({ + getUser: jest.fn().mockResolvedValue({emailVerified: false}), + }) - expect(admin.auth().getUser).toHaveBeenCalledWith(mockAuth.uid); - }); + expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)).rejects.toThrowError( + 'You must verify your email to contact people.', + ) - it('should throw if the user account doesnt exist', async () => { - const mockBody = { - userIds: ["123"] - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; + expect(admin.auth().getUser).toHaveBeenCalledWith(mockAuth.uid) + }) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); - - expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError('Your account was not found'); - }); + it('should throw if the user account doesnt exist', async () => { + const mockBody = { + userIds: ['123'], + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - it('should throw if the authId is banned from posting', async () => { - const mockBody = { - userIds: ["123"] - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockCreator = { - isBannedFromPosting: true - }; + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); + expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)).rejects.toThrowError( + 'Your account was not found', + ) + }) - expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError('You are banned'); - }); + it('should throw if the authId is banned from posting', async () => { + const mockBody = { + userIds: ['123'], + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockCreator = { + isBannedFromPosting: true, + } - it('should throw if the array lengths dont match (privateUsers, userIds)', async () => { - const mockBody = { - userIds: ["123"] - }; - const mockPrivateUsers = [ - { - id: '123', - blockedUserIds: ['111'], - blockedByUserIds: [], - }, - ]; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockCreator = { - isBannedFromPosting: false - }; + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) - (sharedUtils.getUser as jest.Mock) - .mockResolvedValue(mockCreator); - (utilArrayModules.filterDefined as jest.Mock) - .mockReturnValue(mockPrivateUsers); - - expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError(`Private user ${mockAuth.uid} not found`); - }); + expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)).rejects.toThrowError( + 'You are banned', + ) + }) - it('should throw if there is a blocked user in the userId list', async () => { - const mockBody = { - userIds: ["123"] - }; - const mockPrivateUsers = [ - { - id: '123', - blockedUserIds: ['111'], - blockedByUserIds: [], - }, - { - id: '321', - blockedUserIds: ['123'], - blockedByUserIds: [], - }, - ]; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockCreator = { - isBannedFromPosting: false - }; + it('should throw if the array lengths dont match (privateUsers, userIds)', async () => { + const mockBody = { + userIds: ['123'], + } + const mockPrivateUsers = [ + { + id: '123', + blockedUserIds: ['111'], + blockedByUserIds: [], + }, + ] + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockCreator = { + isBannedFromPosting: false, + } - (sharedUtils.getUser as jest.Mock) - .mockResolvedValue(mockCreator); - (utilArrayModules.filterDefined as jest.Mock) - .mockReturnValue(mockPrivateUsers); - - expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError(`One of the users has blocked another user in the list`); - }); - }); -}); \ No newline at end of file + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) + ;(utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers) + + expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)).rejects.toThrowError( + `Private user ${mockAuth.uid} not found`, + ) + }) + + it('should throw if there is a blocked user in the userId list', async () => { + const mockBody = { + userIds: ['123'], + } + const mockPrivateUsers = [ + { + id: '123', + blockedUserIds: ['111'], + blockedByUserIds: [], + }, + { + id: '321', + blockedUserIds: ['123'], + blockedByUserIds: [], + }, + ] + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockCreator = { + isBannedFromPosting: false, + } + + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) + ;(utilArrayModules.filterDefined as jest.Mock).mockReturnValue(mockPrivateUsers) + + expect(createPrivateUserMessageChannel(mockBody, mockAuth, mockReq)).rejects.toThrowError( + `One of the users has blocked another user in the list`, + ) + }) + }) +}) diff --git a/backend/api/tests/unit/create-private-user-message.unit.test.ts b/backend/api/tests/unit/create-private-user-message.unit.test.ts index ca68f400..d5231f54 100644 --- a/backend/api/tests/unit/create-private-user-message.unit.test.ts +++ b/backend/api/tests/unit/create-private-user-message.unit.test.ts @@ -1,102 +1,100 @@ -jest.mock('shared/utils'); -jest.mock('shared/supabase/init'); -jest.mock('api/helpers/private-messages'); +jest.mock('shared/utils') +jest.mock('shared/supabase/init') +jest.mock('api/helpers/private-messages') -import { createPrivateUserMessage } from "api/create-private-user-message"; -import * as sharedUtils from "shared/utils"; -import * as supabaseInit from "shared/supabase/init"; -import * as helpersPrivateMessagesModules from "api/helpers/private-messages"; -import { AuthedUser } from "api/helpers/endpoint"; -import { MAX_COMMENT_JSON_LENGTH } from "api/create-comment"; +import {MAX_COMMENT_JSON_LENGTH} from 'api/create-comment' +import {createPrivateUserMessage} from 'api/create-private-user-message' +import {AuthedUser} from 'api/helpers/endpoint' +import * as helpersPrivateMessagesModules from 'api/helpers/private-messages' +import * as supabaseInit from 'shared/supabase/init' +import * as sharedUtils from 'shared/utils' describe('createPrivateUserMessage', () => { - beforeEach(() => { - jest.resetAllMocks(); - - const mockPg = {} as any; + beforeEach(() => { + jest.resetAllMocks() - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); + const mockPg = {} as any - afterEach(() => { - jest.restoreAllMocks(); - }); + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) - describe('when given valid input', () => { - it('successfully create a private user message', async () => { - const mockBody = { - content: {"": "x".repeat((MAX_COMMENT_JSON_LENGTH-8))}, - channelId: 123 - }; - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockCreator = { - isBannedFromPosting: false - }; + afterEach(() => { + jest.restoreAllMocks() + }) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); + describe('when given valid input', () => { + it('successfully create a private user message', async () => { + const mockBody = { + content: {'': 'x'.repeat(MAX_COMMENT_JSON_LENGTH - 8)}, + channelId: 123, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockCreator = { + isBannedFromPosting: false, + } - await createPrivateUserMessage(mockBody, mockAuth, mockReq); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) - expect(sharedUtils.getUser).toBeCalledTimes(1); - expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid); - expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledTimes(1); - expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledWith( - mockCreator, - mockBody.channelId, - mockBody.content, - expect.any(Object), - 'private' - ); - }); - }); - 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 - } - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - - expect(createPrivateUserMessage(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError(`Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`); - }); + await createPrivateUserMessage(mockBody, mockAuth, mockReq) - it('should throw if the user does not exist', async () => { - const mockBody = { - content: {"mockJson": "mockJsonContent"}, - channelId: 123 - } - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; + expect(sharedUtils.getUser).toBeCalledTimes(1) + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid) + expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledTimes(1) + expect(helpersPrivateMessagesModules.createPrivateUserMessageMain).toBeCalledWith( + mockCreator, + mockBody.channelId, + mockBody.content, + expect.any(Object), + 'private', + ) + }) + }) + 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, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); - - expect(createPrivateUserMessage(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError(`Your account was not found`); - }); + expect(createPrivateUserMessage(mockBody, mockAuth, mockReq)).rejects.toThrowError( + `Message JSON should be less than ${MAX_COMMENT_JSON_LENGTH}`, + ) + }) - it('should throw if the user does not exist', async () => { - const mockBody = { - content: {"mockJson": "mockJsonContent"}, - channelId: 123 - } - const mockAuth = {uid: '321'} as AuthedUser; - const mockReq = {} as any; - const mockCreator = { - isBannedFromPosting: true - }; + it('should throw if the user does not exist', async () => { + const mockBody = { + content: {mockJson: 'mockJsonContent'}, + channelId: 123, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator); - - expect(createPrivateUserMessage(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError(`You are banned`); - }); - }); -}); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) + expect(createPrivateUserMessage(mockBody, mockAuth, mockReq)).rejects.toThrowError( + `Your account was not found`, + ) + }) + + it('should throw if the user does not exist', async () => { + const mockBody = { + content: {mockJson: 'mockJsonContent'}, + channelId: 123, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockCreator = { + isBannedFromPosting: true, + } + + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator) + + expect(createPrivateUserMessage(mockBody, mockAuth, mockReq)).rejects.toThrowError( + `You are banned`, + ) + }) + }) +}) diff --git a/backend/api/tests/unit/create-profile.unit.test.ts b/backend/api/tests/unit/create-profile.unit.test.ts index d717e843..f1b83c05 100644 --- a/backend/api/tests/unit/create-profile.unit.test.ts +++ b/backend/api/tests/unit/create-profile.unit.test.ts @@ -1,379 +1,370 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/utils'); -jest.mock('shared/profiles/parse-photos'); -jest.mock('shared/supabase/users'); -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'); +jest.mock('shared/supabase/init') +jest.mock('shared/utils') +jest.mock('shared/profiles/parse-photos') +jest.mock('shared/supabase/users') +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"; -import * as sharedUtils from "shared/utils"; -import * as supabaseUsers from "shared/supabase/users"; -import * as supabaseUtils from "shared/supabase/utils"; -import {tryCatch} from "common/util/try-catch"; -import {removePinnedUrlFromPhotoUrls} from "shared/profiles/parse-photos"; -import * as sharedAnalytics from "shared/analytics"; -import {sendDiscordMessage} from "common/discord/core"; -import {AuthedUser} from "api/helpers/endpoint"; +import {createProfile} from 'api/create-profile' +import {AuthedUser} from 'api/helpers/endpoint' +import {sendDiscordMessage} from 'common/discord/core' import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as sharedAnalytics from 'shared/analytics' +import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUsers from 'shared/supabase/users' +import * as supabaseUtils from 'shared/supabase/utils' +import * as sharedUtils from 'shared/utils' describe('createProfile', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - one: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + one: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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', + } - 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}) - (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 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( + sqlMatch('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) - expect(results.result).toEqual(mockData); - expect(tryCatch).toBeCalledTimes(2); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('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() - 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', + ) + }) - 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', + } - 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}) - (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 results: any = await createProfile(mockBody, mockAuth, mockReq); + expect(results.result).toEqual(mockData) + ;(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles) - expect(results.result).toEqual(mockData); + await results.continue() - (mockPg.one as jest.Mock).mockReturnValue(mockNProfiles); + expect(mockPg.one).toBeCalledTimes(1) + expect(mockPg.one).toBeCalledWith( + sqlMatch('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', + } - await results.continue(); + ;(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}) - expect(mockPg.one).toBeCalledTimes(1); - expect(mockPg.one).toBeCalledWith( - sqlMatch('SELECT count(*) FROM profiles'), - [], - expect.any(Function) - ); - expect(sendDiscordMessage).toBeCalledTimes(2); - expect(sendDiscordMessage).toHaveBeenNthCalledWith( - 2, - expect.stringContaining(String(mockNProfiles)), - 'general' - ); + const results: any = await createProfile(mockBody, mockAuth, mockReq) - }); - }); - 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" - }; + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - (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}); + ;(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Track error')) - const results: any = await createProfile(mockBody, mockAuth, mockReq); - - const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {}); + await results.continue() - (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'}), + ) + }) - 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', + 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', + } - it('should throw if it failed to send discord new 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: null, error: null}) + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) + ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, 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) - 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(() => {}); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - (sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Sending error')); + ;(sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Sending error')) - await results.continue(); + await results.continue() - expect(errorSpy).toBeCalledWith( - 'Failed to send discord new profile', - expect.objectContaining({name: 'Error'}) - ); - }); + expect(errorSpy).toBeCalledWith( + 'Failed to send discord new profile', + expect.objectContaining({name: 'Error'}), + ) + }) - it('should throw if it failed to send discord user milestone', 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" - }; + it('should throw if it failed to send discord user milestone', 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: null, error: null}); - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); - (tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, 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); + const results: any = await createProfile(mockBody, mockAuth, mockReq) - expect(results.result).toEqual(mockData); - - const errorSpy = jest.spyOn(console , 'error').mockImplementation(() => {}); + expect(results.result).toEqual(mockData) - (sendDiscordMessage as jest.Mock) - .mockResolvedValueOnce(null) - .mockRejectedValueOnce(new Error('Discord error')); - (mockPg.one as jest.Mock).mockReturnValue(mockNProfiles); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - await results.continue(); + ;(sendDiscordMessage as jest.Mock) + .mockResolvedValueOnce(null) + .mockRejectedValueOnce(new Error('Discord error')) + ;(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles) - 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'}) - ); - }); + await results.continue() - 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; + 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'}), + ) + }) - (tryCatch as jest.Mock).mockResolvedValueOnce({data: true, error: null}); + 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 - await expect(createProfile(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError('User already exists'); - }); + ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: true, error: null}) - it('should throw if unable to find the account', 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; + await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError( + 'User already exists', + ) + }) - (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); - (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + it('should throw if unable to find the account', 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 - await expect(createProfile(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError('Your account was not found'); - }); + ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}) + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - it('should throw if anything unexpected happens when creating the user', 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 mockUser = { - createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago - name: "mockName", - username: "mockUserName" - }; + await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError( + 'Your account was not found', + ) + }) - (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}); - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); - (tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: Error}); + it('should throw if anything unexpected happens when creating the user', 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 mockUser = { + createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago + name: 'mockName', + username: 'mockUserName', + } - await expect(createProfile(mockBody, mockAuth, mockReq)) - .rejects - .toThrowError('Error creating user'); - }); - }); -}); \ No newline at end of file + ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null}) + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) + ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: Error}) + + await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError( + 'Error creating user', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/create-user.unit.test.ts b/backend/api/tests/unit/create-user.unit.test.ts index 303ab80a..8ff99edd 100644 --- a/backend/api/tests/unit/create-user.unit.test.ts +++ b/backend/api/tests/unit/create-user.unit.test.ts @@ -1,835 +1,810 @@ -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('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"; + 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 {AuthedUser} from 'api/helpers/endpoint' +import * as apiSetLastTimeOnline from 'api/set-last-online-time' +import * as hostingConstants from 'common/hosting/constants' +import * as supabaseUsers from 'common/supabase/users' +import * as userNotificationPref from 'common/user-notification-preferences' +import * as usernameUtils from 'common/util/clean-username' +import * as objectUtils from 'common/util/object' +import * as emailHelpers from 'email/functions/helpers' +import * as firebaseAdmin from 'firebase-admin' +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 supabaseInit from 'shared/supabase/init' +import * as supabaseUtils from 'shared/supabase/utils' +import * as sharedUtils from 'shared/utils' describe('createUser', () => { - const originalIsLocal = (hostingConstants as any).IS_LOCAL; - let mockPg = {} as any; + 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); - }); + 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, - }); - }); + 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); + 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', + } - 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); + 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 results.continue(); + const results: any = await createUser(mockProps, mockAuth, mockReq) - 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); - }); + 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) - 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" - }; + await results.continue() - const mockGetUser = jest.fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser); + 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) + }) - (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); + 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', + } - expect(supabaseUtils.insert).not.toHaveBeenNthCalledWith( - 2, - expect.any(Object), - 'private_users', - { - id: expect.any(String), - data: expect.objectContaining( - { - initialDeviceToken: mockProps.deviceToken - } - ) - } - ); - - }); + const mockGetUser = jest + .fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser) - 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" - }; + ;(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 mockGetUser = jest.fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser); + await createUser(mockProps, mockAuth, mockReq) - (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(supabaseUtils.insert).not.toHaveBeenNthCalledWith( + 2, + expect.any(Object), + 'private_users', + { + id: expect.any(String), + data: expect.objectContaining({ + initialDeviceToken: mockProps.deviceToken, + }), + }, + ) + }) - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1); - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith( - { - avatarUrl: mockAvatarUrl, - isBannedFromPosting: false, - link: expect.any(Object) - } - ); - - }); + 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', + } - 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) - 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) - (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); + 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, - } - ) - ); - }); + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1) + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith({ + avatarUrl: mockAvatarUrl, + isBannedFromPosting: false, + link: expect.any(Object), + }) + }) - 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" - }; + 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); + 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); + ;(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) - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1); - expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith( - { - avatarUrl: mockFbUser.photoURL, - isBannedFromPosting: true, - link: expect.any(Object) - } - ); - }); - }); + await createUser(mockProps, mockAuth, mockReq) - 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; + 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, + }), + ) + }) - const mockGetUser = jest.fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser); + 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', + } - (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'); - }); + const mockGetUser = jest + .fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser) - 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; + ;(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) - const mockGetUser = jest.fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser); + await createUser(mockProps, mockAuth, mockReq) - (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'); - }); + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1) + expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith({ + avatarUrl: mockFbUser.photoURL, + isBannedFromPosting: true, + link: expect.any(Object), + }) + }) + }) - 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" - }; + 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); + 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); + ;(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) - (sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Tracking failed')); - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(createUser(mockProps, mockAuth, mockReq)).rejects.toThrowError('User already exists') + }) - await results.continue(); + 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 - expect(errorSpy).toHaveBeenCalledWith('Failed to track create profile', expect.any(Error)); - }); + const mockGetUser = jest + .fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser) - 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" - }; + ;(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) - const mockGetUser = jest.fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser); + expect(createUser(mockProps, mockAuth, mockReq)).rejects.toThrowError( + 'Username already taken', + ) + }) - (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); + 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', + } - (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(() => {}); + const mockGetUser = jest + .fn() + .mockResolvedValueOnce(mockFirebaseUser) + .mockResolvedValueOnce(mockFbUser) - await results.continue(); - - expect(errorSpy).toBeCalledWith('Failed to sendWelcomeEmail', expect.any(Error)); - }); + ;(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) - 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 results: any = await createUser(mockProps, mockAuth, mockReq) - const mockGetUser = jest.fn() - .mockResolvedValueOnce(mockFirebaseUser) - .mockResolvedValueOnce(mockFbUser); + ;(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Tracking failed')) + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - (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); + await results.continue() - (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(() => {}); + expect(errorSpy).toHaveBeenCalledWith('Failed to track create profile', expect.any(Error)) + }) - await results.continue(); - - expect(errorSpy).toHaveBeenCalledWith('Failed to set last online time', expect.any(Error)); - }); - }); -}); \ No newline at end of file + 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)) + }) + }) +}) diff --git a/backend/api/tests/unit/create-vote.unit.test.ts b/backend/api/tests/unit/create-vote.unit.test.ts index 3b87da8e..fecf6047 100644 --- a/backend/api/tests/unit/create-vote.unit.test.ts +++ b/backend/api/tests/unit/create-vote.unit.test.ts @@ -1,98 +1,89 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/utils'); -jest.mock('shared/supabase/utils'); -jest.mock('common/util/try-catch'); +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"; +import {createVote} from 'api/create-vote' +import {AuthedUser} from 'api/helpers/endpoint' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUtils from 'shared/supabase/utils' +import * as sharedUtils from 'shared/utils' describe('createVote', () => { - beforeEach(() => { - jest.resetAllMocks(); - const mockPg = {} as any; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg) - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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' - }; + 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}); + ;(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; + 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); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(null) - expect(createVote(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Your account was not found'); - }); + 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; + 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}); + ;(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'); - }); - }); -}); \ No newline at end of file + expect(createVote(mockProps, mockAuth, mockReq)).rejects.toThrow('Error creating question') + }) + }) +}) diff --git a/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts b/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts index 09fee099..811014cb 100644 --- a/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts +++ b/backend/api/tests/unit/delete-bookmarked-search.unit.test.ts @@ -1,44 +1,40 @@ -import {sqlMatch} from "common/test-utils"; -import {deleteBookmarkedSearch} from "api/delete-bookmarked-search"; -import {AuthedUser} from "api/helpers/endpoint"; -import * as supabaseInit from "shared/supabase/init"; +import {deleteBookmarkedSearch} from 'api/delete-bookmarked-search' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') describe('deleteBookmarkedSearch', () => { - let mockPg = {} as any; + let mockPg = {} as any - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - none: jest.fn(), - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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; + 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( - sqlMatch('DELETE FROM bookmarked_searches'), - [ - mockProps.id, - mockAuth.uid - ] - ); - }); - }); -}); \ No newline at end of file + const result = await deleteBookmarkedSearch(mockProps, mockAuth, mockReq) + + expect(result).toStrictEqual({}) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('DELETE FROM bookmarked_searches'), [ + mockProps.id, + mockAuth.uid, + ]) + }) + }) +}) diff --git a/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts b/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts index 731a3239..32c9fcfb 100644 --- a/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts +++ b/backend/api/tests/unit/delete-compatibility-answers.unit.test.ts @@ -1,72 +1,64 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/compatibility/compute-scores'); +jest.mock('shared/supabase/init') +jest.mock('shared/compatibility/compute-scores') -import {sqlMatch} from "common/test-utils"; -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"; +import {deleteCompatibilityAnswer} from 'api/delete-compatibility-answer' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import {recomputeCompatibilityScoresForUser} from 'shared/compatibility/compute-scores' +import * as supabaseInit from 'shared/supabase/init' 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(); - }); + 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; + 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); + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(true) + ;(mockPg.none as jest.Mock).mockResolvedValue(null) - const results: any = await deleteCompatibilityAnswer(mockProps, mockAuth, mockReq); + const results: any = await deleteCompatibilityAnswer(mockProps, mockAuth, mockReq) - expect(results.status).toBe('success'); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch(`SELECT *`), - [mockProps.id, mockAuth.uid] - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('DELETE'), - [mockProps.id, mockAuth.uid] - ); + expect(results.status).toBe('success') + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith(sqlMatch(`SELECT *`), [mockProps.id, mockAuth.uid]) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('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; + 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); + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false) - expect(deleteCompatibilityAnswer(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Item not found'); - }); - }); -}); \ No newline at end of file + expect(deleteCompatibilityAnswer(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'Item not found', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/delete-me.unit.test.ts b/backend/api/tests/unit/delete-me.unit.test.ts index b7847fba..5fa70142 100644 --- a/backend/api/tests/unit/delete-me.unit.test.ts +++ b/backend/api/tests/unit/delete-me.unit.test.ts @@ -1,122 +1,112 @@ -import {sqlMatch} from "common/test-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"; +import {deleteMe} from 'api/delete-me' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import * as firebaseAdmin from 'firebase-admin' +import * as firebaseUtils from 'shared/firebase-utils' +import * as supabaseInit from 'shared/supabase/init' +import * as sharedUtils from 'shared/utils' -jest.mock('shared/supabase/init'); -jest.mock('shared/utils'); +jest.mock('shared/supabase/init') +jest.mock('shared/utils') jest.mock('firebase-admin', () => ({ - auth: jest.fn() -})); -jest.mock('shared/firebase-utils'); + auth: jest.fn(), +})) +jest.mock('shared/firebase-utils') describe('deleteMe', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - none: jest.fn() - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg) - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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; + 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(() => {}); + 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( - sqlMatch('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); + await deleteMe(mockRef, mockAuth, mockRef) - 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; + expect(sharedUtils.getUser).toBeCalledTimes(1) + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('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) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(null); + 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 - expect(deleteMe(mockRef, mockAuth, mockRef)) - .rejects - .toThrow('Your account was not found'); - }); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(null) - 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; + expect(deleteMe(mockRef, mockAuth, mockRef)).rejects.toThrow('Your account was not found') + }) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + 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 - expect(deleteMe(mockRef, mockAuth, mockRef)) - .rejects - .toThrow('Invalid user ID'); - }); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser) - 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; + expect(deleteMe(mockRef, mockAuth, mockRef)).rejects.toThrow('Invalid user ID') + }) - 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(() => {}); + 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 - await deleteMe(mockRef, mockAuth, mockRef); + 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(() => {}) - expect(errorSpy).toBeCalledWith( - expect.stringContaining('Error deleting user from Firebase Auth:'), - expect.any(Error) - ); - }); - }); -}); \ No newline at end of file + await deleteMe(mockRef, mockAuth, mockRef) + + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Error deleting user from Firebase Auth:'), + expect.any(Error), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/delete-message.unit.test.ts b/backend/api/tests/unit/delete-message.unit.test.ts index 7c16f4ca..fe11ae3f 100644 --- a/backend/api/tests/unit/delete-message.unit.test.ts +++ b/backend/api/tests/unit/delete-message.unit.test.ts @@ -1,101 +1,99 @@ -jest.mock('shared/supabase/init'); -jest.mock('api/helpers/private-messages'); +jest.mock('shared/supabase/init') +jest.mock('api/helpers/private-messages') -import {sqlMatch} from "common/test-utils"; -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"; +import {deleteMessage} from 'api/delete-message' +import {AuthedUser} from 'api/helpers/endpoint' +import * as messageHelpers from 'api/helpers/private-messages' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' describe('deleteMessage', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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; + 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); + ;(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(); + const results = await deleteMessage(mockMessageId, mockAuth, mockReq) + expect(results.success).toBeTruthy() - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('SELECT *'), - [mockMessageId.messageId, mockAuth.uid] - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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; + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith(sqlMatch('SELECT *'), [ + mockMessageId.messageId, + mockAuth.uid, + ]) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('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); + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null) - expect(deleteMessage(mockMessageId, mockAuth, mockReq)) - .rejects - .toThrow('Message not found'); - }); + 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; + 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(() => {}); + ;(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); + await deleteMessage(mockMessageId, mockAuth, mockReq) - expect(errorSpy).toBeCalledTimes(1); - expect(errorSpy).toBeCalledWith( - expect.stringContaining('broadcastPrivateMessages failed'), - expect.any(Error) - ); - }); - }); -}); \ No newline at end of file + expect(errorSpy).toBeCalledTimes(1) + expect(errorSpy).toBeCalledWith( + expect.stringContaining('broadcastPrivateMessages failed'), + expect.any(Error), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/edit-message.unit.test.ts b/backend/api/tests/unit/edit-message.unit.test.ts index e2b8cbb5..387c46c7 100644 --- a/backend/api/tests/unit/edit-message.unit.test.ts +++ b/backend/api/tests/unit/edit-message.unit.test.ts @@ -1,127 +1,130 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/encryption'); -jest.mock('api/helpers/private-messages'); +jest.mock('shared/supabase/init') +jest.mock('shared/encryption') +jest.mock('api/helpers/private-messages') -import {sqlMatch} from "common/test-utils"; -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"; +import {editMessage} from 'api/edit-message' +import {AuthedUser} from 'api/helpers/endpoint' +import * as messageHelpers from 'api/helpers/private-messages' +import {sqlMatch} from 'common/test-utils' +import * as encryptionModules from 'shared/encryption' +import * as supabaseInit from 'shared/supabase/init' 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(); - }); + 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 - }; + 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); + ;(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( - sqlMatch('SELECT *'), - [mockProps.messageId, mockAuth.uid] - ); - expect(encryptionModules.encryptMessage).toBeCalledTimes(1); - expect(encryptionModules.encryptMessage).toBeCalledWith(mockPlainTextContent); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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; + const result = await editMessage(mockProps, mockAuth, mockReq) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + expect(result.success).toBeTruthy() + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith(sqlMatch('SELECT *'), [ + mockProps.messageId, + mockAuth.uid, + ]) + expect(encryptionModules.encryptMessage).toBeCalledTimes(1) + expect(encryptionModules.encryptMessage).toBeCalledWith(mockPlainTextContent) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('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, + ) + }) + }) - expect(editMessage(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Message not found or cannot be edited'); - }); + 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 - 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(null) - (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(() => {}); + expect(editMessage(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'Message not found or cannot be edited', + ) + }) - await editMessage(mockProps, mockAuth, mockReq); - expect(errorSpy).toBeCalledTimes(1); - expect(errorSpy).toBeCalledWith( - expect.stringContaining('broadcastPrivateMessages failed'), - expect.any(Error) - ); - }); - }); -}); \ No newline at end of file + 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), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/get-compatibility-questions.unit.test.ts b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts index f29326c4..3f39d986 100644 --- a/backend/api/tests/unit/get-compatibility-questions.unit.test.ts +++ b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts @@ -1,56 +1,53 @@ -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') -import * as compatibililtyQuestionsModules from "api/get-compatibililty-questions"; -import * as supabaseInit from "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 = {locale: 'en'} 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 - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + manyOrNone: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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('FROM compatibility_prompts') - ); - expect(sql).toEqual( - expect.stringContaining('LEFT JOIN compatibility_prompts_translations') - ); - expect(sql).toEqual( - expect.stringContaining('COUNT(ca.question_id)') - ); - }); - }); -}); \ No newline at end of file + describe('when given valid input', () => { + it('should get compatibility questions', async () => { + const mockProps = {locale: 'en'} 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('FROM compatibility_prompts')) + expect(sql).toEqual(expect.stringContaining('LEFT JOIN compatibility_prompts_translations')) + expect(sql).toEqual(expect.stringContaining('COUNT(ca.question_id)')) + }) + }) +}) diff --git a/backend/api/tests/unit/get-current-private-users.unit.test.ts b/backend/api/tests/unit/get-current-private-users.unit.test.ts index 8d83bddb..b326d751 100644 --- a/backend/api/tests/unit/get-current-private-users.unit.test.ts +++ b/backend/api/tests/unit/get-current-private-users.unit.test.ts @@ -1,76 +1,75 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import {getCurrentPrivateUser} from 'api/get-current-private-user' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); -jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/init') +jest.mock('common/util/try-catch') describe('getCurrentPrivateUser', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn() - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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}); + 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( - sqlMatch('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}); + const result = await getCurrentPrivateUser(mockProps, mockAuth, mockReq) - expect(getCurrentPrivateUser(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Error fetching private user data: '); - }); + expect(result).toBe(mockData.data) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('select * from private_users where id = $1'), + [mockAuth.uid], + ) + }) + }) - it('should throw if unable to find user account', async () => { - const mockAuth = { uid: '321' } as AuthedUser; - const mockProps = {} as any; - const mockReq = {} as any; + 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}) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: null, error: null}); + expect(getCurrentPrivateUser(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'Error fetching private user data: ', + ) + }) - expect(getCurrentPrivateUser(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Your account was not found'); - }); - }); -}); \ No newline at end of file + 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', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/get-likes-and-ships.unit.test.ts b/backend/api/tests/unit/get-likes-and-ships.unit.test.ts index d0c7341c..37464c26 100644 --- a/backend/api/tests/unit/get-likes-and-ships.unit.test.ts +++ b/backend/api/tests/unit/get-likes-and-ships.unit.test.ts @@ -1,82 +1,80 @@ -jest.mock('shared/supabase/init'); +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"; +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(); - }); + 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 - }; + 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); + 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] - 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(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) - ); - }); - }); -}); \ No newline at end of file + 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), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/get-me.unit.test.ts b/backend/api/tests/unit/get-me.unit.test.ts index a2d1f175..9a83853c 100644 --- a/backend/api/tests/unit/get-me.unit.test.ts +++ b/backend/api/tests/unit/get-me.unit.test.ts @@ -1,29 +1,29 @@ -jest.mock('api/get-user'); +jest.mock('api/get-user') -import { getMe } from "api/get-me"; -import { getUser } from "api/get-user"; -import { AuthedUser } from "api/helpers/endpoint"; +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(); - }); + 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; + 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); + ;(getUser as jest.Mock).mockResolvedValue(null) - await getMe(mockProps, mockAuth, mockReq); + await getMe(mockProps, mockAuth, mockReq) - expect(getUser).toBeCalledTimes(1); - expect(getUser).toBeCalledWith({id: mockAuth.uid}); - }); - }); -}); \ No newline at end of file + expect(getUser).toBeCalledTimes(1) + expect(getUser).toBeCalledWith({id: mockAuth.uid}) + }) + }) +}) diff --git a/backend/api/tests/unit/get-messages-count.unit.test.ts b/backend/api/tests/unit/get-messages-count.unit.test.ts index 63f14ee4..624c8a91 100644 --- a/backend/api/tests/unit/get-messages-count.unit.test.ts +++ b/backend/api/tests/unit/get-messages-count.unit.test.ts @@ -1,41 +1,37 @@ -import {sqlMatch} from "common/test-utils"; -import {getMessagesCount} from "api/get-messages-count"; -import {AuthedUser} from "api/helpers/endpoint"; -import * as supabaseInit from "shared/supabase/init"; +import {getMessagesCount} from 'api/get-messages-count' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); +jest.mock('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(); - }); + 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"}; + 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); + ;(mockPg.one as jest.Mock).mockResolvedValue(mockResults) - const result: any = await getMessagesCount(mockProps, mockAuth, mockReq); + const result: any = await getMessagesCount(mockProps, mockAuth, mockReq) - expect(result.count).toBe(Number(mockResults.count)); - expect(mockPg.one).toBeCalledTimes(1); - expect(mockPg.one).toBeCalledWith( - sqlMatch('SELECT COUNT(*) AS count'), - expect.any(Object) - ); - }); - }); -}); \ No newline at end of file + expect(result.count).toBe(Number(mockResults.count)) + expect(mockPg.one).toBeCalledTimes(1) + expect(mockPg.one).toBeCalledWith(sqlMatch('SELECT COUNT(*) AS count'), expect.any(Object)) + }) + }) +}) diff --git a/backend/api/tests/unit/get-notifications.unit.test.ts b/backend/api/tests/unit/get-notifications.unit.test.ts index 36bc3a1e..4281200f 100644 --- a/backend/api/tests/unit/get-notifications.unit.test.ts +++ b/backend/api/tests/unit/get-notifications.unit.test.ts @@ -1,45 +1,46 @@ -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') -import {getNotifications} from "api/get-notifications"; -import {AuthedUser} from "api/helpers/endpoint"; -import * as supabaseInit from "shared/supabase/init"; +import {getNotifications} from 'api/get-notifications' +import {AuthedUser} from 'api/helpers/endpoint' import {sqlMatch} from 'common/test-utils' +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(); - }); + 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 user notifications', async () => { - const mockProps = { - limit: 10, - after: 2 - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockNotifications = {} as any; + describe('when given valid input', () => { + it('should send 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); + ;(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( - sqlMatch('from user_notifications un left join notification_templates nt on un.template_id = nt.id'), - [mockAuth.uid, mockProps.limit, mockProps.after], - expect.any(Function) - ); - }); - }); -}); \ No newline at end of file + const result = await getNotifications(mockProps, mockAuth, mockReq) + + expect(result).toBe(mockNotifications) + expect(mockPg.map).toBeCalledTimes(1) + expect(mockPg.map).toBeCalledWith( + sqlMatch( + 'from user_notifications un left join notification_templates nt on un.template_id = nt.id', + ), + [mockAuth.uid, mockProps.limit, mockProps.after], + expect.any(Function), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/get-options.unit.test.ts b/backend/api/tests/unit/get-options.unit.test.ts index d234f948..25ec0266 100644 --- a/backend/api/tests/unit/get-options.unit.test.ts +++ b/backend/api/tests/unit/get-options.unit.test.ts @@ -1,75 +1,68 @@ -jest.mock('shared/supabase/init'); -jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/init') +jest.mock('common/util/try-catch') -import {sqlMatch} from "common/test-utils"; -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"; +import {getOptions} from 'api/get-options' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' describe('getOptions', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - manyOrNone: jest.fn(), - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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" }, - ]; + 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}); + 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); + 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( - sqlMatch('SELECT interests.name') - ); - expect(tryCatch).toBeCalledTimes(1); - }); - }); + expect(result.names).toContain(mockData[0].name) + expect(mockPg.manyOrNone).toBeCalledTimes(1) + expect(mockPg.manyOrNone).toBeCalledWith(sqlMatch('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; + 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); + jest.spyOn(Array.prototype, 'includes').mockReturnValue(false) - expect(getOptions({table: mockTable}, mockAuth, mockReq)) - .rejects - .toThrow('Invalid table'); - }); + 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; + 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}); + 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'); - }); - }); -}); \ No newline at end of file + expect(getOptions({table: mockTable}, mockAuth, mockReq)).rejects.toThrow( + 'Error getting profile options', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/get-private-messages.unit.test.ts b/backend/api/tests/unit/get-private-messages.unit.test.ts index 13fec05c..c0663d68 100644 --- a/backend/api/tests/unit/get-private-messages.unit.test.ts +++ b/backend/api/tests/unit/get-private-messages.unit.test.ts @@ -1,290 +1,296 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import * as getPrivateMessages from 'api/get-private-messages' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); -jest.mock('common/util/try-catch'); -jest.mock('shared/supabase/messages'); +jest.mock('shared/supabase/init') +jest.mock('common/util/try-catch') +jest.mock('shared/supabase/messages') describe('getChannelMemberships', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - map: jest.fn(), - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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); + 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) - expect(mockPg.map).toBeCalledTimes(2); - expect(mockPg.map).toHaveBeenNthCalledWith( - 1, - sqlMatch('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, - sqlMatch('select channel_id, user_id'), - [mockAuth.uid, [mockChannels[0].channel_id]], - expect.any(Function) - ); - }); + const results: any = await getPrivateMessages.getChannelMemberships( + mockProps, + mockAuth, + mockReq, + ) - 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(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, - sqlMatch('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, - sqlMatch('select channel_id, user_id'), - [mockAuth.uid, [mockChannels[0].channel_id]], - expect.any(Function) - ); - }); + expect(mockPg.map).toBeCalledTimes(2) + expect(mockPg.map).toHaveBeenNthCalledWith( + 1, + sqlMatch('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, + sqlMatch('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; + 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) - (mockPg.map as jest.Mock).mockResolvedValueOnce(null); - - const results: any = await getPrivateMessages.getChannelMemberships(mockProps, mockAuth, mockReq); - - console.log(results); - - expect(results).toStrictEqual({ channels: [], memberIdsByChannelId: {} }); + const results: any = await getPrivateMessages.getChannelMemberships( + mockProps, + mockAuth, + mockReq, + ) - expect(mockPg.map).toBeCalledTimes(1); - expect(mockPg.map).toHaveBeenNthCalledWith( - 1, - sqlMatch('select channel_id, notify_after_time, pumcm.created_time, last_updated_time'), - [mockAuth.uid, mockProps.channelId, mockProps.limit], - expect.any(Function) - ); - }); - }); -}); + 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, + sqlMatch( + '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, + sqlMatch('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, + sqlMatch('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; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + map: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (mockPg.map as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}); + 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 - const result = await getPrivateMessages.getChannelMessagesEndpoint(mockProps, mockAuth, mockReq); - - expect(result).toBe(mockData); - expect(mockPg.map).toBeCalledTimes(1); - expect(mockPg.map).toBeCalledWith( - sqlMatch('select *, created_time as created_time_ts'), - [mockProps.channelId, mockAuth.uid, mockProps.limit, mockProps.id], - expect.any(Function) - ); + ;(mockPg.map as jest.Mock).mockResolvedValue(null) + ;(tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}) - }); - }); - - 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; + const result = await getPrivateMessages.getChannelMessagesEndpoint( + mockProps, + mockAuth, + mockReq, + ) - (mockPg.map as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}); + expect(result).toBe(mockData) + expect(mockPg.map).toBeCalledTimes(1) + expect(mockPg.map).toBeCalledWith( + sqlMatch('select *, created_time as created_time_ts'), + [mockProps.channelId, mockAuth.uid, mockProps.limit, mockProps.id], + expect.any(Function), + ) + }) + }) - expect(getPrivateMessages.getChannelMessagesEndpoint(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Error getting messages'); - }); - }); -}); + 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"] - ]; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + map: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (mockPg.map as jest.Mock).mockResolvedValue(mockUnseens); + 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']] - const result = await getPrivateMessages.getLastSeenChannelTime(mockProps, mockAuth, mockReq); + ;(mockPg.map as jest.Mock).mockResolvedValue(mockUnseens) - expect(result).toBe(mockUnseens); - expect(mockPg.map).toBeCalledTimes(1); - expect(mockPg.map).toBeCalledWith( - sqlMatch('select distinct on (channel_id) channel_id, created_time'), - [mockProps.channelIds, mockAuth.uid], - expect.any(Function) - ); + const result = await getPrivateMessages.getLastSeenChannelTime(mockProps, mockAuth, mockReq) - }); - }); -}); + expect(result).toBe(mockUnseens) + expect(mockPg.map).toBeCalledTimes(1) + expect(mockPg.map).toBeCalledWith( + sqlMatch('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; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (mockPg.none as jest.Mock).mockResolvedValue(null); + 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 - await getPrivateMessages.setChannelLastSeenTime(mockProps, mockAuth, mockReq); + ;(mockPg.none as jest.Mock).mockResolvedValue(null) - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('insert into private_user_seen_message_channels (user_id, channel_id)'), - [mockAuth.uid, mockProps.channelId] - ); - }); - }); -}); \ No newline at end of file + await getPrivateMessages.setChannelLastSeenTime(mockProps, mockAuth, mockReq) + + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith( + sqlMatch('insert into private_user_seen_message_channels (user_id, channel_id)'), + [mockAuth.uid, mockProps.channelId], + ) + }) + }) +}) diff --git a/backend/api/tests/unit/get-profile-answers.unit.test.ts b/backend/api/tests/unit/get-profile-answers.unit.test.ts index bed2f091..e3aadb8f 100644 --- a/backend/api/tests/unit/get-profile-answers.unit.test.ts +++ b/backend/api/tests/unit/get-profile-answers.unit.test.ts @@ -1,54 +1,51 @@ -import {sqlMatch} from "common/test-utils"; -import {getProfileAnswers} from "api/get-profile-answers"; -import {AuthedUser} from "api/helpers/endpoint"; -import * as supabaseInit from "shared/supabase/init"; +import {getProfileAnswers} from 'api/get-profile-answers' +import {AuthedUser} from 'api/helpers/endpoint' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') describe('getProfileAnswers', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - manyOrNone: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + manyOrNone: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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, + }, + ] - 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) - (mockPg.manyOrNone as jest.Mock).mockResolvedValue(mockAnswers); + const result: any = await getProfileAnswers(mockProps, mockAuth, mockReq) - 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( - sqlMatch('select * from compatibility_answers'), - [mockProps.userId] - ); - }); - }); -}); \ No newline at end of file + expect(result.status).toBe('success') + expect(result.answers).toBe(mockAnswers) + expect(mockPg.manyOrNone).toBeCalledTimes(1) + expect(mockPg.manyOrNone).toBeCalledWith(sqlMatch('select * from compatibility_answers'), [ + mockProps.userId, + ]) + }) + }) +}) diff --git a/backend/api/tests/unit/get-profiles.unit.test.ts b/backend/api/tests/unit/get-profiles.unit.test.ts index 2e863063..f76b4290 100644 --- a/backend/api/tests/unit/get-profiles.unit.test.ts +++ b/backend/api/tests/unit/get-profiles.unit.test.ts @@ -1,328 +1,327 @@ -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"; +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(() => { - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); + beforeEach(() => { + jest.clearAllMocks() + }) - describe('when given valid input', () => { - it('should successfully return profile information and count', async ()=> { - const mockProfiles = [ - { - diet: ['Jonathon Hammon'], - has_kids: 0 - }, - { - diet: ['Joseph Hammon'], - has_kids: 1 - }, - { - diet: ['Jolene Hammon'], - has_kids: 2, - } - ] as Profile []; - const props = { - limit: 2, - orderBy: "last_online_time" as const, - }; - const mockReq = {} as any; + afterEach(() => { + jest.restoreAllMocks() + }) - jest.spyOn(profilesModule, 'loadProfiles').mockResolvedValue({profiles: mockProfiles, count: 3}); - - const results: any = await profilesModule.getProfiles(props, mockReq, mockReq); + describe('when given valid input', () => { + it('should successfully return profile information and count', async () => { + const mockProfiles = [ + { + diet: ['Jonathon Hammon'], + has_kids: 0, + }, + { + diet: ['Joseph Hammon'], + has_kids: 1, + }, + { + diet: ['Jolene Hammon'], + has_kids: 2, + }, + ] as Profile[] + const props = { + limit: 2, + orderBy: 'last_online_time' as const, + } + const mockReq = {} as any - expect(results.status).toEqual('success'); - expect(results.profiles).toEqual(mockProfiles); - expect(results.profiles[0]).toEqual(mockProfiles[0]); - expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props); - expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1); - }); - }); + jest + .spyOn(profilesModule, 'loadProfiles') + .mockResolvedValue({profiles: mockProfiles, count: 3}) - describe('when an error occurs', () => { - it('should not return profile information', async () => { - jest.spyOn(profilesModule, 'loadProfiles').mockRejectedValue(null); - - const props = { - limit: 2, - orderBy: "last_online_time" as const, - }; - const mockReq = {} as any; - const results: any = await profilesModule.getProfiles(props, mockReq, mockReq); + 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); - }); - }); -}); + expect(results.status).toEqual('success') + expect(results.profiles).toEqual(mockProfiles) + expect(results.profiles[0]).toEqual(mockProfiles[0]) + expect(profilesModule.loadProfiles).toHaveBeenCalledWith(props) + expect(profilesModule.loadProfiles).toHaveBeenCalledTimes(1) + }) + }) + + describe('when an error occurs', () => { + it('should not return profile information', async () => { + jest.spyOn(profilesModule, 'loadProfiles').mockRejectedValue(null) + + const props = { + limit: 2, + orderBy: 'last_online_time' as const, + } + const mockReq = {} as any + 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; - beforeEach(() => { - jest.clearAllMocks(); - mockPg = { - map: jest.fn(), - one: jest.fn() - }; - - jest.spyOn(supabaseInit, 'createSupabaseDirectClient') - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + let mockPg: any + beforeEach(() => { + jest.clearAllMocks() + mockPg = { + map: jest.fn(), + one: jest.fn(), + } - 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, - }; + jest.spyOn(supabaseInit, 'createSupabaseDirectClient').mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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'); + 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, + } - await profilesModule.loadProfiles(mockProps); + ;(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') - 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'], - }); + await profilesModule.loadProfiles(mockProps) - 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] - 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`); - }); + 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 prefer gender filter', async () => { - await profilesModule.loadProfiles({ - pref_gender: ['female'], - }); + it('that contains a gender filter', async () => { + await profilesModule.loadProfiles({ + genders: ['Electrical_gender'], + }) - 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] - it('that contains a minimum age filter', async () => { - await profilesModule.loadProfiles({ - pref_age_min: 20, - }); + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`gender`) + expect(query).toContain(`Electrical_gender`) + }) - 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 education level filter', async () => { + await profilesModule.loadProfiles({ + education_levels: ['High School'], + }) - it('that contains a maximum age filter', async () => { - await profilesModule.loadProfiles({ - pref_age_max: 40, - }); + const [query, _values, _cb] = mockPg.map.mock.calls[0] - const [query, values, cb] = mockPg.map.mock.calls[0] - - expect(mockPg.map.mock.calls).toHaveLength(1) - expect(query).toContain(`age`); - expect(query).toContain(`<= 40`); - }); + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`education_level`) + expect(query).toContain(`High School`) + }) - it('that contains a minimum drinks per month filter', async () => { - await profilesModule.loadProfiles({ - drinks_min: 4, - }); + it('that contains a prefer gender filter', async () => { + await profilesModule.loadProfiles({ + pref_gender: ['female'], + }) - 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] + console.log(query) - it('that contains a maximum drinks per month filter', async () => { - await profilesModule.loadProfiles({ - drinks_max: 20, - }); + 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(`drinks_per_month`); - expect(query).toContain('20'); - }); + it('that contains a minimum age filter', async () => { + await profilesModule.loadProfiles({ + pref_age_min: 20, + }) - 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] - 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'); - }); + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`age`) + expect(query).toContain(`>= 20`) + }) - it('that contains a romantic style filter', async () => { - await profilesModule.loadProfiles({ - pref_romantic_styles: ['Sexy'], - }); + 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(`pref_romantic_styles`); - expect(query).toContain('Sexy'); - }); + const [query, _values, _cb] = mockPg.map.mock.calls[0] - it('that contains a diet filter', async () => { - await profilesModule.loadProfiles({ - diet: ['Glutton'], - }); + 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(`diet`); - expect(query).toContain('Glutton'); - }); + it('that contains a minimum drinks per month filter', async () => { + await profilesModule.loadProfiles({ + drinks_min: 4, + }) - 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] - 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'); - }); + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`drinks_per_month`) + expect(query).toContain('4') + }) - it('that contains a religion filter', async () => { - await profilesModule.loadProfiles({ - religion: ['The blood god'], - }); + 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(`religion`); - expect(query).toContain('The blood god'); - }); + const [query, _values, _cb] = mockPg.map.mock.calls[0] - it('that contains a has kids filter', async () => { - await profilesModule.loadProfiles({ - has_kids: 3, - }); + 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(`has_kids`); - expect(query).toContain('> 0'); - }); + it('that contains a relationship style filter', async () => { + await profilesModule.loadProfiles({ + pref_relation_styles: ['Chill and relaxing'], + }) - 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; + const [query, _values, _cb] = mockPg.map.mock.calls[0] - (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}); - }); - }); - }); + expect(mockPg.map.mock.calls).toHaveLength(1) + expect(query).toContain(`pref_relation_styles`) + expect(query).toContain('Chill and relaxing') + }) - describe('when an error occurs', () => { - it('throw if there is no compatability', async () => { - const props = { - orderBy: 'compatibility_score' - } + it('that contains a romantic style filter', async () => { + await profilesModule.loadProfiles({ + pref_romantic_styles: ['Sexy'], + }) - expect(profilesModule.loadProfiles(props)) - .rejects - .toThrowError('Incompatible with user ID') - }); - }); -}); \ No newline at end of file + 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 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(`diet`) + expect(query).toContain('Glutton') + }) + + 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(`political_beliefs`) + expect(query).toContain('For the people') + }) + + 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(`religion`) + expect(query).toContain('The blood god') + }) + + it('that contains a has kids filter', async () => { + await profilesModule.loadProfiles({ + has_kids: 3, + }) + + 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') + }) + + 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 + + ;(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}) + }) + }) + }) + + 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') + }) + }) +}) diff --git a/backend/api/tests/unit/get-supabase-token.unit.test.ts b/backend/api/tests/unit/get-supabase-token.unit.test.ts index a695ba4c..0a294b71 100644 --- a/backend/api/tests/unit/get-supabase-token.unit.test.ts +++ b/backend/api/tests/unit/get-supabase-token.unit.test.ts @@ -1,49 +1,40 @@ -jest.mock('jsonwebtoken'); - +jest.mock('jsonwebtoken') describe.skip('getSupabaseToken', () => { - // const originalSupabaseJwtSecret = process.env.SUPABASE_JWT_SECRET - // const originalInstanceId = constants.ENV_CONFIG.supabaseInstanceId - // const originalProjectId = constants.ENV_CONFIG.firebaseConfig.projectId - - // describe('should', () => { - // beforeEach(() => { - // jest.resetAllMocks(); - - // process.env.SUPABASE_JWT_SECRET = 'test-jwt-secret-123'; - // constants.ENV_CONFIG.supabaseInstanceId = 'test-instance-id'; - // constants.ENV_CONFIG.firebaseConfig.projectId = 'test-project-id'; - - // (jsonWebtokenModules.sign as jest.Mock).mockReturnValue('fake-jwt-token-abc123'); - // }); - - // afterEach(() => { - // if (originalSupabaseJwtSecret === undefined) { - // delete process.env.SUPABASE_JWT_SECRET; - // } else { - // process.env.SUPABASE_JWT_SECRET = originalSupabaseJwtSecret; - // } - // constants.ENV_CONFIG.supabaseInstanceId = originalInstanceId; - // constants.ENV_CONFIG.firebaseConfig.projectId = originalProjectId; - - // jest.restoreAllMocks(); - // }); - - // it('successfully generate a JTW token with correct parameters', async () => { - // const mockParams = {} as any; - // const mockAuth = {uid: '321'} as AuthedUser; - // const result = await getSupabaseToken(mockParams, mockAuth, mockParams) - - // expect(result).toEqual({ - // jwt: 'fake-jwt-token-abc123' - // }) - // }) - // }); -}); + // const originalSupabaseJwtSecret = process.env.SUPABASE_JWT_SECRET + // const originalInstanceId = constants.ENV_CONFIG.supabaseInstanceId + // const originalProjectId = constants.ENV_CONFIG.firebaseConfig.projectId + // describe('should', () => { + // beforeEach(() => { + // jest.resetAllMocks(); + // process.env.SUPABASE_JWT_SECRET = 'test-jwt-secret-123'; + // constants.ENV_CONFIG.supabaseInstanceId = 'test-instance-id'; + // constants.ENV_CONFIG.firebaseConfig.projectId = 'test-project-id'; + // (jsonWebtokenModules.sign as jest.Mock).mockReturnValue('fake-jwt-token-abc123'); + // }); + // afterEach(() => { + // if (originalSupabaseJwtSecret === undefined) { + // delete process.env.SUPABASE_JWT_SECRET; + // } else { + // process.env.SUPABASE_JWT_SECRET = originalSupabaseJwtSecret; + // } + // constants.ENV_CONFIG.supabaseInstanceId = originalInstanceId; + // constants.ENV_CONFIG.firebaseConfig.projectId = originalProjectId; + // jest.restoreAllMocks(); + // }); + // it('successfully generate a JTW token with correct parameters', async () => { + // const mockParams = {} as any; + // const mockAuth = {uid: '321'} as AuthedUser; + // const result = await getSupabaseToken(mockParams, mockAuth, mockParams) + // expect(result).toEqual({ + // jwt: 'fake-jwt-token-abc123' + // }) + // }) + // }); +}) describe('getCompatibleProfiles', () => { - it('skip', async () => { - console.log('This needs tests'); - - }) -}) \ No newline at end of file + it('skip', async () => { + console.log('This needs tests') + }) +}) diff --git a/backend/api/tests/unit/get-users.unit.test.ts b/backend/api/tests/unit/get-users.unit.test.ts index 112095e4..0970053a 100644 --- a/backend/api/tests/unit/get-users.unit.test.ts +++ b/backend/api/tests/unit/get-users.unit.test.ts @@ -1,91 +1,85 @@ -import {sqlMatch} from "common/test-utils"; -import {getUser} from "api/get-user"; -import * as supabaseInit from "shared/supabase/init"; -import {toUserAPIResponse} from "common/api/user-types"; +import {getUser} from 'api/get-user' +import {toUserAPIResponse} from 'common/api/user-types' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' -jest.mock("shared/supabase/init"); -jest.mock("common/supabase/users"); -jest.mock("common/api/user-types"); +jest.mock('shared/supabase/init') +jest.mock('common/supabase/users') +jest.mock('common/api/user-types') -describe('getUser', () =>{ - let mockPg: any; +describe('getUser', () => { + let mockPg: any - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - }; + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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 - 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; + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser) + ;(toUserAPIResponse as jest.Mock).mockReturnValue('mockApiResponse') - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser); - (toUserAPIResponse as jest.Mock).mockReturnValue('mockApiResponse'); - - const result = await getUser(mockProps); + const result = await getUser(mockProps) - expect(result).toBe('mockApiResponse'); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('select * from users'), - [mockProps.id], - expect.any(Function) - ); - expect(toUserAPIResponse).toBeCalledTimes(1); - expect(toUserAPIResponse).toBeCalledWith(mockUser); - }); - }); + expect(result).toBe('mockApiResponse') + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('select * from users'), + [mockProps.id], + expect.any(Function), + ) + expect(toUserAPIResponse).toBeCalledTimes(1) + expect(toUserAPIResponse).toBeCalledWith(mockUser) + }) + }) - describe('when fetching by username', () => { - it('should fetch user successfully by username', async () => { - const mockProps = {username: "mockUsername"}; - const mockUser = {} as any; + describe('when fetching by username', () => { + it('should fetch user successfully by username', async () => { + const mockProps = {username: 'mockUsername'} + const mockUser = {} as any - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser); - - await getUser(mockProps) - - expect(mockPg.oneOrNone).toHaveBeenCalledWith( - sqlMatch('where username = $1'), - [mockProps.username], - expect.any(Function) - ); - }); - }); - }); + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockUser) - 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"}; + await getUser(mockProps) - (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"}; + expect(mockPg.oneOrNone).toHaveBeenCalledWith( + sqlMatch('where username = $1'), + [mockProps.username], + expect.any(Function), + ) + }) + }) + }) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); - - expect(getUser(mockProps)) - .rejects - .toThrow('User not found'); - }); - }); - }); -}); \ No newline at end of file + 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') + }) + }) + }) +}) diff --git a/backend/api/tests/unit/has-free-like.unit.test.ts b/backend/api/tests/unit/has-free-like.unit.test.ts index 88018570..8813044a 100644 --- a/backend/api/tests/unit/has-free-like.unit.test.ts +++ b/backend/api/tests/unit/has-free-like.unit.test.ts @@ -1,57 +1,54 @@ -jest.mock('shared/supabase/init'); +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"; +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() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 - 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) - jest.spyOn( freeLikeModule, 'getHasFreeLike'); - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + const result: any = await freeLikeModule.hasFreeLike(mockProps, mockAuth, mockReq) - 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, + ]) + }) - 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 - 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) - jest.spyOn( freeLikeModule, 'getHasFreeLike'); - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(true); + const result: any = await freeLikeModule.hasFreeLike(mockProps, mockAuth, mockReq) - const result: any = await freeLikeModule.hasFreeLike(mockProps, mockAuth, mockReq); - - expect(result.hasFreeLike).toBeFalsy(); - }); - }); -}); + expect(result.hasFreeLike).toBeFalsy() + }) + }) +}) diff --git a/backend/api/tests/unit/health-unit.test.ts b/backend/api/tests/unit/health-unit.test.ts index cc0fe452..1fd0a9d7 100644 --- a/backend/api/tests/unit/health-unit.test.ts +++ b/backend/api/tests/unit/health-unit.test.ts @@ -1,16 +1,16 @@ -import { health } from "api/health"; -import { AuthedUser } from "api/helpers/endpoint"; +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; + 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); - }); - }); -}); \ No newline at end of file + const result: any = await health(mockProps, mockAuth, mockReq) + expect(result.message).toBe('Server is working.') + expect(result.uid).toBe(mockAuth.uid) + }) + }) +}) diff --git a/backend/api/tests/unit/hide-comment.unit.test.ts b/backend/api/tests/unit/hide-comment.unit.test.ts index 76157486..e4fd096e 100644 --- a/backend/api/tests/unit/hide-comment.unit.test.ts +++ b/backend/api/tests/unit/hide-comment.unit.test.ts @@ -1,166 +1,162 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {hideComment} from 'api/hide-comment' +import * as envConsts from 'common/envs/constants' +import {convertComment} from 'common/supabase/comment' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' +import * as websocketHelpers from 'shared/websockets/helpers' -jest.mock('shared/supabase/init'); -jest.mock('common/supabase/comment'); -jest.mock('shared/websockets/helpers'); +jest.mock('shared/supabase/init') +jest.mock('common/supabase/comment') +jest.mock('shared/websockets/helpers') describe('hideComment', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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' - 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) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment); - jest.spyOn(envConsts, 'isAdminId').mockReturnValue(true); - (convertComment as jest.Mock).mockReturnValue(mockConvertedComment); + await hideComment(mockProps, mockAuth, mockReq) - await hideComment(mockProps, mockAuth, mockReq); + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('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) + }) - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('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', + } - 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) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment); - jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false); + await hideComment(mockProps, mockAuth, mockReq) + }) - 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', + } - 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) - (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 - 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) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + expect(hideComment(mockProps, mockAuth, mockReq)).rejects.toThrow('Comment not found') + }) - 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", - }; + 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); + ;(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'); - }); - }); -}); \ No newline at end of file + expect(hideComment(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'You are not allowed to hide this comment', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/leave-private-user-message-channel.unit.test.ts b/backend/api/tests/unit/leave-private-user-message-channel.unit.test.ts index 2df647f4..87287574 100644 --- a/backend/api/tests/unit/leave-private-user-message-channel.unit.test.ts +++ b/backend/api/tests/unit/leave-private-user-message-channel.unit.test.ts @@ -1,95 +1,93 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/utils'); -jest.mock('api/helpers/private-messages'); +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"; +import {AuthedUser} from 'api/helpers/endpoint' +import * as messageHelpers from 'api/helpers/private-messages' +import {leavePrivateUserMessageChannel} from 'api/leave-private-user-message-channel' import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' +import * as sharedUtils from 'shared/utils' 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(); - }); + 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"; + 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); + ;(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); + 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( - sqlMatch('select status from private_user_message_channel_members'), - [mockProps.channelId, mockAuth.uid] - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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; + 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( + sqlMatch('select status from private_user_message_channel_members'), + [mockProps.channelId, mockAuth.uid], + ) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('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); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - expect(leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Your account was not found'); - }); + 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" }; + 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); + ;(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'); - }); - }); -}); \ No newline at end of file + expect(leavePrivateUserMessageChannel(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'You are not authorized to post to this channel', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/like-profile.unit.test.ts b/backend/api/tests/unit/like-profile.unit.test.ts index ac1fe4c1..ed87e8ea 100644 --- a/backend/api/tests/unit/like-profile.unit.test.ts +++ b/backend/api/tests/unit/like-profile.unit.test.ts @@ -1,188 +1,180 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import * as likeModules from 'api/has-free-like' +import {AuthedUser} from 'api/helpers/endpoint' +import {likeProfile} from 'api/like-profile' +import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as profileNotifiction from 'shared/create-profile-notification' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); -jest.mock('shared/create-profile-notification'); -jest.mock('api/has-free-like'); -jest.mock('common/util/try-catch'); +jest.mock('shared/supabase/init') +jest.mock('shared/create-profile-notification') +jest.mock('api/has-free-like') +jest.mock('common/util/try-catch') 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(); - }); + 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" - }; + 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); + ;(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); + const result: any = await likeProfile(mockProps, mockAuth, mockReq) - expect(result.result.status).toBe('success'); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('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( - sqlMatch('insert into profile_likes (creator_id, target_id) values ($1, $2) returning *'), - [mockAuth.uid, mockProps.targetUserId] - ); + expect(result.result.status).toBe('success') + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('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( + sqlMatch('insert into profile_likes (creator_id, target_id) values ($1, $2) returning *'), + [mockAuth.uid, mockProps.targetUserId], + ) + ;(profileNotifiction.createProfileLikeNotification as jest.Mock).mockResolvedValue(null) - (profileNotifiction.createProfileLikeNotification as jest.Mock).mockResolvedValue(null); + await result.continue() - await result.continue(); + expect(profileNotifiction.createProfileLikeNotification).toBeCalledTimes(1) + expect(profileNotifiction.createProfileLikeNotification).toBeCalledWith(mockData) + }) - 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 - 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}) - (tryCatch as jest.Mock).mockResolvedValue({data: true}); + const result: any = await likeProfile(mockProps, mockAuth, mockReq) - const result: any = await likeProfile(mockProps, mockAuth, mockReq); + expect(result.status).toBe('success') + }) - 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', + } - 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}) - (tryCatch as jest.Mock).mockResolvedValue({data: mockData, error: null}); + const result: any = await likeProfile(mockProps, mockAuth, mockReq) - const result: any = await likeProfile(mockProps, mockAuth, mockReq); + expect(result.status).toBe('success') + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith( + sqlMatch('delete from profile_likes where creator_id = $1 and target_id = $2'), + [mockAuth.uid, mockProps.targetUserId], + ) + }) + }) - expect(result.status).toBe('success'); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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', + } - 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}) - (tryCatch as jest.Mock) - .mockResolvedValueOnce({data: mockData, error: Error}); + expect(likeProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Failed to remove like: ') + }) - 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', + } - 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) - (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', + } - expect(likeProfile(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('You already liked someone today!'); - }); + ;(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) - 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: '); - }); - }); -}); \ No newline at end of file + expect(likeProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Failed to add like: ') + }) + }) +}) diff --git a/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts b/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts index 62d34fff..7dbfd89c 100644 --- a/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts +++ b/backend/api/tests/unit/mark-all-notifications-read.unit.test.ts @@ -1,38 +1,33 @@ -jest.mock('shared/supabase/init'); +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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {markAllNotifsRead} from 'api/mark-all-notifications-read' import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' describe('markAllNotifsRead', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 - 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) - await markAllNotifsRead(mockProps, mockAuth, mockReq); - - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('update user_notifications'), - [mockAuth.uid] - ); - }); - }); -}); \ No newline at end of file + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('update user_notifications'), [mockAuth.uid]) + }) + }) +}) diff --git a/backend/api/tests/unit/react-to-message.unit.test.ts b/backend/api/tests/unit/react-to-message.unit.test.ts index 7dd2dcf4..9ed6ba8f 100644 --- a/backend/api/tests/unit/react-to-message.unit.test.ts +++ b/backend/api/tests/unit/react-to-message.unit.test.ts @@ -1,140 +1,128 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import * as messageHelpers from 'api/helpers/private-messages' +import {reactToMessage} from 'api/react-to-message' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); -jest.mock('api/helpers/private-messages'); +jest.mock('shared/supabase/init') +jest.mock('api/helpers/private-messages') 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(); - }); + 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"}; + 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); + ;(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( - sqlMatch('SELECT *') - ); - expect(sql).toEqual( - sqlMatch('FROM private_user_message_channel_members m') - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(params1).toEqual([mockProps.reaction, mockAuth.uid, mockProps.messageId]) - expect(sql1).toEqual( - sqlMatch('UPDATE private_user_messages') - ); - expect(sql1).toEqual( - sqlMatch('SET reactions =') - ); - expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1); - expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith( - expect.any(Object), - mockMessage.channel_id, - mockAuth.uid - ); - }); + const result = await reactToMessage(mockProps, mockAuth, mockReq) + const [sql, params] = mockPg.oneOrNone.mock.calls[0] + const [sql1, params1] = mockPg.none.mock.calls[0] - 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"}; + expect(result.success).toBeTruthy() + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(params).toEqual([mockAuth.uid, mockProps.messageId]) + expect(sql).toEqual(sqlMatch('SELECT *')) + expect(sql).toEqual(sqlMatch('FROM private_user_message_channel_members m')) + expect(mockPg.none).toBeCalledTimes(1) + expect(params1).toEqual([mockProps.reaction, mockAuth.uid, mockProps.messageId]) + expect(sql1).toEqual(sqlMatch('UPDATE private_user_messages')) + expect(sql1).toEqual(sqlMatch('SET reactions =')) + expect(messageHelpers.broadcastPrivateMessages).toBeCalledTimes(1) + expect(messageHelpers.broadcastPrivateMessages).toBeCalledWith( + expect.any(Object), + mockMessage.channel_id, + mockAuth.uid, + ) + }) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockMessage); - (mockPg.none as jest.Mock).mockResolvedValue(null); - (messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null); + 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'} - 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( - sqlMatch('UPDATE private_user_messages') - ); - expect(sql1).toEqual( - sqlMatch('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(mockMessage) + ;(mockPg.none as jest.Mock).mockResolvedValue(null) + ;(messageHelpers.broadcastPrivateMessages as jest.Mock).mockResolvedValue(null) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); + const result = await reactToMessage(mockProps, mockAuth, mockReq) + const [_sql, _params] = mockPg.oneOrNone.mock.calls[0] + const [sql1, params1] = mockPg.none.mock.calls[0] - expect(reactToMessage(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Not authorized to react to this message'); - }); + 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(sqlMatch('UPDATE private_user_messages')) + expect(sql1).toEqual(sqlMatch('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 - 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(false) - (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(() => {}); + expect(reactToMessage(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'Not authorized to react to this message', + ) + }) - await reactToMessage(mockProps, mockAuth, mockReq); + 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'} - expect(errorSpy).toBeCalledWith( - expect.stringContaining('broadcastPrivateMessages failed'), - expect.any(Error) - ); - }); - }); -}); \ No newline at end of file + ;(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), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/remove-pinned-photo.unit.test.ts b/backend/api/tests/unit/remove-pinned-photo.unit.test.ts index 2485d6c5..9d80e4f9 100644 --- a/backend/api/tests/unit/remove-pinned-photo.unit.test.ts +++ b/backend/api/tests/unit/remove-pinned-photo.unit.test.ts @@ -1,76 +1,74 @@ -jest.mock('shared/supabase/init'); -jest.mock('common/envs/constants'); -jest.mock('common/util/try-catch'); +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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {removePinnedPhoto} from 'api/remove-pinned-photo' +import * as envConstants from 'common/envs/constants' import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' describe('removePinnedPhoto', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 - 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}) - 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) - 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( + sqlMatch('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 - expect(result.success).toBeTruthy(); - expect(envConstants.isAdminId).toBeCalledTimes(1); - expect(envConstants.isAdminId).toBeCalledWith(mockAuth.uid); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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) - jest.spyOn(envConstants, 'isAdminId').mockReturnValue(false); + expect(removePinnedPhoto(mockBody, mockAuth, mockReq)).rejects.toThrow( + 'Only admins can remove pinned photo', + ) + }) - 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 - 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}) - 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'); - }); - }); -}); \ No newline at end of file + expect(removePinnedPhoto(mockBody, mockAuth, mockReq)).rejects.toThrow( + 'Failed to remove pinned photo', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/report.unit.test.ts b/backend/api/tests/unit/report.unit.test.ts index 8d3af70d..07b7c784 100644 --- a/backend/api/tests/unit/report.unit.test.ts +++ b/backend/api/tests/unit/report.unit.test.ts @@ -1,226 +1,212 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {report} from 'api/report' +import {sendDiscordMessage} from 'common/discord/core' +import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUtils from 'shared/supabase/utils' -jest.mock('shared/supabase/init'); -jest.mock('common/util/try-catch'); -jest.mock('shared/supabase/utils'); -jest.mock('common/discord/core'); +jest.mock('shared/supabase/init') +jest.mock('common/util/try-catch') +jest.mock('shared/supabase/utils') +jest.mock('common/discord/core') describe('report', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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', + } - 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}) - (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + const result = await report(mockBody, mockAuth, mockReq) - 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) - expect(result.success).toBeTruthy(); - expect(result.result).toStrictEqual({}); + await result.continue() - (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); + expect(mockPg.oneOrNone).toBeCalledTimes(2) + expect(mockPg.oneOrNone).toHaveBeenNthCalledWith( + 1, + sqlMatch('select * from users where id = $1'), + [mockAuth.uid], + ) + expect(mockPg.oneOrNone).toHaveBeenNthCalledWith( + 2, + sqlMatch('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 - await result.continue(); + ;(supabaseUtils.insert as jest.Mock).mockResolvedValue(null) + ;(tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}) - expect(mockPg.oneOrNone).toBeCalledTimes(2); - expect(mockPg.oneOrNone).toHaveBeenNthCalledWith( - 1, - sqlMatch('select * from users where id = $1'), - [mockAuth.uid] - ); - expect(mockPg.oneOrNone).toHaveBeenNthCalledWith( - 2, - sqlMatch('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; + expect(report(mockBody, mockAuth, mockReq)).rejects.toThrow('Failed to create report: ') + }) - (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: null, error: Error}); + 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 - expect(report(mockBody, mockAuth, mockReq)) - .rejects - .toThrow('Failed to create report: '); - }); + ;(supabaseUtils.insert as jest.Mock).mockResolvedValue(null) + ;(tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}) - 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; + const result = await report(mockBody, mockAuth, mockReq) - (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + ;(mockPg.oneOrNone as jest.Mock).mockReturnValueOnce(null) + ;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: Error}) - const result = await report(mockBody, mockAuth, mockReq); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - (mockPg.oneOrNone as jest.Mock) - .mockReturnValueOnce(null); - (tryCatch as jest.Mock) - .mockResolvedValueOnce({data: null, error: Error}); + await result.continue() - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Failed to get user for report'), + expect.objectContaining({name: 'Error'}), + ) + }) - await result.continue(); + 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', + } - expect(errorSpy).toBeCalledWith( - expect.stringContaining('Failed to get user for report'), - expect.objectContaining({name: 'Error'}) - ); - }); + ;(supabaseUtils.insert as jest.Mock).mockResolvedValue(null) + ;(tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}) - 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", - }; + const result = await report(mockBody, mockAuth, mockReq) - (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + ;(mockPg.oneOrNone as jest.Mock).mockReturnValueOnce(null).mockReturnValueOnce(null) + ;(tryCatch as jest.Mock) + .mockResolvedValueOnce({data: mockReporter, error: null}) + .mockResolvedValueOnce({data: null, error: Error}) - const result = await report(mockBody, mockAuth, mockReq); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - (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() - await result.continue(); + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Failed to get reported user for report'), + expect.objectContaining({name: 'Error'}), + ) + }) - 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', + } - 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}) - (supabaseUtils.insert as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: mockBody, error: null}); + const result = await report(mockBody, mockAuth, mockReq) - 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')) - (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(() => {}) - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + await result.continue() - await result.continue(); - - expect(errorSpy).toBeCalledWith( - expect.stringContaining('Failed to send discord reports'), - expect.any(Error) - ); - - }); - }); -}); \ No newline at end of file + expect(errorSpy).toBeCalledWith( + expect.stringContaining('Failed to send discord reports'), + expect.any(Error), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/save-subscription-mobile.unit.test.ts b/backend/api/tests/unit/save-subscription-mobile.unit.test.ts index 96915775..8ea7409b 100644 --- a/backend/api/tests/unit/save-subscription-mobile.unit.test.ts +++ b/backend/api/tests/unit/save-subscription-mobile.unit.test.ts @@ -1,71 +1,68 @@ -import {sqlMatch} from "common/test-utils"; -import {AuthedUser} from "api/helpers/endpoint"; -import {saveSubscriptionMobile} from "api/save-subscription-mobile"; -import * as supabaseInit from "shared/supabase/init"; +import {AuthedUser} from 'api/helpers/endpoint' +import {saveSubscriptionMobile} from 'api/save-subscription-mobile' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') describe('saveSubscriptionMobile', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 - 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) - (mockPg.none as jest.Mock).mockResolvedValue(null); + const result = await saveSubscriptionMobile(mockBody, mockAuth, mockReq) - const result = await saveSubscriptionMobile(mockBody, mockAuth, mockReq); + expect(result.success).toBeTruthy() + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith( + sqlMatch('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(result.success).toBeTruthy(); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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', + ) + }) - 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(() => {}) - 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) - // ); - }); - }); -}); \ No newline at end of file + 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) + // ); + }) + }) +}) diff --git a/backend/api/tests/unit/save-subscription.unit.test.ts b/backend/api/tests/unit/save-subscription.unit.test.ts index 7ce43b68..6e1272ac 100644 --- a/backend/api/tests/unit/save-subscription.unit.test.ts +++ b/backend/api/tests/unit/save-subscription.unit.test.ts @@ -1,119 +1,117 @@ -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') -import {AuthedUser} from "api/helpers/endpoint"; -import {saveSubscription} from "api/save-subscription"; -import * as supabaseInit from "shared/supabase/init"; +import {AuthedUser} from 'api/helpers/endpoint' +import {saveSubscription} from 'api/save-subscription' import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' describe('saveSubscription', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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'} - 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) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockExists); - (mockPg.none as jest.Mock).mockResolvedValue(null); - - const result = await saveSubscription(mockBody, mockAuth, mockReq); + const result = await saveSubscription(mockBody, mockAuth, mockReq) - expect(result.success).toBeTruthy(); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('select id from push_subscriptions where endpoint = $1'), - [mockBody.subscription.endpoint] - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('update push_subscriptions set keys = $1, user_id = $2 where id = $3'), - [mockBody.subscription.keys, mockAuth.uid, mockExists.id] - ); - }); + expect(result.success).toBeTruthy() + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('select id from push_subscriptions where endpoint = $1'), + [mockBody.subscription.endpoint], + ) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith( + sqlMatch('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; + 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); + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(false) + ;(mockPg.none as jest.Mock).mockResolvedValue(null) - expect(result.success).toBeTruthy(); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('select id from push_subscriptions where endpoint = $1'), - [mockBody.subscription.endpoint] - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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'); - }); + const result = await saveSubscription(mockBody, mockAuth, mockReq) - 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" }; + expect(result.success).toBeTruthy() + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('select id from push_subscriptions where endpoint = $1'), + [mockBody.subscription.endpoint], + ) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith( + sqlMatch('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 - (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(saveSubscription(mockBody, mockAuth, mockReq)).rejects.toThrow( + 'Invalid subscription object', + ) + }) - // expect(errorSpy).toBeCalledTimes(1); - // expect(errorSpy).toBeCalledWith( - // expect.stringContaining('Error saving subscription'), - // expect.any(Error) - // ); - }); - }); -}); \ No newline at end of file + 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) + // ); + }) + }) +}) diff --git a/backend/api/tests/unit/search-location.unit.test.ts b/backend/api/tests/unit/search-location.unit.test.ts index f95a3f10..4405529d 100644 --- a/backend/api/tests/unit/search-location.unit.test.ts +++ b/backend/api/tests/unit/search-location.unit.test.ts @@ -1,36 +1,38 @@ -jest.mock('common/geodb'); +jest.mock('common/geodb') -import { AuthedUser } from "api/helpers/endpoint"; -import { searchLocation } from "api/search-location"; -import * as geodbModules from "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(); - }); + 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"; + 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); + ;(geodbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn) - const result = await searchLocation(mockBody, mockAuth, mockReq); + 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`) - ); - }); - }); -}); \ No newline at end of file + 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`, + ), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/search-near-city.unit.test.ts b/backend/api/tests/unit/search-near-city.unit.test.ts index 84a337e1..bd265c46 100644 --- a/backend/api/tests/unit/search-near-city.unit.test.ts +++ b/backend/api/tests/unit/search-near-city.unit.test.ts @@ -1,72 +1,74 @@ -jest.mock('common/geodb'); +jest.mock('common/geodb') -import * as citySearchModules from "api/search-near-city"; -import * as geoDbModules from "common/geodb"; -import { AuthedUser } from "api/helpers/endpoint"; +import {AuthedUser} from 'api/helpers/endpoint' +import * as citySearchModules from 'api/search-near-city' +import * as geoDbModules from 'common/geodb' describe('searchNearCity', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + 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"; + 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); + ;(geoDbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn) - 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`) - ); - }); - }); -}); + 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(); - }); + 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" } - ] - } - }; + 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); + ;(geoDbModules.geodbFetch as jest.Mock).mockResolvedValue(mockReturn) - 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`) - ); - }); - }); -}); + 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`, + ), + ) + }) + }) +}) diff --git a/backend/api/tests/unit/search-users.unit.test.ts b/backend/api/tests/unit/search-users.unit.test.ts index d905b72f..d65ff5da 100644 --- a/backend/api/tests/unit/search-users.unit.test.ts +++ b/backend/api/tests/unit/search-users.unit.test.ts @@ -1,155 +1,139 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {searchUsers} from 'api/search-users' +import {toUserAPIResponse} from 'common/api/user-types' +import * as supabaseUsers from 'common/supabase/users' +import {sqlMatch} from 'common/test-utils' +import * as searchHelpers from 'shared/helpers/search' +import * as supabaseInit from 'shared/supabase/init' +import * as sqlBuilderModules from 'shared/supabase/sql-builder' -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'); +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') describe('searchUsers', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - map: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + map: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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'}] - 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) - (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) - 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(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.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( - sqlMatch("name_username_vector @@ websearch_to_tsquery('english', $1)"), - [mockProps.term, 'ConstructPrefix'] - ); - expect(sqlBuilderModules.orderBy).toBeCalledTimes(1); - expect(sqlBuilderModules.orderBy).toBeCalledWith( - sqlMatch("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) - ); - }); + 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( + sqlMatch("name_username_vector @@ websearch_to_tsquery('english', $1)"), + [mockProps.term, 'ConstructPrefix'], + ) + expect(sqlBuilderModules.orderBy).toBeCalledTimes(1) + expect(sqlBuilderModules.orderBy).toBeCalledWith( + sqlMatch('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"}, - ]; + 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); + ;(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); + 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(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) - ); - }); - }); -}); \ No newline at end of file + 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)) + }) + }) +}) diff --git a/backend/api/tests/unit/send-search-notifications.unit.test.ts b/backend/api/tests/unit/send-search-notifications.unit.test.ts index 075487f7..ee8f604f 100644 --- a/backend/api/tests/unit/send-search-notifications.unit.test.ts +++ b/backend/api/tests/unit/send-search-notifications.unit.test.ts @@ -1,316 +1,221 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/supabase/sql-builder'); -jest.mock('api/get-profiles'); -jest.mock('email/functions/helpers'); -jest.mock('lodash'); +jest.mock('shared/supabase/init') +jest.mock('shared/supabase/sql-builder') +jest.mock('api/get-profiles') +jest.mock('email/functions/helpers') -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"; +import * as profileModules from 'api/get-profiles' +import * as searchNotificationModules from 'api/send-search-notifications' +import * as helperModules from 'email/functions/helpers' +import * as supabaseInit from 'shared/supabase/init' +import * as sqlBuilderModules from 'shared/supabase/sql-builder' describe('sendSearchNotification', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - map: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + map: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 _mockPrivateUsers = [ + { + data: {mockData: 'mockDataValue'}, + id: 'mockId', + }, + { + data: {mockData1: 'mockDataValue1'}, + id: 'mockId1', + }, + ] + const mockProfiles = [ + { + name: 'mockProfileName', + username: 'mockProfileUsername', + }, + { + name: 'mockProfileName1', + username: 'mockProfileUsername1', + }, + ] + const mockProps = [ + { + skipId: 'mockCreatorId', + userId: 'mockCreatorId', + lastModificationWithin: '24 hours', + shortBio: true, + }, + { + skipId: 'mockCreatorId1', + userId: '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) + ;(profileModules.loadProfiles as jest.Mock) + .mockResolvedValueOnce({profiles: mockProfiles}) + .mockResolvedValueOnce({profiles: mockProfiles}) + jest.spyOn(searchNotificationModules, 'notifyBookmarkedSearch') + ;(helperModules.sendSearchAlertsEmail as jest.Mock).mockResolvedValue(null) - 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", - userId: "mockCreatorId", - lastModificationWithin: '24 hours', - shortBio: true, - }, - { - skipId: "mockCreatorId1", - userId: "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() - 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({}) + }) - 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: {mockCreatorId: '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 _mockPrivateUsers = [ + { + data: {mockData: 'mockDataValue'}, + id: 'mockCreatorId', + }, + { + 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) + ;(profileModules.loadProfiles as jest.Mock) + .mockResolvedValueOnce({profiles: mockProfiles}) + .mockResolvedValueOnce({profiles: mockProfiles}) + jest.spyOn(searchNotificationModules, 'notifyBookmarkedSearch') + ;(helperModules.sendSearchAlertsEmail as jest.Mock).mockResolvedValue(null) - 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() - await searchNotificationModules.sendSearchNotifications(); - - expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledTimes(1); - expect(searchNotificationModules.notifyBookmarkedSearch).not.toBeCalledWith({}); - - }); - }); -}); \ No newline at end of file + expect(searchNotificationModules.notifyBookmarkedSearch).toBeCalledTimes(1) + expect(searchNotificationModules.notifyBookmarkedSearch).not.toBeCalledWith({}) + }) + }) +}) diff --git a/backend/api/tests/unit/set-compatibility-answers.unit.test.ts b/backend/api/tests/unit/set-compatibility-answers.unit.test.ts index e2e13325..569b78a0 100644 --- a/backend/api/tests/unit/set-compatibility-answers.unit.test.ts +++ b/backend/api/tests/unit/set-compatibility-answers.unit.test.ts @@ -1,75 +1,71 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/compatibility/compute-scores'); +jest.mock('shared/supabase/init') +jest.mock('shared/compatibility/compute-scores') -import {sqlMatch} from "common/test-utils"; -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {setCompatibilityAnswer} from 'api/set-compatibility-answer' +import {sqlMatch} from 'common/test-utils' +import {recomputeCompatibilityScoresForUser} from 'shared/compatibility/compute-scores' +import * as supabaseInit from 'shared/supabase/init' describe('setCompatibilityAnswer', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - one: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + one: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); + afterEach(() => { + jest.restoreAllMocks() + }) - 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 - 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) - (mockPg.one as jest.Mock).mockResolvedValue(mockResult); - (recomputeCompatibilityScoresForUser as jest.Mock).mockResolvedValue(null); + const result: any = await setCompatibilityAnswer(mockProps, mockAuth, mockReq) - const result: any = await setCompatibilityAnswer(mockProps, mockAuth, mockReq); + expect(result.result).toBe(mockResult) + expect(mockPg.one).toBeCalledTimes(1) + expect(mockPg.one).toBeCalledWith({ + text: sqlMatch('INSERT INTO compatibility_answers'), + values: [ + mockAuth.uid, + mockProps.questionId, + mockProps.multipleChoice, + mockProps.prefChoices, + mockProps.importance, + mockProps.explanation, + ], + }) - expect(result.result).toBe(mockResult); - expect(mockPg.one).toBeCalledTimes(1); - expect(mockPg.one).toBeCalledWith( - { - text: sqlMatch('INSERT INTO compatibility_answers'), - values: [ - mockAuth.uid, - mockProps.questionId, - mockProps.multipleChoice, - mockProps.prefChoices, - mockProps.importance, - mockProps.explanation, - ] - } - ); + await result.continue() - await result.continue(); - - expect(recomputeCompatibilityScoresForUser).toBeCalledTimes(1); - expect(recomputeCompatibilityScoresForUser).toBeCalledWith(mockAuth.uid, expect.any(Object)); - }); - }); -}); \ No newline at end of file + expect(recomputeCompatibilityScoresForUser).toBeCalledTimes(1) + expect(recomputeCompatibilityScoresForUser).toBeCalledWith(mockAuth.uid, expect.any(Object)) + }) + }) +}) diff --git a/backend/api/tests/unit/set-last-online-time.unit.test.ts b/backend/api/tests/unit/set-last-online-time.unit.test.ts index 36806db6..bcb82a2c 100644 --- a/backend/api/tests/unit/set-last-online-time.unit.test.ts +++ b/backend/api/tests/unit/set-last-online-time.unit.test.ts @@ -1,56 +1,55 @@ -jest.mock('shared/supabase/init'); +jest.mock('shared/supabase/init') -import { AuthedUser } from "api/helpers/endpoint"; -import * as setLastTimeOnlineModule from "api/set-last-online-time"; -import * as supabaseInit from "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('setLastOnlineTimeUser', () => { - let mockPg: any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - none: jest.fn(), - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + let mockPg: 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 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'); + 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 - 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'"); - }); + ;(mockPg.none as jest.Mock).mockResolvedValue(null) + jest.spyOn(setLastTimeOnlineModule, 'setLastOnlineTimeUser') - 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) + const [query, userId] = mockPg.none.mock.calls[0] - await setLastTimeOnlineModule.setLastOnlineTime(mockProps, mockAuth, mockReq); - - expect(setLastTimeOnlineModule.setLastOnlineTimeUser).not.toBeCalled(); - }); - }); -}); \ No newline at end of file + 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'") + }) + + 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() + }) + }) +}) diff --git a/backend/api/tests/unit/ship-profiles.unit.test.ts b/backend/api/tests/unit/ship-profiles.unit.test.ts index 186ed407..84baf713 100644 --- a/backend/api/tests/unit/ship-profiles.unit.test.ts +++ b/backend/api/tests/unit/ship-profiles.unit.test.ts @@ -1,228 +1,217 @@ -jest.mock('shared/supabase/init'); -jest.mock('common/util/try-catch'); -jest.mock('shared/supabase/utils'); -jest.mock('shared/create-profile-notification'); +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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {shipProfiles} from 'api/ship-profiles' import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as profileNotificationModules from 'shared/create-profile-notification' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUtils from 'shared/supabase/utils' describe('shipProfiles', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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, + } - 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) - (tryCatch as jest.Mock).mockResolvedValue(mockExisting); - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + const result: any = await shipProfiles(mockProps, mockAuth, mockReq) - 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(sqlMatch('select ship_id from profile_ships'), [ + mockAuth.uid, + mockProps.targetUserId1, + mockProps.targetUserId2, + ]) + }) - expect(result.status).toBe('success'); - expect(tryCatch).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('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, + } - 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) - (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) - 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(sqlMatch('select ship_id from profile_ships'), [ + mockAuth.uid, + mockProps.targetUserId1, + mockProps.targetUserId2, + ]) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('delete from profile_ships where ship_id = $1'), [ + mockExisting.data.ship_id, + ]) + }) - expect(result.status).toBe('success'); - expect(tryCatch).toBeCalledTimes(2); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('select ship_id from profile_ships'), - [mockAuth.uid, mockProps.targetUserId1, mockProps.targetUserId2] - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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', + } - 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) - (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) - 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(sqlMatch('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) - expect(result.result.status).toBe('success'); - expect(tryCatch).toBeCalledTimes(2); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('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, - } - ); + await result.continue() - (profileNotificationModules.createProfileShipNotification as jest.Mock).mockReturnValue(null); + 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, + } - await result.continue(); + ;(tryCatch as jest.Mock).mockResolvedValue(mockExisting) + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(null) - 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 - }; + expect(shipProfiles(mockProps, mockAuth, mockReq)).rejects.toThrow( + 'Error when checking ship: ', + ) + }) - (tryCatch as jest.Mock).mockResolvedValue(mockExisting); - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); + 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, + } - expect(shipProfiles(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Error when checking ship: '); + ;(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 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 - }; + 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({error: Error}); - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); - (mockPg.none as jest.Mock).mockResolvedValue(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 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: '); - - }); - }); -}); \ No newline at end of file + expect(shipProfiles(mockProps, mockAuth, mockReq)).rejects.toThrow('Failed to create ship: ') + }) + }) +}) diff --git a/backend/api/tests/unit/star-profile.unit.test.ts b/backend/api/tests/unit/star-profile.unit.test.ts index 911c36d8..f44d3171 100644 --- a/backend/api/tests/unit/star-profile.unit.test.ts +++ b/backend/api/tests/unit/star-profile.unit.test.ts @@ -1,147 +1,136 @@ -import {sqlMatch} from "common/test-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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {starProfile} from 'api/star-profile' +import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUtils from 'shared/supabase/utils' -jest.mock('common/util/try-catch'); -jest.mock('shared/supabase/init'); -jest.mock('shared/supabase/utils'); +jest.mock('common/util/try-catch') +jest.mock('shared/supabase/init') +jest.mock('shared/supabase/utils') describe('startProfile', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - none: jest.fn(), - oneOrNone: jest.fn(), - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + none: jest.fn(), + oneOrNone: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 - 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) - (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) - 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( + sqlMatch('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, + }) + }) - expect(result.status).toBe('success'); - expect(tryCatch).toBeCalledTimes(2); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('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', + } - 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}) - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({data: mockExisting}); + const result: any = await starProfile(mockProps, mockAuth, mockReq) - 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) + }) - 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 - 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}) - (mockPg.none as jest.Mock).mockResolvedValue(null); - (tryCatch as jest.Mock).mockResolvedValue({error: null}); + const result: any = await starProfile(mockProps, mockAuth, mockReq) - 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( + sqlMatch('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 - expect(result.status).toBe('success'); - expect(tryCatch).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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}) - (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') + }) - 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 - 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) - (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'); - - }); - }); -}); \ No newline at end of file + expect(starProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Failed to add star') + }) + }) +}) diff --git a/backend/api/tests/unit/update-me.unit.test.ts b/backend/api/tests/unit/update-me.unit.test.ts index 838ca2bb..579495d7 100644 --- a/backend/api/tests/unit/update-me.unit.test.ts +++ b/backend/api/tests/unit/update-me.unit.test.ts @@ -1,235 +1,158 @@ -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'); +jest.mock('shared/supabase/init') +jest.mock('shared/utils') +jest.mock('shared/supabase/users') +jest.mock('shared/websockets/helpers') -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {updateMe} from 'api/update-me' import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUsers from 'shared/supabase/users' +import * as sharedUtils from 'shared/utils' +import * as websocketHelperModules from 'shared/websockets/helpers' describe('updateMe', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + describe('when given valid input', () => { + it('should update user information', async () => { + const mockProps = { + name: 'mockName', + username: 'mockUsername', + avatarUrl: 'mockAvatarUrl', + link: {mockLink: 'mockLinkValue'}, + } as any + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockData = {link: mockProps.link} - 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", - link: {"mockLink" : "mockLinkValue"}, - }; - const mockStripped = { - bio: "mockBio" - }; - const mockData = {link: "mockNewLinks"}; - const arrySpy = jest.spyOn(Array.prototype, 'includes'); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true) + ;(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(false) + ;(supabaseUsers.updateUser as jest.Mock) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null) + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockData) + ;(mockPg.none as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null) + ;(websocketHelperModules.broadcastUpdatedUser as jest.Mock).mockReturnValue(null) - (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) - 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( - sqlMatch('update users'), - { - adds: expect.any(Object), - removes: expect.any(Array), - id: mockAuth.uid - } - ); - expect(mockPg.none).toBeCalledTimes(2); - expect(mockPg.none).toHaveBeenNthCalledWith( - 1, - sqlMatch(`update users + expect(sharedUtils.getUser).toBeCalledTimes(1) + expect(sharedUtils.getUser).toBeCalledWith(mockAuth.uid) + expect(sharedUtils.getUserByUsername).toBeCalledTimes(1) + expect(sharedUtils.getUserByUsername).toBeCalledWith(mockProps.username) + expect(supabaseUsers.updateUser).toBeCalledTimes(2) + expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + mockAuth.uid, + {}, + ) + expect(supabaseUsers.updateUser).toHaveBeenNthCalledWith( + 2, + expect.any(Object), + mockAuth.uid, + {avatarUrl: mockProps.avatarUrl}, + ) + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith(sqlMatch('update users'), { + adds: expect.any(Object), + removes: expect.any(Array), + id: mockAuth.uid, + }) + expect(mockPg.none).toBeCalledTimes(2) + expect(mockPg.none).toHaveBeenNthCalledWith( + 1, + sqlMatch(`update users set name = $1 where id = $2`), - [mockUpdate.name, mockAuth.uid] - ); - expect(mockPg.none).toHaveBeenNthCalledWith( - 2, - sqlMatch(`update users + [mockProps.name, mockAuth.uid], + ) + expect(mockPg.none).toHaveBeenNthCalledWith( + 2, + sqlMatch(`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", - link: {"mockLink" : "mockLinkValue"}, - }; + [mockProps.username, mockAuth.uid], + ) + expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledTimes(1) + expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledWith({ + ...mockProps, + id: mockAuth.uid, + }) + }) + }) + 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 - (lodashModules.cloneDeep as jest.Mock).mockReturnValue(mockUpdate); - (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - expect(updateMe(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Your account was not found'); - }); + 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", - link: {"mockLink" : "mockLinkValue"}, - }; + it('should throw if the username is invalid', async () => { + const mockProps = { + name: 'mockName', + username: ';#', + avatarUrl: 'mockAvatarUrl', + link: {mockLink: 'mockLinkValue'}, + } as any + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - (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'); - }); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true) - 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", - link: {"mockLink" : "mockLinkValue"}, - }; + expect(updateMe(mockProps, mockAuth, mockReq)).rejects.toThrow('Invalid username') + }) - const arrySpy = jest.spyOn(Array.prototype, 'includes'); + it('should throw if the username is reserved', async () => { + const mockProps = { + name: 'mockName', + username: 'mockUsername', + avatarUrl: 'mockAvatarUrl', + link: {mockLink: 'mockLinkValue'}, + } as any + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - (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); + const arraySpy = jest.spyOn(Array.prototype, 'includes') - expect(updateMe(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('This username is reserved'); - }); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true) + arraySpy.mockReturnValue(true) - 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", - link: {"mockLink" : "mockLinkValue"}, - }; - const arrySpy = jest.spyOn(Array.prototype, 'includes'); + expect(updateMe(mockProps, mockAuth, mockReq)).rejects.toThrow('This username is reserved') + }) - (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); + it('should throw if the username is taken', async () => { + const mockProps = { + name: 'mockName', + username: 'mockUsername', + avatarUrl: 'mockAvatarUrl', + link: {mockLink: 'mockLinkValue'}, + } as any + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const arraySpy = jest.spyOn(Array.prototype, 'includes') - expect(updateMe(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Username already taken'); - }); - }); -}); \ No newline at end of file + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true) + arraySpy.mockReturnValue(false) + ;(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(true) + + expect(updateMe(mockProps, mockAuth, mockReq)).rejects.toThrow('Username already taken') + }) + }) +}) diff --git a/backend/api/tests/unit/update-notif-setting.unit.test.ts b/backend/api/tests/unit/update-notif-setting.unit.test.ts index fd7578b0..c6fdbdcd 100644 --- a/backend/api/tests/unit/update-notif-setting.unit.test.ts +++ b/backend/api/tests/unit/update-notif-setting.unit.test.ts @@ -1,72 +1,69 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/supabase/users'); -jest.mock('shared/websockets/helpers'); +jest.mock('shared/supabase/init') +jest.mock('shared/supabase/users') +jest.mock('shared/websockets/helpers') -import {sqlMatch} from "common/test-utils"; -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {updateNotifSettings} from 'api/update-notif-setting' +import {sqlMatch} from 'common/test-utils' +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() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 - 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) - (mockPg.none as jest.Mock).mockResolvedValue(null); - (websocketHelpers.broadcastUpdatedPrivateUser as jest.Mock).mockReturnValue(null); + await updateNotifSettings(mockProps, mockAuth, mockReq) - await updateNotifSettings(mockProps, mockAuth, mockReq); + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('update private_users'), [ + mockProps.type, + mockProps.medium, + mockAuth.uid, + ]) + expect(websocketHelpers.broadcastUpdatedPrivateUser).toBeCalledTimes(1) + expect(websocketHelpers.broadcastUpdatedPrivateUser).toBeCalledWith(mockAuth.uid) + }) - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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 - 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) - (supabaseUsers.updatePrivateUser as jest.Mock).mockResolvedValue(null); + await updateNotifSettings(mockProps, mockAuth, mockReq) - await updateNotifSettings(mockProps, mockAuth, mockReq); - - expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(1); - expect(supabaseUsers.updatePrivateUser).toBeCalledWith( - expect.any(Object), - mockAuth.uid, - {interestedInPushNotifications: !mockProps.enabled} - ); - }); - }); -}); \ No newline at end of file + expect(supabaseUsers.updatePrivateUser).toBeCalledTimes(1) + expect(supabaseUsers.updatePrivateUser).toBeCalledWith(expect.any(Object), mockAuth.uid, { + interestedInPushNotifications: !mockProps.enabled, + }) + }) + }) +}) diff --git a/backend/api/tests/unit/update-options.unit.test.ts b/backend/api/tests/unit/update-options.unit.test.ts index 97626af0..c5cf2239 100644 --- a/backend/api/tests/unit/update-options.unit.test.ts +++ b/backend/api/tests/unit/update-options.unit.test.ts @@ -1,177 +1,164 @@ -import {sqlMatch} from "common/test-utils"; -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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {updateOptions} from 'api/update-options' +import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import * as supabaseInit from 'shared/supabase/init' -jest.mock('common/util/try-catch'); -jest.mock('shared/supabase/init'); +jest.mock('common/util/try-catch') +jest.mock('shared/supabase/init') describe('updateOptions', () => { - let mockPg = {} as any; - let mockTx = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockTx = { - one: jest.fn(), - none: jest.fn(), - manyOrNone: jest.fn(), - }; - mockPg = { - oneOrNone: jest.fn(), - manyOrNone: jest.fn(), - tx: jest.fn(async (cb) => await cb(mockTx)) - }; - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + let mockPg = {} as any + let mockTx = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockTx = { + one: jest.fn(), + none: jest.fn(), + manyOrNone: jest.fn(), + } + mockPg = { + oneOrNone: jest.fn(), + manyOrNone: 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, - values: ["mockNamesOne", "mockNamesTwo"] - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockProfileIdResult = {id: 123}; - const mockRow1 = { - id: 1234, - }; - const mockRow2 = { - id: 12345, - }; + describe('when given valid input', () => { + it('should update user', async () => { + const mockProps = { + table: 'causes' as const, + values: ['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); - (mockTx.manyOrNone as jest.Mock).mockResolvedValue([]); - - const result: any = await updateOptions(mockProps, mockAuth, mockReq); + 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) + ;(mockTx.manyOrNone as jest.Mock).mockResolvedValue([]) - expect(result.updatedIds).toStrictEqual([mockRow1.id, mockRow2.id]); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('SELECT id FROM profiles WHERE user_id = $1'), - [mockAuth.uid] - ); - expect(tryCatch).toBeCalledTimes(1); - expect(mockTx.one).toBeCalledTimes(2); - expect(mockTx.one).toHaveBeenNthCalledWith( - 1, - sqlMatch(`INSERT INTO ${mockProps.table} (name, creator_id)`), - [mockProps.values[0], mockAuth.uid] - ); - expect(mockTx.one).toHaveBeenNthCalledWith( - 2, - sqlMatch(`INSERT INTO ${mockProps.table} (name, creator_id)`), - [mockProps.values[1], mockAuth.uid] - ); - expect(mockTx.none).toBeCalledTimes(2); - expect(mockTx.none).toHaveBeenNthCalledWith( - 1, - sqlMatch(`DELETE + 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( + sqlMatch('SELECT id FROM profiles WHERE user_id = $1'), + [mockAuth.uid], + ) + expect(tryCatch).toBeCalledTimes(1) + expect(mockTx.one).toBeCalledTimes(2) + expect(mockTx.one).toHaveBeenNthCalledWith( + 1, + sqlMatch(`INSERT INTO ${mockProps.table} (name, creator_id)`), + [mockProps.values[0], mockAuth.uid], + ) + expect(mockTx.one).toHaveBeenNthCalledWith( + 2, + sqlMatch(`INSERT INTO ${mockProps.table} (name, creator_id)`), + [mockProps.values[1], mockAuth.uid], + ) + expect(mockTx.none).toBeCalledTimes(2) + expect(mockTx.none).toHaveBeenNthCalledWith( + 1, + sqlMatch(`DELETE FROM profile_${mockProps.table} WHERE profile_id = $1`), - [mockProfileIdResult.id] - ); - expect(mockTx.none).toHaveBeenNthCalledWith( - 2, - sqlMatch(`INSERT INTO profile_${mockProps.table} (profile_id, option_id) + [mockProfileIdResult.id], + ) + expect(mockTx.none).toHaveBeenNthCalledWith( + 2, + sqlMatch(`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, - values: ["mockNamesOne", "mockNamesTwo"] - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; + [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, + values: ['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'); - }); + jest.spyOn(Array.prototype, 'includes').mockReturnValue(false) - it('should throw if the names param is not provided', async () => { - const mockProps = { - table: 'causes' as const, - values: undefined - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; + expect(updateOptions(mockProps, mockAuth, mockReq)).rejects.toThrow('Invalid table') + }) - jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); - - expect(updateOptions(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('No ids provided'); - }); + it('should throw if the names param is not provided', async () => { + const mockProps = { + table: 'causes' as const, + values: undefined, + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any - it('should throw if unable to find profile', async () => { - const mockProps = { - table: 'causes' as const, - values: ["mockNamesOne", "mockNamesTwo"] - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; + jest.spyOn(Array.prototype, 'includes').mockReturnValue(true) - jest.spyOn(Array.prototype, 'includes').mockReturnValue(true); - (mockPg.oneOrNone as jest.Mock).mockResolvedValue(false); - - expect(updateOptions(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Profile not found'); - }); + expect(updateOptions(mockProps, mockAuth, mockReq)).rejects.toThrow('No ids provided') + }) - it('should update user', async () => { - const mockProps = { - table: 'causes' as const, - values: ["mockNamesOne", "mockNamesTwo"] - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockProfileIdResult = {id: 123}; - const mockRow1 = { - id: 1234, - }; - const mockRow2 = { - id: 12345, - }; + it('should throw if unable to find profile', async () => { + const mockProps = { + table: 'causes' as const, + values: ['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(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'); - }); - }); -}); \ No newline at end of file + 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, + values: ['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', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/update-private-user-message-channel.unit.test.ts b/backend/api/tests/unit/update-private-user-message-channel.unit.test.ts index 1b74216e..810ea8df 100644 --- a/backend/api/tests/unit/update-private-user-message-channel.unit.test.ts +++ b/backend/api/tests/unit/update-private-user-message-channel.unit.test.ts @@ -1,92 +1,90 @@ -import {sqlMatch} from "common/test-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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {updatePrivateUserMessageChannel} from 'api/update-private-user-message-channel' +import * as supabaseUtils from 'common/supabase/utils' +import {sqlMatch} from 'common/test-utils' +import * as supabaseInit from 'shared/supabase/init' +import * as sharedUtils from 'shared/utils' -jest.mock('shared/supabase/init'); -jest.mock('shared/utils'); -jest.mock('common/supabase/utils'); +jest.mock('shared/supabase/init') +jest.mock('shared/utils') +jest.mock('common/supabase/utils') describe('updatePrivateUserMessageChannel', () => { - let mockPg = {} as any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - none: jest.fn() - }; + let mockPg = {} as any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + none: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (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 - 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'); + ;(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); + 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( - sqlMatch('select status from private_user_message_channel_members'), - [mockBody.channelId, mockAuth.uid] - ); - expect(mockPg.none).toBeCalledTimes(1); - expect(mockPg.none).toBeCalledWith( - sqlMatch('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(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( + sqlMatch('select status from private_user_message_channel_members'), + [mockBody.channelId, mockAuth.uid], + ) + expect(mockPg.none).toBeCalledTimes(1) + expect(mockPg.none).toBeCalledWith(sqlMatch('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 - expect(updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq)) - .rejects - .toThrow('Your account was not found'); - }); + ;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false) - 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( + 'Your account was not found', + ) + }) - expect(updatePrivateUserMessageChannel(mockBody, mockAuth, mockReq)) - .rejects - .toThrow('You are not authorized to this channel'); + 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 - }); - }); -}); \ No newline at end of file + ;(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', + ) + }) + }) +}) diff --git a/backend/api/tests/unit/update-profile.unit.test.ts b/backend/api/tests/unit/update-profile.unit.test.ts index 3880c3fa..8de010ed 100644 --- a/backend/api/tests/unit/update-profile.unit.test.ts +++ b/backend/api/tests/unit/update-profile.unit.test.ts @@ -1,101 +1,93 @@ -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"); +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 {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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {updateProfile} from 'api/update-profile' import {sqlMatch} from 'common/test-utils' +import {tryCatch} from 'common/util/try-catch' +import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos' +import * as supabaseInit from 'shared/supabase/init' +import * as supabaseUsers from 'shared/supabase/users' +import * as supabaseUtils from 'shared/supabase/utils' describe('updateProfiles', () => { - let mockPg: any; - beforeEach(() => { - jest.resetAllMocks(); - mockPg = { - oneOrNone: jest.fn(), - }; + let mockPg: any + beforeEach(() => { + jest.resetAllMocks() + mockPg = { + oneOrNone: jest.fn(), + } + ;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg) + }) + afterEach(() => { + jest.restoreAllMocks() + }) - (supabaseInit.createSupabaseDirectClient as jest.Mock) - .mockReturnValue(mockPg); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); + describe('when given valid input', () => { + it('should update an existing profile when provided the user id', async () => { + const mockProps = { + pinned_url: 'mockAvatarUrl', + } + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockData = 'success' - describe('when given valid input', () => { - it('should update an existing profile when provided the user id', async () => { - const mockProps = { - pinned_url: "mockAvatarUrl" - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; - const mockData = "success"; + ;(tryCatch as jest.Mock) + .mockResolvedValueOnce({data: true}) + .mockResolvedValueOnce({data: mockData, error: null}) - (tryCatch as jest.Mock) - .mockResolvedValueOnce({data: true}) - .mockResolvedValueOnce({data: mockData, error: null}); + const result = await updateProfile(mockProps, mockAuth, mockReq) - const result = await updateProfile(mockProps, mockAuth, mockReq); + expect(result).toBe(mockData) + expect(mockPg.oneOrNone).toBeCalledTimes(1) + expect(mockPg.oneOrNone).toBeCalledWith( + sqlMatch('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.pinned_url, + }) + expect(supabaseUtils.update).toBeCalledTimes(1) + expect(supabaseUtils.update).toBeCalledWith( + expect.any(Object), + 'profiles', + 'user_id', + expect.any(Object), + ) + }) + }) - expect(result).toBe(mockData); - expect(mockPg.oneOrNone).toBeCalledTimes(1); - expect(mockPg.oneOrNone).toBeCalledWith( - sqlMatch('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.pinned_url} - ); - expect(supabaseUtils.update).toBeCalledTimes(1); - expect(supabaseUtils.update).toBeCalledWith( - expect.any(Object), - 'profiles', - 'user_id', - expect.any(Object) - ); - }); - }); + 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 - 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}) - (tryCatch as jest.Mock).mockResolvedValue({data: false}); + expect(updateProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Profile not found') + }) - expect(updateProfile(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('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('should throw if unable to update the profile', async () => { - const mockProps = { - avatar_url: "mockAvatarUrl" - }; - const mockAuth = { uid: '321' } as AuthedUser; - const mockReq = {} as any; + ;(tryCatch as jest.Mock) + .mockResolvedValueOnce({data: true}) + .mockResolvedValueOnce({data: null, error: Error}) - (tryCatch as jest.Mock) - .mockResolvedValueOnce({data: true}) - .mockResolvedValueOnce({data: null, error: Error}); - - expect(updateProfile(mockProps, mockAuth, mockReq)) - .rejects - .toThrow('Error updating profile'); - }); - }); -}); \ No newline at end of file + expect(updateProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Error updating profile') + }) + }) +}) diff --git a/backend/api/tests/unit/vote-unit.test.ts b/backend/api/tests/unit/vote-unit.test.ts index 94e960f8..d2f56a2d 100644 --- a/backend/api/tests/unit/vote-unit.test.ts +++ b/backend/api/tests/unit/vote-unit.test.ts @@ -1,102 +1,94 @@ -jest.mock('shared/supabase/init'); -jest.mock('shared/utils'); +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"; +import {AuthedUser} from 'api/helpers/endpoint' +import {vote} from 'api/vote' import {sqlMatch} from 'common/test-utils' +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() - }; + 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' - (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) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); - (mockPg.one as jest.Mock).mockResolvedValue(mockResult); + const result = await vote(mockProps, mockAuth, mockReq) - 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( + sqlMatch('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 - 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( - sqlMatch('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) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(false); + expect(vote(mockProps, mockAuth, mockReq)).rejects.toThrow('Your account was not found') + }) - 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'} - 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) - (sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser); + expect(vote(mockProps, mockAuth, mockReq)).rejects.toThrow('Invalid choice') + }) - 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'} - 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')) - (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'); - }); - }); -}); \ No newline at end of file + expect(vote(mockProps, mockAuth, mockReq)).rejects.toThrow('Error recording vote') + }) + }) +}) diff --git a/backend/api/tsconfig.json b/backend/api/tsconfig.json index 70ee3aa5..348df297 100644 --- a/backend/api/tsconfig.json +++ b/backend/api/tsconfig.json @@ -1,11 +1,8 @@ { "compilerOptions": { - "rootDir": "src", - "composite": true, "module": "commonjs", "noImplicitReturns": true, "outDir": "./lib", - "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", "sourceMap": true, "strict": true, "resolveJsonModule": true, @@ -15,43 +12,16 @@ "skipLibCheck": true, "jsx": "react-jsx", "paths": { - "common/*": [ - "../../common/src/*", - "../../../common/lib/*" - ], - "shared/*": [ - "../shared/src/*", - "../../shared/lib/*" - ], - "email/*": [ - "../email/emails/*", - "../../email/lib/*" - ], - "api/*": [ - "./src/*" - ] + "common/*": ["../../common/src/*", "../../../common/lib/*"], + "shared/*": ["../shared/src/*", "../../shared/lib/*"], + "email/*": ["../email/emails/*", "../../email/lib/*"], + "api/*": ["./src/*"] } }, "ts-node": { - "require": [ - "tsconfig-paths/register" - ] + "require": ["tsconfig-paths/register"] }, - "references": [ - { - "path": "../../common" - }, - { - "path": "../shared" - }, - { - "path": "../email" - } - ], "compileOnSave": true, - "include": [ - "src/**/*.ts", - "package.json", - "backend/api/package.json" - ] + "include": ["src/**/*.ts", "package.json", "backend/api/package.json"], + "exclude": ["**/*.test.ts", "**/*.spec.ts"] } diff --git a/backend/api/tsconfig.test.json b/backend/api/tsconfig.test.json index 6c128f30..dbe5af44 100644 --- a/backend/api/tsconfig.test.json +++ b/backend/api/tsconfig.test.json @@ -13,10 +13,6 @@ "email/*": ["../email/emails/*"] } }, - "include": [ - "tests/**/*.ts", - "src/**/*.ts", - "../shared/src/**/*.ts", - "../../common/src/**/*.ts" - ] + "include": ["tests/**/*.ts", "src/**/*.ts", "../shared/src/**/*.ts", "../../common/src/**/*.ts"], + "exclude": [] } diff --git a/backend/email/.prettierignore b/backend/email/.prettierignore new file mode 100644 index 00000000..d4cf806b --- /dev/null +++ b/backend/email/.prettierignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules +.yarn + +# Build outputs +dist +build +.next +out +lib + +# Generated files +coverage +*.min.js +*.min.css + +# Database / migrations +**/*.sql + +# Config / lock files +yarn.lock +package-lock.json +pnpm-lock.yaml + +# Android / iOS +android +ios +capacitor.config.ts + +# Playwright +tests/reports +playwright-report + +coverage \ No newline at end of file diff --git a/backend/email/README.md b/backend/email/README.md index 062fd2cf..992a50de 100644 --- a/backend/email/README.md +++ b/backend/email/README.md @@ -18,12 +18,12 @@ yarn dev Open [localhost:3001](http://localhost:3001) with your browser to see the result. - ### Notes Right now, I can't make the email server run without breaking the backend API and web, as they require different versions of react. To run the email server, temporarily install the deps in this folder. They require react 19. + ```bash yarn add -D @react-email/preview-server react-email ``` @@ -34,4 +34,4 @@ When you are done, reinstall react 18.2 by running `yarn clean-install` at the r ```bash -``` \ No newline at end of file +``` diff --git a/backend/email/emails/functions/helpers.tsx b/backend/email/emails/functions/helpers.tsx index 4fa16e5a..557d1b59 100644 --- a/backend/email/emails/functions/helpers.tsx +++ b/backend/email/emails/functions/helpers.tsx @@ -1,17 +1,20 @@ -import React from 'react'; +import React from 'react' import {PrivateUser, User} from 'common/user' -import {getNotificationDestinationsForUser, UNSUBSCRIBE_URL} from 'common/user-notification-preferences' +import { + getNotificationDestinationsForUser, + UNSUBSCRIBE_URL, +} from 'common/user-notification-preferences' import {sendEmail} from './send-email' import {NewMessageEmail} from '../new-message' import {NewEndorsementEmail} from '../new-endorsement' import {Test} from '../test' import {getProfile} from 'shared/profiles/supabase' -import {render} from "@react-email/render" -import {MatchesType} from "common/profiles/bookmarked_searches"; -import NewSearchAlertsEmail from "email/new-search_alerts"; -import WelcomeEmail from "email/welcome"; -import * as admin from "firebase-admin"; -import {getOptionsIdsToLabels} from "shared/supabase/options"; +import {render} from '@react-email/render' +import {MatchesType} from 'common/profiles/bookmarked_searches' +import NewSearchAlertsEmail from 'email/new-search_alerts' +import WelcomeEmail from 'email/welcome' +import * as admin from 'firebase-admin' +import {getOptionsIdsToLabels} from 'shared/supabase/options' export const fromEmail = 'Compass ' @@ -47,11 +50,11 @@ export const sendNewMessageEmail = async ( privateUser: PrivateUser, fromUser: User, toUser: User, - channelId: number + channelId: number, ) => { const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser( privateUser, - 'new_message' + 'new_message', ) if (!privateUser.email || !sendToEmail) return @@ -74,17 +77,14 @@ export const sendNewMessageEmail = async ( channelId={channelId} unsubscribeUrl={unsubscribeUrl} email={privateUser.email} - /> + />, ), }) } -export const sendWelcomeEmail = async ( - toUser: User, - privateUser: PrivateUser, -) => { +export const sendWelcomeEmail = async (toUser: User, privateUser: PrivateUser) => { if (!privateUser.email) return - const verificationLink = await admin.auth().generateEmailVerificationLink(privateUser.email); + const verificationLink = await admin.auth().generateEmailVerificationLink(privateUser.email) return await sendEmail({ from: fromEmail, subject: `Welcome to Compass!`, @@ -95,7 +95,7 @@ export const sendWelcomeEmail = async ( unsubscribeUrl={UNSUBSCRIBE_URL} email={privateUser.email} verificationLink={verificationLink} - /> + />, ), }) } @@ -107,9 +107,9 @@ export const sendSearchAlertsEmail = async ( ) => { const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser( privateUser, - 'new_search_alerts' + 'new_search_alerts', ) - const email = privateUser.email; + const email = privateUser.email if (!email || !sendToEmail) return // Determine locale (fallback to 'en') and load option labels before rendering @@ -128,7 +128,7 @@ export const sendSearchAlertsEmail = async ( unsubscribeUrl={unsubscribeUrl} email={email} optionIdsToLabels={optionIdsToLabels} - /> + />, ), }) } @@ -137,11 +137,11 @@ export const sendNewEndorsementEmail = async ( privateUser: PrivateUser, fromUser: User, onUser: User, - text: string + text: string, ) => { const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser( privateUser, - 'new_endorsement' + 'new_endorsement', ) if (!privateUser.email || !sendToEmail) return @@ -156,7 +156,7 @@ export const sendNewEndorsementEmail = async ( endorsementText={text} unsubscribeUrl={unsubscribeUrl} email={privateUser.email} - /> + />, ), }) } @@ -166,6 +166,6 @@ export const sendTestEmail = async (toEmail: string) => { from: fromEmail, subject: 'Test email from Compass', to: toEmail, - html: await render(), + html: await render(), }) } diff --git a/backend/email/emails/functions/send-email.ts b/backend/email/emails/functions/send-email.ts index ed32de65..413eec07 100644 --- a/backend/email/emails/functions/send-email.ts +++ b/backend/email/emails/functions/send-email.ts @@ -1,36 +1,33 @@ -import {type CreateEmailOptions, CreateEmailRequestOptions, Resend,} from 'resend' +import {type CreateEmailOptions, CreateEmailRequestOptions, Resend} from 'resend' import {log} from 'shared/utils' -import {sleep} from "common/util/time"; - +import {sleep} from 'common/util/time' /* * typically: { subject: string, to: string | string[] } & ({ text: string } | { react: ReactNode }) */ export const sendEmail = async ( payload: CreateEmailOptions, - options?: CreateEmailRequestOptions + options?: CreateEmailRequestOptions, ) => { const resend = getResend() console.debug(resend, payload, options) const skip = false if (skip) { - console.warn("Skipping email send") + console.warn('Skipping email send') return null } if (!resend) return null - const { data, error } = await resend.emails.send( - { replyTo: 'Compass ', ...payload }, - options + const {data, error} = await resend.emails.send( + {replyTo: 'Compass ', ...payload}, + options, ) console.debug('resend.emails.send', data, error) if (error) { - log.error( - `Failed to send email to ${payload.to} with subject ${payload.subject}` - ) + log.error(`Failed to send email to ${payload.to} with subject ${payload.subject}`) log.error(error) return null } diff --git a/backend/email/emails/functions/send-test-email.ts b/backend/email/emails/functions/send-test-email.ts index b54baa7a..14983bc5 100755 --- a/backend/email/emails/functions/send-test-email.ts +++ b/backend/email/emails/functions/send-test-email.ts @@ -1,4 +1,4 @@ -import { sendTestEmail } from './helpers' +import {sendTestEmail} from './helpers' if (require.main === module) { const email = process.argv[2] @@ -11,4 +11,4 @@ if (require.main === module) { sendTestEmail(email) .then(() => console.debug('Email sent successfully!')) .catch((error) => console.error('Failed to send email:', error)) -} \ No newline at end of file +} diff --git a/backend/email/emails/new-endorsement.tsx b/backend/email/emails/new-endorsement.tsx index 1eb7f857..19d210d3 100644 --- a/backend/email/emails/new-endorsement.tsx +++ b/backend/email/emails/new-endorsement.tsx @@ -1,9 +1,20 @@ -import React from 'react'; -import {Body, Button, Column, Container, Head, Html, Preview, Row, Section, Text,} from '@react-email/components' +import React from 'react' +import { + Body, + Button, + Column, + Container, + Head, + Html, + Preview, + Row, + Section, + Text, +} from '@react-email/components' import {type User} from 'common/user' import {DOMAIN} from 'common/envs/constants' import {jamesUser, mockUser} from './functions/mock' -import {button, container, content, Footer, main, paragraph} from "email/utils"; +import {button, container, content, Footer, main, paragraph} from 'email/utils' interface NewEndorsementEmailProps { fromUser: User @@ -14,19 +25,19 @@ interface NewEndorsementEmailProps { } export const NewEndorsementEmail = ({ - fromUser, - onUser, - endorsementText, - unsubscribeUrl, - email, - }: NewEndorsementEmailProps) => { + fromUser, + onUser, + endorsementText, + unsubscribeUrl, + email, +}: NewEndorsementEmailProps) => { const name = onUser.name.split(' ')[0] const endorsementUrl = `https://${DOMAIN}/${onUser.username}` return ( - + New endorsement from {fromUser.name} @@ -66,7 +77,7 @@ export const NewEndorsementEmail = ({ -