Skip to content

Commit

Permalink
feat: ✨ keycloak client management for configuring sso oidc of each a…
Browse files Browse the repository at this point in the history
…rgocd zone
  • Loading branch information
mathieulaude committed Sep 30, 2024
1 parent 49061ae commit 32e775f
Show file tree
Hide file tree
Showing 26 changed files with 276 additions and 52 deletions.
2 changes: 1 addition & 1 deletion apps/client/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default defineConfig({
supportFile: 'cypress/e2e/support/index.ts',
video: false,
screenshotsFolder: 'cypress/e2e/screenshots',
numTestsKeptInMemory: 1,
numTestsKeptInMemory: 2,
chromeWebSecurity: false,
experimentalModifyObstructiveThirdPartyCode: false,
experimentalWebKitSupport: false,
Expand Down
11 changes: 11 additions & 0 deletions apps/client/cypress/components/specs/zone-form.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ describe('ZoneForm.vue', () => {
cy.getByDataTestid('labelInput')
.clear()
.type('Zone à Défendre')
cy.getByDataTestid('argocdUrlInput')
.clear()
.type('https://vousetesici.fr')
cy.getByDataTestid('addZoneBtn').should('be.enabled')
cy.getByDataTestid('descriptionInput')
.clear()
Expand Down Expand Up @@ -69,6 +72,9 @@ describe('ZoneForm.vue', () => {
cy.getByDataTestid('labelInput')
.clear()
.type('Zone à Défendre')
cy.getByDataTestid('argocdUrlInput')
.clear()
.type('https://vousetesici.fr')
cy.getByDataTestid('addZoneBtn').should('be.enabled')
cy.getByDataTestid('descriptionInput')
.clear()
Expand Down Expand Up @@ -104,6 +110,11 @@ describe('ZoneForm.vue', () => {
.and('be.enabled')
.clear()
.type('Zone à Détruire')
cy.getByDataTestid('argocdUrlInput')
.should('have.value', props.zone.argocdUrl)
.and('be.enabled')
.clear()
.type('https://vousetesici.fr')
cy.getByDataTestid('updateZoneBtn').should('be.enabled')
cy.getByDataTestid('descriptionInput')
.should('have.value', props.zone.description)
Expand Down
23 changes: 23 additions & 0 deletions apps/client/cypress/e2e/specs/admin/zones.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('Administration zones', () => {
const newZone = {
slug: 'zad',
label: 'Zone à Défendre',
argocdUrl: 'https://vousetesici.fr',
description: 'Il faut défendre cette zone.',
// Ne font pas partie de la réponse d'API
clusters,
Expand Down Expand Up @@ -40,6 +41,9 @@ describe('Administration zones', () => {
cy.getByDataTestid('labelInput')
.should('have.value', zone.label)
.and('be.enabled')
cy.getByDataTestid('argocdUrlInput')
.should('have.value', zone.argocdUrl)
.and('be.enabled')
cy.getByDataTestid('descriptionInput')
.should('have.value', zone.description)
.and('be.enabled')
Expand All @@ -56,6 +60,7 @@ describe('Administration zones', () => {
const zone = zones.find(({ slug }) => slug === 'pr')
const updatedZone = {
label: 'Zone Mise à Jour',
argocdUrl: 'https://vousnetesplusici.fr',
description: 'Cette zone a été mise à jour.',
}
cy.getByDataTestid(`zoneTile-${zone.label}`)
Expand All @@ -67,6 +72,9 @@ describe('Administration zones', () => {
cy.getByDataTestid('labelInput')
.clear()
.type(updatedZone.label)
cy.getByDataTestid('argocdUrlInput')
.clear()
.type(updatedZone.argocdUrl)
cy.getByDataTestid('descriptionInput')
.clear()
.type(updatedZone.description)
Expand All @@ -85,6 +93,9 @@ describe('Administration zones', () => {
cy.getByDataTestid('labelInput')
.should('have.value', updatedZone.label)
.and('be.enabled')
cy.getByDataTestid('argocdUrlInput')
.should('have.value', updatedZone.argocdUrl)
.and('be.enabled')
cy.getByDataTestid('descriptionInput')
.should('have.value', updatedZone.description)
.and('be.enabled')
Expand All @@ -101,6 +112,9 @@ describe('Administration zones', () => {
cy.getByDataTestid('labelInput')
.clear()
.type(zone.label)
cy.getByDataTestid('argocdUrlInput')
.clear()
.type(zone.argocdUrl)
cy.getByDataTestid('descriptionInput')
.clear()
.type(zone.description)
Expand All @@ -121,6 +135,9 @@ describe('Administration zones', () => {
cy.getByDataTestid('labelInput')
.clear()
.type(newZone.label)
cy.getByDataTestid('argocdUrlInput')
.clear()
.type(newZone.argocdUrl)
cy.getByDataTestid('descriptionInput')
.clear()
.type(newZone.description)
Expand All @@ -146,6 +163,9 @@ describe('Administration zones', () => {
cy.getByDataTestid('labelInput')
.should('have.value', newZone.label)
.and('be.enabled')
cy.getByDataTestid('argocdUrlInput')
.should('have.value', newZone.argocdUrl)
.and('be.enabled')
cy.getByDataTestid('descriptionInput')
.should('have.value', newZone.description)
.and('be.enabled')
Expand Down Expand Up @@ -176,6 +196,9 @@ describe('Administration zones', () => {
cy.getByDataTestid('labelInput')
.clear()
.type(newZone.label)
cy.getByDataTestid('argocdUrlInput')
.clear()
.type(newZone.argocdUrl)
cy.getByDataTestid('descriptionInput')
.clear()
.type(newZone.description)
Expand Down
20 changes: 15 additions & 5 deletions apps/client/src/components/ZoneForm.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
<script lang="ts" setup>
import { computed, onBeforeMount, ref } from 'vue'
import type { Cluster, CreateZoneBody, Quota, SharedZodError, UpdateZoneBody, Zone } from '@cpn-console/shared'
import type { Cluster, CreateZoneBody, SharedZodError, UpdateZoneBody, Zone } from '@cpn-console/shared'
import { ZoneSchema } from '@cpn-console/shared'
import { useSnackbarStore } from '@/stores/snackbar.js'
const props = withDefaults(defineProps<{
isNewZone: boolean
zone: Zone & { clusterIds: Cluster['id'][] }
allQuotas: Quota[]
zone?: Zone & { clusterIds: Cluster['id'][] }
allClusters: Cluster[]
associatedClusters: unknown[]
associatedClusters: Cluster[]
}>(), {
isNewZone: false,
zone: () => ({
id: '',
label: '',
slug: '',
argocdUrl: '',
description: '',
clusterIds: [],
}),
allQuotas: () => [],
allClusters: () => [],
associatedClusters: () => [],
})
Expand Down Expand Up @@ -57,6 +56,7 @@ function updateZone() {
const updatedZone = {
zoneId: localZone.value.id,
label: localZone.value.label,
argocdUrl: localZone.value.argocdUrl,
description: localZone.value.description,
clusterIds: localZone.value.clusterIds,
}
Expand Down Expand Up @@ -97,6 +97,16 @@ onBeforeMount(() => {
data-testid="labelInput"
placeholder="Zone Publique"
/>
<DsfrInputGroup
v-model="localZone.argocdUrl"
data-testid="argocdUrlInput"
:error-message="errorSchema?.flatten().fieldErrors.argocdUrl"
type="text"
:required="true"
label="URL de l'instance ArgoCD dédiée aux déploiements de cette zone."
label-visible
hint="Cette URL est notamment utilisée pour configurer l'OIDC Keycloak. Elle peut être mise à jour ultérieurement."
/>
<DsfrInputGroup
v-model="localZone.description"
data-testid="descriptionInput"
Expand Down
5 changes: 3 additions & 2 deletions apps/client/src/views/admin/ListZones.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ async function createZone(zone: CreateZoneBody) {
snackbarStore.isWaitingForResponse = false
}
async function updateZone({ zoneId, label, description }: UpdateZoneBody & { zoneId: Zone['id'] }) {
async function updateZone({ zoneId, label, argocdUrl, description }: UpdateZoneBody & { zoneId: Zone['id'] }) {
snackbarStore.isWaitingForResponse = true
await zoneStore.updateZone(zoneId, { label, description })
await zoneStore.updateZone(zoneId, { label, argocdUrl, description })
await Promise.all([
zoneStore.getAllZones(),
clusterStore.getClusters(),
Expand Down Expand Up @@ -129,6 +129,7 @@ watch(zones, async () => {
>
<ZoneForm
:all-clusters="allClusters"
:associated-clusters="[]"
class="w-full"
:is-new-zone="true"
@add="(zone) => createZone(zone)"
Expand Down
14 changes: 9 additions & 5 deletions apps/server/src/__mocks__/utils/hook-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,29 @@ import { mockDeep, mockReset } from 'vitest-mock-extended'
vi.mock('../utils/hook-wrapper.ts')

export const hook = {
project: {
upsert: vi.fn(),
cluster: {
delete: vi.fn(),
getSecrets: vi.fn(),
upsert: vi.fn(),
},
misc: {
checkServices: vi.fn(),
fetchOrganizations: vi.fn(),
syncRepository: vi.fn(),
},
cluster: {
delete: vi.fn(),
project: {
upsert: vi.fn(),
delete: vi.fn(),
getSecrets: vi.fn(),
},
user: {
retrieveAdminUsers: vi.fn(),
retrieveUserByEmail: vi.fn(),
updateUserAdminGroupMembership: vi.fn(),
},
zone: {
delete: vi.fn(),
upsert: vi.fn(),
},
} as const

const hookMock = mockDeep<typeof hook>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Zone" ADD COLUMN "argocdUrl" TEXT NOT NULL DEFAULT 'https://example.com';
1 change: 1 addition & 0 deletions apps/server/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ model Zone {
id String @id @unique @default(uuid()) @db.Uuid
slug String @unique @db.VarChar(10)
label String @db.VarChar(50)
argocdUrl String @default("https://example.com")
description String? @db.VarChar(200)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/resources/cluster/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export async function getClustersAssociatedWithProject(projectId: Project['id'])
select: {
id: true,
slug: true,
argocdUrl: true,
},
},
},
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/resources/project/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ const clusterInfosSelect = {
select: {
id: true,
slug: true,
argocdUrl: true,
},
},
}
Expand Down
39 changes: 28 additions & 11 deletions apps/server/src/resources/zone/business.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Cluster, Zone } from '@prisma/client'
import prisma from '../../__mocks__/prisma.js'
import { BadRequest400 } from '../../utils/errors.ts'
import { hook } from '../../__mocks__/utils/hook-wrapper.ts'
import { createZone, deleteZone, listZones, updateZone } from './business.ts'
import * as queries from './queries.js'

const userId = faker.string.uuid()
const reqId = faker.string.uuid()
const linkZoneToClustersMock = vi.spyOn(queries, 'linkZoneToClusters')
vi.mock('../../utils/hook-wrapper.ts', async () => ({
hook,
}))

describe('test zone business', () => {
const zones: Zone[] = [{
id: faker.string.uuid(),
label: faker.company.name(),
argocdUrl: faker.internet.url(),
createdAt: new Date(),
updatedAt: new Date(),
description: faker.lorem.lines(1),
slug: faker.string.alphanumeric(5),
}, {
id: faker.string.uuid(),
label: faker.company.name(),
argocdUrl: faker.internet.url(),
createdAt: new Date(),
updatedAt: new Date(),
description: faker.lorem.lines(1),
Expand All @@ -43,43 +51,47 @@ describe('test zone business', () => {
})
describe('createZone', () => {
it('should create zone without description and clusterIds', async () => {
const newZone = { label: zones[0].label, slug: zones[0].slug }
const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl }

hook.zone.upsert.mockResolvedValue({})
prisma.zone.create.mockResolvedValueOnce(zones[0])
const response = await createZone(newZone)
const response = await createZone(newZone, userId, reqId)

expect(response).toEqual(zones[0])
expect(prisma.zone.create).toHaveBeenCalledWith({
data: {
description: undefined,
label: newZone.label,
slug: newZone.slug,
label: newZone.label,
argocdUrl: newZone.argocdUrl,
description: undefined,
},
})
expect(linkZoneToClustersMock).toHaveBeenCalledTimes(0)
})
it('should create zone with description and clusterIds', async () => {
const newZone = { label: zones[0].label, slug: zones[0].slug, clusterIds: clusters.map(({ id }) => id), description: faker.lorem.lines(2) }
const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl, clusterIds: clusters.map(({ id }) => id), description: faker.lorem.lines(2) }

hook.zone.upsert.mockResolvedValue({})
prisma.zone.create.mockResolvedValueOnce(zones[0])
const response = await createZone(newZone)
const response = await createZone(newZone, userId, reqId)

expect(response).toEqual(zones[0])
expect(prisma.zone.create).toHaveBeenCalledWith({
data: {
description: newZone.description,
label: newZone.label,
argocdUrl: newZone.argocdUrl,
slug: newZone.slug,
},
})
expect(linkZoneToClustersMock).toHaveBeenCalledTimes(1)
})
it('should not create zone, conflict label', async () => {
const newZone = { label: zones[0].label, slug: zones[0].slug }
const newZone = { label: zones[0].label, slug: zones[0].slug, argocdUrl: zones[0].argocdUrl }

prisma.zone.findUnique.mockResolvedValueOnce(zones[0])
prisma.zone.create.mockResolvedValueOnce(zones[0])
const response = await createZone(newZone)
const response = await createZone(newZone, userId, reqId)

expect(response).instanceOf(BadRequest400)
expect(prisma.zone.create).toHaveBeenCalledTimes(0)
Expand All @@ -88,27 +100,32 @@ describe('test zone business', () => {
})
describe('updateZone', () => {
it('should filter keys and update zone', async () => {
prisma.zone.update.mockResolvedValueOnce(zones[0])
hook.zone.upsert.mockResolvedValue({})
await updateZone(zones[0].id, {
description: '',
label: zones[0].label,
argocdUrl: zones[0].argocdUrl,
extraKey: 1,
})
}, userId, reqId)
expect(prisma.zone.update).toHaveBeenCalledWith({ where: { id: zones[0].id }, data: {
description: '',
label: zones[0].label,
argocdUrl: zones[0].argocdUrl,
} })
})
})
describe('deleteZone', () => {
it('should not delete zone, cluster attached', async () => {
prisma.cluster.findFirst.mockResolvedValueOnce(clusters[0])
const response = await deleteZone(zones[0].id)
const response = await deleteZone(zones[0].id, userId, reqId)
expect(response).instanceOf(BadRequest400)
expect(prisma.cluster.delete).toHaveBeenCalledTimes(0)
})
it('should delete zone', async () => {
prisma.cluster.findFirst.mockResolvedValueOnce(undefined)
const response = await deleteZone(zones[0].id)
hook.zone.delete.mockResolvedValue({})
const response = await deleteZone(zones[0].id, userId, reqId)
expect(response).toEqual(null)
expect(prisma.zone.delete).toHaveBeenCalledTimes(1)
})
Expand Down
Loading

0 comments on commit 32e775f

Please sign in to comment.