mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
fix(flare): enforce real profile photos
This commit is contained in:
@@ -2878,6 +2878,19 @@ mockMedia.push(
|
||||
url: mockGalleryImage(title, sky, landscape, accent),
|
||||
})),
|
||||
)
|
||||
|
||||
function mockPhotoUrls(ids) {
|
||||
return ids.map((id) => {
|
||||
const photo = mockMedia.find(
|
||||
(item) => item.id === id && item.mediaType === 'photo',
|
||||
)
|
||||
if (!photo) {
|
||||
throw new Error(`Missing mock gallery photo ${id}`)
|
||||
}
|
||||
return photo.url
|
||||
})
|
||||
}
|
||||
|
||||
const weazelNewsCategoryIds = ['official', 'events', 'jobs', 'news', 'business']
|
||||
const weazelNewsMaxImages = 6
|
||||
let weazelNewsSequence = 8
|
||||
@@ -3452,8 +3465,8 @@ let flareProfile = {
|
||||
interests: ['Night drives', 'Music', 'Coffee'],
|
||||
lookingFor: 'dates',
|
||||
discoverable: true,
|
||||
photoMediaIds: [],
|
||||
photoUrls: [],
|
||||
photoMediaIds: [1, 3],
|
||||
photoUrls: mockPhotoUrls([1, 3]),
|
||||
}
|
||||
let flareSuggestions = [
|
||||
{
|
||||
@@ -3465,7 +3478,7 @@ let flareSuggestions = [
|
||||
avatar: 0,
|
||||
interests: ['Beach days', 'Food spots', 'Art'],
|
||||
lookingFor: 'longTerm',
|
||||
photoUrls: [],
|
||||
photoUrls: mockPhotoUrls([3, 7]),
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
@@ -3476,7 +3489,7 @@ let flareSuggestions = [
|
||||
avatar: 1,
|
||||
interests: ['Architecture', 'Karaoke', 'Travel'],
|
||||
lookingFor: 'dates',
|
||||
photoUrls: [],
|
||||
photoUrls: mockPhotoUrls([4, 11]),
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
@@ -3487,7 +3500,7 @@ let flareSuggestions = [
|
||||
avatar: 2,
|
||||
interests: ['Coffee', 'Photography', 'Dogs'],
|
||||
lookingFor: 'friends',
|
||||
photoUrls: [],
|
||||
photoUrls: mockPhotoUrls([5, 9]),
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
@@ -3498,7 +3511,7 @@ let flareSuggestions = [
|
||||
avatar: 3,
|
||||
interests: ['Cars', 'Road trips', 'Vinyl'],
|
||||
lookingFor: 'longTerm',
|
||||
photoUrls: [],
|
||||
photoUrls: mockPhotoUrls([8, 13]),
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
@@ -3509,7 +3522,7 @@ let flareSuggestions = [
|
||||
avatar: 4,
|
||||
interests: ['Sailing', 'Fitness', 'Brunch'],
|
||||
lookingFor: 'dates',
|
||||
photoUrls: [],
|
||||
photoUrls: mockPhotoUrls([12, 14]),
|
||||
},
|
||||
]
|
||||
const flareSuggestionFixtures = flareSuggestions.map((profile) => ({
|
||||
@@ -3529,7 +3542,7 @@ const flareMatches = [
|
||||
avatar: 5,
|
||||
interests: ['Live music', 'Cooking'],
|
||||
lookingFor: 'dates',
|
||||
photoUrls: [],
|
||||
photoUrls: mockPhotoUrls([15, 16]),
|
||||
},
|
||||
lastMessage: 'Friday night jazz',
|
||||
lastMessageAt: isoTime(-18 * 60 * 1000),
|
||||
@@ -3600,6 +3613,36 @@ function freshFlareSuggestions() {
|
||||
}))
|
||||
}
|
||||
|
||||
function flarePhotoRemovalWouldEmptyProfile(mediaIds) {
|
||||
if (!flareProfile || !Array.isArray(flareProfile.photoMediaIds)) {
|
||||
return false
|
||||
}
|
||||
const currentIds = flareProfile.photoMediaIds
|
||||
const removedIds = new Set(mediaIds)
|
||||
return (
|
||||
currentIds.length > 0 &&
|
||||
currentIds.some((id) => removedIds.has(id)) &&
|
||||
currentIds.every((id) => removedIds.has(id))
|
||||
)
|
||||
}
|
||||
|
||||
function removeFlareProfilePhotos(mediaIds) {
|
||||
if (!flareProfile || !Array.isArray(flareProfile.photoMediaIds)) return
|
||||
const removedIds = new Set(mediaIds)
|
||||
const photoMediaIds = []
|
||||
const photoUrls = []
|
||||
flareProfile.photoMediaIds.forEach((id, index) => {
|
||||
if (removedIds.has(id)) return
|
||||
photoMediaIds.push(id)
|
||||
photoUrls.push(flareProfile.photoUrls[index])
|
||||
})
|
||||
flareProfile = {
|
||||
...flareProfile,
|
||||
photoMediaIds,
|
||||
photoUrls,
|
||||
}
|
||||
}
|
||||
|
||||
const companyCategories = [
|
||||
{ id: 'public_services', name: 'Public Services' },
|
||||
{ id: 'medical', name: 'Medical' },
|
||||
@@ -6437,28 +6480,26 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
}
|
||||
if (endpoint === 'flare:save-profile') {
|
||||
const requestedPhotoIds = request.body.photoMediaIds
|
||||
let photoUpdate = {}
|
||||
if (requestedPhotoIds !== undefined) {
|
||||
const validIds =
|
||||
Array.isArray(requestedPhotoIds) &&
|
||||
requestedPhotoIds.length <= 6 &&
|
||||
new Set(requestedPhotoIds).size === requestedPhotoIds.length &&
|
||||
requestedPhotoIds.every((id) => Number.isInteger(id) && id > 0)
|
||||
const photos = validIds
|
||||
? requestedPhotoIds.map((id) =>
|
||||
mockMedia.find(
|
||||
(item) => item.id === id && item.mediaType === 'photo',
|
||||
),
|
||||
)
|
||||
: []
|
||||
if (!validIds || photos.some((photo) => !photo)) {
|
||||
response.json({ success: false, error: 'invalid_profile_photos' })
|
||||
return
|
||||
}
|
||||
photoUpdate = {
|
||||
photoMediaIds: [...requestedPhotoIds],
|
||||
photoUrls: photos.map((photo) => photo.url),
|
||||
}
|
||||
const validIds =
|
||||
Array.isArray(requestedPhotoIds) &&
|
||||
requestedPhotoIds.length >= 1 &&
|
||||
requestedPhotoIds.length <= 6 &&
|
||||
new Set(requestedPhotoIds).size === requestedPhotoIds.length &&
|
||||
requestedPhotoIds.every((id) => Number.isInteger(id) && id > 0)
|
||||
const photos = validIds
|
||||
? requestedPhotoIds.map((id) =>
|
||||
mockMedia.find(
|
||||
(item) => item.id === id && item.mediaType === 'photo',
|
||||
),
|
||||
)
|
||||
: []
|
||||
if (!validIds || photos.some((photo) => !photo)) {
|
||||
response.json({ success: false, error: 'invalid_profile_photos' })
|
||||
return
|
||||
}
|
||||
const photoUpdate = {
|
||||
photoMediaIds: [...requestedPhotoIds],
|
||||
photoUrls: photos.map((photo) => photo.url),
|
||||
}
|
||||
flareProfile = {
|
||||
...flareProfile,
|
||||
@@ -9109,7 +9150,13 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
return
|
||||
}
|
||||
if (endpoint === 'gallery:delete') {
|
||||
mockMedia = mockMedia.filter((item) => item.id !== Number(request.body.id))
|
||||
const mediaId = Number(request.body.id)
|
||||
if (flarePhotoRemovalWouldEmptyProfile([mediaId])) {
|
||||
response.json({ success: false, error: 'profile_photo_required' })
|
||||
return
|
||||
}
|
||||
mockMedia = mockMedia.filter((item) => item.id !== mediaId)
|
||||
removeFlareProfilePhotos([mediaId])
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
@@ -9133,7 +9180,12 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
const deletedIds = mockMedia
|
||||
.filter((item) => ids.includes(item.id))
|
||||
.map((item) => item.id)
|
||||
if (flarePhotoRemovalWouldEmptyProfile(deletedIds)) {
|
||||
response.json({ success: false, error: 'profile_photo_required' })
|
||||
return
|
||||
}
|
||||
mockMedia = mockMedia.filter((item) => !deletedIds.includes(item.id))
|
||||
removeFlareProfilePhotos(deletedIds)
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
||||
@@ -852,10 +852,92 @@ async function verifyStatefulActions(baseUrl) {
|
||||
true,
|
||||
)
|
||||
assert(flareBeforeDelete.profile, 'flare:bootstrap did not include a profile')
|
||||
assert(
|
||||
flareBeforeDelete.profile.photoMediaIds.length >= 1 &&
|
||||
flareBeforeDelete.profile.photoMediaIds.length <= 6,
|
||||
'flare:bootstrap profile did not include one to six gallery photos',
|
||||
)
|
||||
assert.equal(
|
||||
flareBeforeDelete.profile.photoMediaIds.length,
|
||||
flareBeforeDelete.profile.photoUrls.length,
|
||||
'flare:bootstrap profile photo IDs and URLs were out of sync',
|
||||
)
|
||||
const galleryPhotoUrls = new Set(
|
||||
gallery
|
||||
.filter((item) => item.mediaType === 'photo')
|
||||
.map((item) => item.url),
|
||||
)
|
||||
assert(
|
||||
flareBeforeDelete.profile.photoUrls.every((url) =>
|
||||
galleryPhotoUrls.has(url),
|
||||
),
|
||||
'flare:bootstrap profile used photos outside the phone gallery',
|
||||
)
|
||||
assert(
|
||||
flareBeforeDelete.suggestions.every(
|
||||
(profile) =>
|
||||
profile.photoUrls.length >= 1 &&
|
||||
profile.photoUrls.every((url) => galleryPhotoUrls.has(url)),
|
||||
),
|
||||
'Flare suggestions used photos outside the phone gallery',
|
||||
)
|
||||
assert(
|
||||
flareBeforeDelete.matches.length > 0,
|
||||
'flare:bootstrap did not include a deletable match',
|
||||
)
|
||||
assert(
|
||||
flareBeforeDelete.matches.every(
|
||||
(match) =>
|
||||
match.profile.photoUrls.length >= 1 &&
|
||||
match.profile.photoUrls.every((url) => galleryPhotoUrls.has(url)),
|
||||
),
|
||||
'Flare matches used photos outside the phone gallery',
|
||||
)
|
||||
const [removedProfilePhotoId, retainedProfilePhotoId] =
|
||||
flareBeforeDelete.profile.photoMediaIds
|
||||
await expectSuccess(baseUrl, 'gallery:delete', {
|
||||
id: removedProfilePhotoId,
|
||||
})
|
||||
const flareAfterGalleryDelete = await expectSuccess(
|
||||
baseUrl,
|
||||
'flare:bootstrap',
|
||||
{},
|
||||
true,
|
||||
)
|
||||
assert.deepEqual(
|
||||
flareAfterGalleryDelete.profile.photoMediaIds,
|
||||
[retainedProfilePhotoId],
|
||||
'gallery:delete did not remove the deleted photo from the Flare profile',
|
||||
)
|
||||
assert.deepEqual(
|
||||
flareAfterGalleryDelete.profile.photoUrls,
|
||||
[gallery.find((item) => item.id === retainedProfilePhotoId)?.url],
|
||||
'gallery:delete left Flare photo IDs and URLs out of sync',
|
||||
)
|
||||
const rejectedLastPhotoDelete = await post(baseUrl, 'gallery:delete', {
|
||||
id: retainedProfilePhotoId,
|
||||
})
|
||||
assert.deepEqual(rejectedLastPhotoDelete, {
|
||||
error: 'profile_photo_required',
|
||||
success: false,
|
||||
})
|
||||
const nonProfilePhotoId = gallery.find(
|
||||
(item) =>
|
||||
item.mediaType === 'photo' &&
|
||||
!flareBeforeDelete.profile.photoMediaIds.includes(item.id),
|
||||
)?.id
|
||||
const rejectedBulkLastPhotoDelete = await post(
|
||||
baseUrl,
|
||||
'gallery:delete-many',
|
||||
{
|
||||
correlationId: 'flare-last-photo-protection',
|
||||
ids: [retainedProfilePhotoId, nonProfilePhotoId],
|
||||
},
|
||||
)
|
||||
assert.deepEqual(rejectedBulkLastPhotoDelete, {
|
||||
error: 'profile_photo_required',
|
||||
success: false,
|
||||
})
|
||||
const swipedProfile = flareBeforeDelete.suggestions[0]
|
||||
await expectSuccess(baseUrl, 'flare:swipe', {
|
||||
choice: 'pass',
|
||||
@@ -889,12 +971,32 @@ async function verifyStatefulActions(baseUrl) {
|
||||
error: 'profile_not_found',
|
||||
success: false,
|
||||
})
|
||||
const rejectedEmptyFlare = await post(baseUrl, 'flare:save-profile', {
|
||||
...flareBeforeDelete.profile,
|
||||
photoMediaIds: [],
|
||||
photoUrls: undefined,
|
||||
})
|
||||
assert.deepEqual(rejectedEmptyFlare, {
|
||||
error: 'invalid_profile_photos',
|
||||
success: false,
|
||||
})
|
||||
const flareStillDeleted = await expectSuccess(
|
||||
baseUrl,
|
||||
'flare:bootstrap',
|
||||
{},
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
flareStillDeleted.profile,
|
||||
null,
|
||||
'an invalid empty Flare profile was persisted',
|
||||
)
|
||||
const recreatedFlare = await expectSuccess(
|
||||
baseUrl,
|
||||
'flare:save-profile',
|
||||
{
|
||||
...flareBeforeDelete.profile,
|
||||
photoMediaIds: [],
|
||||
photoMediaIds: [retainedProfilePhotoId],
|
||||
photoUrls: undefined,
|
||||
},
|
||||
true,
|
||||
@@ -905,6 +1007,11 @@ async function verifyStatefulActions(baseUrl) {
|
||||
),
|
||||
'recreated Flare profile retained a deleted swipe filter',
|
||||
)
|
||||
assert.deepEqual(
|
||||
recreatedFlare.profile.photoMediaIds,
|
||||
[retainedProfilePhotoId],
|
||||
'recreated Flare profile did not retain its valid gallery photo',
|
||||
)
|
||||
|
||||
await expectSuccess(baseUrl, 'account:logout')
|
||||
const signedOutFlareBootstrap = await post(baseUrl, 'flare:bootstrap')
|
||||
|
||||
Reference in New Issue
Block a user