mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-24 17:41:27 -04:00
Move avatar URL and is-banned to separate columns and social links to profiles table
This commit is contained in:
@@ -11,7 +11,7 @@ android {
|
||||
applicationId "com.compassconnections.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 56
|
||||
versionCode 57
|
||||
versionName "1.11.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"version": "1.22.2",
|
||||
"version": "1.23.0",
|
||||
"private": true,
|
||||
"description": "Backend API endpoints",
|
||||
"main": "src/serve.ts",
|
||||
|
||||
@@ -92,7 +92,7 @@ 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'
|
||||
import {updateProfileEndpoint} from './update-profile'
|
||||
import {updateUserLocale} from './update-user-locale'
|
||||
import {validateUsernameEndpoint} from './validate-username'
|
||||
|
||||
@@ -618,7 +618,7 @@ const handlers: {[k in APIPath]: APIHandler<k>} = {
|
||||
'update-options': updateOptions,
|
||||
'update-user-locale': updateUserLocale,
|
||||
'update-private-user-message-channel': updatePrivateUserMessageChannel,
|
||||
'update-profile': updateProfile,
|
||||
'update-profile': updateProfileEndpoint,
|
||||
'get-connection-interests': getConnectionInterestsEndpoint,
|
||||
'update-connection-interest': updateConnectionInterests,
|
||||
'user/by-id/:id': getUser,
|
||||
|
||||
@@ -2,20 +2,16 @@ import {APIErrors, 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 db = createSupabaseDirectClient()
|
||||
await throwErrorIfNotMod(auth.uid)
|
||||
if (isAdminId(userId)) throw APIErrors.forbidden('Cannot ban admin')
|
||||
await trackPublicEvent(auth.uid, 'ban user', {
|
||||
userId,
|
||||
})
|
||||
await updateUser(db, userId, {
|
||||
isBannedFromPosting: !unban,
|
||||
})
|
||||
await updateUser(userId, {isBannedFromPosting: !unban})
|
||||
log('updated user')
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 {updateUserData} from 'shared/supabase/users'
|
||||
import {insert} from 'shared/supabase/utils'
|
||||
import {getUser, log} from 'shared/utils'
|
||||
|
||||
@@ -29,7 +29,7 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) =>
|
||||
if (!user) throw APIErrors.unauthorized('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 || undefined})
|
||||
updateUserData(pg, auth.uid, {avatarUrl: body.pinned_url || undefined})
|
||||
}
|
||||
|
||||
debug('body', body)
|
||||
|
||||
@@ -35,7 +35,6 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async
|
||||
locale = defaultLocale,
|
||||
username,
|
||||
name,
|
||||
link,
|
||||
profile,
|
||||
interests,
|
||||
causes,
|
||||
@@ -55,7 +54,7 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async
|
||||
const email = fbUser.email
|
||||
|
||||
const bucket = getBucket()
|
||||
const avatarUrl = await generateAvatarUrl(auth.uid, cleanName, bucket)
|
||||
const avatarUrl = profile.pinned_url ?? (await generateAvatarUrl(auth.uid, cleanName, bucket))
|
||||
|
||||
let finalUsername = username
|
||||
const validation = await validateUsername(username)
|
||||
@@ -88,15 +87,6 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async
|
||||
})
|
||||
}
|
||||
|
||||
const userData = removeUndefinedProps({
|
||||
avatarUrl,
|
||||
isBannedFromPosting: Boolean(
|
||||
(deviceToken && bannedDeviceTokens.includes(deviceToken)) ||
|
||||
(ip && bannedIpAddresses.includes(ip)),
|
||||
),
|
||||
link: link,
|
||||
})
|
||||
|
||||
const privateUserData: PrivateUser = {
|
||||
id: auth.uid,
|
||||
email,
|
||||
@@ -112,7 +102,12 @@ export const createUserAndProfile: APIHandler<'create-user-and-profile'> = async
|
||||
id: auth.uid,
|
||||
name: cleanName,
|
||||
username: finalUsername,
|
||||
data: userData,
|
||||
avatar_url: avatarUrl,
|
||||
is_banned_from_posting: Boolean(
|
||||
(deviceToken && bannedDeviceTokens.includes(deviceToken)) ||
|
||||
(ip && bannedIpAddresses.includes(ip)),
|
||||
),
|
||||
data: {},
|
||||
})
|
||||
|
||||
const newPrivateUserRow = await insert(tx, 'private_users', {
|
||||
|
||||
@@ -76,7 +76,7 @@ export const createUser: APIHandler<'create-user'> = async (props, auth, req) =>
|
||||
const {user, privateUser} = await pg.tx(async (tx) => {
|
||||
const preexistingUser = await getUser(auth.uid, tx)
|
||||
if (preexistingUser)
|
||||
throw APIErrors.forbidden('User already exists', {
|
||||
throw APIErrors.forbidden('An account for this user already exists', {
|
||||
field: 'userId',
|
||||
context: `User with ID ${auth.uid} already exists`,
|
||||
})
|
||||
@@ -84,14 +84,14 @@ export const createUser: APIHandler<'create-user'> = async (props, auth, req) =>
|
||||
// Check exact username to avoid problems with duplicate requests
|
||||
const sameNameUser = await getUserByUsername(username, tx)
|
||||
if (sameNameUser)
|
||||
throw APIErrors.conflict('Username already taken', {
|
||||
throw APIErrors.conflict('Username is already taken', {
|
||||
field: 'username',
|
||||
context: `Username "${username}" is already taken`,
|
||||
})
|
||||
|
||||
const user = removeUndefinedProps({
|
||||
avatarUrl,
|
||||
isBannedFromPosting: Boolean(
|
||||
is_banned_from_posting: Boolean(
|
||||
(deviceToken && bannedDeviceTokens.includes(deviceToken)) ||
|
||||
(ip && bannedIpAddresses.includes(ip)),
|
||||
),
|
||||
|
||||
@@ -52,7 +52,7 @@ export const getEvents: APIHandler<'get-events'> = async () => {
|
||||
username: string
|
||||
avatar_url: string | null
|
||||
}>(
|
||||
`SELECT id, name, username, data ->> 'avatarUrl' as avatar_url
|
||||
`SELECT id, name, username, avatar_url
|
||||
FROM users
|
||||
WHERE id = ANY ($1)`,
|
||||
[creatorIds],
|
||||
|
||||
@@ -25,7 +25,7 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
||||
where creator_id = $1
|
||||
and looking_for_matches
|
||||
and profiles.pinned_url is not null
|
||||
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
||||
and not is_banned_from_posting
|
||||
order by created_time desc
|
||||
`,
|
||||
[userId],
|
||||
@@ -47,7 +47,7 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
||||
where target_id = $1
|
||||
and looking_for_matches
|
||||
and profiles.pinned_url is not null
|
||||
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
||||
and not is_banned_from_posting
|
||||
order by created_time desc
|
||||
`,
|
||||
[userId],
|
||||
@@ -74,7 +74,7 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
||||
where target2_id = $1
|
||||
and profiles.looking_for_matches
|
||||
and profiles.pinned_url is not null
|
||||
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
||||
and not is_banned_from_posting
|
||||
|
||||
union all
|
||||
|
||||
@@ -87,7 +87,7 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
||||
where target1_id = $1
|
||||
and profiles.looking_for_matches
|
||||
and profiles.pinned_url is not null
|
||||
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
||||
and not is_banned_from_posting
|
||||
`,
|
||||
[userId],
|
||||
(r) => ({
|
||||
|
||||
@@ -239,9 +239,8 @@ 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->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
|
||||
where(`not users.is_banned_from_posting`),
|
||||
// where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
|
||||
|
||||
...keywords.map((word) =>
|
||||
where(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {toUserAPIResponse} from 'common/api/user-types'
|
||||
import {RESERVED_PATHS} from 'common/envs/constants'
|
||||
import {debug} from 'common/logger'
|
||||
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 {cloneDeep} from 'lodash'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
import {updateUser} from 'shared/supabase/users'
|
||||
import {updateUser, updateUserData} from 'shared/supabase/users'
|
||||
import {getUser, getUserByUsername} from 'shared/utils'
|
||||
import {broadcastUpdatedUser} from 'shared/websockets/helpers'
|
||||
|
||||
@@ -29,7 +28,7 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => {
|
||||
if (reservedName) throw APIErrors.forbidden('This username is reserved')
|
||||
const otherUserExists = await getUserByUsername(cleanedUsername)
|
||||
if (otherUserExists && otherUserExists.id !== auth.uid)
|
||||
throw APIErrors.conflict('Username already taken')
|
||||
throw APIErrors.conflict('Username is already taken')
|
||||
update.username = cleanedUsername
|
||||
}
|
||||
|
||||
@@ -37,69 +36,23 @@ export const updateMe: APIHandler<'me/update'> = async (props, auth) => {
|
||||
|
||||
debug({update})
|
||||
|
||||
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 adds = {} as {[key: string]: string}
|
||||
const removes = []
|
||||
for (const [key, value] of Object.entries(stripped)) {
|
||||
if (value === null || value === '') {
|
||||
removes.push(key)
|
||||
} else if (value) {
|
||||
adds[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
let newLinks: any = null
|
||||
if (Object.keys(adds).length > 0 || removes.length > 0) {
|
||||
const data = await pg.oneOrNone(
|
||||
`update users
|
||||
set data = jsonb_set(
|
||||
data, '{link}',
|
||||
(data -> 'link' || $(adds)) - $(removes)
|
||||
)
|
||||
where id = $(id)
|
||||
returning data -> 'link' as link`,
|
||||
{adds, removes, id: auth.uid},
|
||||
)
|
||||
newLinks = data?.link
|
||||
}
|
||||
|
||||
if (name) {
|
||||
await pg.none(
|
||||
`update users
|
||||
set name = $1
|
||||
where id = $2`,
|
||||
[name, auth.uid],
|
||||
)
|
||||
}
|
||||
if (username) {
|
||||
await pg.none(
|
||||
`update users
|
||||
set username = $1
|
||||
where id = $2`,
|
||||
[username, auth.uid],
|
||||
)
|
||||
}
|
||||
if (avatarUrl) {
|
||||
await updateUser(pg, auth.uid, {avatarUrl})
|
||||
}
|
||||
const {name, username, avatarUrl, ...rest} = update
|
||||
await updateUserData(pg, auth.uid, removeUndefinedProps(rest))
|
||||
|
||||
// Ensure clients listening on `user/{id}` (e.g. AuthContext via useWebsocketUser)
|
||||
// get notified about link-only changes as well.
|
||||
if (name || username || avatarUrl || newLinks != null) {
|
||||
if (name || username || avatarUrl) {
|
||||
await updateUser(auth.uid, {name, username, avatarUrl})
|
||||
|
||||
broadcastUpdatedUser(
|
||||
removeUndefinedProps({
|
||||
id: auth.uid,
|
||||
name,
|
||||
username,
|
||||
avatarUrl,
|
||||
link: newLinks ?? undefined,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return toUserAPIResponse({...user, ...update, link: newLinks})
|
||||
return toUserAPIResponse({...user, ...update})
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ 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 {update} from 'shared/supabase/utils'
|
||||
import {updateProfile, updateUser} from 'shared/supabase/users'
|
||||
import {log} from 'shared/utils'
|
||||
|
||||
/**
|
||||
@@ -25,7 +24,7 @@ import {log} from 'shared/utils'
|
||||
* @returns Updated profile data
|
||||
* @throws {APIError} 404 if profile doesn't exist, 500 for database errors
|
||||
*/
|
||||
export const updateProfile: APIHandler<'update-profile'> = async (parsedBody, auth) => {
|
||||
export const updateProfileEndpoint: APIHandler<'update-profile'> = async (parsedBody, auth) => {
|
||||
trimStrings(parsedBody)
|
||||
log('Updating profile', parsedBody)
|
||||
const pg = createSupabaseDirectClient()
|
||||
@@ -43,12 +42,10 @@ export const updateProfile: APIHandler<'update-profile'> = async (parsedBody, au
|
||||
await removePinnedUrlFromPhotoUrls(parsedBody)
|
||||
|
||||
if (parsedBody.pinned_url) {
|
||||
await updateUser(pg, auth.uid, {avatarUrl: parsedBody.pinned_url})
|
||||
await updateUser(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(updateProfile(auth.uid, parsedBody))
|
||||
|
||||
if (error) {
|
||||
log('Error updating profile', error)
|
||||
|
||||
@@ -45,9 +45,9 @@ describe('banUser', () => {
|
||||
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,
|
||||
expect(supabaseUsers.updateUserData).toBeCalledTimes(1)
|
||||
expect(supabaseUsers.updateUserData).toBeCalledWith(mockPg, mockUser.userId, {
|
||||
is_banned_from_posting: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -68,8 +68,8 @@ describe('banUser', () => {
|
||||
expect(sharedAnalytics.trackPublicEvent).toBeCalledWith(mockAuth.uid, 'ban user', {
|
||||
userId: mockUser.userId,
|
||||
})
|
||||
expect(supabaseUsers.updateUser).toBeCalledWith(mockPg, mockUser.userId, {
|
||||
isBannedFromPosting: false,
|
||||
expect(supabaseUsers.updateUserData).toBeCalledWith(mockPg, mockUser.userId, {
|
||||
is_banned_from_posting: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('createComment', () => {
|
||||
name: 'Mock Creator',
|
||||
username: 'mock.creator.username',
|
||||
avatarUrl: 'mock.creator.avatarurl',
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
const mockContent = {
|
||||
content: {
|
||||
@@ -143,7 +143,7 @@ describe('createComment', () => {
|
||||
name: 'Mock Creator',
|
||||
username: 'mock.creator.username',
|
||||
avatarUrl: 'mock.creator.avatarurl',
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
const mockContent = {
|
||||
content: {
|
||||
@@ -227,7 +227,7 @@ describe('createComment', () => {
|
||||
name: 'Mock Creator',
|
||||
username: 'mock.creator.username',
|
||||
avatarUrl: 'mock.creator.avatarurl',
|
||||
isBannedFromPosting: true,
|
||||
is_banned_from_posting: true,
|
||||
}
|
||||
const mockContent = {
|
||||
content: {
|
||||
@@ -270,7 +270,7 @@ describe('createComment', () => {
|
||||
name: 'Mock Creator',
|
||||
username: 'mock.creator.username',
|
||||
avatarUrl: 'mock.creator.avatarurl',
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
const mockContent = {
|
||||
content: {
|
||||
@@ -316,7 +316,7 @@ describe('createComment', () => {
|
||||
name: 'Mock Creator',
|
||||
username: 'mock.creator.username',
|
||||
avatarUrl: 'mock.creator.avatarurl',
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
const mockContent = {
|
||||
content: {
|
||||
@@ -362,7 +362,7 @@ describe('createComment', () => {
|
||||
name: 'Mock Creator',
|
||||
username: 'mock.creator.username',
|
||||
avatarUrl: 'mock.creator.avatarurl',
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
const mockContent = {
|
||||
content: {
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('createPrivateUserMessageChannel', () => {
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockCreator = {
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator)
|
||||
@@ -104,7 +104,7 @@ describe('createPrivateUserMessageChannel', () => {
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockCreator = {
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator)
|
||||
@@ -183,7 +183,7 @@ describe('createPrivateUserMessageChannel', () => {
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockCreator = {
|
||||
isBannedFromPosting: true,
|
||||
is_banned_from_posting: true,
|
||||
}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator)
|
||||
@@ -207,7 +207,7 @@ describe('createPrivateUserMessageChannel', () => {
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockCreator = {
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator)
|
||||
@@ -237,7 +237,7 @@ describe('createPrivateUserMessageChannel', () => {
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockCreator = {
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator)
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('createPrivateUserMessage', () => {
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockCreator = {
|
||||
isBannedFromPosting: false,
|
||||
is_banned_from_posting: false,
|
||||
}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator)
|
||||
@@ -87,7 +87,7 @@ describe('createPrivateUserMessage', () => {
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockCreator = {
|
||||
isBannedFromPosting: true,
|
||||
is_banned_from_posting: true,
|
||||
}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockCreator)
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
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 {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(),
|
||||
}
|
||||
;(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',
|
||||
}
|
||||
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null})
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser)
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null})
|
||||
|
||||
const results: any = await createProfile(mockBody, mockAuth, mockReq)
|
||||
|
||||
expect(results.result).toEqual(mockData)
|
||||
expect(tryCatch).toBeCalledTimes(2)
|
||||
expect(mockPg.oneOrNone).toBeCalledTimes(1)
|
||||
expect(mockPg.oneOrNone).toBeCalledWith(
|
||||
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()
|
||||
|
||||
expect(sharedAnalytics.track).toBeCalledTimes(1)
|
||||
expect(sharedAnalytics.track).toBeCalledWith(mockAuth.uid, 'create profile', {
|
||||
username: mockUser.username,
|
||||
})
|
||||
expect(sendDiscordMessage).toBeCalledTimes(1)
|
||||
expect(sendDiscordMessage).toBeCalledWith(
|
||||
expect.stringContaining(mockUser.name && mockUser.username),
|
||||
'members',
|
||||
)
|
||||
})
|
||||
|
||||
it('should successfully create milestone profile', async () => {
|
||||
const mockBody = {
|
||||
city: 'mockCity',
|
||||
gender: 'mockGender',
|
||||
looking_for_matches: true,
|
||||
photo_urls: ['mockPhotoUrl1'],
|
||||
pinned_url: 'mockPinnedUrl',
|
||||
pref_gender: ['mockPrefGender'],
|
||||
pref_relation_styles: ['mockPrefRelationStyles'],
|
||||
visibility: 'public' as 'public' | 'member',
|
||||
wants_kids_strength: 2,
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
const mockNProfiles = 15
|
||||
const mockData = {
|
||||
age: 30,
|
||||
city: 'mockCity',
|
||||
}
|
||||
const mockUser = {
|
||||
createdTime: Date.now() - 2 * 60 * 60 * 1000, //2 hours ago
|
||||
name: 'mockName',
|
||||
username: 'mockUserName',
|
||||
}
|
||||
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null})
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser)
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null})
|
||||
|
||||
const results: any = await createProfile(mockBody, mockAuth, mockReq)
|
||||
|
||||
expect(results.result).toEqual(mockData)
|
||||
;(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles)
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(mockPg.one).toBeCalledTimes(1)
|
||||
expect(mockPg.one).toBeCalledWith(
|
||||
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',
|
||||
}
|
||||
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: false, error: null})
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(mockUser)
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: mockData, error: null})
|
||||
|
||||
const results: any = await createProfile(mockBody, mockAuth, mockReq)
|
||||
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
;(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Track error'))
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(errorSpy).toBeCalledWith(
|
||||
'Failed to track create profile',
|
||||
expect.objectContaining({name: 'Error'}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw if it failed to send discord new profile', async () => {
|
||||
const mockBody = {
|
||||
city: 'mockCity',
|
||||
gender: 'mockGender',
|
||||
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})
|
||||
|
||||
const results: any = await createProfile(mockBody, mockAuth, mockReq)
|
||||
|
||||
expect(results.result).toEqual(mockData)
|
||||
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
;(sendDiscordMessage as jest.Mock).mockRejectedValue(new Error('Sending error'))
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(errorSpy).toBeCalledWith(
|
||||
'Failed to send discord new profile',
|
||||
expect.objectContaining({name: 'Error'}),
|
||||
)
|
||||
})
|
||||
|
||||
it('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})
|
||||
|
||||
const results: any = await createProfile(mockBody, mockAuth, mockReq)
|
||||
|
||||
expect(results.result).toEqual(mockData)
|
||||
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
;(sendDiscordMessage as jest.Mock)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockRejectedValueOnce(new Error('Discord error'))
|
||||
;(mockPg.one as jest.Mock).mockReturnValue(mockNProfiles)
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(sendDiscordMessage).toBeCalledTimes(2)
|
||||
expect(sendDiscordMessage).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.stringContaining(String(mockNProfiles)),
|
||||
'general',
|
||||
)
|
||||
expect(errorSpy).toBeCalledWith(
|
||||
'Failed to send discord user milestone',
|
||||
expect.objectContaining({name: 'Error'}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw if the profile already exists', async () => {
|
||||
const mockBody = {
|
||||
city: 'mockCity',
|
||||
gender: 'mockGender',
|
||||
looking_for_matches: true,
|
||||
photo_urls: ['mockPhotoUrl1'],
|
||||
pinned_url: 'mockPinnedUrl',
|
||||
pref_gender: ['mockPrefGender'],
|
||||
pref_relation_styles: ['mockPrefRelationStyles'],
|
||||
visibility: 'public' as 'public' | 'member',
|
||||
wants_kids_strength: 2,
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: true, error: null})
|
||||
|
||||
await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError(
|
||||
'Profile already exists',
|
||||
)
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
;(tryCatch as jest.Mock).mockResolvedValueOnce({data: null, error: null})
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
|
||||
await expect(createProfile(mockBody, mockAuth, mockReq)).rejects.toThrowError(
|
||||
'Your account was not found',
|
||||
)
|
||||
})
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
;(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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -161,7 +161,7 @@ describe('createUserAndProfile', () => {
|
||||
expect(usernameUtils.cleanDisplayName).toBeCalledTimes(1)
|
||||
expect(usernameUtils.cleanDisplayName).toHaveBeenCalledWith(mockProps.name)
|
||||
expect(firebaseUtils.getBucket).toBeCalledTimes(1)
|
||||
expect(avatarHelpers.generateAvatarUrl).toBeCalledTimes(1)
|
||||
expect(avatarHelpers.generateAvatarUrl).toBeCalledTimes(0)
|
||||
expect(validateUsernameModule.validateUsername).toBeCalledTimes(1)
|
||||
expect(validateUsernameModule.validateUsername).toHaveBeenCalledWith(mockProps.username)
|
||||
expect(parsePhotos.removePinnedUrlFromPhotoUrls).toBeCalledTimes(1)
|
||||
@@ -318,11 +318,11 @@ describe('createUserAndProfile', () => {
|
||||
const mockAvatarUrl = 'mockGeneratedAvatarUrl'
|
||||
const mockNewUserRow = {
|
||||
created_time: 'mockCreatedTime',
|
||||
data: {isBannedFromPosting: true},
|
||||
id: 'mockNewUserId',
|
||||
name: 'mockName',
|
||||
name_username_vector: 'mockNameUsernameVector',
|
||||
username: 'mockUsername',
|
||||
is_banned_from_posting: true,
|
||||
}
|
||||
const mockPrivateUserRow = {
|
||||
data: {},
|
||||
@@ -357,9 +357,11 @@ describe('createUserAndProfile', () => {
|
||||
|
||||
await createUserAndProfile(mockProps, mockAuth, mockReq)
|
||||
|
||||
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith(
|
||||
expect(supabaseUtils.insert).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
'users',
|
||||
expect.objectContaining({
|
||||
isBannedFromPosting: true,
|
||||
is_banned_from_posting: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -405,7 +407,7 @@ describe('createUserAndProfile', () => {
|
||||
})
|
||||
|
||||
await expect(createUserAndProfile(mockProps, mockAuth, mockReq)).rejects.toThrowError(
|
||||
'User already exists',
|
||||
'An account for this user already exists',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -450,7 +452,7 @@ describe('createUserAndProfile', () => {
|
||||
})
|
||||
|
||||
await expect(createUserAndProfile(mockProps, mockAuth, mockReq)).rejects.toThrowError(
|
||||
'Username already taken',
|
||||
'Username is already taken',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,719 +0,0 @@
|
||||
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 {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
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
mockPg = {
|
||||
one: jest.fn(),
|
||||
tx: jest.fn(async (cb) => {
|
||||
const mockTx = {} as any
|
||||
return cb(mockTx)
|
||||
}),
|
||||
}
|
||||
;(supabaseInit.createSupabaseDirectClient as jest.Mock).mockReturnValue(mockPg)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
Object.defineProperty(hostingConstants, 'IS_LOCAL', {
|
||||
value: originalIsLocal,
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
describe('when given valid input', () => {
|
||||
it('should successfully create a user', async () => {
|
||||
Object.defineProperty(hostingConstants, 'IS_LOCAL', {
|
||||
value: false,
|
||||
writable: true,
|
||||
})
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const 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(mockFbUser)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false)
|
||||
;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null)
|
||||
;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow)
|
||||
;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow)
|
||||
|
||||
const results: any = await createUser(mockProps, mockAuth, mockReq)
|
||||
|
||||
expect(results.result.user).toEqual(mockNewUserRow)
|
||||
expect(results.result.privateUser).toEqual(mockPrivateUserRow)
|
||||
expect(mockGetUser).toBeCalledTimes(1)
|
||||
expect(mockGetUser).toHaveBeenNthCalledWith(1, mockAuth.uid)
|
||||
expect(usernameUtils.cleanDisplayName).toBeCalledTimes(1)
|
||||
expect(usernameUtils.cleanDisplayName).toHaveBeenCalledWith(mockFbUser.displayName)
|
||||
expect(usernameUtils.cleanUsername).toBeCalledTimes(1)
|
||||
expect(usernameUtils.cleanUsername).toBeCalledWith(mockFbUser.displayName)
|
||||
expect(mockPg.one).toBeCalledTimes(1)
|
||||
expect(mockPg.tx).toBeCalledTimes(1)
|
||||
expect(sharedUtils.getUser).toBeCalledTimes(1)
|
||||
expect(sharedUtils.getUser).toHaveBeenCalledWith(mockAuth.uid, expect.any(Object))
|
||||
expect(userNotificationPref.getDefaultNotificationPreferences).toBeCalledTimes(1)
|
||||
expect(supabaseUtils.insert).toBeCalledTimes(2)
|
||||
expect(supabaseUtils.insert).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.any(Object),
|
||||
'users',
|
||||
expect.objectContaining({
|
||||
id: mockAuth.uid,
|
||||
name: mockFbUser.displayName,
|
||||
username: mockFbUser.displayName,
|
||||
}),
|
||||
)
|
||||
expect(supabaseUtils.insert).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.any(Object),
|
||||
'private_users',
|
||||
expect.objectContaining({
|
||||
id: mockAuth.uid,
|
||||
}),
|
||||
)
|
||||
;(sharedAnalytics.track as jest.Mock).mockResolvedValue(null)
|
||||
;(emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null)
|
||||
;(apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockResolvedValue(null)
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(sharedAnalytics.track).toBeCalledTimes(1)
|
||||
expect(sharedAnalytics.track).toBeCalledWith(mockAuth.uid, 'create profile', {
|
||||
username: mockNewUserRow.username,
|
||||
})
|
||||
expect(emailHelpers.sendWelcomeEmail).toBeCalledTimes(1)
|
||||
expect(emailHelpers.sendWelcomeEmail).toBeCalledWith(mockNewUserRow, mockPrivateUserRow)
|
||||
expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledTimes(1)
|
||||
expect(apiSetLastTimeOnline.setLastOnlineTimeUser).toBeCalledWith(mockAuth.uid)
|
||||
})
|
||||
|
||||
it('should generate a avatar Url when creating a user', async () => {
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'password',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
const mockAvatarUrl = 'mockGeneratedAvatarUrl'
|
||||
const mockNewUserRow = {
|
||||
created_time: 'mockCreatedTime',
|
||||
data: {mockNewUserJson: 'mockNewUserJsonData'},
|
||||
id: 'mockNewUserId',
|
||||
name: 'mockName',
|
||||
name_username_vector: 'mockNameUsernameVector',
|
||||
username: 'mockUsername',
|
||||
}
|
||||
const mockPrivateUserRow = {
|
||||
data: {mockPrivateUserJson: 'mockPrivateUserJsonData'},
|
||||
id: 'mockPrivateUserId',
|
||||
}
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(avatarHelpers.generateAvatarUrl as jest.Mock).mockResolvedValue(mockAvatarUrl)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false)
|
||||
;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null)
|
||||
;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow)
|
||||
;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow)
|
||||
|
||||
await createUser(mockProps, mockAuth, mockReq)
|
||||
|
||||
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1)
|
||||
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith({
|
||||
avatarUrl: mockAvatarUrl,
|
||||
isBannedFromPosting: false,
|
||||
link: expect.any(Object),
|
||||
})
|
||||
})
|
||||
|
||||
it('should not allow a username that already exists when creating a user', async () => {
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'passwords',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
photoURL: 'mockPhotoUrl',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
const mockNewUserRow = {
|
||||
created_time: 'mockCreatedTime',
|
||||
data: {mockNewUserJson: 'mockNewUserJsonData'},
|
||||
id: 'mockNewUserId',
|
||||
name: 'mockName',
|
||||
name_username_vector: 'mockNameUsernameVector',
|
||||
username: 'mockUsername',
|
||||
}
|
||||
const mockPrivateUserRow = {
|
||||
data: {mockPrivateUserJson: 'mockPrivateUserJsonData'},
|
||||
id: 'mockPrivateUserId',
|
||||
}
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(1)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false)
|
||||
;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null)
|
||||
;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow)
|
||||
;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow)
|
||||
|
||||
await createUser(mockProps, mockAuth, mockReq)
|
||||
|
||||
expect(mockPg.one).toBeCalledTimes(1)
|
||||
expect(supabaseUtils.insert).toBeCalledTimes(2)
|
||||
expect(supabaseUtils.insert).not.toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.any(Object),
|
||||
'users',
|
||||
expect.objectContaining({
|
||||
id: mockAuth.uid,
|
||||
name: mockFbUser.displayName,
|
||||
username: mockFbUser.displayName,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should successfully create a user who is banned from posting if there ip/device token is banned', async () => {
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'passwords',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
photoURL: 'mockPhotoUrl',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
const mockNewUserRow = {
|
||||
created_time: 'mockCreatedTime',
|
||||
data: {mockNewUserJson: 'mockNewUserJsonData'},
|
||||
id: 'mockNewUserId',
|
||||
name: 'mockName',
|
||||
name_username_vector: 'mockNameUsernameVector',
|
||||
username: 'mockUsername',
|
||||
}
|
||||
const mockPrivateUserRow = {
|
||||
data: {mockPrivateUserJson: 'mockPrivateUserJsonData'},
|
||||
id: 'mockPrivateUserId',
|
||||
}
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(avatarHelpers.generateAvatarUrl as jest.Mock).mockResolvedValue(mockFbUser.photoURL)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false)
|
||||
jest.spyOn(Array.prototype, 'includes').mockReturnValue(true)
|
||||
;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null)
|
||||
;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow)
|
||||
;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow)
|
||||
|
||||
await createUser(mockProps, mockAuth, mockReq)
|
||||
|
||||
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledTimes(1)
|
||||
expect(objectUtils.removeUndefinedProps).toHaveBeenCalledWith({
|
||||
avatarUrl: mockFbUser.photoURL,
|
||||
isBannedFromPosting: true,
|
||||
link: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an error occurs', () => {
|
||||
it('should throw if the user already exists', async () => {
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'passwords',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
photoURL: 'mockPhotoUrl',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true)
|
||||
|
||||
expect(createUser(mockProps, mockAuth, mockReq)).rejects.toThrowError('User already exists')
|
||||
})
|
||||
|
||||
it('should throw if the username is already taken', async () => {
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'passwords',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
photoURL: 'mockPhotoUrl',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(true)
|
||||
|
||||
expect(createUser(mockProps, mockAuth, mockReq)).rejects.toThrowError(
|
||||
'Username already taken',
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw if failed to track create profile', async () => {
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'passwords',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
photoURL: 'mockPhotoUrl',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
const mockNewUserRow = {
|
||||
created_time: 'mockCreatedTime',
|
||||
data: {mockNewUserJson: 'mockNewUserJsonData'},
|
||||
id: 'mockNewUserId',
|
||||
name: 'mockName',
|
||||
name_username_vector: 'mockNameUsernameVector',
|
||||
username: 'mockUsername',
|
||||
}
|
||||
const mockPrivateUserRow = {
|
||||
data: {mockPrivateUserJson: 'mockPrivateUserJsonData'},
|
||||
id: 'mockPrivateUserId',
|
||||
}
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false)
|
||||
;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null)
|
||||
;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow)
|
||||
;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow)
|
||||
|
||||
const results: any = await createUser(mockProps, mockAuth, mockReq)
|
||||
|
||||
;(sharedAnalytics.track as jest.Mock).mockRejectedValue(new Error('Tracking failed'))
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('Failed to track create profile', expect.any(Error))
|
||||
})
|
||||
|
||||
it('should throw if failed to send a welcome email', async () => {
|
||||
Object.defineProperty(hostingConstants, 'IS_LOCAL', {
|
||||
value: false,
|
||||
writable: true,
|
||||
})
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'passwords',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
photoURL: 'mockPhotoUrl',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
const mockNewUserRow = {
|
||||
created_time: 'mockCreatedTime',
|
||||
data: {mockNewUserJson: 'mockNewUserJsonData'},
|
||||
id: 'mockNewUserId',
|
||||
name: 'mockName',
|
||||
name_username_vector: 'mockNameUsernameVector',
|
||||
username: 'mockUsername',
|
||||
}
|
||||
const mockPrivateUserRow = {
|
||||
data: {mockPrivateUserJson: 'mockPrivateUserJsonData'},
|
||||
id: 'mockPrivateUserId',
|
||||
}
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false)
|
||||
;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null)
|
||||
;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow)
|
||||
;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow)
|
||||
|
||||
const results: any = await createUser(mockProps, mockAuth, mockReq)
|
||||
|
||||
;(sharedAnalytics.track as jest.Mock).mockResolvedValue(null)
|
||||
;(emailHelpers.sendWelcomeEmail as jest.Mock).mockRejectedValue(
|
||||
new Error('Welcome email failed'),
|
||||
)
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(errorSpy).toBeCalledWith('Failed to sendWelcomeEmail', expect.any(Error))
|
||||
})
|
||||
|
||||
it('should throw if failed to set last time online', async () => {
|
||||
const mockProps = {
|
||||
deviceToken: 'mockDeviceToken',
|
||||
adminToken: 'mockAdminToken',
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReferer = {
|
||||
headers: {
|
||||
referer: 'mockReferer',
|
||||
},
|
||||
}
|
||||
const mockReq = {get: jest.fn().mockReturnValue(mockReferer)} as any
|
||||
const mockFirebaseUser = {
|
||||
providerData: [
|
||||
{
|
||||
providerId: 'passwords',
|
||||
},
|
||||
],
|
||||
}
|
||||
const mockFbUser = {
|
||||
email: 'mockEmail@mockServer.com',
|
||||
displayName: 'mockDisplayName',
|
||||
photoURL: 'mockPhotoUrl',
|
||||
}
|
||||
const mockIp = 'mockIP'
|
||||
const mockBucket = {} as any
|
||||
const mockNewUserRow = {
|
||||
created_time: 'mockCreatedTime',
|
||||
data: {mockNewUserJson: 'mockNewUserJsonData'},
|
||||
id: 'mockNewUserId',
|
||||
name: 'mockName',
|
||||
name_username_vector: 'mockNameUsernameVector',
|
||||
username: 'mockUsername',
|
||||
}
|
||||
const mockPrivateUserRow = {
|
||||
data: {mockPrivateUserJson: 'mockPrivateUserJsonData'},
|
||||
id: 'mockPrivateUserId',
|
||||
}
|
||||
|
||||
const mockGetUser = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockFirebaseUser)
|
||||
.mockResolvedValueOnce(mockFbUser)
|
||||
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(sharedAnalytics.getIp as jest.Mock).mockReturnValue(mockIp)
|
||||
;(firebaseAdmin.auth as jest.Mock).mockReturnValue({
|
||||
getUser: mockGetUser,
|
||||
})
|
||||
;(usernameUtils.cleanDisplayName as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(firebaseUtils.getBucket as jest.Mock).mockReturnValue(mockBucket)
|
||||
;(usernameUtils.cleanUsername as jest.Mock).mockReturnValue(mockFbUser.displayName)
|
||||
;(mockPg.one as jest.Mock).mockResolvedValue(0)
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockResolvedValue(false)
|
||||
;(userNotificationPref.getDefaultNotificationPreferences as jest.Mock).mockReturnValue(null)
|
||||
;(supabaseUtils.insert as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(supabaseUsers.convertUser as jest.Mock).mockReturnValue(mockNewUserRow)
|
||||
;(supabaseUsers.convertPrivateUser as jest.Mock).mockReturnValue(mockPrivateUserRow)
|
||||
|
||||
const results: any = await createUser(mockProps, mockAuth, mockReq)
|
||||
|
||||
;(sharedAnalytics.track as jest.Mock).mockResolvedValue(null)
|
||||
;(emailHelpers.sendWelcomeEmail as jest.Mock).mockResolvedValue(null)
|
||||
;(apiSetLastTimeOnline.setLastOnlineTimeUser as jest.Mock).mockRejectedValue(
|
||||
new Error('Failed to set last online time'),
|
||||
)
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
await results.continue()
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('Failed to set last online time', expect.any(Error))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,6 @@ jest.mock('shared/websockets/helpers')
|
||||
|
||||
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'
|
||||
@@ -31,18 +30,16 @@ describe('updateMe', () => {
|
||||
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}
|
||||
|
||||
;(sharedUtils.getUser as jest.Mock).mockResolvedValue(true)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(false)
|
||||
;(supabaseUsers.updateUser as jest.Mock)
|
||||
;(supabaseUsers.updateUserData as jest.Mock)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(null)
|
||||
;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockData)
|
||||
;(mockPg.oneOrNone as jest.Mock).mockResolvedValue({})
|
||||
;(mockPg.none as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(null)
|
||||
;(websocketHelperModules.broadcastUpdatedUser as jest.Mock).mockReturnValue(null)
|
||||
|
||||
@@ -52,40 +49,14 @@ describe('updateMe', () => {
|
||||
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(supabaseUsers.updateUserData).toBeCalledTimes(1)
|
||||
expect(supabaseUsers.updateUserData).toHaveBeenCalledWith(
|
||||
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`),
|
||||
[mockProps.name, mockAuth.uid],
|
||||
)
|
||||
expect(mockPg.none).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
sqlMatch(`update users
|
||||
set username = $1
|
||||
where id = $2`),
|
||||
[mockProps.username, mockAuth.uid],
|
||||
)
|
||||
expect(supabaseUsers.updateUser).toBeCalledTimes(1)
|
||||
expect(supabaseUsers.updateUser).toHaveBeenCalledWith(mockAuth.uid, mockProps)
|
||||
expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledTimes(1)
|
||||
expect(websocketHelperModules.broadcastUpdatedUser).toBeCalledWith({
|
||||
...mockProps,
|
||||
@@ -149,7 +120,7 @@ describe('updateMe', () => {
|
||||
arraySpy.mockReturnValue(false)
|
||||
;(sharedUtils.getUserByUsername as jest.Mock).mockReturnValue(true)
|
||||
|
||||
expect(updateMe(mockProps, mockAuth, mockReq)).rejects.toThrow('Username already taken')
|
||||
expect(updateMe(mockProps, mockAuth, mockReq)).rejects.toThrow('Username is already taken')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ jest.mock('shared/profiles/parse-photos')
|
||||
jest.mock('shared/supabase/users')
|
||||
|
||||
import {AuthedUser} from 'api/helpers/endpoint'
|
||||
import {updateProfile} from 'api/update-profile'
|
||||
import {updateProfileEndpoint} from 'api/update-profile'
|
||||
import {sqlMatch} from 'common/test-utils'
|
||||
import {tryCatch} from 'common/util/try-catch'
|
||||
import {removePinnedUrlFromPhotoUrls} from 'shared/profiles/parse-photos'
|
||||
@@ -39,7 +39,7 @@ describe('updateProfiles', () => {
|
||||
.mockResolvedValueOnce({data: true})
|
||||
.mockResolvedValueOnce({data: mockData, error: null})
|
||||
|
||||
const result = await updateProfile(mockProps, mockAuth, mockReq)
|
||||
const result = await updateProfileEndpoint(mockProps, mockAuth, mockReq)
|
||||
|
||||
expect(result).toBe(mockData)
|
||||
expect(mockPg.oneOrNone).toBeCalledTimes(1)
|
||||
@@ -49,8 +49,8 @@ describe('updateProfiles', () => {
|
||||
)
|
||||
expect(removePinnedUrlFromPhotoUrls).toBeCalledTimes(1)
|
||||
expect(removePinnedUrlFromPhotoUrls).toBeCalledWith(mockProps)
|
||||
expect(supabaseUsers.updateUser).toBeCalledTimes(1)
|
||||
expect(supabaseUsers.updateUser).toBeCalledWith(expect.any(Object), mockAuth.uid, {
|
||||
expect(supabaseUsers.updateUserData).toBeCalledTimes(1)
|
||||
expect(supabaseUsers.updateUserData).toBeCalledWith(expect.any(Object), mockAuth.uid, {
|
||||
avatarUrl: mockProps.pinned_url,
|
||||
})
|
||||
expect(supabaseUtils.update).toBeCalledTimes(1)
|
||||
@@ -66,19 +66,21 @@ describe('updateProfiles', () => {
|
||||
describe('when an error occurs', () => {
|
||||
it('should throw if the profile does not exist', async () => {
|
||||
const mockProps = {
|
||||
avatar_url: 'mockAvatarUrl',
|
||||
age: 28,
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
|
||||
;(tryCatch as jest.Mock).mockResolvedValue({data: false})
|
||||
|
||||
expect(updateProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Profile not found')
|
||||
expect(updateProfileEndpoint(mockProps, mockAuth, mockReq)).rejects.toThrow(
|
||||
'Profile not found',
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw if unable to update the profile', async () => {
|
||||
const mockProps = {
|
||||
avatar_url: 'mockAvatarUrl',
|
||||
age: 28,
|
||||
}
|
||||
const mockAuth = {uid: '321'} as AuthedUser
|
||||
const mockReq = {} as any
|
||||
@@ -87,7 +89,9 @@ describe('updateProfiles', () => {
|
||||
.mockResolvedValueOnce({data: true})
|
||||
.mockResolvedValueOnce({data: null, error: Error})
|
||||
|
||||
expect(updateProfile(mockProps, mockAuth, mockReq)).rejects.toThrow('Error updating profile')
|
||||
expect(updateProfileEndpoint(mockProps, mockAuth, mockReq)).rejects.toThrow(
|
||||
'Error updating profile',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,10 +23,6 @@ export const mockUser: User = {
|
||||
id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
|
||||
username: 'Martin',
|
||||
name: 'Martin',
|
||||
link: {
|
||||
site: 'martinbraquet.com',
|
||||
x: 'martin',
|
||||
},
|
||||
}
|
||||
|
||||
export const jamesUser: User = {
|
||||
@@ -35,9 +31,6 @@ export const jamesUser: User = {
|
||||
id: '5LZ4LgYuySdL1huCWe7bti02ghx2',
|
||||
username: 'James',
|
||||
name: 'James',
|
||||
link: {
|
||||
x: 'james',
|
||||
},
|
||||
}
|
||||
|
||||
export const jamesProfile: PartialProfile = {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {runScript} from './run-script'
|
||||
import {log} from 'shared/monitoring/log'
|
||||
import {bulkUpdate} from 'shared/supabase/utils'
|
||||
import {chunk} from 'lodash'
|
||||
import {removeUndefinedProps} from 'common/util/object'
|
||||
|
||||
runScript(async ({pg}) => {
|
||||
const users = await pg.manyOrNone(`
|
||||
select u.data, u.id, p.id as pid
|
||||
from users u
|
||||
left join profiles p on p.user_id = u.id
|
||||
`)
|
||||
|
||||
log('Found', users.length, 'users to migrate')
|
||||
|
||||
const userUpdates: {id: string; avatar_url: string; is_banned_from_posting: boolean}[] = []
|
||||
const profileUpdates: {id: string; links: Record<string, string>}[] = []
|
||||
|
||||
for (const {id, pid, data} of users) {
|
||||
const {link: links, avatarUrl: avatar_url, isBannedFromPosting: is_banned_from_posting} = data
|
||||
|
||||
if (avatar_url || is_banned_from_posting) {
|
||||
userUpdates.push(
|
||||
removeUndefinedProps({
|
||||
id,
|
||||
avatar_url: avatar_url ?? null,
|
||||
is_banned_from_posting,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (links && pid) {
|
||||
profileUpdates.push({id: pid, links})
|
||||
} else if (links && !pid) {
|
||||
log('Warning: user has links but no profile', id)
|
||||
}
|
||||
}
|
||||
|
||||
log(`Migrating ${userUpdates.length} users, ${profileUpdates.length} profiles`)
|
||||
|
||||
let userCount = 0
|
||||
for (const batch of chunk(userUpdates, 100)) {
|
||||
await bulkUpdate(pg, 'users', ['id'], batch)
|
||||
log('Updated users', (userCount += batch.length))
|
||||
}
|
||||
|
||||
let profileCount = 0
|
||||
for (const batch of chunk(profileUpdates, 100)) {
|
||||
// If not, write the profiles update directly:
|
||||
await pg.none(`
|
||||
UPDATE profiles SET links = v.links::jsonb
|
||||
FROM (VALUES ${batch.map((r) => `(${r.id}, '${JSON.stringify(r.links)}')`).join(',')})
|
||||
AS v(id, links)
|
||||
WHERE profiles.id = v.id::bigint
|
||||
`)
|
||||
log('Updated profiles', (profileCount += batch.length))
|
||||
}
|
||||
|
||||
// Strip migrated fields from the JSON blob
|
||||
await pg.none(`
|
||||
update users
|
||||
set data = data - '{link,avatarUrl,isBannedFromPosting}'::text[]
|
||||
`)
|
||||
|
||||
log('Migration complete')
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
42
backend/scripts/restore_data_column.ts
Normal file
42
backend/scripts/restore_data_column.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {runScript} from './run-script'
|
||||
import {log} from 'shared/monitoring/log'
|
||||
import {chunk} from 'lodash'
|
||||
import {Client} from 'pg'
|
||||
|
||||
runScript(async ({pg}) => {
|
||||
// Connect to local Supabase emulator
|
||||
const localClient = new Client({
|
||||
host: '127.0.0.1',
|
||||
port: 54322,
|
||||
user: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'postgres',
|
||||
})
|
||||
await localClient.connect()
|
||||
log('Connected to local emulator')
|
||||
|
||||
// Read all user data from the backup
|
||||
let {rows} = await localClient.query<{id: string; data: object}>(`SELECT id, data FROM users`)
|
||||
await localClient.end()
|
||||
|
||||
// rows = rows.slice(0, 5)
|
||||
// log(rows)
|
||||
|
||||
log(`Found ${rows.length} users in backup`)
|
||||
|
||||
// Update remote in batches
|
||||
let count = 0
|
||||
for (const batch of chunk(rows, 100)) {
|
||||
await pg.none(
|
||||
`UPDATE users AS target
|
||||
SET data = v.data::jsonb
|
||||
FROM (VALUES ${batch.map((_, i) => `($${i * 2 + 1}, $${i * 2 + 2})`).join(', ')})
|
||||
AS v(id, data)
|
||||
WHERE target.id = v.id`,
|
||||
batch.flatMap((r) => [r.id, JSON.stringify(r.data)]),
|
||||
)
|
||||
log(`Restored data for ${(count += batch.length)} / ${rows.length} users`)
|
||||
}
|
||||
|
||||
log('Done restoring data column from backup')
|
||||
})
|
||||
@@ -66,8 +66,8 @@ export const getGenderCompatibleProfiles = async (profile: ProfileRow) => {
|
||||
users on users.id = profiles.user_id
|
||||
where user_id != $(user_id)
|
||||
and looking_for_matches
|
||||
and (data ->> 'isBannedFromPosting' != 'true' or data ->> 'isBannedFromPosting' is null)
|
||||
and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null)
|
||||
and not is_banned_from_posting
|
||||
-- and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null)
|
||||
and profiles.pinned_url is not null
|
||||
`,
|
||||
{...profile},
|
||||
@@ -86,8 +86,8 @@ export const getCompatibleProfiles = async (profile: ProfileRow, radiusKm: numbe
|
||||
users on users.id = profiles.user_id
|
||||
where user_id != $(user_id)
|
||||
and looking_for_matches
|
||||
and (data ->> 'isBannedFromPosting' != 'true' or data ->> 'isBannedFromPosting' is null)
|
||||
and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null)
|
||||
and not is_banned_from_posting
|
||||
-- and (data ->> 'userDeleted' != 'true' or data ->> 'userDeleted' is null)
|
||||
|
||||
-- Gender
|
||||
and (profiles.gender = any ($(pref_gender)) or profiles.gender = 'non-binary')
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
import {ProfileRow} from 'common/profiles/profile'
|
||||
import {convertUserToDb} from 'common/supabase/users'
|
||||
import {User} from 'common/user'
|
||||
import {SupabaseDirectClient} from 'shared/supabase/init'
|
||||
import {removeUndefinedProps} from 'common/util/object'
|
||||
import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init'
|
||||
import {broadcastUpdatedPrivateUser, broadcastUpdatedUser} from 'shared/websockets/helpers'
|
||||
|
||||
import {DataUpdate, updateData} from './utils'
|
||||
import {DataUpdate, update, updateData} from './utils'
|
||||
|
||||
export const updateProfile = async (user_id: string, updated: Partial<ProfileRow>) => {
|
||||
updated = removeUndefinedProps(updated)
|
||||
// if (!updated) return
|
||||
const fullUpdate = {user_id, ...updated}
|
||||
const pg = createSupabaseDirectClient()
|
||||
return await update(pg, 'profiles', 'user_id', fullUpdate)
|
||||
}
|
||||
|
||||
export const updateUser = async (id: string, updated: Partial<User>) => {
|
||||
updated = removeUndefinedProps(updated)
|
||||
if (!updated) return
|
||||
const fullUpdate = {id, ...updated}
|
||||
const pg = createSupabaseDirectClient()
|
||||
const result = await update(pg, 'users', 'id', convertUserToDb(fullUpdate))
|
||||
broadcastUpdatedUser(fullUpdate)
|
||||
return result
|
||||
}
|
||||
|
||||
/** only updates data column. do not use for name, username */
|
||||
export const updateUser = async (db: SupabaseDirectClient, id: string, update: Partial<User>) => {
|
||||
const fullUpdate = {id, ...update}
|
||||
await updateData(db, 'users', 'id', fullUpdate)
|
||||
broadcastUpdatedUser(fullUpdate)
|
||||
export const updateUserData = async (
|
||||
db: SupabaseDirectClient,
|
||||
id: string,
|
||||
updated: DataUpdate<'users'>,
|
||||
) => {
|
||||
updated = removeUndefinedProps(updated)
|
||||
if (!updated) return
|
||||
const fullUpdate = {id, ...updated}
|
||||
const result = await updateData(db, 'users', 'id', fullUpdate)
|
||||
broadcastUpdatedUser(fullUpdate as any) // maybe fix
|
||||
return result
|
||||
}
|
||||
|
||||
export const updatePrivateUser = async (
|
||||
db: SupabaseDirectClient,
|
||||
id: string,
|
||||
update: DataUpdate<'private_users'>,
|
||||
updated: DataUpdate<'private_users'>,
|
||||
) => {
|
||||
await updateData(db, 'private_users', 'id', {id, ...update})
|
||||
updated = removeUndefinedProps(updated)
|
||||
if (!updated) return
|
||||
const result = await updateData(db, 'private_users', 'id', {id, ...updated})
|
||||
broadcastUpdatedPrivateUser(id)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS profiles
|
||||
image_descriptions jsonb,
|
||||
is_smoker BOOLEAN,
|
||||
last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
links JSONB default '{}'::jsonb not null,
|
||||
looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
allow_direct_messaging BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
allow_interest_indicating BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
@@ -61,12 +62,10 @@ CREATE TABLE IF NOT EXISTS profiles
|
||||
religion TEXT[],
|
||||
religious_belief_strength INTEGER,
|
||||
religious_beliefs TEXT,
|
||||
twitter TEXT,
|
||||
university TEXT,
|
||||
user_id TEXT NOT NULL,
|
||||
visibility profile_visibility DEFAULT 'member'::profile_visibility NOT NULL,
|
||||
wants_kids_strength INTEGER DEFAULT 0,
|
||||
website TEXT,
|
||||
CONSTRAINT profiles_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
name_username_vector tsvector GENERATED ALWAYS AS (
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
id TEXT DEFAULT random_alphanumeric(12) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
name_username_vector tsvector GENERATED ALWAYS AS (
|
||||
to_tsvector(
|
||||
'english'::regconfig,
|
||||
(name || ' '::text) || username
|
||||
'english'::regconfig,
|
||||
(name || ' '::text) || username
|
||||
)
|
||||
) STORED,
|
||||
username TEXT NOT NULL,
|
||||
) STORED,
|
||||
username TEXT NOT NULL,
|
||||
avatar_url TEXT,
|
||||
is_banned_from_posting BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
|
||||
CONSTRAINT users_pkey PRIMARY KEY (id)
|
||||
);
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE users
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policies
|
||||
DROP POLICY IF EXISTS "public read" ON users;
|
||||
CREATE POLICY "public read" ON users
|
||||
FOR SELECT
|
||||
USING (true);
|
||||
FOR SELECT
|
||||
USING (true);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS user_username_idx ON public.users USING btree (username);
|
||||
@@ -28,7 +33,3 @@ CREATE INDEX IF NOT EXISTS user_username_idx ON public.users USING btree (userna
|
||||
CREATE INDEX IF NOT EXISTS users_created_time ON public.users USING btree (created_time DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_name_idx ON public.users USING btree (name);
|
||||
|
||||
-- Remove these if you trust PRIMARY KEY auto-index:
|
||||
-- DROP INDEX IF EXISTS users_pkey;
|
||||
-- CREATE UNIQUE INDEX users_pkey ON public.users USING btree (id);
|
||||
|
||||
@@ -309,7 +309,6 @@ export const API = (_apiTypeCheck = {
|
||||
locale: z.string().optional(),
|
||||
username: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
link: z.record(z.string().nullable()).optional(),
|
||||
profile: combinedProfileSchema,
|
||||
interests: arraybeSchema.optional(),
|
||||
causes: arraybeSchema.optional(),
|
||||
@@ -378,7 +377,6 @@ export const API = (_apiTypeCheck = {
|
||||
name: z.string().trim().min(1).optional(),
|
||||
username: z.string().trim().min(1).optional(),
|
||||
avatarUrl: z.string().optional(),
|
||||
link: z.record(z.string().nullable()).optional(),
|
||||
}),
|
||||
returns: {} as FullUser,
|
||||
summary: 'Update authenticated user profile and settings',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ENV_CONFIG, MOD_USERNAMES} from 'common/envs/constants'
|
||||
import {ENV_CONFIG, MOD_USERNAMES, WEB_URL} from 'common/envs/constants'
|
||||
import {User} from 'common/user'
|
||||
import {removeUndefinedProps} from 'common/util/object'
|
||||
|
||||
@@ -20,7 +20,7 @@ export type FullUser = User & {
|
||||
export function toUserAPIResponse(user: User): FullUser {
|
||||
return removeUndefinedProps({
|
||||
...user,
|
||||
url: `https://${ENV_CONFIG.domain}/${user.username}`,
|
||||
url: `${WEB_URL}/${user.username}`,
|
||||
isAdmin: ENV_CONFIG.adminIds.includes(user.id),
|
||||
isTrustworthy: MOD_USERNAMES.includes(user.username),
|
||||
})
|
||||
|
||||
@@ -70,7 +70,6 @@ export const baseProfilesSchema = z.object({
|
||||
})
|
||||
|
||||
const optionalProfilesSchema = z.object({
|
||||
avatar_url: z.string().optional().nullable(),
|
||||
bio: contentSchema.optional().nullable(),
|
||||
big5_openness: z.number().min(0).max(100).optional().nullable(),
|
||||
big5_conscientiousness: z.number().min(0).max(100).optional().nullable(),
|
||||
@@ -96,6 +95,7 @@ const optionalProfilesSchema = z.object({
|
||||
image_descriptions: z.any().optional().nullable(),
|
||||
interests: z.array(z.string()).optional().nullable(),
|
||||
is_smoker: zBoolean.optional().nullable(),
|
||||
links: z.record(z.string().nullable()).optional(),
|
||||
mbti: z.string().optional().nullable(),
|
||||
occupation: z.string().optional().nullable(),
|
||||
occupation_title: z.string().optional().nullable(),
|
||||
|
||||
@@ -1105,12 +1105,13 @@ export type Database = {
|
||||
has_kids: number | null
|
||||
headline: string | null
|
||||
height_in_inches: number | null
|
||||
keywords: string[] | null
|
||||
id: number
|
||||
image_descriptions: Json | null
|
||||
is_smoker: boolean | null
|
||||
keywords: string[] | null
|
||||
languages: string[] | null
|
||||
last_modification_time: string
|
||||
links: Json
|
||||
looking_for_matches: boolean
|
||||
mbti: string | null
|
||||
messaging_status: string
|
||||
@@ -1140,12 +1141,10 @@ export type Database = {
|
||||
religious_beliefs: string | null
|
||||
search_text: string | null
|
||||
search_tsv: unknown
|
||||
twitter: string | null
|
||||
university: string | null
|
||||
user_id: string
|
||||
visibility: Database['public']['Enums']['lover_visibility']
|
||||
wants_kids_strength: number | null
|
||||
website: string | null
|
||||
}
|
||||
Insert: {
|
||||
age?: number | null
|
||||
@@ -1180,10 +1179,11 @@ export type Database = {
|
||||
height_in_inches?: number | null
|
||||
id?: number
|
||||
image_descriptions?: Json | null
|
||||
keywords?: string[] | null
|
||||
is_smoker?: boolean | null
|
||||
keywords?: string[] | null
|
||||
languages?: string[] | null
|
||||
last_modification_time?: string
|
||||
links?: Json
|
||||
looking_for_matches?: boolean
|
||||
mbti?: string | null
|
||||
messaging_status?: string
|
||||
@@ -1213,12 +1213,10 @@ export type Database = {
|
||||
religious_beliefs?: string | null
|
||||
search_text?: string | null
|
||||
search_tsv?: unknown
|
||||
twitter?: string | null
|
||||
university?: string | null
|
||||
user_id: string
|
||||
visibility?: Database['public']['Enums']['lover_visibility']
|
||||
wants_kids_strength?: number | null
|
||||
website?: string | null
|
||||
}
|
||||
Update: {
|
||||
age?: number | null
|
||||
@@ -1253,10 +1251,11 @@ export type Database = {
|
||||
height_in_inches?: number | null
|
||||
id?: number
|
||||
image_descriptions?: Json | null
|
||||
keywords?: string[] | null
|
||||
is_smoker?: boolean | null
|
||||
keywords?: string[] | null
|
||||
languages?: string[] | null
|
||||
last_modification_time?: string
|
||||
links?: Json
|
||||
looking_for_matches?: boolean
|
||||
mbti?: string | null
|
||||
messaging_status?: string
|
||||
@@ -1286,12 +1285,10 @@ export type Database = {
|
||||
religious_beliefs?: string | null
|
||||
search_text?: string | null
|
||||
search_tsv?: unknown
|
||||
twitter?: string | null
|
||||
university?: string | null
|
||||
user_id?: string
|
||||
visibility?: Database['public']['Enums']['lover_visibility']
|
||||
wants_kids_strength?: number | null
|
||||
website?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
@@ -1538,25 +1535,31 @@ export type Database = {
|
||||
}
|
||||
users: {
|
||||
Row: {
|
||||
avatar_url: string | null
|
||||
created_time: string
|
||||
data: Json
|
||||
id: string
|
||||
is_banned_from_posting: boolean | null
|
||||
name: string
|
||||
name_username_vector: unknown
|
||||
username: string
|
||||
}
|
||||
Insert: {
|
||||
avatar_url?: string | null
|
||||
created_time?: string
|
||||
data: Json
|
||||
id?: string
|
||||
is_banned_from_posting?: boolean | null
|
||||
name: string
|
||||
name_username_vector?: unknown
|
||||
username: string
|
||||
}
|
||||
Update: {
|
||||
avatar_url?: string | null
|
||||
created_time?: string
|
||||
data?: Json
|
||||
id?: string
|
||||
is_banned_from_posting?: boolean | null
|
||||
name?: string
|
||||
name_username_vector?: unknown
|
||||
username?: string
|
||||
@@ -1741,7 +1744,7 @@ export type Database = {
|
||||
is_admin: {Args: never; Returns: boolean} | {Args: {user_id: string}; Returns: boolean}
|
||||
millis_interval: {
|
||||
Args: {end_millis: number; start_millis: number}
|
||||
Returns: unknown
|
||||
Returns: string
|
||||
}
|
||||
millis_to_ts: {Args: {millis: number}; Returns: string}
|
||||
random_alphanumeric: {Args: {length: number}; Returns: string}
|
||||
|
||||
@@ -1,23 +1,49 @@
|
||||
import {PrivateUser, User} from 'common/user'
|
||||
import {removeUndefinedProps} from 'common/util/object'
|
||||
|
||||
import {Row, run, SupabaseClient, tsToMillis} from './utils'
|
||||
import {millisToTs, Row, run, SupabaseClient, tsToMillis} from './utils'
|
||||
|
||||
export async function getUserForStaticProps(db: SupabaseClient, username: string) {
|
||||
const {data} = await run(db.from('users').select().ilike('username', username))
|
||||
return convertUser(data[0] ?? null)
|
||||
}
|
||||
|
||||
export function convertUser(row: Row<'users'>): User
|
||||
export function convertUser(row: Row<'users'> | null): User | null {
|
||||
if (!row) return null
|
||||
|
||||
function toDb(row: any): any {
|
||||
return {
|
||||
...(row.data as any),
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
name: row.name,
|
||||
createdTime: tsToMillis(row.created_time),
|
||||
} as User
|
||||
avatarUrl: row.avatar_url,
|
||||
isBannedFromPosting: row.is_banned_from_posting,
|
||||
createdTime: row.created_time ? tsToMillis(row.created_time) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// From DB to typescript
|
||||
export function convertUser(row: Row<'users'>): User
|
||||
export function convertUser(row: Row<'users'> | null): User | null {
|
||||
if (!row) return null
|
||||
return toDb(row)
|
||||
}
|
||||
|
||||
export function convertPartialUser(row: Partial<Row<'users'>>): Partial<User> {
|
||||
return removeUndefinedProps(toDb(row))
|
||||
}
|
||||
|
||||
// Reciprocal of convertUser, from typescript to DB
|
||||
export function convertUserToDb(user: Partial<User>): Partial<Row<'users'>>
|
||||
export function convertUserToDb(user: Partial<User> | null): Partial<Row<'users'>> | null {
|
||||
if (!user) return null
|
||||
|
||||
return removeUndefinedProps({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
avatar_url: user.avatarUrl,
|
||||
is_banned_from_posting: user.isBannedFromPosting,
|
||||
created_time: millisToTs(user.createdTime),
|
||||
})
|
||||
}
|
||||
|
||||
export function convertPrivateUser(row: Row<'private_users'>): PrivateUser
|
||||
@@ -26,4 +52,4 @@ export function convertPrivateUser(row: Row<'private_users'> | null): PrivateUse
|
||||
return row.data as PrivateUser
|
||||
}
|
||||
|
||||
export const displayUserColumns = `id,name,username,data->>'avatarUrl' as "avatarUrl",data->'isBannedFromPosting' as "isBannedFromPosting"`
|
||||
export const displayUserColumns = `id,name,username, avatar_url as "avatarUrl",is_banned_from_posting as "isBannedFromPosting"`
|
||||
|
||||
@@ -92,7 +92,8 @@ export function selectFrom<
|
||||
return db.from(table).select<string, TResult>(query)
|
||||
}
|
||||
|
||||
export function millisToTs(millis: number) {
|
||||
export function millisToTs(millis: number | undefined) {
|
||||
if (!millis) return
|
||||
return new Date(millis).toISOString()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {Socials} from './socials'
|
||||
import {notification_preferences} from './user-notification-preferences'
|
||||
|
||||
export type BaseUser = {
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
link: Socials // Social links
|
||||
}
|
||||
|
||||
export type User = BaseUser & {
|
||||
|
||||
@@ -67,12 +67,6 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?:
|
||||
const profileData =
|
||||
profileType === 'basic' ? basicProfile : profileType === 'medium' ? mediumProfile : fullProfile
|
||||
|
||||
const user = {
|
||||
// avatarUrl,
|
||||
isBannedFromPosting: false,
|
||||
link: {},
|
||||
}
|
||||
|
||||
const privateUser: PrivateUser = {
|
||||
id: userId,
|
||||
email: userInfo.email,
|
||||
@@ -91,7 +85,7 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?:
|
||||
id: userId,
|
||||
name: userInfo.name,
|
||||
username: cleanUsername(userInfo.name),
|
||||
data: user,
|
||||
data: {},
|
||||
})
|
||||
|
||||
await insert(tx, 'private_users', {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import {debug} from 'common/logger'
|
||||
import {MultipleChoiceOptions} from 'common/profiles/multiple-choice'
|
||||
import {Profile, ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {PLATFORM_LABELS, type Site, SITE_ORDER} from 'common/socials'
|
||||
import {PLATFORM_LABELS, type Site, SITE_ORDER, Socials} from 'common/socials'
|
||||
import {BaseUser} from 'common/user'
|
||||
import {range} from 'lodash'
|
||||
import {Fragment, useEffect, useRef, useState} from 'react'
|
||||
@@ -51,20 +51,11 @@ export const OptionalProfileUserForm = (props: {
|
||||
profile: ProfileWithoutUser
|
||||
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void
|
||||
user: BaseUser
|
||||
setUser: <K extends keyof BaseUser>(key: K, value: BaseUser[K]) => void
|
||||
buttonLabel?: string
|
||||
bottomNavBarVisible?: boolean
|
||||
onSubmit: () => Promise<void>
|
||||
}) => {
|
||||
const {
|
||||
profile,
|
||||
user,
|
||||
buttonLabel,
|
||||
setProfile,
|
||||
setUser,
|
||||
onSubmit,
|
||||
bottomNavBarVisible = true,
|
||||
} = props
|
||||
const {profile, user, buttonLabel, setProfile, onSubmit, bottomNavBarVisible = true} = props
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [lookingRelationship, setLookingRelationship] = useState(
|
||||
@@ -125,7 +116,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
}
|
||||
|
||||
const updateUserLink = (platform: string, value: string | null) => {
|
||||
setUser('link', {...user.link, [platform]: value})
|
||||
setProfile('links', {...((profile.links as Socials) ?? {}), [platform]: value})
|
||||
}
|
||||
|
||||
const addNewLink = () => {
|
||||
@@ -913,7 +904,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
{/*</label>*/}
|
||||
|
||||
<div className="grid w-full grid-cols-[8rem_1fr_auto] gap-2">
|
||||
{Object.entries(user.link)
|
||||
{Object.entries(profile.links as Socials)
|
||||
.filter(([_, value]) => value != null)
|
||||
.map(([platform, value]) => (
|
||||
<Fragment key={platform}>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {convertGenderPlural, Gender} from 'common/gender'
|
||||
import {getLocationText} from 'common/geodb'
|
||||
import {formatHeight, MeasurementSystem} from 'common/measurement-utils'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {Socials} from 'common/socials'
|
||||
import {UserActivity} from 'common/user'
|
||||
import {Home} from 'lucide-react'
|
||||
import React, {ReactNode} from 'react'
|
||||
@@ -171,7 +172,7 @@ export default function ProfileAbout(props: {
|
||||
<HasKids profile={profile} />
|
||||
<WantsKids profile={profile} />
|
||||
{!isCurrentUser && <LastOnline lastOnlineTime={userActivity?.last_online_time} />}
|
||||
<UserHandles links={profile.user.link} />
|
||||
<UserHandles links={profile.links as Socials} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ function FreshBadge() {
|
||||
// name: string
|
||||
// username: string
|
||||
// createdTime: number
|
||||
// isBannedFromPosting?: boolean
|
||||
// is_banned_from_posting?: boolean
|
||||
// }
|
||||
// followsYou?: boolean
|
||||
// className?: string
|
||||
@@ -167,7 +167,7 @@ function FreshBadge() {
|
||||
// fresh={isFresh(user.createdTime)}
|
||||
// />
|
||||
// }
|
||||
// {user.isBannedFromPosting && <BannedBadge/>}
|
||||
// {user.is_banned_from_posting && <BannedBadge/>}
|
||||
// </div>
|
||||
// <Row className={'flex-shrink flex-wrap gap-x-2'}>
|
||||
// <span className={clsx('text-ink-400 text-sm', usernameClassName)}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {DisplayUser} from 'common/api/user-types'
|
||||
import {APIError} from 'common/api/utils'
|
||||
import {convertPartialUser} from 'common/supabase/users'
|
||||
import {run, TableName} from 'common/supabase/utils'
|
||||
import {MONTH_MS} from 'common/util/time'
|
||||
import {api} from 'web/lib/api'
|
||||
@@ -51,11 +52,11 @@ export async function getDisplayUsers(userIds: string[]) {
|
||||
const {data} = await run(
|
||||
db
|
||||
.from('users')
|
||||
.select(`id, name, username, data->avatarUrl, data->isBannedFromPosting`)
|
||||
.select(`id, name, username, avatar_url, is_banned_from_posting`)
|
||||
.in('id', userIds),
|
||||
)
|
||||
|
||||
return data as unknown as DisplayUser[]
|
||||
return data.map(convertPartialUser) as unknown as DisplayUser[]
|
||||
}
|
||||
|
||||
export async function getProfilesCreations() {
|
||||
|
||||
@@ -114,7 +114,6 @@ function ProfilePageInner(props: {user: User; profile: Profile}) {
|
||||
profile={profile}
|
||||
setProfile={setProfileState}
|
||||
user={baseUser}
|
||||
setUser={setBaseUserState}
|
||||
buttonLabel={t('profile.save', 'Save')}
|
||||
onSubmit={async () => await submitForm()}
|
||||
/>
|
||||
|
||||
@@ -92,7 +92,6 @@ export default function SignupPage() {
|
||||
name,
|
||||
locale,
|
||||
deviceToken,
|
||||
link: baseUser.link,
|
||||
profile: otherProfileProps,
|
||||
interests,
|
||||
causes,
|
||||
@@ -142,7 +141,6 @@ export default function SignupPage() {
|
||||
profile={profileForm}
|
||||
setProfile={setProfileState}
|
||||
user={baseUser}
|
||||
setUser={setBaseUserState}
|
||||
bottomNavBarVisible={false}
|
||||
onSubmit={handleFinalSubmit}
|
||||
/>
|
||||
@@ -168,7 +166,6 @@ function getInitialBaseUser() {
|
||||
id: auth.currentUser?.uid ?? '',
|
||||
username: cleanUsername(name),
|
||||
name: name,
|
||||
link: {},
|
||||
}
|
||||
return initialState
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user