Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,64 @@ describe('deleteGroup', () => {
await expect(service.deleteGroup('gone-id')).resolves.toBeUndefined()
})
})

describe('getOrCreateGroupByPath root resolution (issue #2518)', () => {
let module: TestingModule
let service: KeycloakClientService

const groupsUrl = `${keycloakUrl}/admin/realms/${projectRealm}/groups`
const rootChildrenUrl = `${keycloakUrl}/admin/realms/${projectRealm}/groups/root-id/children`

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
beforeEach(async () => {
module = await createKeycloakClientServiceTestingModule().compile()
service = module.get(KeycloakClientService)
useTokenEndpoint()
await module.init()
})
afterEach(async () => {
await module.close()
server.resetHandlers()
})
afterAll(() => server.close())

it('should resolve the root group by exact path, not a same-named subgroup elsewhere', async () => {
// A subgroup at a different path shares the slug name; the name-fallback
// must not pick it up as the project root (was the #2518 bug).
server.use(
http.get(groupsUrl, () => HttpResponse.json([
{ id: 'wrong-id', name: 'myproject', path: '/console/other/myproject' },
{ id: 'root-id', name: 'myproject', path: '/myproject' },
])),
http.get(rootChildrenUrl, () => HttpResponse.json([])),
)

const result = await service.getOrCreateGroupByPath('/myproject')

expect(result).toMatchObject({ id: 'root-id', path: '/myproject' })
})

it('should create the root group when only a wrong-path subgroup matches the name', async () => {
let created = false
server.use(
http.get(groupsUrl, () => HttpResponse.json(
created
? [{ id: 'created-id', name: 'myproject', path: '/myproject' }]
: [{ id: 'wrong-id', name: 'myproject', path: '/console/other/myproject' }],
)),
http.post(groupsUrl, async ({ request }) => {
expect(await request.json()).toEqual({ name: 'myproject' })
created = true
return new HttpResponse(null, {
status: 201,
headers: { location: `${keycloakUrl}/admin/realms/${projectRealm}/groups/created-id` },
})
}),
http.get(rootChildrenUrl, () => HttpResponse.json([])),
)

const result = await service.getOrCreateGroupByPath('/myproject')

expect(result).toMatchObject({ id: 'created-id', path: '/myproject' })
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class KeycloakClientService implements OnModuleInit {

private async getRootGroupByName(name: string): Promise<GroupRepresentationWithIdNamePath | undefined> {
const candidates = await this.client.groups.find({ search: name, briefRepresentation: false }) ?? []
const match = candidates.find(g => g.path === `/${name}`) ?? candidates.find(g => g.name === name)
const match = candidates.find(g => g.path === `/${name}`)
const parsed = groupSchema.safeParse(match)
return parsed.success ? parsed.data : undefined
}
Expand Down Expand Up @@ -185,7 +185,7 @@ export class KeycloakClientService implements OnModuleInit {
}

const [rootName, ...rest] = parts
let current = await this.getGroupByName(rootName) ?? await this.createGroup(rootName)
let current = await this.getRootGroupByName(rootName) ?? await this.createGroup(rootName)
for (const name of rest) {
current = await this.getOrCreateSubGroupByName(current.id, name)
}
Expand Down