Skip to content

Commit 6bd9292

Browse files
doublefacedoubleface
authored andcommitted
fix(cozy-sharing): Normalize recipient id in dedup helper
Added id/_id normalization into mergeAndDeduplicateRecipients to have correct deduplication of recipients Added unit tests for SharedDrive helpers.
1 parent a626ee4 commit 6bd9292

File tree

2 files changed

+185
-7
lines changed

2 files changed

+185
-7
lines changed

packages/cozy-sharing/src/components/SharedDrive/helpers.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,34 @@ const ContactModel = models.contact
55
export const RECIPIENT_INDEX_PREFIX = 'virtual-shared-drive-sharing-'
66

77
export const mergeAndDeduplicateRecipients = arrays => {
8-
const combinedArray = arrays.flat()
8+
const combinedArray = arrays.flat().map(item => ({
9+
...item,
10+
id: item.id || item._id
11+
}))
912

1013
const seenIds = new Set()
1114

12-
const uniqueArray = combinedArray.filter(item => {
13-
if (!seenIds.has(item.id)) {
14-
seenIds.add(item.id)
15-
return true
15+
const uniqueArray = combinedArray.reduce((acc, item) => {
16+
const id = item.id || item._id
17+
18+
if (id == null) {
19+
// Recipients without any identifier are kept as-is to avoid silent data loss
20+
acc.push(item)
21+
return acc
1622
}
17-
return false
18-
})
23+
24+
if (!seenIds.has(id)) {
25+
seenIds.add(id)
26+
// Ensure both id and _id exist for downstream compatibility
27+
const normalizedItem =
28+
!item.id || !item._id
29+
? { ...item, id: item.id || item._id, _id: item._id || item.id }
30+
: item
31+
acc.push(normalizedItem)
32+
}
33+
34+
return acc
35+
}, [])
1936

2037
return uniqueArray
2138
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import {
2+
mergeAndDeduplicateRecipients,
3+
moveRecipientToReadWrite,
4+
moveRecipientToReadOnly,
5+
formatRecipients,
6+
RECIPIENT_INDEX_PREFIX
7+
} from './helpers'
8+
9+
describe('mergeAndDeduplicateRecipients', () => {
10+
it('should merge multiple arrays into one', () => {
11+
const result = mergeAndDeduplicateRecipients([
12+
[{ id: '1', name: 'Alice' }],
13+
[{ id: '2', name: 'Bob' }]
14+
])
15+
expect(result).toEqual([
16+
{ id: '1', _id: '1', name: 'Alice' },
17+
{ id: '2', _id: '2', name: 'Bob' }
18+
])
19+
})
20+
21+
it('should deduplicate recipients with the same id', () => {
22+
const result = mergeAndDeduplicateRecipients([
23+
[{ id: '1', name: 'Alice' }],
24+
[{ id: '1', name: 'Alice' }]
25+
])
26+
expect(result).toEqual([{ id: '1', _id: '1', name: 'Alice' }])
27+
})
28+
29+
it('should normalize _id to id for deduplication', () => {
30+
const result = mergeAndDeduplicateRecipients([
31+
[{ _id: '1', name: 'Alice' }],
32+
[{ _id: '2', name: 'Bob' }]
33+
])
34+
expect(result).toEqual([
35+
{ _id: '1', id: '1', name: 'Alice' },
36+
{ _id: '2', id: '2', name: 'Bob' }
37+
])
38+
})
39+
40+
it('should deduplicate recipients when one has id and the other has _id', () => {
41+
const result = mergeAndDeduplicateRecipients([
42+
[{ id: '1', name: 'Alice' }],
43+
[{ _id: '1', name: 'Alice' }]
44+
])
45+
expect(result).toEqual([{ id: '1', _id: '1', name: 'Alice' }])
46+
})
47+
48+
it('should prefer existing id over _id when both are present', () => {
49+
const result = mergeAndDeduplicateRecipients([
50+
[{ id: 'real-id', _id: 'other-id', name: 'Alice' }]
51+
])
52+
expect(result).toEqual([{ id: 'real-id', _id: 'other-id', name: 'Alice' }])
53+
})
54+
55+
it('should handle empty arrays', () => {
56+
const result = mergeAndDeduplicateRecipients([[], []])
57+
expect(result).toEqual([])
58+
})
59+
})
60+
61+
describe('moveRecipientToReadWrite', () => {
62+
it('should move a recipient from readOnly to readWrite', () => {
63+
const state = {
64+
recipients: [{ _id: '1', name: 'Alice' }],
65+
readOnlyRecipients: [{ _id: '2', name: 'Bob' }]
66+
}
67+
const result = moveRecipientToReadWrite(state, '2')
68+
expect(result.recipients).toEqual([
69+
{ _id: '1', name: 'Alice' },
70+
{ _id: '2', name: 'Bob' }
71+
])
72+
expect(result.readOnlyRecipients).toEqual([])
73+
})
74+
75+
it('should return unchanged state if recipient not found', () => {
76+
const state = {
77+
recipients: [{ _id: '1', name: 'Alice' }],
78+
readOnlyRecipients: []
79+
}
80+
const result = moveRecipientToReadWrite(state, '999')
81+
expect(result).toBe(state)
82+
})
83+
})
84+
85+
describe('moveRecipientToReadOnly', () => {
86+
it('should move a recipient from readWrite to readOnly', () => {
87+
const state = {
88+
recipients: [{ _id: '1', name: 'Alice' }],
89+
readOnlyRecipients: [{ _id: '2', name: 'Bob' }]
90+
}
91+
const result = moveRecipientToReadOnly(state, '1')
92+
expect(result.recipients).toEqual([])
93+
expect(result.readOnlyRecipients).toEqual([
94+
{ _id: '2', name: 'Bob' },
95+
{ _id: '1', name: 'Alice' }
96+
])
97+
})
98+
99+
it('should return unchanged state if recipient not found', () => {
100+
const state = {
101+
recipients: [],
102+
readOnlyRecipients: [{ _id: '1', name: 'Alice' }]
103+
}
104+
const result = moveRecipientToReadOnly(state, '999')
105+
expect(result).toBe(state)
106+
})
107+
})
108+
109+
describe('formatRecipients', () => {
110+
it('should format readWrite and readOnly recipients with correct types', () => {
111+
const input = {
112+
recipients: [
113+
{
114+
_id: '1',
115+
displayName: 'Alice',
116+
email: [{ address: 'alice@example.com', primary: true }]
117+
}
118+
],
119+
readOnlyRecipients: [
120+
{
121+
_id: '2',
122+
displayName: 'Bob',
123+
email: [{ address: 'bob@example.com', primary: true }]
124+
}
125+
]
126+
}
127+
const result = formatRecipients(input)
128+
expect(result).toHaveLength(2)
129+
expect(result[0]).toMatchObject({
130+
index: `${RECIPIENT_INDEX_PREFIX}1`,
131+
type: 'two-way',
132+
public_name: 'Alice'
133+
})
134+
expect(result[1]).toMatchObject({
135+
index: `${RECIPIENT_INDEX_PREFIX}2`,
136+
type: 'one-way',
137+
public_name: 'Bob'
138+
})
139+
})
140+
141+
it('should sort recipients by public_name', () => {
142+
const input = {
143+
recipients: [
144+
{
145+
_id: '1',
146+
displayName: 'Zara',
147+
email: [{ address: 'zara@example.com', primary: true }]
148+
},
149+
{
150+
_id: '2',
151+
displayName: 'Alice',
152+
email: [{ address: 'alice@example.com', primary: true }]
153+
}
154+
],
155+
readOnlyRecipients: []
156+
}
157+
const result = formatRecipients(input)
158+
expect(result[0].public_name).toBe('Alice')
159+
expect(result[1].public_name).toBe('Zara')
160+
})
161+
})

0 commit comments

Comments
 (0)