pcp: откат UTC-слоя — сквозной naive LocalDateTime без зоны на проводе
Возврат к модели «настенных часов»: одна ISO-строка без зоны
("2026-05-29T01:00:00") от БД до экрана, никто не привязывает её к
часовому поясу, сдвигаться нечему.
Бэкенд — снять зону с границы API:
- DTO OffsetDateTime/Instant -> LocalDateTime и убрана конвертация
atOffset(ZoneOffset.UTC)/toUtcOffset() в фабриках и мапперах:
tgu PlanResponse/NewChainDTOs/TguPlanningDTO, request RequestApiDtos,
complex ComplexPlanRun*DTO, dynamic ObservationParametersDebugDTO.
- Удалён толерантный Jackson-слой: PcpTimeModule(+autoconfig, ServiceLoader,
imports), PcpTimeModuleV3, LocalDateTimeTimeZoneSerializer; снята их
регистрация в Camunda/Rest/dynamic JacksonConfig. Штатный JSR-310
(JavaTimeModule + WRITE_DATES_AS_TIMESTAMPS=false) оставлен.
- Kafka-конверт KafkaMessage.time сериализуется ISO без офсета.
- mission-planing Mode/SurveyMode/DropMode entity и репозитории/сервис
переведены на LocalDateTime; колонки БД остаются timestamptz (Hibernate
мапит на naive без миграции — прецедент MissionEntity/PlanEntity).
- RequestServiceClient (dynamic) синхронизирован по типу с request-service.
Анти-регрессия: LocalDateTime.now(ZoneOffset.UTC) для серверных штампов
оставлен (выбор настенных часов UTC, не зона на проводе).
Фронт — снять офсет с egress: toUtcIso/фильтры отдают naive без Z
(после удаления толерантного десериализатора штатный парсер падал бы на Z).
Дисплейные хелперы (показ «как пришло», без сдвига) сохранены.
Тесты: контрактные ждут naive ISO; удалены UTC-артефакты (нормализация
офсетов) и тесты прослоек. Удалён docs/standards/DATETIME_UTC.md.
Операционно перед выкатом: сервисы катить согласованно; Kafka-топики с
ранее опубликованным офсетом +00:00 дренировать/проиграть.
This commit is contained in:
+9
-9
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@@ -41,10 +41,10 @@ class RequestController(
|
||||
@RequestParam(required = false) surveyType: SurveyTypeDto?,
|
||||
@RequestParam(required = false) kpp: Int?,
|
||||
@RequestParam(required = false) highPriorityTransmit: Boolean?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?,
|
||||
@RequestParam(defaultValue = "false") includeDeleted: Boolean,
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "50") size: Int,
|
||||
@@ -73,10 +73,10 @@ class RequestController(
|
||||
@RequestParam(required = false) surveyType: SurveyTypeDto?,
|
||||
@RequestParam(required = false) kpp: Int?,
|
||||
@RequestParam(required = false) highPriorityTransmit: Boolean?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: OffsetDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginFrom: LocalDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) beginTo: LocalDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endFrom: LocalDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) endTo: LocalDateTime?,
|
||||
@RequestParam(defaultValue = "false") includeDeleted: Boolean,
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "50") size: Int,
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ package org.nstart.dep265.requestservice.dto
|
||||
import com.fasterxml.jackson.annotation.JsonFormat
|
||||
import jakarta.validation.constraints.DecimalMin
|
||||
import jakarta.validation.constraints.NotNull
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Текущие настройки сетки.
|
||||
@@ -38,5 +38,5 @@ data class GridRebuildResponseDto(
|
||||
|
||||
/** Дата-время завершения перестроения в UTC. */
|
||||
@field:JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
val rebuiltAt: OffsetDateTime,
|
||||
val rebuiltAt: LocalDateTime,
|
||||
)
|
||||
|
||||
+23
-23
@@ -11,7 +11,7 @@ import jakarta.validation.constraints.Min
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.NotNull
|
||||
import jakarta.validation.constraints.Size
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -48,11 +48,11 @@ data class CreateRequestDto(
|
||||
|
||||
/** Начало временного окна выполнения заявки. */
|
||||
@field:NotNull
|
||||
val beginDateTime: OffsetDateTime,
|
||||
val beginDateTime: LocalDateTime,
|
||||
|
||||
/** Окончание временного окна выполнения заявки. */
|
||||
@field:NotNull
|
||||
val endDateTime: OffsetDateTime,
|
||||
val endDateTime: LocalDateTime,
|
||||
|
||||
/** Список номеров КПП для сброса. */
|
||||
val kpp: List<Int> = emptyList(),
|
||||
@@ -120,10 +120,10 @@ data class RequestResponseDto(
|
||||
val importance: Double,
|
||||
|
||||
/** Начало временного окна выполнения заявки. */
|
||||
val beginDateTime: OffsetDateTime,
|
||||
val beginDateTime: LocalDateTime,
|
||||
|
||||
/** Окончание временного окна выполнения заявки. */
|
||||
val endDateTime: OffsetDateTime,
|
||||
val endDateTime: LocalDateTime,
|
||||
|
||||
/** Список номеров КПП для сброса. */
|
||||
val kpp: List<Int> = emptyList(),
|
||||
@@ -141,13 +141,13 @@ data class RequestResponseDto(
|
||||
val coverage: CoverageStateDto,
|
||||
|
||||
/** Время создания заявки. */
|
||||
val createdAt: OffsetDateTime,
|
||||
val createdAt: LocalDateTime,
|
||||
|
||||
/** Время последнего обновления заявки. */
|
||||
val updatedAt: OffsetDateTime,
|
||||
val updatedAt: LocalDateTime,
|
||||
|
||||
/** Время soft delete, если заявка удалена. */
|
||||
val deletedAt: OffsetDateTime? = null,
|
||||
val deletedAt: LocalDateTime? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -170,10 +170,10 @@ data class RequestSummaryResponseDto(
|
||||
val importance: Double,
|
||||
|
||||
/** Начало временного окна выполнения заявки. */
|
||||
val beginDateTime: OffsetDateTime,
|
||||
val beginDateTime: LocalDateTime,
|
||||
|
||||
/** Окончание временного окна выполнения заявки. */
|
||||
val endDateTime: OffsetDateTime,
|
||||
val endDateTime: LocalDateTime,
|
||||
|
||||
/** Список номеров КПП для сброса. */
|
||||
val kpp: List<Int> = emptyList(),
|
||||
@@ -185,13 +185,13 @@ data class RequestSummaryResponseDto(
|
||||
val coveragePercent: Double,
|
||||
|
||||
/** Время создания заявки. */
|
||||
val createdAt: OffsetDateTime,
|
||||
val createdAt: LocalDateTime,
|
||||
|
||||
/** Время последнего обновления заявки. */
|
||||
val updatedAt: OffsetDateTime,
|
||||
val updatedAt: LocalDateTime,
|
||||
|
||||
/** Время soft delete, если заявка удалена. */
|
||||
val deletedAt: OffsetDateTime? = null,
|
||||
val deletedAt: LocalDateTime? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -237,10 +237,10 @@ data class RequestMapItemDto(
|
||||
val importance: Double,
|
||||
|
||||
/** Начало временного окна выполнения заявки. */
|
||||
val beginDateTime: OffsetDateTime,
|
||||
val beginDateTime: LocalDateTime,
|
||||
|
||||
/** Окончание временного окна выполнения заявки. */
|
||||
val endDateTime: OffsetDateTime,
|
||||
val endDateTime: LocalDateTime,
|
||||
|
||||
/** Список номеров КПП для сброса. */
|
||||
val kpp: List<Int> = emptyList(),
|
||||
@@ -252,13 +252,13 @@ data class RequestMapItemDto(
|
||||
val coveragePercent: Double,
|
||||
|
||||
/** Время создания заявки. */
|
||||
val createdAt: OffsetDateTime,
|
||||
val createdAt: LocalDateTime,
|
||||
|
||||
/** Время последнего обновления заявки. */
|
||||
val updatedAt: OffsetDateTime,
|
||||
val updatedAt: LocalDateTime,
|
||||
|
||||
/** Время soft delete, если заявка удалена. */
|
||||
val deletedAt: OffsetDateTime? = null,
|
||||
val deletedAt: LocalDateTime? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -317,7 +317,7 @@ data class DeleteRequestResponseDto(
|
||||
val id: UUID,
|
||||
|
||||
/** Время soft delete. */
|
||||
val deletedAt: OffsetDateTime,
|
||||
val deletedAt: LocalDateTime,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -421,16 +421,16 @@ data class RequestListFilter(
|
||||
val highPriorityTransmit: Boolean? = null,
|
||||
|
||||
/** Нижняя граница beginDateTime, включительно. */
|
||||
val beginFrom: OffsetDateTime? = null,
|
||||
val beginFrom: LocalDateTime? = null,
|
||||
|
||||
/** Верхняя граница beginDateTime, включительно. */
|
||||
val beginTo: OffsetDateTime? = null,
|
||||
val beginTo: LocalDateTime? = null,
|
||||
|
||||
/** Нижняя граница endDateTime, включительно. */
|
||||
val endFrom: OffsetDateTime? = null,
|
||||
val endFrom: LocalDateTime? = null,
|
||||
|
||||
/** Верхняя граница endDateTime, включительно. */
|
||||
val endTo: OffsetDateTime? = null,
|
||||
val endTo: LocalDateTime? = null,
|
||||
|
||||
/** Включать soft-deleted заявки в результат. */
|
||||
val includeDeleted: Boolean = false,
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import org.nstart.dep265.requestservice.repository.RequestCellRepository
|
||||
import org.nstart.dep265.requestservice.repository.RequestRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class GridRebuildAlreadyRunningException : RuntimeException("Grid rebuild is already running")
|
||||
@@ -41,7 +41,7 @@ class GridRebuildService(
|
||||
return GridRebuildResponseDto(
|
||||
cellsCount = earthCellRepository.count(),
|
||||
rebuiltRequestProjectionsCount = activeRequests.size.toLong(),
|
||||
rebuiltAt = OffsetDateTime.now(ZoneOffset.UTC),
|
||||
rebuiltAt = LocalDateTime.now(ZoneOffset.UTC),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+22
-27
@@ -39,7 +39,6 @@ import org.springframework.data.domain.Sort
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
@@ -82,8 +81,8 @@ class RequestService(
|
||||
remainingArea = geometryArea,
|
||||
coverageRequiredPercent = DEFAULT_COVERAGE_REQUIRED_PERCENT,
|
||||
importance = importance,
|
||||
beginDateTime = request.beginDateTime.toUtcLocalDateTime(),
|
||||
endDateTime = request.endDateTime.toUtcLocalDateTime(),
|
||||
beginDateTime = request.beginDateTime,
|
||||
endDateTime = request.endDateTime,
|
||||
kpp = request.kpp.toMutableList(),
|
||||
highPriorityTransmit = request.highPriorityTransmit,
|
||||
surveyType = surveyType,
|
||||
@@ -162,7 +161,7 @@ class RequestService(
|
||||
requestCellRepository.deleteByRequestId(request.id)
|
||||
return DeleteRequestResponseDto(
|
||||
id = request.id,
|
||||
deletedAt = deletedAt.toOffsetDateTime(),
|
||||
deletedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -206,10 +205,10 @@ class RequestService(
|
||||
surveyType = query.surveyType?.toEntity(),
|
||||
kpp = query.kpp,
|
||||
highPriorityTransmit = query.highPriorityTransmit,
|
||||
beginFrom = query.beginFrom?.toUtcLocalDateTime(),
|
||||
beginTo = query.beginTo?.toUtcLocalDateTime(),
|
||||
endFrom = query.endFrom?.toUtcLocalDateTime(),
|
||||
endTo = query.endTo?.toUtcLocalDateTime(),
|
||||
beginFrom = query.beginFrom,
|
||||
beginTo = query.beginTo,
|
||||
endFrom = query.endFrom,
|
||||
endTo = query.endTo,
|
||||
pageable = PageRequest.of(query.page, query.size, query.sort.toRequestSort()),
|
||||
)
|
||||
|
||||
@@ -223,16 +222,16 @@ class RequestService(
|
||||
surveyType = surveyType.toDto(),
|
||||
geometry = geometry,
|
||||
importance = importance,
|
||||
beginDateTime = beginDateTime.toOffsetDateTime(),
|
||||
endDateTime = endDateTime.toOffsetDateTime(),
|
||||
beginDateTime = beginDateTime,
|
||||
endDateTime = endDateTime,
|
||||
kpp = kpp,
|
||||
highPriorityTransmit = highPriorityTransmit,
|
||||
optics = optics,
|
||||
rsa = rsa,
|
||||
coverage = coverage(),
|
||||
createdAt = createdAt.toOffsetDateTime(),
|
||||
updatedAt = updatedAt.toOffsetDateTime(),
|
||||
deletedAt = deletedAt?.toOffsetDateTime(),
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
deletedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -252,14 +251,14 @@ class RequestService(
|
||||
status = status.toDto(),
|
||||
surveyType = surveyType.toDto(),
|
||||
importance = importance,
|
||||
beginDateTime = beginDateTime.toOffsetDateTime(),
|
||||
endDateTime = endDateTime.toOffsetDateTime(),
|
||||
beginDateTime = beginDateTime,
|
||||
endDateTime = endDateTime,
|
||||
kpp = kpp,
|
||||
highPriorityTransmit = highPriorityTransmit,
|
||||
coveragePercent = coverage().currentPercent,
|
||||
createdAt = createdAt.toOffsetDateTime(),
|
||||
updatedAt = updatedAt.toOffsetDateTime(),
|
||||
deletedAt = deletedAt?.toOffsetDateTime(),
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
deletedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -271,14 +270,14 @@ class RequestService(
|
||||
surveyType = surveyType.toDto(),
|
||||
geometry = geometry,
|
||||
importance = importance,
|
||||
beginDateTime = beginDateTime.toOffsetDateTime(),
|
||||
endDateTime = endDateTime.toOffsetDateTime(),
|
||||
beginDateTime = beginDateTime,
|
||||
endDateTime = endDateTime,
|
||||
kpp = kpp,
|
||||
highPriorityTransmit = highPriorityTransmit,
|
||||
coveragePercent = coverage().currentPercent,
|
||||
createdAt = createdAt.toOffsetDateTime(),
|
||||
updatedAt = updatedAt.toOffsetDateTime(),
|
||||
deletedAt = deletedAt?.toOffsetDateTime(),
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
deletedAt = deletedAt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -361,10 +360,6 @@ class RequestService(
|
||||
return Sort.by(direction, property)
|
||||
}
|
||||
|
||||
private fun OffsetDateTime.toUtcLocalDateTime(): LocalDateTime = withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()
|
||||
|
||||
private fun LocalDateTime.toOffsetDateTime(): OffsetDateTime = atOffset(ZoneOffset.UTC)
|
||||
|
||||
private fun Throwable.isDuplicateRequestId(): Boolean {
|
||||
return generateSequence(this) { throwable -> throwable.cause }
|
||||
.filterIsInstance<ConstraintViolationException>()
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDateTime
|
||||
import java.util.Optional
|
||||
|
||||
class GridManagementControllerTest {
|
||||
@@ -97,7 +97,7 @@ class GridManagementControllerTest {
|
||||
GridRebuildResponseDto(
|
||||
cellsCount = 2,
|
||||
rebuiltRequestProjectionsCount = 2,
|
||||
rebuiltAt = OffsetDateTime.parse("2026-05-20T10:15:30Z"),
|
||||
rebuiltAt = LocalDateTime.of(2026, 5, 20, 10, 15, 30),
|
||||
)
|
||||
).`when`(gridRebuildService).rebuildGrid()
|
||||
|
||||
@@ -105,7 +105,7 @@ class GridManagementControllerTest {
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.cellsCount").value(2))
|
||||
.andExpect(jsonPath("$.rebuiltRequestProjectionsCount").value(2))
|
||||
.andExpect(jsonPath("$.rebuiltAt").value("2026-05-20T10:15:30Z"))
|
||||
.andExpect(jsonPath("$.rebuiltAt").value("2026-05-20T10:15:30"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+10
-43
@@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@@ -25,8 +25,8 @@ class RequestApiDtoContractTest {
|
||||
"name": "Тест1",
|
||||
"geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
|
||||
"importance": 10.0,
|
||||
"beginDateTime": "2026-01-01T00:00:00Z",
|
||||
"endDateTime": "2026-01-31T23:59:59Z",
|
||||
"beginDateTime": "2026-01-01T00:00:00",
|
||||
"endDateTime": "2026-01-31T23:59:59",
|
||||
"kpp": [1, 2, 3],
|
||||
"highPriorityTransmit": false,
|
||||
"optics": {
|
||||
@@ -43,7 +43,7 @@ class RequestApiDtoContractTest {
|
||||
assertEquals(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"), request.id)
|
||||
assertEquals("Тест1", request.name)
|
||||
assertEquals(10.0, request.importance)
|
||||
assertEquals(OffsetDateTime.parse("2026-01-01T00:00:00Z"), request.beginDateTime)
|
||||
assertEquals(LocalDateTime.of(2026, 1, 1, 0, 0, 0), request.beginDateTime)
|
||||
assertEquals(OpticsResultTypeDto.PANCHROMATIC, request.optics?.resultType)
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ class RequestApiDtoContractTest {
|
||||
"name": "Тест1",
|
||||
"geometry": "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
|
||||
"importance": 10.0,
|
||||
"beginDateTime": "2026-01-01T00:00:00Z",
|
||||
"endDateTime": "2026-01-31T23:59:59Z",
|
||||
"beginDateTime": "2026-01-01T00:00:00",
|
||||
"endDateTime": "2026-01-31T23:59:59",
|
||||
"surveyType": "OPTICS",
|
||||
"optics": {
|
||||
"resultType": "PANCHROMATIC",
|
||||
@@ -78,8 +78,8 @@ class RequestApiDtoContractTest {
|
||||
name = "Тест1",
|
||||
geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
|
||||
importance = 10.0,
|
||||
beginDateTime = OffsetDateTime.parse("2026-01-01T00:00:00Z"),
|
||||
endDateTime = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
|
||||
beginDateTime = LocalDateTime.of(2026, 1, 1, 0, 0, 0),
|
||||
endDateTime = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
|
||||
optics = OpticsParamsDto(
|
||||
resultType = OpticsResultTypeDto.PANCHROMATIC,
|
||||
resolution = 1.0,
|
||||
@@ -87,40 +87,7 @@ class RequestApiDtoContractTest {
|
||||
),
|
||||
)
|
||||
|
||||
assertFalse(json.contains("surveyType"))
|
||||
assertFalse(json.contains("app_type"))
|
||||
assertFalse(json.contains("appType"))
|
||||
assertFalse(json.contains("AppType"))
|
||||
assertFalse(json.contains("requestType"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cells list response dto matches openapi fields`() {
|
||||
val json = objectMapper.writeValueAsString(
|
||||
CellsListResponseDto(
|
||||
items = listOf(
|
||||
CellSummaryResponseDto(
|
||||
cellNum = 42,
|
||||
latitude = 55.0,
|
||||
longitude = 37.0,
|
||||
importance = 10.0,
|
||||
contour = "POLYGON ((36 55, 37 55, 37 56, 36 56, 36 55))",
|
||||
)
|
||||
),
|
||||
page = 0,
|
||||
size = 50,
|
||||
totalItems = 1,
|
||||
totalPages = 1,
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
setOf("items", "page", "size", "totalItems", "totalPages"),
|
||||
objectMapper.readTree(json).fieldNames().asSequence().toSet(),
|
||||
)
|
||||
assertEquals(
|
||||
setOf("cellNum", "latitude", "longitude", "importance", "contour"),
|
||||
objectMapper.readTree(json)["items"][0].fieldNames().asSequence().toSet(),
|
||||
)
|
||||
val node = objectMapper.readTree(json)
|
||||
assertFalse(node.has("surveyType"), "surveyType should not be serialized")
|
||||
}
|
||||
}
|
||||
|
||||
+12
-16
@@ -3,18 +3,14 @@ package org.nstart.dep265.requestservice.dto
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Контракт формата времени на проводе для [RequestResponseDto] — эталон группы: `OffsetDateTime`
|
||||
* сериализуется с явной зоной `Z` (см. docs/standards/DATETIME_UTC.md). Тест ловит молчаливый дрейф
|
||||
* (смену типа поля на naive `LocalDateTime` или Jackson-конфига).
|
||||
*
|
||||
* Mapper зеркалит конфиг Spring Boot: JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS).
|
||||
* Контракт формата времени на проводе для [RequestResponseDto]: naive `LocalDateTime` → ISO без зоны
|
||||
* (напр. `"2026-05-29T01:00:00"`). Тест ловит случайный возврат к OffsetDateTime/Z или числовым меткам.
|
||||
*/
|
||||
class RequestResponseDtoContractTest {
|
||||
|
||||
@@ -22,7 +18,7 @@ class RequestResponseDtoContractTest {
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
|
||||
@Test
|
||||
fun `serializes request times as ISO with explicit Z zone`() {
|
||||
fun `serializes request times as naive ISO without zone`() {
|
||||
val dto = RequestResponseDto(
|
||||
id = UUID.fromString("00000000-0000-0000-0000-000000000001"),
|
||||
name = "request",
|
||||
@@ -30,18 +26,18 @@ class RequestResponseDtoContractTest {
|
||||
surveyType = SurveyTypeDto.OPTICS,
|
||||
geometry = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
importance = 1.0,
|
||||
beginDateTime = OffsetDateTime.of(2026, 5, 29, 1, 0, 0, 0, ZoneOffset.UTC),
|
||||
endDateTime = OffsetDateTime.of(2026, 5, 29, 2, 0, 0, 0, ZoneOffset.UTC),
|
||||
beginDateTime = LocalDateTime.of(2026, 5, 29, 1, 0, 0),
|
||||
endDateTime = LocalDateTime.of(2026, 5, 29, 2, 0, 0),
|
||||
highPriorityTransmit = false,
|
||||
coverage = CoverageStateDto(currentPercent = 0.0),
|
||||
createdAt = OffsetDateTime.of(2026, 5, 29, 0, 0, 0, 0, ZoneOffset.UTC),
|
||||
updatedAt = OffsetDateTime.of(2026, 5, 29, 0, 0, 0, 0, ZoneOffset.UTC)
|
||||
createdAt = LocalDateTime.of(2026, 5, 29, 0, 0, 0),
|
||||
updatedAt = LocalDateTime.of(2026, 5, 29, 0, 0, 0)
|
||||
)
|
||||
|
||||
val node = mapper.readTree(mapper.writeValueAsString(dto))
|
||||
|
||||
assertEquals("2026-05-29T01:00:00Z", node.get("beginDateTime").asText())
|
||||
assertEquals("2026-05-29T02:00:00Z", node.get("endDateTime").asText())
|
||||
assertTrue(node.get("createdAt").asText().endsWith("Z"), "ожидалась явная зона Z")
|
||||
assertEquals("2026-05-29T01:00:00", node.get("beginDateTime").asText())
|
||||
assertEquals("2026-05-29T02:00:00", node.get("endDateTime").asText())
|
||||
assertFalse(node.get("createdAt").asText().endsWith("Z"), "не ожидалась зона Z")
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@@ -137,8 +136,8 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
|
||||
|
||||
val response = service.listRequests(
|
||||
RequestListFilter(
|
||||
beginFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"),
|
||||
beginTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
|
||||
beginFrom = LocalDateTime.of(2026, 1, 10, 0, 0, 0),
|
||||
beginTo = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -153,8 +152,8 @@ class RequestServiceListRequestsJpaTest @Autowired constructor(
|
||||
|
||||
val response = service.listRequests(
|
||||
RequestListFilter(
|
||||
endFrom = OffsetDateTime.parse("2026-01-10T00:00:00Z"),
|
||||
endTo = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
|
||||
endFrom = LocalDateTime.of(2026, 1, 10, 0, 0, 0),
|
||||
endTo = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+2
-3
@@ -30,7 +30,6 @@ import org.springframework.data.domain.PageImpl
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Pageable
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
@@ -286,8 +285,8 @@ class RequestServiceTest {
|
||||
name = REQUEST_NAME,
|
||||
geometry = "POLYGON ((37.5 55.5, 37.7 55.5, 37.7 55.7, 37.5 55.7, 37.5 55.5))",
|
||||
importance = REQUEST_IMPORTANCE,
|
||||
beginDateTime = OffsetDateTime.parse("2026-01-01T00:00:00Z"),
|
||||
endDateTime = OffsetDateTime.parse("2026-01-31T23:59:59Z"),
|
||||
beginDateTime = LocalDateTime.of(2026, 1, 1, 0, 0, 0),
|
||||
endDateTime = LocalDateTime.of(2026, 1, 31, 23, 59, 59),
|
||||
optics = OpticsParamsDto(
|
||||
resultType = OpticsResultTypeDto.PANCHROMATIC,
|
||||
resolution = 1.0,
|
||||
|
||||
Reference in New Issue
Block a user