fix(flare): enforce real profile photos

This commit is contained in:
Dominik
2026-08-17 01:30:41 +02:00
parent ae8134bf98
commit ecfe089051
15 changed files with 680 additions and 150 deletions
+83 -31
View File
@@ -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: {
+108 -1
View File
@@ -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')