Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Moyens et unités] Ajout de la suppression de base dans le BackOffice #881

Merged
merged 3 commits into from
Oct 17, 2023
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
9 changes: 9 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Related Pull Requests & Issues

- Resolve #123
- MTES-MCT/monitorfish#123
- MTES-MCT/rapportnav2#123

----

- [ ] Tests E2E (Cypress)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package fr.gouv.cacem.monitorenv.domain.use_cases.base

import fr.gouv.cacem.monitorenv.config.UseCase
import fr.gouv.cacem.monitorenv.domain.repositories.IBaseRepository

@UseCase
class DeleteBase(private val baseRepository: IBaseRepository) {
fun execute(baseId: Int) {
baseRepository.deleteById(baseId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ data class FullBaseDataOutput(
val id: Int,
val controlUnitResourceIds: List<Int>,
val controlUnitResources: List<ControlUnitResourceDataOutput>,
val latitude: Double,
val longitude: Double,
val name: String,
) {
companion object {
Expand All @@ -17,6 +19,8 @@ data class FullBaseDataOutput(
id = requireNotNull(fullBase.base.id),
controlUnitResourceIds = controlUnitResources.map { it.id },
controlUnitResources,
latitude = fullBase.base.latitude,
longitude = fullBase.base.longitude,
name = fullBase.base.name,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ class ApiAdministrationsController(
fun delete(
@PathParam("Administration ID")
@PathVariable(name = "administrationId")
controlUnitId: Int,
administrationId: Int,
) {
deleteAdministration.execute(controlUnitId)
deleteAdministration.execute(administrationId)
}

@GetMapping("/{administrationId}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.publicapi

import fr.gouv.cacem.monitorenv.domain.use_cases.base.CreateOrUpdateBase
import fr.gouv.cacem.monitorenv.domain.use_cases.base.DeleteBase
import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBaseById
import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBases
import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.inputs.CreateOrUpdateBaseDataInput
Expand All @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.*
@Tag(name = "Bases", description = "API bases")
class ApiBasesController(
private val createOrUpdateBase: CreateOrUpdateBase,
private val deleteBase: DeleteBase,
private val getBases: GetBases,
private val getBaseById: GetBaseById,
) {
Expand All @@ -33,6 +35,16 @@ class ApiBasesController(
return BaseDataOutput.fromBase(createdBase)
}

@DeleteMapping("/{baseId}")
@Operation(summary = "Delete a base")
fun delete(
@PathParam("Administration ID")
@PathVariable(name = "baseId")
baseId: Int,
) {
deleteBase.execute(baseId)
}

@GetMapping("/{baseId}")
@Operation(summary = "Get a base by its ID")
fun get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ data class AdministrationModel(
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int? = null,

// TODO This shouldn't be nullable but there because of `MissionControlUnitModel.fromControlUnitEntity()`.
@OneToMany(fetch = FetchType.LAZY, mappedBy = "administration")
@JsonManagedReference
val controlUnits: List<ControlUnitModel>? = mutableListOf(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fr.gouv.cacem.monitorenv.infrastructure.database.model
import com.fasterxml.jackson.annotation.JsonManagedReference
import fr.gouv.cacem.monitorenv.domain.entities.base.BaseEntity
import fr.gouv.cacem.monitorenv.domain.use_cases.base.dtos.FullBaseDTO
import fr.gouv.cacem.monitorenv.infrastructure.database.repositories.exceptions.ForeignKeyConstraintException
import jakarta.persistence.*
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
Expand All @@ -18,7 +19,7 @@ data class BaseModel(

@OneToMany(fetch = FetchType.LAZY, mappedBy = "base")
@JsonManagedReference
val controlUnitResources: List<ControlUnitResourceModel>? = mutableListOf(),
val controlUnitResources: List<ControlUnitResourceModel> = listOf(),

@Column(name = "latitude", nullable = false)
val latitude: Double,
Expand All @@ -37,6 +38,15 @@ data class BaseModel(
@UpdateTimestamp
val updatedAtUtc: Instant? = null,
) {
@PreRemove
fun canBeDeleted() {
if (controlUnitResources.isNotEmpty()) {
throw ForeignKeyConstraintException(
"Cannot delete base (ID=$id) due to existing relationships.",
)
}
}

companion object {
/**
* @param controlUnitResourceModels Return control unit resources relations when provided.
Expand All @@ -47,7 +57,7 @@ data class BaseModel(
): BaseModel {
return BaseModel(
id = base.id,
controlUnitResources = controlUnitResourceModels,
controlUnitResources = controlUnitResourceModels ?: listOf(),
latitude = base.latitude,
longitude = base.longitude,
name = base.name,
Expand All @@ -63,7 +73,7 @@ data class BaseModel(
): BaseModel {
return BaseModel(
id = fullBase.base.id,
controlUnitResources = controlUnitResourceModels,
controlUnitResources = controlUnitResourceModels ?: listOf(),
latitude = fullBase.base.latitude,
longitude = fullBase.base.longitude,
name = fullBase.base.name,
Expand All @@ -81,9 +91,11 @@ data class BaseModel(
}

fun toFullBase(): FullBaseDTO {
val controlUnitResourceModels = controlUnitResources

return FullBaseDTO(
base = toBase(),
controlUnitResources = requireNotNull(controlUnitResources).map { it.toControlUnitResource() },
controlUnitResources = controlUnitResourceModels.map { it.toControlUnitResource() },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ data class ControlUnitResourceModel(
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int? = null,

// TODO Make that non-nullable once all resources will have been attached to a base.
@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "base_id", nullable = false)
@JsonBackReference
val base: BaseModel,

@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "control_unit_id", nullable = false)
@JsonBackReference
val controlUnit: ControlUnitModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fr.gouv.cacem.monitorenv.config.MapperConfiguration
import fr.gouv.cacem.monitorenv.config.WebSecurityConfig
import fr.gouv.cacem.monitorenv.domain.entities.base.BaseEntity
import fr.gouv.cacem.monitorenv.domain.use_cases.base.CreateOrUpdateBase
import fr.gouv.cacem.monitorenv.domain.use_cases.base.DeleteBase
import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBaseById
import fr.gouv.cacem.monitorenv.domain.use_cases.base.GetBases
import fr.gouv.cacem.monitorenv.domain.use_cases.base.dtos.FullBaseDTO
Expand Down Expand Up @@ -34,6 +35,9 @@ class ApiBasesControllerITests {
@MockBean
private lateinit var createOrUpdateBase: CreateOrUpdateBase

@MockBean
private lateinit var deleteBase: DeleteBase

@MockBean
private lateinit var getBaseById: GetBaseById

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import fr.gouv.cacem.monitorenv.domain.entities.base.BaseEntity
import fr.gouv.cacem.monitorenv.domain.entities.controlUnit.ControlUnitResourceEntity
import fr.gouv.cacem.monitorenv.domain.entities.controlUnit.ControlUnitResourceType
import fr.gouv.cacem.monitorenv.domain.use_cases.base.dtos.FullBaseDTO
import fr.gouv.cacem.monitorenv.infrastructure.database.repositories.exceptions.ForeignKeyConstraintException
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
Expand All @@ -13,6 +15,16 @@ class JpaBaseRepositoryITests : AbstractDBTests() {
@Autowired
private lateinit var jpaBaseRepository: JpaBaseRepository

@Test
@Transactional
fun `deleteById() should throw the expected exception when the base is linked to some control unit resources`() {
val throwable = Assertions.catchThrowable {
jpaBaseRepository.deleteById(1)
}

assertThat(throwable).isInstanceOf(ForeignKeyConstraintException::class.java)
}

@Test
@Transactional
fun `findAll() should find all bases`() {
Expand Down
69 changes: 51 additions & 18 deletions frontend/cypress/e2e/back_office/base_form.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FAKE_API_POST_RESPONSE, FAKE_API_PUT_RESPONSE } from '../constants'
import { faker } from '@faker-js/faker'

context('Back Office > Base Form', () => {
beforeEach(() => {
Expand All @@ -9,14 +9,31 @@ context('Back Office > Base Form', () => {
cy.wait('@getBases')
})

it('Should create a base', () => {
cy.intercept('POST', `/api/v1/bases`, FAKE_API_POST_RESPONSE).as('createBase')
it('Should validate the form', () => {
cy.clickButton('Nouvelle base')

cy.clickButton('Créer')

cy.contains('Le nom est obligatoire.').should('be.visible')
cy.contains('Les coordonnées sont obligatoires.').should('be.visible')

cy.clickButton('Annuler')

cy.get('h1').contains('Administration des bases').should('be.visible')
})

it('Should create, edit and delete a base', () => {
// -------------------------------------------------------------------------
// Create

cy.intercept('POST', `/api/v1/bases`).as('createBase')

cy.clickButton('Nouvelle base')

cy.fill('Nom', 'Base 1')
cy.fill('Latitude', 1.2)
cy.fill('Longitude', 3.4)
const newBaseName = faker.location.city()
cy.fill('Nom', newBaseName)
cy.getDataCy('coordinates-dd-input-lat').type('1.2')
cy.getDataCy('coordinates-dd-input-lon').type('3.4').wait(500)

cy.clickButton('Créer')

Expand All @@ -28,21 +45,23 @@ context('Back Office > Base Form', () => {
assert.deepEqual(interception.request.body, {
latitude: 1.2,
longitude: 3.4,
name: 'Base 1'
name: newBaseName
})
})
})

it('Should edit a base', () => {
cy.intercept('PUT', `/api/v1/bases/1`, FAKE_API_PUT_RESPONSE).as('updateBase')
cy.getTableRowByText(newBaseName).should('exist')

cy.clickButton('Éditer cette base', {
withinSelector: 'tbody > tr:nth-child(2)'
})
// -------------------------------------------------------------------------
// Edit

cy.intercept('PUT', `/api/v1/bases/*`).as('updateBase')

cy.getTableRowByText(newBaseName).clickButton('Éditer cette base')

cy.fill('Nom', 'Base 2')
cy.fill('Latitude', 5.6)
cy.fill('Longitude', 7.8)
const nextBaseName = faker.location.city()
cy.fill('Nom', nextBaseName)
cy.getDataCy('coordinates-dd-input-lat').clear().type('5.6')
cy.getDataCy('coordinates-dd-input-lon').clear().type('7.8').wait(500)

cy.clickButton('Mettre à jour')

Expand All @@ -52,11 +71,25 @@ context('Back Office > Base Form', () => {
}

assert.deepInclude(interception.request.body, {
id: 1,
latitude: 5.6,
longitude: 7.8,
name: 'Base 2'
name: nextBaseName
})
})

cy.getTableRowByText(newBaseName).should('not.exist')
cy.getTableRowByText(nextBaseName).should('exist')

// -------------------------------------------------------------------------
// Delete

cy.intercept('DELETE', `/api/v1/bases/*`).as('deleteBase')

cy.getTableRowByText(nextBaseName).clickButton('Supprimer cette base')
cy.clickButton('Supprimer')

cy.wait('@deleteBase')

cy.getTableRowByText(nextBaseName).should('not.exist')
})
})
22 changes: 22 additions & 0 deletions frontend/cypress/e2e/back_office/base_table/row_actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Successful archiving and deleting use cases are tested in `base_form.spec.ts` for Test Idempotency purpose
context('Back Office > Base Table > Row Actions', () => {
beforeEach(() => {
cy.intercept('GET', `/api/v1/bases`).as('getBases')

cy.visit(`/backoffice/bases`)

cy.wait('@getBases')
})

it('Should show a dialog when trying to delete a base linked to some control unit resources', () => {
cy.intercept('DELETE', `/api/v1/bases/3`).as('deleteBase')

cy.getTableRowById(3).clickButton('Supprimer cette base')
cy.clickButton('Supprimer')

cy.wait('@deleteBase')

cy.get('.Component-Dialog').should('be.visible')
cy.contains('Suppression impossible').should('be.visible')
})
})
8 changes: 8 additions & 0 deletions frontend/cypress/e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { sortBy } from 'lodash/fp'

import type { CollectionItem } from '@mtes-mct/monitor-ui'

export function getLastIdFromCollection(collection: CollectionItem[]) {
return (sortBy('id', collection)[collection.length - 1] as CollectionItem).id
}

export function expectPathToBe(expectedPath: string) {
cy.location().should(location => {
assert.equal(location.pathname, expectedPath)
Expand Down
4 changes: 0 additions & 4 deletions frontend/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { registerMonitorUiCustomCommands } from '@mtes-mct/monitor-ui/cypress'

import { getTableRowByText } from './commands/getTableRowByText'

registerMonitorUiCustomCommands()
function unquote(str: string): string {
return str.replace(/(^")|("$)/g, '')
Expand Down Expand Up @@ -31,5 +29,3 @@ Cypress.Commands.add(
Cypress.Commands.add('cleanScreenshots', (fromNumber: number): void => {
cy.exec(`cd cypress/e2e/__image_snapshots__/ && find . | grep -P "[${fromNumber}-7]\\.png" | xargs -i rm {}\n`)
})

Cypress.Commands.add('getTableRowByText', { prevSubject: 'optional' } as any, getTableRowByText)
13 changes: 0 additions & 13 deletions frontend/cypress/support/commands/getTableRowByText.ts

This file was deleted.

1 change: 0 additions & 1 deletion frontend/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ declare global {
isSmooth: boolean
}>
): void
getTableRowByText(path: string): Chainable<JQuery<HTMLElement>>
loadPath(path: string): void
toMatchImageSnapshot(settings: any): Chainable<Element>
}
Expand Down
Loading
Loading