mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-25 09:30:27 +00:00
UBERF-10672: Fix person duplicates (#9004)
This commit is contained in:
parent
7f69a4c777
commit
f914027b85
@ -344,8 +344,21 @@ export function registerRPC (app: Express, sessions: SessionManager, ctx: Measur
|
||||
name: combineName(firstName, lastName),
|
||||
personUuid: uuid
|
||||
})
|
||||
const createUniquePersonTx = txFactory.createTxApplyIf(
|
||||
core.space.Workspace,
|
||||
socialId,
|
||||
[],
|
||||
[
|
||||
{
|
||||
_class: contact.class.Person,
|
||||
query: { personUuid: uuid }
|
||||
}
|
||||
],
|
||||
[createPersonTx],
|
||||
'createLocalPerson'
|
||||
)
|
||||
|
||||
await session.txRaw(ctx, createPersonTx)
|
||||
await session.txRaw(ctx, createUniquePersonTx)
|
||||
personRef = createPersonTx.objectId
|
||||
}
|
||||
|
||||
|
@ -36,8 +36,8 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'test@example.com',
|
||||
firstName: 'test',
|
||||
lastName: 'example.com'
|
||||
firstName: 'test@example.com',
|
||||
lastName: ''
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -47,8 +47,8 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john.doe@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
firstName: '<john.doe@example.com>',
|
||||
lastName: 'John Doe'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -58,8 +58,8 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john.doe@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
firstName: '<john.doe@example.com>',
|
||||
lastName: 'John Doe'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -69,8 +69,8 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john.doe@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe Smith'
|
||||
firstName: '<john.doe@example.com>',
|
||||
lastName: 'John Doe Smith'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -80,13 +80,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'test1@example.com',
|
||||
firstName: 'test1',
|
||||
lastName: 'example.com'
|
||||
firstName: 'test1@example.com',
|
||||
lastName: ''
|
||||
},
|
||||
{
|
||||
email: 'test2@example.com',
|
||||
firstName: 'test2',
|
||||
lastName: 'example.com'
|
||||
firstName: 'test2@example.com',
|
||||
lastName: ''
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -96,13 +96,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'example.com'
|
||||
firstName: '<john@example.com>',
|
||||
lastName: 'John'
|
||||
},
|
||||
{
|
||||
email: 'jane@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
firstName: '<jane@example.com>',
|
||||
lastName: 'Jane Doe'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -112,13 +112,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
firstName: 'Doe,',
|
||||
lastName: 'John'
|
||||
firstName: '<john@example.com>',
|
||||
lastName: 'Doe, John'
|
||||
},
|
||||
{
|
||||
email: 'jane@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
firstName: '<jane@example.com>',
|
||||
lastName: 'Jane Doe'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -128,13 +128,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
firstName: 'john',
|
||||
lastName: 'example.com'
|
||||
firstName: 'john@example.com',
|
||||
lastName: ''
|
||||
},
|
||||
{
|
||||
email: 'jane@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
firstName: '<jane@example.com>',
|
||||
lastName: 'Jane Doe'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -144,13 +144,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
firstName: 'john',
|
||||
lastName: 'example.com'
|
||||
firstName: 'john@example.com',
|
||||
lastName: ''
|
||||
},
|
||||
{
|
||||
email: 'jane@example.com',
|
||||
firstName: 'jane',
|
||||
lastName: 'example.com'
|
||||
firstName: 'jane@example.com',
|
||||
lastName: ''
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -160,13 +160,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
firstName: 'john',
|
||||
lastName: 'example.com'
|
||||
firstName: 'john@example.com',
|
||||
lastName: ''
|
||||
},
|
||||
{
|
||||
email: 'jane@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe'
|
||||
firstName: '<jane@example.com>',
|
||||
lastName: 'Jane Doe'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -176,13 +176,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'example-staff@example.com',
|
||||
firstName: 'example',
|
||||
lastName: 'staff'
|
||||
firstName: '<example-staff@example.com>',
|
||||
lastName: 'example staff'
|
||||
},
|
||||
{
|
||||
email: 'personnel@example.com',
|
||||
firstName: 'personnel',
|
||||
lastName: 'example.com'
|
||||
firstName: '<personnel@example.com>',
|
||||
lastName: 'personnel'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -192,13 +192,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'abc@test.com',
|
||||
firstName: 'abc',
|
||||
lastName: 'test.com'
|
||||
firstName: 'abc@test.com',
|
||||
lastName: ''
|
||||
},
|
||||
{
|
||||
email: '123@test.com',
|
||||
firstName: '123',
|
||||
lastName: 'test.com'
|
||||
firstName: '123@test.com',
|
||||
lastName: ''
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -208,13 +208,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'example.com'
|
||||
firstName: '<john@example.com>',
|
||||
lastName: 'John'
|
||||
},
|
||||
{
|
||||
email: 'jane@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'example.com'
|
||||
firstName: '<jane@example.com>',
|
||||
lastName: 'Jane'
|
||||
}
|
||||
])
|
||||
})
|
||||
@ -224,13 +224,13 @@ describe('parseEmailHeader', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'example.com'
|
||||
firstName: '<john@example.com>',
|
||||
lastName: 'John'
|
||||
},
|
||||
{
|
||||
email: 'jane@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'example.com'
|
||||
firstName: '<jane@example.com>',
|
||||
lastName: 'Jane'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
@ -1,3 +1,18 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { parseNameFromEmailHeader } from '../utils'
|
||||
import { EmailContact } from '../types'
|
||||
|
||||
@ -6,8 +21,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = '"John Doe" <john.doe@example.com>'
|
||||
const expected: EmailContact = {
|
||||
email: 'john.doe@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
firstName: '<john.doe@example.com>',
|
||||
lastName: 'John Doe'
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
@ -17,8 +32,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = 'Jane Smith <jane.smith@example.com>'
|
||||
const expected: EmailContact = {
|
||||
email: 'jane.smith@example.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith'
|
||||
firstName: '<jane.smith@example.com>',
|
||||
lastName: 'Jane Smith'
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
@ -28,8 +43,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = 'no-reply@example.com'
|
||||
const expected: EmailContact = {
|
||||
email: 'no-reply@example.com',
|
||||
firstName: 'no-reply',
|
||||
lastName: 'example.com'
|
||||
firstName: 'no-reply@example.com',
|
||||
lastName: ''
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
@ -39,8 +54,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = '<support@example.com>'
|
||||
const expected: EmailContact = {
|
||||
email: 'support@example.com',
|
||||
firstName: 'support',
|
||||
lastName: 'example.com'
|
||||
firstName: 'support@example.com',
|
||||
lastName: ''
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
@ -50,8 +65,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = 'Maria Van Der Berg <maria@example.com>'
|
||||
const expected: EmailContact = {
|
||||
email: 'maria@example.com',
|
||||
firstName: 'Maria',
|
||||
lastName: 'Van Der Berg'
|
||||
firstName: '<maria@example.com>',
|
||||
lastName: 'Maria Van Der Berg'
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
@ -82,8 +97,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = 'John Doe john.doe@example.com'
|
||||
const expected: EmailContact = {
|
||||
email: 'John Doe john.doe@example.com',
|
||||
firstName: 'John Doe john.doe',
|
||||
lastName: 'example.com'
|
||||
firstName: 'John Doe john.doe@example.com',
|
||||
lastName: ''
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
@ -93,8 +108,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = 'Support <help@example.com>'
|
||||
const expected: EmailContact = {
|
||||
email: 'help@example.com',
|
||||
firstName: 'Support',
|
||||
lastName: 'example.com'
|
||||
firstName: '<help@example.com>',
|
||||
lastName: 'Support'
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
@ -104,8 +119,8 @@ describe('parseNameFromEmailHeader', () => {
|
||||
const input = '"O\'Neill, James" <james.oneill@example.com>'
|
||||
const expected: EmailContact = {
|
||||
email: 'james.oneill@example.com',
|
||||
firstName: "O'Neill,",
|
||||
lastName: 'James'
|
||||
firstName: '<james.oneill@example.com>',
|
||||
lastName: "O'Neill, James"
|
||||
}
|
||||
|
||||
expect(parseNameFromEmailHeader(input)).toEqual(expected)
|
||||
|
@ -146,6 +146,140 @@ describe('PersonCache', () => {
|
||||
expect(result2).toEqual(mockPerson2)
|
||||
expect(mockRestClient.ensurePerson).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should normalize email with whitespace and different case', async () => {
|
||||
// Arrange
|
||||
const email1 = ' MiXeD@ExAmPlE.com ' // With whitespace and mixed case
|
||||
const email2 = 'mixed@example.com' // Normal lowercase
|
||||
|
||||
const contact1: EmailContact = {
|
||||
email: email1,
|
||||
firstName: 'Mixed',
|
||||
lastName: 'Case'
|
||||
}
|
||||
|
||||
const contact2: EmailContact = {
|
||||
email: email2,
|
||||
firstName: 'Mixed',
|
||||
lastName: 'Case'
|
||||
}
|
||||
|
||||
const mockPerson = {
|
||||
socialId: 'person-789' as PersonId,
|
||||
uuid: 'uuid-789' as PersonUuid,
|
||||
localPerson: 'local-789'
|
||||
}
|
||||
|
||||
mockRestClient.ensurePerson.mockResolvedValue(mockPerson)
|
||||
|
||||
// Act
|
||||
await personCache.ensurePerson(contact1)
|
||||
await personCache.ensurePerson(contact2)
|
||||
|
||||
// Assert
|
||||
expect(mockRestClient.ensurePerson).toHaveBeenCalledTimes(1)
|
||||
expect(mockRestClient.ensurePerson).toHaveBeenCalledWith(
|
||||
SocialIdType.EMAIL,
|
||||
'mixed@example.com', // normalized email
|
||||
'Mixed',
|
||||
'Case'
|
||||
)
|
||||
})
|
||||
|
||||
it('should create different person entries for different emails', async () => {
|
||||
// Arrange
|
||||
const email1 = 'first@example.com'
|
||||
const email2 = 'second@example.com'
|
||||
|
||||
const contact1: EmailContact = {
|
||||
email: email1,
|
||||
firstName: 'First',
|
||||
lastName: 'User'
|
||||
}
|
||||
|
||||
const contact2: EmailContact = {
|
||||
email: email2,
|
||||
firstName: 'Second',
|
||||
lastName: 'User'
|
||||
}
|
||||
|
||||
const mockPerson1 = {
|
||||
socialId: 'person-1' as PersonId,
|
||||
uuid: 'uuid-1' as PersonUuid,
|
||||
localPerson: 'local-1'
|
||||
}
|
||||
|
||||
const mockPerson2 = {
|
||||
socialId: 'person-2' as PersonId,
|
||||
uuid: 'uuid-2' as PersonUuid,
|
||||
localPerson: 'local-2'
|
||||
}
|
||||
|
||||
mockRestClient.ensurePerson.mockResolvedValueOnce(mockPerson1).mockResolvedValueOnce(mockPerson2)
|
||||
|
||||
// Act
|
||||
const result1 = await personCache.ensurePerson(contact1)
|
||||
const result2 = await personCache.ensurePerson(contact2)
|
||||
|
||||
// Assert
|
||||
expect(mockRestClient.ensurePerson).toHaveBeenCalledTimes(2)
|
||||
expect(result1).toEqual(mockPerson1)
|
||||
expect(result2).toEqual(mockPerson2)
|
||||
expect(personCache.size()).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle concurrent requests for the same email', async () => {
|
||||
// Arrange
|
||||
const email = 'concurrent@example.com'
|
||||
const contact: EmailContact = {
|
||||
email,
|
||||
firstName: 'Concurrent',
|
||||
lastName: 'User'
|
||||
}
|
||||
|
||||
// Create a delayed mock response to simulate server latency
|
||||
const mockPerson = {
|
||||
socialId: 'person-123' as PersonId,
|
||||
uuid: 'uuid-123' as PersonUuid,
|
||||
localPerson: 'local-123'
|
||||
}
|
||||
|
||||
// Create a delayed promise that resolves after 50ms
|
||||
mockRestClient.ensurePerson.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockPerson)
|
||||
}, 50)
|
||||
})
|
||||
)
|
||||
|
||||
// Act - Create multiple concurrent requests
|
||||
const requests = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
requests.push(personCache.ensurePerson(contact))
|
||||
}
|
||||
|
||||
// Wait for all requests to complete
|
||||
const results = await Promise.all(requests)
|
||||
|
||||
// Assert
|
||||
expect(mockRestClient.ensurePerson).toHaveBeenCalledTimes(1)
|
||||
expect(mockRestClient.ensurePerson).toHaveBeenCalledWith(
|
||||
SocialIdType.EMAIL,
|
||||
'concurrent@example.com',
|
||||
'Concurrent',
|
||||
'User'
|
||||
)
|
||||
|
||||
// All results should reference the same person
|
||||
results.forEach((result) => {
|
||||
expect(result).toEqual(mockPerson)
|
||||
})
|
||||
|
||||
// Cache size should be 1
|
||||
expect(personCache.size()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearCache', () => {
|
||||
|
@ -243,7 +243,7 @@ async function saveMessageToSpaces (
|
||||
space._id,
|
||||
mail.tag.MailThread,
|
||||
{},
|
||||
Date.now(),
|
||||
createdDate,
|
||||
owner
|
||||
)
|
||||
threadId = newThreadId as Ref<Card>
|
||||
|
@ -72,41 +72,20 @@ export function parseNameFromEmailHeader (headerValue: string | undefined): Emai
|
||||
|
||||
if (match == null) {
|
||||
const address = headerValue.trim()
|
||||
const parts = address.split('@')
|
||||
return {
|
||||
email: address,
|
||||
firstName: parts[0],
|
||||
lastName: parts[1]
|
||||
firstName: address,
|
||||
lastName: ''
|
||||
}
|
||||
}
|
||||
|
||||
const displayName = match[1]?.trim()
|
||||
const email = match[2].trim()
|
||||
|
||||
if (displayName == null || displayName === '') {
|
||||
const parts = email.split('@')
|
||||
return {
|
||||
email,
|
||||
firstName: parts[0],
|
||||
lastName: parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
const nameParts = displayName.split(/\s+/)
|
||||
let firstName: string | undefined
|
||||
let lastName: string | undefined
|
||||
|
||||
if (nameParts.length === 1) {
|
||||
firstName = nameParts[0]
|
||||
} else if (nameParts.length > 1) {
|
||||
firstName = nameParts[0]
|
||||
lastName = nameParts.slice(1).join(' ')
|
||||
}
|
||||
|
||||
const parts = email.split('@')
|
||||
const wrappedEmail = displayName != null && displayName.length > 0 ? `<${email}>` : email
|
||||
return {
|
||||
email,
|
||||
firstName: firstName ?? parts[0],
|
||||
lastName: lastName ?? parts[1]
|
||||
firstName: wrappedEmail,
|
||||
lastName: displayName ?? ''
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user