From 8490f8f6b541a23739161c8b619c80884908ec04 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:34:46 +0100 Subject: [PATCH 01/13] feat(SKY-9): add aircraft endpoints to OpenAPI spec --- .../src/main/resources/api/openapi.yaml | 137 +++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index ca19940..0064c51 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -122,6 +122,68 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/aircraft: + post: + operationId: addAircraft + tags: [Aircraft] + summary: Add a new aircraft to the fleet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddAircraftRequest' + responses: + '201': + description: Aircraft created + content: + application/json: + schema: + $ref: '#/components/schemas/AircraftResponse' + '400': + description: Bad request (invalid type or base) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Aircraft with this registration already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + operationId: getAircraft + tags: [Aircraft] + summary: List all aircraft in the fleet + responses: + '200': + description: List of aircraft + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AircraftResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + components: securitySchemes: bearerAuth: @@ -225,4 +287,77 @@ components: date: type: string format: date - example: "2026-03-20" \ No newline at end of file + example: "2026-03-20" + + AddAircraftRequest: + type: object + required: + - registrationNumber + - aircraftTypeId + - operationalBaseId + properties: + registrationNumber: + type: string + description: Aircraft registration number + example: "SP-LRA" + aircraftTypeId: + type: string + format: uuid + description: ID of the aircraft type + operationalBaseId: + type: string + format: uuid + description: ID of the operational base + + AircraftTypeInfo: + type: object + required: + - id + - icaoCode + - name + properties: + id: + type: string + format: uuid + icaoCode: + type: string + example: "B738" + name: + type: string + example: "Boeing 737-800" + + OperationalBaseInfo: + type: object + required: + - id + - icaoCode + - name + properties: + id: + type: string + format: uuid + icaoCode: + type: string + example: "EPWA" + name: + type: string + example: "Warsaw Chopin" + + AircraftResponse: + type: object + required: + - id + - registrationNumber + - aircraftType + - operationalBase + properties: + id: + type: string + format: uuid + registrationNumber: + type: string + example: "SP-LRA" + aircraftType: + $ref: '#/components/schemas/AircraftTypeInfo' + operationalBase: + $ref: '#/components/schemas/OperationalBaseInfo' \ No newline at end of file From 5a7b6e34655a876cfcc29f269bf90570c290f78e Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:37:05 +0100 Subject: [PATCH 02/13] feat(SKY-9): add aircraft database schema and seed data --- .../V2__aircraft_schema_and_seed.sql | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 skyroster-backend/src/main/resources/db/migration/V2__aircraft_schema_and_seed.sql diff --git a/skyroster-backend/src/main/resources/db/migration/V2__aircraft_schema_and_seed.sql b/skyroster-backend/src/main/resources/db/migration/V2__aircraft_schema_and_seed.sql new file mode 100644 index 0000000..03ccf67 --- /dev/null +++ b/skyroster-backend/src/main/resources/db/migration/V2__aircraft_schema_and_seed.sql @@ -0,0 +1,31 @@ +CREATE TABLE operational_bases ( + id UUID PRIMARY KEY, + icao_code VARCHAR(4) UNIQUE NOT NULL, + name VARCHAR(100) NOT NULL +); + +CREATE TABLE aircraft_types ( + id UUID PRIMARY KEY, + icao_code VARCHAR(4) UNIQUE NOT NULL, + name VARCHAR(100) NOT NULL +); + +CREATE TABLE aircraft ( + id UUID PRIMARY KEY, + registration_number VARCHAR(15) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + aircraft_type_id UUID NOT NULL REFERENCES aircraft_types(id), + operational_base_id UUID NOT NULL REFERENCES operational_bases(id) +); + +-- Seed: operational bases +INSERT INTO operational_bases (id, icao_code, name) VALUES + ('a0000000-0000-0000-0000-000000000001', 'EPWA', 'Warsaw Chopin'), + ('a0000000-0000-0000-0000-000000000002', 'EPKK', 'Krakow Balice'), + ('a0000000-0000-0000-0000-000000000003', 'EPGD', 'Gdansk Lech Walesa'); + +-- Seed: aircraft types +INSERT INTO aircraft_types (id, icao_code, name) VALUES + ('b0000000-0000-0000-0000-000000000001', 'B738', 'Boeing 737-800'), + ('b0000000-0000-0000-0000-000000000002', 'A320', 'Airbus A320'), + ('b0000000-0000-0000-0000-000000000003', 'AT76', 'ATR 72-600'); From e80d80df389537cf6d95565bb6a9365048dad07f Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:38:26 +0100 Subject: [PATCH 03/13] feat(SKY-9): add aircraft domain models, ports, and exceptions --- .../AircraftAlreadyExistsException.java | 7 +++ .../AircraftTypeNotFoundException.java | 9 ++++ .../OperationalBaseNotFoundException.java | 9 ++++ .../domain/model/Aircraft.java | 48 +++++++++++++++++++ .../domain/model/AircraftType.java | 28 +++++++++++ .../domain/model/OperationalBase.java | 28 +++++++++++ .../domain/port/AircraftRepository.java | 11 +++++ .../domain/port/AircraftTypeRepository.java | 10 ++++ .../port/OperationalBaseRepository.java | 10 ++++ 9 files changed, 160 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftAlreadyExistsException.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/AircraftType.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/OperationalBase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftAlreadyExistsException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftAlreadyExistsException.java new file mode 100644 index 0000000..22949c7 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftAlreadyExistsException.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +public class AircraftAlreadyExistsException extends RuntimeException { + public AircraftAlreadyExistsException(String registrationNumber) { + super("Aircraft with registration number '%s' already exists".formatted(registrationNumber)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java new file mode 100644 index 0000000..6665456 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +import java.util.UUID; + +public class AircraftTypeNotFoundException extends RuntimeException { + public AircraftTypeNotFoundException(UUID id) { + super("Aircraft type with id '%s' not found".formatted(id)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java new file mode 100644 index 0000000..c83f3f3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.exception; + +import java.util.UUID; + +public class OperationalBaseNotFoundException extends RuntimeException { + public OperationalBaseNotFoundException(UUID id) { + super("Operational base with id '%s' not found".formatted(id)); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java new file mode 100644 index 0000000..8e05ae6 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java @@ -0,0 +1,48 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "aircraft") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Aircraft { + + @Id + private UUID id; + + @Column(name = "registration_number", nullable = false, unique = true, length = 15) + private String registrationNumber; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "aircraft_type_id", nullable = false) + private AircraftType aircraftType; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "operational_base_id", nullable = false) + private OperationalBase operationalBase; + + public Aircraft(UUID id, String registrationNumber, AircraftType aircraftType, OperationalBase operationalBase) { + this.id = id; + this.registrationNumber = registrationNumber; + this.createdAt = LocalDateTime.now(); + this.aircraftType = aircraftType; + this.operationalBase = operationalBase; + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/AircraftType.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/AircraftType.java new file mode 100644 index 0000000..6b5910b --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/AircraftType.java @@ -0,0 +1,28 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Table(name = "aircraft_types") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class AircraftType { + + @Id + private UUID id; + + @Column(name = "icao_code", nullable = false, unique = true, length = 4) + private String icaoCode; + + @Column(nullable = false, length = 100) + private String name; +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/OperationalBase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/OperationalBase.java new file mode 100644 index 0000000..c21b0d4 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/OperationalBase.java @@ -0,0 +1,28 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Table(name = "operational_bases") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class OperationalBase { + + @Id + private UUID id; + + @Column(name = "icao_code", nullable = false, unique = true, length = 4) + private String icaoCode; + + @Column(nullable = false, length = 100) + private String name; +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftRepository.java new file mode 100644 index 0000000..aad2ced --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftRepository.java @@ -0,0 +1,11 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import pl.skyroster.skyroster_backend.domain.model.Aircraft; + +import java.util.List; + +public interface AircraftRepository { + Aircraft save(Aircraft aircraft); + List findAll(); + boolean existsByRegistrationNumber(String registrationNumber); +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java new file mode 100644 index 0000000..cc7d878 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import pl.skyroster.skyroster_backend.domain.model.AircraftType; + +import java.util.Optional; +import java.util.UUID; + +public interface AircraftTypeRepository { + Optional findById(UUID id); +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java new file mode 100644 index 0000000..194beeb --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import pl.skyroster.skyroster_backend.domain.model.OperationalBase; + +import java.util.Optional; +import java.util.UUID; + +public interface OperationalBaseRepository { + Optional findById(UUID id); +} From 3ccfd56acac5b558e6827d38318a102ceed90b52 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:39:19 +0100 Subject: [PATCH 04/13] feat(SKY-9): add JPA repository adapters for aircraft domain --- .../adapter/out/persistence/JpaAircraftRepository.java | 10 ++++++++++ .../out/persistence/JpaAircraftTypeRepository.java | 10 ++++++++++ .../out/persistence/JpaOperationalBaseRepository.java | 10 ++++++++++ 3 files changed, 30 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftTypeRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaOperationalBaseRepository.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftRepository.java new file mode 100644 index 0000000..04d3f60 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import pl.skyroster.skyroster_backend.domain.model.Aircraft; +import pl.skyroster.skyroster_backend.domain.port.AircraftRepository; + +import java.util.UUID; + +public interface JpaAircraftRepository extends JpaRepository, AircraftRepository { +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftTypeRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftTypeRepository.java new file mode 100644 index 0000000..1853265 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaAircraftTypeRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.port.AircraftTypeRepository; + +import java.util.UUID; + +public interface JpaAircraftTypeRepository extends JpaRepository, AircraftTypeRepository { +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaOperationalBaseRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaOperationalBaseRepository.java new file mode 100644 index 0000000..6f2f239 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaOperationalBaseRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import pl.skyroster.skyroster_backend.domain.model.OperationalBase; +import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository; + +import java.util.UUID; + +public interface JpaOperationalBaseRepository extends JpaRepository, OperationalBaseRepository { +} From a8854b2b91e41444f4642a5ef2f14930c23724f0 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:44:20 +0100 Subject: [PATCH 05/13] feat(SKY-9): add aircraft use cases with unit tests --- .../aircraft/AddAircraftUseCase.java | 51 +++++++++ .../aircraft/GetAircraftUseCase.java | 23 ++++ .../aircraft/AddAircraftUseCaseTest.java | 100 ++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/GetAircraftUseCase.java create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java new file mode 100644 index 0000000..0bd893f --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java @@ -0,0 +1,51 @@ +package pl.skyroster.skyroster_backend.application.aircraft; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import pl.skyroster.skyroster_backend.domain.exception.AircraftAlreadyExistsException; +import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; +import pl.skyroster.skyroster_backend.domain.model.Aircraft; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.OperationalBase; +import pl.skyroster.skyroster_backend.domain.port.AircraftRepository; +import pl.skyroster.skyroster_backend.domain.port.AircraftTypeRepository; +import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository; + +import java.util.UUID; + +@Service +public class AddAircraftUseCase { + + private final AircraftRepository aircraftRepository; + private final AircraftTypeRepository aircraftTypeRepository; + private final OperationalBaseRepository operationalBaseRepository; + + public AddAircraftUseCase(AircraftRepository aircraftRepository, + AircraftTypeRepository aircraftTypeRepository, + OperationalBaseRepository operationalBaseRepository) { + this.aircraftRepository = aircraftRepository; + this.aircraftTypeRepository = aircraftTypeRepository; + this.operationalBaseRepository = operationalBaseRepository; + } + + @Transactional + public Aircraft execute(String registrationNumber, UUID aircraftTypeId, UUID operationalBaseId) { + if (registrationNumber == null || registrationNumber.isBlank()) { + throw new IllegalArgumentException("Registration number must not be blank"); + } + + if (aircraftRepository.existsByRegistrationNumber(registrationNumber)) { + throw new AircraftAlreadyExistsException(registrationNumber); + } + + AircraftType type = aircraftTypeRepository.findById(aircraftTypeId) + .orElseThrow(() -> new AircraftTypeNotFoundException(aircraftTypeId)); + + OperationalBase base = operationalBaseRepository.findById(operationalBaseId) + .orElseThrow(() -> new OperationalBaseNotFoundException(operationalBaseId)); + + Aircraft aircraft = new Aircraft(UUID.randomUUID(), registrationNumber, type, base); + return aircraftRepository.save(aircraft); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/GetAircraftUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/GetAircraftUseCase.java new file mode 100644 index 0000000..5a985a8 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/GetAircraftUseCase.java @@ -0,0 +1,23 @@ +package pl.skyroster.skyroster_backend.application.aircraft; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import pl.skyroster.skyroster_backend.domain.model.Aircraft; +import pl.skyroster.skyroster_backend.domain.port.AircraftRepository; + +import java.util.List; + +@Service +public class GetAircraftUseCase { + + private final AircraftRepository aircraftRepository; + + public GetAircraftUseCase(AircraftRepository aircraftRepository) { + this.aircraftRepository = aircraftRepository; + } + + @Transactional(readOnly = true) + public List execute() { + return aircraftRepository.findAll(); + } +} diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java new file mode 100644 index 0000000..628432c --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java @@ -0,0 +1,100 @@ +package pl.skyroster.skyroster_backend.application.aircraft; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.skyroster.skyroster_backend.domain.exception.AircraftAlreadyExistsException; +import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; +import pl.skyroster.skyroster_backend.domain.model.Aircraft; +import pl.skyroster.skyroster_backend.domain.model.AircraftType; +import pl.skyroster.skyroster_backend.domain.model.OperationalBase; +import pl.skyroster.skyroster_backend.domain.port.AircraftRepository; +import pl.skyroster.skyroster_backend.domain.port.AircraftTypeRepository; +import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AddAircraftUseCaseTest { + + @Mock + private AircraftRepository aircraftRepository; + @Mock + private AircraftTypeRepository aircraftTypeRepository; + @Mock + private OperationalBaseRepository operationalBaseRepository; + + private AddAircraftUseCase useCase; + + private static final UUID TYPE_ID = UUID.randomUUID(); + private static final UUID BASE_ID = UUID.randomUUID(); + private static final String REGISTRATION = "SP-LRA"; + + private final AircraftType type = new AircraftType(TYPE_ID, "B738", "Boeing 737-800"); + private final OperationalBase base = new OperationalBase(BASE_ID, "EPWA", "Warsaw Chopin"); + + @BeforeEach + void setUp() { + useCase = new AddAircraftUseCase(aircraftRepository, aircraftTypeRepository, operationalBaseRepository); + } + + @Test + void shouldAddAircraft_whenValidData() { + when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(false); + when(aircraftTypeRepository.findById(TYPE_ID)).thenReturn(Optional.of(type)); + when(operationalBaseRepository.findById(BASE_ID)).thenReturn(Optional.of(base)); + when(aircraftRepository.save(any(Aircraft.class))).thenAnswer(inv -> inv.getArgument(0)); + + Aircraft result = useCase.execute(REGISTRATION, TYPE_ID, BASE_ID); + + assertThat(result.getRegistrationNumber()).isEqualTo(REGISTRATION); + assertThat(result.getAircraftType()).isEqualTo(type); + assertThat(result.getOperationalBase()).isEqualTo(base); + assertThat(result.getId()).isNotNull(); + assertThat(result.getCreatedAt()).isNotNull(); + } + + @Test + void shouldThrow_whenRegistrationAlreadyExists() { + when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(true); + + assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_ID, BASE_ID)) + .isInstanceOf(AircraftAlreadyExistsException.class) + .hasMessageContaining(REGISTRATION); + } + + @Test + void shouldThrow_whenAircraftTypeNotFound() { + when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(false); + when(aircraftTypeRepository.findById(TYPE_ID)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_ID, BASE_ID)) + .isInstanceOf(AircraftTypeNotFoundException.class); + } + + @Test + void shouldThrow_whenOperationalBaseNotFound() { + when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(false); + when(aircraftTypeRepository.findById(TYPE_ID)).thenReturn(Optional.of(type)); + when(operationalBaseRepository.findById(BASE_ID)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_ID, BASE_ID)) + .isInstanceOf(OperationalBaseNotFoundException.class); + } + + @Test + void shouldThrow_whenRegistrationIsBlank() { + assertThatThrownBy(() -> useCase.execute("", TYPE_ID, BASE_ID)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Registration number"); + } +} From ae909196796024c7ae59c381a7e2a16ecd5957c7 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:45:54 +0100 Subject: [PATCH 06/13] feat(SKY-9): add global exception handler for domain exceptions --- .../config/GlobalExceptionHandler.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..4d1539a --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/GlobalExceptionHandler.java @@ -0,0 +1,52 @@ +package pl.skyroster.skyroster_backend.infrastructure.config; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import pl.skyroster.skyroster_backend.domain.exception.AircraftAlreadyExistsException; +import pl.skyroster.skyroster_backend.domain.exception.AircraftTypeNotFoundException; +import pl.skyroster.skyroster_backend.domain.exception.OperationalBaseNotFoundException; +import pl.skyroster.skyroster_backend.generated.model.ErrorResponse; + +import java.time.OffsetDateTime; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(AircraftAlreadyExistsException.class) + public ResponseEntity handleAircraftAlreadyExists(AircraftAlreadyExistsException ex, + HttpServletRequest request) { + return buildResponse(HttpStatus.CONFLICT, ex.getMessage(), request); + } + + @ExceptionHandler(AircraftTypeNotFoundException.class) + public ResponseEntity handleAircraftTypeNotFound(AircraftTypeNotFoundException ex, + HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); + } + + @ExceptionHandler(OperationalBaseNotFoundException.class) + public ResponseEntity handleOperationalBaseNotFound(OperationalBaseNotFoundException ex, + HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(IllegalArgumentException ex, + HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request); + } + + private ResponseEntity buildResponse(HttpStatus status, String message, + HttpServletRequest request) { + ErrorResponse error = new ErrorResponse() + .status(status.value()) + .error(status.getReasonPhrase()) + .message(message) + .timestamp(OffsetDateTime.now()) + .path(request.getRequestURI()); + return ResponseEntity.status(status).body(error); + } +} From cc79ddcee21ba6683dec2221ca22b1aa37a63f7b Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:46:51 +0100 Subject: [PATCH 07/13] feat(SKY-9): add aircraft endpoint authorization rules --- .../infrastructure/config/security/SecurityConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index ad5659a..c7dbe85 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -46,6 +47,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers("/api/admin/**").hasRole("operations_administrator") .requestMatchers("/api/compliance/**").hasRole("compliance_officer") .requestMatchers("/api/planning/**").hasRole("schedule_planner") + .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") + .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() .requestMatchers("/api/**").authenticated() .anyRequest().denyAll() ) From 47c372856212440bef6b436727adf4adc09bf907 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:49:35 +0100 Subject: [PATCH 08/13] feat(SKY-9): add AircraftController for POST and GET endpoints --- .../adapter/in/web/AircraftController.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java new file mode 100644 index 0000000..b7676e3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java @@ -0,0 +1,62 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.aircraft.AddAircraftUseCase; +import pl.skyroster.skyroster_backend.application.aircraft.GetAircraftUseCase; +import pl.skyroster.skyroster_backend.domain.model.Aircraft; +import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.AddAircraftRequest; +import pl.skyroster.skyroster_backend.generated.model.AircraftResponse; +import pl.skyroster.skyroster_backend.generated.model.AircraftTypeInfo; +import pl.skyroster.skyroster_backend.generated.model.OperationalBaseInfo; + +import java.util.List; + +@RestController +public class AircraftController { + + private final AddAircraftUseCase addAircraftUseCase; + private final GetAircraftUseCase getAircraftUseCase; + + public AircraftController(AddAircraftUseCase addAircraftUseCase, GetAircraftUseCase getAircraftUseCase) { + this.addAircraftUseCase = addAircraftUseCase; + this.getAircraftUseCase = getAircraftUseCase; + } + + @PostMapping(ApiApi.PATH_ADD_AIRCRAFT) + public ResponseEntity addAircraft(@RequestBody AddAircraftRequest request) { + Aircraft aircraft = addAircraftUseCase.execute( + request.getRegistrationNumber(), + request.getAircraftTypeId(), + request.getOperationalBaseId() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(aircraft)); + } + + @GetMapping(ApiApi.PATH_GET_AIRCRAFT) + public ResponseEntity> getAircraft() { + List response = getAircraftUseCase.execute().stream() + .map(this::toResponse) + .toList(); + return ResponseEntity.ok(response); + } + + private AircraftResponse toResponse(Aircraft aircraft) { + return new AircraftResponse() + .id(aircraft.getId()) + .registrationNumber(aircraft.getRegistrationNumber()) + .aircraftType(new AircraftTypeInfo() + .id(aircraft.getAircraftType().getId()) + .icaoCode(aircraft.getAircraftType().getIcaoCode()) + .name(aircraft.getAircraftType().getName())) + .operationalBase(new OperationalBaseInfo() + .id(aircraft.getOperationalBase().getId()) + .icaoCode(aircraft.getOperationalBase().getIcaoCode()) + .name(aircraft.getOperationalBase().getName())); + } +} From acecea9aefb32471129a9720007817e9cb98e190 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 21:53:35 +0100 Subject: [PATCH 09/13] test(SKY-9): add aircraft endpoint integration tests --- .../in/web/AircraftIntegrationTest.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java new file mode 100644 index 0000000..cddb624 --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java @@ -0,0 +1,215 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import dasniko.testcontainers.keycloak.KeycloakContainer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import pl.skyroster.skyroster_backend.TestcontainersConfiguration; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Import(TestcontainersConfiguration.class) +class AircraftIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private KeycloakContainer keycloak; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + // Seed UUIDs from V2 migration + private static final String TYPE_B738_ID = "b0000000-0000-0000-0000-000000000001"; + private static final String BASE_EPWA_ID = "a0000000-0000-0000-0000-000000000001"; + + @Test + void addAircraft_shouldReturn201_whenAdminWithValidData() throws Exception { + String token = getToken("admin", "test1234"); + String body = """ + { + "registrationNumber": "SP-LRA", + "aircraftTypeId": "%s", + "operationalBaseId": "%s" + } + """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + + mockMvc.perform(post("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.registrationNumber").value("SP-LRA")) + .andExpect(jsonPath("$.aircraftType.icaoCode").value("B738")) + .andExpect(jsonPath("$.operationalBase.icaoCode").value("EPWA")) + .andExpect(jsonPath("$.id").exists()); + } + + @Test + void addAircraft_shouldReturn409_whenDuplicateRegistration() throws Exception { + String token = getToken("admin", "test1234"); + String body = """ + { + "registrationNumber": "SP-DUP", + "aircraftTypeId": "%s", + "operationalBaseId": "%s" + } + """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + + // First request — should succeed + mockMvc.perform(post("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()); + + // Second request — should conflict + mockMvc.perform(post("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.status").value(409)); + } + + @Test + void addAircraft_shouldReturn400_whenAircraftTypeNotFound() throws Exception { + String token = getToken("admin", "test1234"); + String body = """ + { + "registrationNumber": "SP-BAD", + "aircraftTypeId": "00000000-0000-0000-0000-000000000099", + "operationalBaseId": "%s" + } + """.formatted(BASE_EPWA_ID); + + mockMvc.perform(post("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value(400)); + } + + @Test + void addAircraft_shouldReturn400_whenOperationalBaseNotFound() throws Exception { + String token = getToken("admin", "test1234"); + String body = """ + { + "registrationNumber": "SP-BAD2", + "aircraftTypeId": "%s", + "operationalBaseId": "00000000-0000-0000-0000-000000000099" + } + """.formatted(TYPE_B738_ID); + + mockMvc.perform(post("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value(400)); + } + + @Test + void addAircraft_shouldReturn401_whenNoToken() throws Exception { + String body = """ + { + "registrationNumber": "SP-NO", + "aircraftTypeId": "%s", + "operationalBaseId": "%s" + } + """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + + mockMvc.perform(post("/api/aircraft") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isUnauthorized()); + } + + @Test + void addAircraft_shouldReturn403_whenNotAdmin() throws Exception { + String token = getToken("schedule_planner", "test1234"); + String body = """ + { + "registrationNumber": "SP-NOPERM", + "aircraftTypeId": "%s", + "operationalBaseId": "%s" + } + """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + + mockMvc.perform(post("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isForbidden()); + } + + @Test + void getAircraft_shouldReturnFleetList() throws Exception { + String token = getToken("admin", "test1234"); + + // Add an aircraft first + String body = """ + { + "registrationNumber": "SP-GET", + "aircraftTypeId": "%s", + "operationalBaseId": "%s" + } + """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + + mockMvc.perform(post("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()); + + // Get fleet list + mockMvc.perform(get("/api/aircraft") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$[?(@.registrationNumber == 'SP-GET')]").exists()); + } + + private String getToken(String username, String password) throws Exception { + String tokenUrl = keycloak.getAuthServerUrl() + "/realms/skyroster/protocol/openid-connect/token"; + + String reqBody = "grant_type=password" + + "&client_id=skyroster-frontend" + + "&username=" + username + + "&password=" + password; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(tokenUrl)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(reqBody)) + .build(); + + HttpResponse response = HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Failed to get token: " + response.body()); + } + + JsonNode json = objectMapper.readTree(response.body()); + return json.get("access_token").asText(); + } +} From 385c8124bf99f285b7fcf99cd0ceede9e8bd4d9f Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 22:04:44 +0100 Subject: [PATCH 10/13] refactor(SKY-9): remove stub AdminController and related references Admin endpoint was a demo stub. Now that aircraft endpoints use the operations_administrator role, it is no longer needed. --- .../skyroster.postman_collection.json | 42 ++++++++++++++++--- .../adapter/in/web/AdminController.java | 24 ----------- .../config/security/SecurityConfig.java | 1 - .../src/main/resources/api/openapi.yaml | 27 ------------ .../in/web/SecurityIntegrationTest.java | 40 ------------------ 5 files changed, 37 insertions(+), 97 deletions(-) delete mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AdminController.java diff --git a/docs/collection/skyroster.postman_collection.json b/docs/collection/skyroster.postman_collection.json index 4b4fd0e..b990665 100644 --- a/docs/collection/skyroster.postman_collection.json +++ b/docs/collection/skyroster.postman_collection.json @@ -199,10 +199,43 @@ ] }, { - "name": "Admin", + "name": "Aircraft", "item": [ { - "name": "GET /api/admin/users", + "name": "POST /api/aircraft", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"registrationNumber\": \"SP-LRA\",\n \"aircraftTypeId\": \"\",\n \"operationalBaseId\": \"\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/aircraft", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "aircraft" + ] + } + }, + "response": [] + }, + { + "name": "GET /api/aircraft", "request": { "method": "GET", "header": [ @@ -213,14 +246,13 @@ } ], "url": { - "raw": "{{base_url}}/api/admin/users", + "raw": "{{base_url}}/api/aircraft", "host": [ "{{base_url}}" ], "path": [ "api", - "admin", - "users" + "aircraft" ] } }, diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AdminController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AdminController.java deleted file mode 100644 index c66baad..0000000 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AdminController.java +++ /dev/null @@ -1,24 +0,0 @@ -package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import pl.skyroster.skyroster_backend.generated.api.ApiApi; -import pl.skyroster.skyroster_backend.generated.model.UserInfo; - -import java.util.List; - -// TODO: Temporary stub controller — replace with real implementation -@RestController -public class AdminController { - - @GetMapping(ApiApi.PATH_GET_ADMIN_USERS) - public ResponseEntity> getAdminUsers() { - List mockUsers = List.of( - new UserInfo().username("admin").email("admin@skyroster.local").roles(List.of("operations_administrator")), - new UserInfo().username("compliance").email("compliance@skyroster.local").roles(List.of("compliance_officer")), - new UserInfo().username("schedule_planner").email("schedule_planner@skyroster.local").roles(List.of("schedule_planner")) - ); - return ResponseEntity.ok(mockUsers); - } -} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index c7dbe85..2df005b 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -44,7 +44,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .authorizeHttpRequests(auth -> auth .requestMatchers("/api/health").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/api/admin/**").hasRole("operations_administrator") .requestMatchers("/api/compliance/**").hasRole("compliance_officer") .requestMatchers("/api/planning/**").hasRole("schedule_planner") .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 0064c51..974814c 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -41,33 +41,6 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /api/admin/users: - get: - operationId: getAdminUsers - tags: [Admin] - summary: List users (admin only) - responses: - '200': - description: List of users - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/UserInfo' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /api/compliance/reports: get: operationId: getComplianceReports diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/SecurityIntegrationTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/SecurityIntegrationTest.java index 237ea8c..1afd0c0 100644 --- a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/SecurityIntegrationTest.java +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/SecurityIntegrationTest.java @@ -7,7 +7,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import pl.skyroster.skyroster_backend.TestcontainersConfiguration; @@ -20,7 +19,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -44,44 +42,6 @@ void healthEndpoint_shouldBeAccessible_whenNoToken() throws Exception { .andExpect(jsonPath("$.status").value("UP")); } - @Test - void adminEndpoint_shouldReturn401_whenNoToken() throws Exception { - mockMvc.perform(get("/api/admin/users")) - .andExpect(status().isUnauthorized()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.status").value(401)) - .andExpect(jsonPath("$.error").value("Unauthorized")) - .andExpect(jsonPath("$.path").value("/api/admin/users")); - } - - @Test - void adminEndpoint_shouldReturn401_whenInvalidToken() throws Exception { - mockMvc.perform(get("/api/admin/users") - .header(HttpHeaders.AUTHORIZATION, "Bearer invalid-token")) - .andExpect(status().isUnauthorized()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.status").value(401)); - } - - @Test - void adminEndpoint_shouldReturn403_whenSchedulePlannerToken() throws Exception { - String token = getToken("schedule_planner", "test1234"); - mockMvc.perform(get("/api/admin/users") - .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) - .andExpect(status().isForbidden()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.status").value(403)) - .andExpect(jsonPath("$.error").value("Forbidden")); - } - - @Test - void adminEndpoint_shouldReturn200_whenAdminToken() throws Exception { - String token = getToken("admin", "test1234"); - mockMvc.perform(get("/api/admin/users") - .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) - .andExpect(status().isOk()); - } - @Test void complianceEndpoint_shouldReturn200_whenComplianceToken() throws Exception { String token = getToken("compliance", "test1234"); From 6ee3df6eddeed76031dc2eb130b065a78c2966c4 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 22:24:42 +0100 Subject: [PATCH 11/13] refactor(SKY-9): pass createdAt explicitly instead of using LocalDateTime.now() in domain model --- .../application/aircraft/AddAircraftUseCase.java | 3 ++- .../skyroster/skyroster_backend/domain/model/Aircraft.java | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java index 0bd893f..27c5f26 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java @@ -12,6 +12,7 @@ import pl.skyroster.skyroster_backend.domain.port.AircraftTypeRepository; import pl.skyroster.skyroster_backend.domain.port.OperationalBaseRepository; +import java.time.LocalDateTime; import java.util.UUID; @Service @@ -45,7 +46,7 @@ public Aircraft execute(String registrationNumber, UUID aircraftTypeId, UUID ope OperationalBase base = operationalBaseRepository.findById(operationalBaseId) .orElseThrow(() -> new OperationalBaseNotFoundException(operationalBaseId)); - Aircraft aircraft = new Aircraft(UUID.randomUUID(), registrationNumber, type, base); + Aircraft aircraft = new Aircraft(UUID.randomUUID(), registrationNumber, LocalDateTime.now(), type, base); return aircraftRepository.save(aircraft); } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java index 8e05ae6..e77ae9a 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Aircraft.java @@ -38,11 +38,4 @@ public class Aircraft { @JoinColumn(name = "operational_base_id", nullable = false) private OperationalBase operationalBase; - public Aircraft(UUID id, String registrationNumber, AircraftType aircraftType, OperationalBase operationalBase) { - this.id = id; - this.registrationNumber = registrationNumber; - this.createdAt = LocalDateTime.now(); - this.aircraftType = aircraftType; - this.operationalBase = operationalBase; - } } From 26fbc92af47f4aa798c95fb43f0c2b4e38276356 Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 22:25:16 +0100 Subject: [PATCH 12/13] fix(SKY-9): fix duplicated prefix in keycloak clientId fallback --- skyroster-frontend/src/security/keycloak.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyroster-frontend/src/security/keycloak.js b/skyroster-frontend/src/security/keycloak.js index 81f8adc..1d3609a 100644 --- a/skyroster-frontend/src/security/keycloak.js +++ b/skyroster-frontend/src/security/keycloak.js @@ -3,7 +3,7 @@ import Keycloak from 'keycloak-js'; const keycloakConfig = { url: import.meta.env.VITE_KEYCLOAK_URL || 'http://localhost:8180', realm: import.meta.env.VITE_KEYCLOAK_REALM || 'skyroster', - clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || 'skyroster-skyroster-frontend', + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || 'skyroster-frontend', }; const keycloak = new Keycloak(keycloakConfig); From a29c977f045d87404f0ab09ab3dc627693be62ae Mon Sep 17 00:00:00 2001 From: Krywion Date: Fri, 27 Mar 2026 22:35:06 +0100 Subject: [PATCH 13/13] refactor(SKY-9): use ICAO codes instead of UUIDs in add aircraft request API now accepts aircraftTypeCode and operationalBaseCode (e.g. "B738", "EPWA") instead of internal UUIDs, making the API more user-friendly. --- .../skyroster.postman_collection.json | 2 +- .../aircraft/AddAircraftUseCase.java | 10 ++-- .../AircraftTypeNotFoundException.java | 6 +-- .../OperationalBaseNotFoundException.java | 6 +-- .../domain/port/AircraftTypeRepository.java | 3 +- .../port/OperationalBaseRepository.java | 3 +- .../adapter/in/web/AircraftController.java | 4 +- .../src/main/resources/api/openapi.yaml | 16 +++--- .../aircraft/AddAircraftUseCaseTest.java | 28 +++++------ .../in/web/AircraftIntegrationTest.java | 50 ++++++++----------- 10 files changed, 57 insertions(+), 71 deletions(-) diff --git a/docs/collection/skyroster.postman_collection.json b/docs/collection/skyroster.postman_collection.json index b990665..8365c29 100644 --- a/docs/collection/skyroster.postman_collection.json +++ b/docs/collection/skyroster.postman_collection.json @@ -219,7 +219,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"registrationNumber\": \"SP-LRA\",\n \"aircraftTypeId\": \"\",\n \"operationalBaseId\": \"\"\n}" + "raw": "{\n \"registrationNumber\": \"SP-LRA\",\n \"aircraftTypeCode\": \"B738\",\n \"operationalBaseCode\": \"EPWA\"\n}" }, "url": { "raw": "{{base_url}}/api/aircraft", diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java index 27c5f26..7b6756d 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCase.java @@ -31,7 +31,7 @@ public AddAircraftUseCase(AircraftRepository aircraftRepository, } @Transactional - public Aircraft execute(String registrationNumber, UUID aircraftTypeId, UUID operationalBaseId) { + public Aircraft execute(String registrationNumber, String aircraftTypeCode, String operationalBaseCode) { if (registrationNumber == null || registrationNumber.isBlank()) { throw new IllegalArgumentException("Registration number must not be blank"); } @@ -40,11 +40,11 @@ public Aircraft execute(String registrationNumber, UUID aircraftTypeId, UUID ope throw new AircraftAlreadyExistsException(registrationNumber); } - AircraftType type = aircraftTypeRepository.findById(aircraftTypeId) - .orElseThrow(() -> new AircraftTypeNotFoundException(aircraftTypeId)); + AircraftType type = aircraftTypeRepository.findByIcaoCode(aircraftTypeCode) + .orElseThrow(() -> new AircraftTypeNotFoundException(aircraftTypeCode)); - OperationalBase base = operationalBaseRepository.findById(operationalBaseId) - .orElseThrow(() -> new OperationalBaseNotFoundException(operationalBaseId)); + OperationalBase base = operationalBaseRepository.findByIcaoCode(operationalBaseCode) + .orElseThrow(() -> new OperationalBaseNotFoundException(operationalBaseCode)); Aircraft aircraft = new Aircraft(UUID.randomUUID(), registrationNumber, LocalDateTime.now(), type, base); return aircraftRepository.save(aircraft); diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java index 6665456..934ee2e 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/AircraftTypeNotFoundException.java @@ -1,9 +1,7 @@ package pl.skyroster.skyroster_backend.domain.exception; -import java.util.UUID; - public class AircraftTypeNotFoundException extends RuntimeException { - public AircraftTypeNotFoundException(UUID id) { - super("Aircraft type with id '%s' not found".formatted(id)); + public AircraftTypeNotFoundException(String icaoCode) { + super("Aircraft type with code '%s' not found".formatted(icaoCode)); } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java index c83f3f3..c326912 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/exception/OperationalBaseNotFoundException.java @@ -1,9 +1,7 @@ package pl.skyroster.skyroster_backend.domain.exception; -import java.util.UUID; - public class OperationalBaseNotFoundException extends RuntimeException { - public OperationalBaseNotFoundException(UUID id) { - super("Operational base with id '%s' not found".formatted(id)); + public OperationalBaseNotFoundException(String icaoCode) { + super("Operational base with code '%s' not found".formatted(icaoCode)); } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java index cc7d878..b88e9c7 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/AircraftTypeRepository.java @@ -3,8 +3,7 @@ import pl.skyroster.skyroster_backend.domain.model.AircraftType; import java.util.Optional; -import java.util.UUID; public interface AircraftTypeRepository { - Optional findById(UUID id); + Optional findByIcaoCode(String icaoCode); } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java index 194beeb..cd26ebd 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/OperationalBaseRepository.java @@ -3,8 +3,7 @@ import pl.skyroster.skyroster_backend.domain.model.OperationalBase; import java.util.Optional; -import java.util.UUID; public interface OperationalBaseRepository { - Optional findById(UUID id); + Optional findByIcaoCode(String icaoCode); } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java index b7676e3..3e2a7c6 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftController.java @@ -32,8 +32,8 @@ public AircraftController(AddAircraftUseCase addAircraftUseCase, GetAircraftUseC public ResponseEntity addAircraft(@RequestBody AddAircraftRequest request) { Aircraft aircraft = addAircraftUseCase.execute( request.getRegistrationNumber(), - request.getAircraftTypeId(), - request.getOperationalBaseId() + request.getAircraftTypeCode(), + request.getOperationalBaseCode() ); return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(aircraft)); } diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 974814c..2e8006f 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -266,21 +266,21 @@ components: type: object required: - registrationNumber - - aircraftTypeId - - operationalBaseId + - aircraftTypeCode + - operationalBaseCode properties: registrationNumber: type: string description: Aircraft registration number example: "SP-LRA" - aircraftTypeId: + aircraftTypeCode: type: string - format: uuid - description: ID of the aircraft type - operationalBaseId: + description: ICAO aircraft type code + example: "B738" + operationalBaseCode: type: string - format: uuid - description: ID of the operational base + description: ICAO operational base code + example: "EPWA" AircraftTypeInfo: type: object diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java index 628432c..55266ad 100644 --- a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/aircraft/AddAircraftUseCaseTest.java @@ -35,12 +35,12 @@ class AddAircraftUseCaseTest { private AddAircraftUseCase useCase; - private static final UUID TYPE_ID = UUID.randomUUID(); - private static final UUID BASE_ID = UUID.randomUUID(); + private static final String TYPE_CODE = "B738"; + private static final String BASE_CODE = "EPWA"; private static final String REGISTRATION = "SP-LRA"; - private final AircraftType type = new AircraftType(TYPE_ID, "B738", "Boeing 737-800"); - private final OperationalBase base = new OperationalBase(BASE_ID, "EPWA", "Warsaw Chopin"); + private final AircraftType type = new AircraftType(UUID.randomUUID(), TYPE_CODE, "Boeing 737-800"); + private final OperationalBase base = new OperationalBase(UUID.randomUUID(), BASE_CODE, "Warsaw Chopin"); @BeforeEach void setUp() { @@ -50,11 +50,11 @@ void setUp() { @Test void shouldAddAircraft_whenValidData() { when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(false); - when(aircraftTypeRepository.findById(TYPE_ID)).thenReturn(Optional.of(type)); - when(operationalBaseRepository.findById(BASE_ID)).thenReturn(Optional.of(base)); + when(aircraftTypeRepository.findByIcaoCode(TYPE_CODE)).thenReturn(Optional.of(type)); + when(operationalBaseRepository.findByIcaoCode(BASE_CODE)).thenReturn(Optional.of(base)); when(aircraftRepository.save(any(Aircraft.class))).thenAnswer(inv -> inv.getArgument(0)); - Aircraft result = useCase.execute(REGISTRATION, TYPE_ID, BASE_ID); + Aircraft result = useCase.execute(REGISTRATION, TYPE_CODE, BASE_CODE); assertThat(result.getRegistrationNumber()).isEqualTo(REGISTRATION); assertThat(result.getAircraftType()).isEqualTo(type); @@ -67,7 +67,7 @@ void shouldAddAircraft_whenValidData() { void shouldThrow_whenRegistrationAlreadyExists() { when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(true); - assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_ID, BASE_ID)) + assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_CODE, BASE_CODE)) .isInstanceOf(AircraftAlreadyExistsException.class) .hasMessageContaining(REGISTRATION); } @@ -75,25 +75,25 @@ void shouldThrow_whenRegistrationAlreadyExists() { @Test void shouldThrow_whenAircraftTypeNotFound() { when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(false); - when(aircraftTypeRepository.findById(TYPE_ID)).thenReturn(Optional.empty()); + when(aircraftTypeRepository.findByIcaoCode(TYPE_CODE)).thenReturn(Optional.empty()); - assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_ID, BASE_ID)) + assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_CODE, BASE_CODE)) .isInstanceOf(AircraftTypeNotFoundException.class); } @Test void shouldThrow_whenOperationalBaseNotFound() { when(aircraftRepository.existsByRegistrationNumber(REGISTRATION)).thenReturn(false); - when(aircraftTypeRepository.findById(TYPE_ID)).thenReturn(Optional.of(type)); - when(operationalBaseRepository.findById(BASE_ID)).thenReturn(Optional.empty()); + when(aircraftTypeRepository.findByIcaoCode(TYPE_CODE)).thenReturn(Optional.of(type)); + when(operationalBaseRepository.findByIcaoCode(BASE_CODE)).thenReturn(Optional.empty()); - assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_ID, BASE_ID)) + assertThatThrownBy(() -> useCase.execute(REGISTRATION, TYPE_CODE, BASE_CODE)) .isInstanceOf(OperationalBaseNotFoundException.class); } @Test void shouldThrow_whenRegistrationIsBlank() { - assertThatThrownBy(() -> useCase.execute("", TYPE_ID, BASE_ID)) + assertThatThrownBy(() -> useCase.execute("", TYPE_CODE, BASE_CODE)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Registration number"); } diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java index cddb624..94aeb6d 100644 --- a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/AircraftIntegrationTest.java @@ -36,20 +36,16 @@ class AircraftIntegrationTest { private static final ObjectMapper objectMapper = new ObjectMapper(); - // Seed UUIDs from V2 migration - private static final String TYPE_B738_ID = "b0000000-0000-0000-0000-000000000001"; - private static final String BASE_EPWA_ID = "a0000000-0000-0000-0000-000000000001"; - @Test void addAircraft_shouldReturn201_whenAdminWithValidData() throws Exception { String token = getToken("admin", "test1234"); String body = """ { "registrationNumber": "SP-LRA", - "aircraftTypeId": "%s", - "operationalBaseId": "%s" + "aircraftTypeCode": "B738", + "operationalBaseCode": "EPWA" } - """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + """; mockMvc.perform(post("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) @@ -68,19 +64,17 @@ void addAircraft_shouldReturn409_whenDuplicateRegistration() throws Exception { String body = """ { "registrationNumber": "SP-DUP", - "aircraftTypeId": "%s", - "operationalBaseId": "%s" + "aircraftTypeCode": "B738", + "operationalBaseCode": "EPWA" } - """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + """; - // First request — should succeed mockMvc.perform(post("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isCreated()); - // Second request — should conflict mockMvc.perform(post("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) @@ -95,10 +89,10 @@ void addAircraft_shouldReturn400_whenAircraftTypeNotFound() throws Exception { String body = """ { "registrationNumber": "SP-BAD", - "aircraftTypeId": "00000000-0000-0000-0000-000000000099", - "operationalBaseId": "%s" + "aircraftTypeCode": "XXXX", + "operationalBaseCode": "EPWA" } - """.formatted(BASE_EPWA_ID); + """; mockMvc.perform(post("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) @@ -114,10 +108,10 @@ void addAircraft_shouldReturn400_whenOperationalBaseNotFound() throws Exception String body = """ { "registrationNumber": "SP-BAD2", - "aircraftTypeId": "%s", - "operationalBaseId": "00000000-0000-0000-0000-000000000099" + "aircraftTypeCode": "B738", + "operationalBaseCode": "XXXX" } - """.formatted(TYPE_B738_ID); + """; mockMvc.perform(post("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) @@ -132,10 +126,10 @@ void addAircraft_shouldReturn401_whenNoToken() throws Exception { String body = """ { "registrationNumber": "SP-NO", - "aircraftTypeId": "%s", - "operationalBaseId": "%s" + "aircraftTypeCode": "B738", + "operationalBaseCode": "EPWA" } - """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + """; mockMvc.perform(post("/api/aircraft") .contentType(MediaType.APPLICATION_JSON) @@ -149,10 +143,10 @@ void addAircraft_shouldReturn403_whenNotAdmin() throws Exception { String body = """ { "registrationNumber": "SP-NOPERM", - "aircraftTypeId": "%s", - "operationalBaseId": "%s" + "aircraftTypeCode": "B738", + "operationalBaseCode": "EPWA" } - """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + """; mockMvc.perform(post("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) @@ -165,14 +159,13 @@ void addAircraft_shouldReturn403_whenNotAdmin() throws Exception { void getAircraft_shouldReturnFleetList() throws Exception { String token = getToken("admin", "test1234"); - // Add an aircraft first String body = """ { "registrationNumber": "SP-GET", - "aircraftTypeId": "%s", - "operationalBaseId": "%s" + "aircraftTypeCode": "A320", + "operationalBaseCode": "EPKK" } - """.formatted(TYPE_B738_ID, BASE_EPWA_ID); + """; mockMvc.perform(post("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) @@ -180,7 +173,6 @@ void getAircraft_shouldReturnFleetList() throws Exception { .content(body)) .andExpect(status().isCreated()); - // Get fleet list mockMvc.perform(get("/api/aircraft") .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) .andExpect(status().isOk())