Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
eval "$(fnm env)" && fnm use --silent-if-unchanged
npm run quality
17 changes: 15 additions & 2 deletions api/src/storages/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,21 @@ class MongodbStorage implements SdStorage {
return o.id === orga.id && (o.department || null) === (department || null)
})

if (config.singleMembership && !userOrga && user.organizations.find(o => o.id === orga.id)) {
throw httpError(400, 'cet utilisateur est déjà membre de cette organisation.')
if (config.singleMembership && !userOrga) {
const existingOrga = user.organizations.find(o => o.id === orga.id)
if (existingOrga) {
// update existing membership instead of rejecting
userOrga = existingOrga
if (department) {
const fullDepartment = orga.departments?.find(d => d.id === department)
if (!fullDepartment) throw httpError(404, 'department not found')
userOrga.department = department
userOrga.departmentName = fullDepartment.name
} else {
delete userOrga.department
delete userOrga.departmentName
}
}
}

if (!userOrga) {
Expand Down
114 changes: 114 additions & 0 deletions test-it/single-membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { strict as assert } from 'node:assert'
import { it, describe, before, beforeEach, after } from 'node:test'
import { clean, startApiServer, stopApiServer, createUser } from './utils/index.ts'

process.env.STORAGE_TYPE = 'mongo'

describe('singleMembership addMember', () => {
before(startApiServer)
beforeEach(async () => await clean())
after(stopApiServer)

it('should update department when user already belongs to org', async () => {
const config = (await import('../api/src/config.ts')).default
config.singleMembership = true
config.alwaysAcceptInvitation = true

try {
const { ax } = await createUser('owner-sm@test.com')
const org = (await ax.post('/api/organizations', {
name: 'test-sm',
departments: [
{ id: 'dep1', name: 'Department 1' },
{ id: 'dep2', name: 'Department 2' }
]
})).data
ax.setOrg(org.id)

// invite a user into dep1
await ax.post('/api/invitations', { id: org.id, name: org.name, department: 'dep1', email: 'member-sm@test.com', role: 'user' })

let members = (await ax.get(`/api/organizations/${org.id}/members`)).data.results
const member = members.find(m => m.email === 'member-sm@test.com')
assert.equal(member.department, 'dep1')
assert.equal(member.departmentName, 'Department 1')

// now re-invite the same user into dep2 — with singleMembership this should update, not reject
await ax.post('/api/invitations', { id: org.id, name: org.name, department: 'dep2', email: 'member-sm@test.com', role: 'user' })

members = (await ax.get(`/api/organizations/${org.id}/members`)).data.results
const memberEntries = members.filter(m => m.email === 'member-sm@test.com')
// singleMembership: only one entry, department updated
assert.equal(memberEntries.length, 1)
assert.equal(memberEntries[0].department, 'dep2')
assert.equal(memberEntries[0].departmentName, 'Department 2')
} finally {
config.singleMembership = false
config.alwaysAcceptInvitation = false
}
})

it('should update role when user already belongs to org', async () => {
const config = (await import('../api/src/config.ts')).default
config.singleMembership = true
config.alwaysAcceptInvitation = true

try {
const { ax } = await createUser('owner-sm2@test.com')
const org = (await ax.post('/api/organizations', { name: 'test-sm2' })).data
ax.setOrg(org.id)

// invite a user as 'user'
await ax.post('/api/invitations', { id: org.id, name: org.name, email: 'member-sm2@test.com', role: 'user' })

let members = (await ax.get(`/api/organizations/${org.id}/members`)).data.results
const member = members.find(m => m.email === 'member-sm2@test.com')
assert.equal(member.role, 'user')

// re-invite as 'admin' — should update existing membership
await ax.post('/api/invitations', { id: org.id, name: org.name, email: 'member-sm2@test.com', role: 'admin' })

members = (await ax.get(`/api/organizations/${org.id}/members`)).data.results
const memberEntries = members.filter(m => m.email === 'member-sm2@test.com')
assert.equal(memberEntries.length, 1)
assert.equal(memberEntries[0].role, 'admin')
} finally {
config.singleMembership = false
config.alwaysAcceptInvitation = false
}
})

it('should remove department when re-added without one', async () => {
const config = (await import('../api/src/config.ts')).default
config.singleMembership = true
config.alwaysAcceptInvitation = true

try {
const { ax } = await createUser('owner-sm3@test.com')
const org = (await ax.post('/api/organizations', {
name: 'test-sm3',
departments: [{ id: 'dep1', name: 'Department 1' }]
})).data
ax.setOrg(org.id)

// invite a user into dep1
await ax.post('/api/invitations', { id: org.id, name: org.name, department: 'dep1', email: 'member-sm3@test.com', role: 'user' })

let members = (await ax.get(`/api/organizations/${org.id}/members`)).data.results
const member = members.find(m => m.email === 'member-sm3@test.com')
assert.equal(member.department, 'dep1')

// re-invite without department — should move to org root
await ax.post('/api/invitations', { id: org.id, name: org.name, email: 'member-sm3@test.com', role: 'user' })

members = (await ax.get(`/api/organizations/${org.id}/members`)).data.results
const memberEntries = members.filter(m => m.email === 'member-sm3@test.com')
assert.equal(memberEntries.length, 1)
assert.equal(memberEntries[0].department, undefined)
assert.equal(memberEntries[0].departmentName, undefined)
} finally {
config.singleMembership = false
config.alwaysAcceptInvitation = false
}
})
})
Loading