Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Rule> execute() {
return ruleRepository.findAll();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pl.skyroster.skyroster_backend.domain.model;

public enum RulePeriod {
DAY,
WEEK,
MONTH
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pl.skyroster.skyroster_backend.domain.model;

public enum RuleType {
MAX_WORK_TIME,
MIN_REST_TIME,
MIN_FLIGHT_TIME
}
Original file line number Diff line number Diff line change
@@ -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<Rule> findAll();
}
Original file line number Diff line number Diff line change
@@ -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<List<RuleResponse>> getRules() {
List<RuleResponse> response = getRulesUseCase.execute().stream()
.map(RuleResponseMapper::map)
.toList();
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -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<Rule, UUID>, RuleRepository {
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
60 changes: 60 additions & 0 deletions skyroster-backend/src/main/resources/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}
Loading