diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java new file mode 100644 index 0000000..2909a23 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java @@ -0,0 +1,23 @@ +package pl.skyroster.skyroster_backend.application.rule; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.List; + +@Service +public class GetRulesUseCase { + + private final RuleRepository ruleRepository; + + public GetRulesUseCase(RuleRepository ruleRepository) { + this.ruleRepository = ruleRepository; + } + + @Transactional(readOnly = true) + public List execute() { + return ruleRepository.findAll(); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java new file mode 100644 index 0000000..36ee6ec --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java @@ -0,0 +1,41 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Table(name = "rules") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Rule { + + @Id + private UUID id; + + @Column(nullable = false, length = 100) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private RuleType type; + + @Column(nullable = false) + private Integer value; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private RulePeriod period; + + @Column(length = 500) + private String description; +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java new file mode 100644 index 0000000..0ea8f93 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.model; + +public enum RulePeriod { + DAY, + WEEK, + MONTH +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java new file mode 100644 index 0000000..7fa13e3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.model; + +public enum RuleType { + MAX_WORK_TIME, + MIN_REST_TIME, + MIN_FLIGHT_TIME +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java new file mode 100644 index 0000000..b5a45d0 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import pl.skyroster.skyroster_backend.domain.model.Rule; + +import java.util.List; + +public interface RuleRepository { + List findAll(); +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java new file mode 100644 index 0000000..a4c8217 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java @@ -0,0 +1,27 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.rule.GetRulesUseCase; +import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.RuleResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.RuleResponseMapper; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +public class RuleController { + + private final GetRulesUseCase getRulesUseCase; + + @GetMapping(ApiApi.PATH_GET_RULES) + public ResponseEntity> getRules() { + List response = getRulesUseCase.execute().stream() + .map(RuleResponseMapper::map) + .toList(); + return ResponseEntity.ok(response); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java new file mode 100644 index 0000000..23fdc7d --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.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.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.UUID; + +public interface JpaRuleRepository extends JpaRepository, RuleRepository { +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java new file mode 100644 index 0000000..6377711 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java @@ -0,0 +1,18 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.generated.model.RulePeriod; +import pl.skyroster.skyroster_backend.generated.model.RuleResponse; +import pl.skyroster.skyroster_backend.generated.model.RuleType; + +public class RuleResponseMapper { + public static RuleResponse map(Rule rule) { + return new RuleResponse( + rule.getId(), + rule.getName(), + RuleType.valueOf(rule.getType().name()), + rule.getValue(), + RulePeriod.valueOf(rule.getPeriod().name()) + ).description(rule.getDescription()); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index cb1c775..474b1c3 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -248,6 +248,27 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/rules: + get: + operationId: getRules + tags: [ Rules ] + summary: List operational rules + responses: + '200': + description: List of operational rules + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RuleResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/flights: get: operationId: displayFlights @@ -793,6 +814,45 @@ components: operationalBase: $ref: '#/components/schemas/OperationalBaseInfo' + RuleType: + type: string + enum: + - MAX_WORK_TIME + - MIN_REST_TIME + - MIN_FLIGHT_TIME + + RulePeriod: + type: string + enum: + - DAY + - WEEK + - MONTH + + RuleResponse: + type: object + required: + - id + - name + - type + - value + - period + properties: + id: + type: string + format: uuid + name: + type: string + type: + $ref: '#/components/schemas/RuleType' + value: + type: integer + minimum: 1 + description: Value in hours + period: + $ref: '#/components/schemas/RulePeriod' + description: + type: string + FlightResponse: type: object required: diff --git a/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql b/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql new file mode 100644 index 0000000..e49fa0f --- /dev/null +++ b/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql @@ -0,0 +1,22 @@ +CREATE TABLE rules ( + id UUID PRIMARY KEY, + name VARCHAR(100) NOT NULL, + type VARCHAR(30) NOT NULL, + value INTEGER NOT NULL CHECK (value > 0), + period VARCHAR(10) NOT NULL, + description VARCHAR(500) +); + +INSERT INTO rules (id, name, type, value, period, description) VALUES + ('11111111-0000-0000-0000-000000000001', 'Maksymalny czas pracy miesięczny', + 'MAX_WORK_TIME', 100, 'MONTH', + 'Pilot nie może przekroczyć 100 godzin pracy w miesiącu'), + ('11111111-0000-0000-0000-000000000002', 'Maksymalny czas pracy dzienny', + 'MAX_WORK_TIME', 10, 'DAY', + 'Pilot nie może przekroczyć 10 godzin pracy dziennie'), + ('11111111-0000-0000-0000-000000000003', 'Minimalny odpoczynek między lotami', + 'MIN_REST_TIME', 12, 'DAY', + 'Wymagany minimalny odpoczynek 12 godzin między lotami'), + ('11111111-0000-0000-0000-000000000004', 'Minimalny nalot miesięczny', + 'MIN_FLIGHT_TIME', 10, 'MONTH', + 'Pilot musi wykonać minimum 10 godzin nalotu miesięcznie'); diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java new file mode 100644 index 0000000..2734b6c --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java @@ -0,0 +1,91 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dasniko.testcontainers.keycloak.KeycloakContainer; +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.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.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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 RuleIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private KeycloakContainer keycloak; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void getRules_shouldReturn401_whenNoToken() throws Exception { + mockMvc.perform(get("/api/rules")) + .andExpect(status().isUnauthorized()); + } + + @Test + void getRules_shouldReturnSeededRules_whenAuthenticated() throws Exception { + String token = getToken("admin", "test1234"); + + mockMvc.perform(get("/api/rules") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$", hasSize(4))) + .andExpect(jsonPath( + "$[?(@.name == 'Maksymalny czas pracy miesięczny' && @.type == 'MAX_WORK_TIME' && @.value == 100 && @.period == 'MONTH')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Maksymalny czas pracy dzienny' && @.type == 'MAX_WORK_TIME' && @.value == 10 && @.period == 'DAY')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Minimalny odpoczynek między lotami' && @.type == 'MIN_REST_TIME' && @.value == 12 && @.period == 'DAY')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Minimalny nalot miesięczny' && @.type == 'MIN_FLIGHT_TIME' && @.value == 10 && @.period == 'MONTH')]") + .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(); + } +}