From 3654f9144d20242245cf99969e1347ccb0e989c8 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 15:52:00 +0700 Subject: [PATCH 01/22] =?UTF-8?q?tests/qg-xxx:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20ics-=D0=BA=D0=B0=D0=BB=D0=B5=D0=BD=D0=B4=D0=B0=D1=80?= =?UTF-8?q?=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ранее ZonedDateTime передавался напрямую в VEvent, из-за чего ical4j нормализовывал время через Instant и свои tz-правила (с устаревшей базой - +06 вместо +07) и писал. Что приводило к сдвигу на −1 час. Плюс при парсинге событий он так же использовал свою базу с +06. Для фикса пришлось: 1. руками формировать VEvent с корректными локальной датой-временем и таймзоной (чтобы при сериализации, ical не терял час) 2. добавить в календарь секцию VTimeZone сформиорванную на базе актальных на данный момент правил, чтобы при парсинге ical использовал её и так же не трял час. --- .../i9ns/calendars/ical/ICalCalendarsRepo.kt | 4 +- .../ical/ICalCalendarsObjectMother.kt | 50 ++++++++++++++----- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/ICalCalendarsRepo.kt b/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/ICalCalendarsRepo.kt index 02242cb5..8e904a77 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/ICalCalendarsRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/ICalCalendarsRepo.kt @@ -50,9 +50,7 @@ class ICalCalendarsRepo( eventId: ICalEventId ): CalendarItem? { return iCalCalendarsDao.findAllByOwner(therapistRef) - .asSequence() - .mapNotNull { it.findById(eventId) } - .firstOrNull() + .firstNotNullOfOrNull { it.findById(eventId) } ?.toICalCalendarItem() } diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/calendars/ical/ICalCalendarsObjectMother.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/calendars/ical/ICalCalendarsObjectMother.kt index 3d4b6e5f..708bb9a0 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/calendars/ical/ICalCalendarsObjectMother.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/calendars/ical/ICalCalendarsObjectMother.kt @@ -3,11 +3,10 @@ package pro.qyoga.tests.fixture.object_mothers.calendars.ical import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.component.CalendarComponent +import net.fortuna.ical4j.model.component.Standard import net.fortuna.ical4j.model.component.VEvent -import net.fortuna.ical4j.model.property.Description -import net.fortuna.ical4j.model.property.Location -import net.fortuna.ical4j.model.property.RecurrenceId -import net.fortuna.ical4j.model.property.Uid +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.property.* import pro.azhidkov.platform.kotlin.ifNotNull import pro.qyoga.i9ns.calendars.ical.model.ICalCalendar import pro.qyoga.i9ns.calendars.ical.model.ICalZonedCalendarItem @@ -16,7 +15,10 @@ import pro.qyoga.tests.fixture.object_mothers.calendars.CalendarsObjectMother import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_REF import java.io.StringWriter import java.net.URI +import java.time.ZoneId +import java.time.ZonedDateTime import java.time.temporal.Temporal +import net.fortuna.ical4j.model.parameter.TzId as TzIdParam object ICalCalendarsObjectMother { @@ -43,16 +45,40 @@ object ICalCalendarsObjectMother { .withDefaults() .fluentTarget - calendar.withComponent( - VEvent(event.dateTime, event.duration, event.title) - .withProperty(Uid(event.id.uid)) - .ifNotNull(event.id.recurrenceId) { withProperty(RecurrenceId(it)) } - .withProperty(Description(event.description)) - .withProperty(Location(event.location)) - as CalendarComponent + val vEvent = VEvent() + .withProperty(Uid(event.id.uid)) + .withProperty(DtStart(event.dateTime.toLocalDateTime()).withParameter(TzIdParam(event.dateTime.zone.id)).fluentTarget) + .withProperty(Duration(event.duration)) + .withProperty(Summary(event.title)) + .ifNotNull(event.id.recurrenceId) { withProperty(RecurrenceId(it)) } + .withProperty(Description(event.description)) + .withProperty(Location(event.location)) + as CalendarComponent + + calendar + .withComponent(vTimeZoneFrom(event.dateTime.zone, event.dateTime)) + .withComponent(vEvent) - ) return calendar } + private fun vTimeZoneFrom(zone: ZoneId, reference: ZonedDateTime): VTimeZone { + val offset = zone.rules.getOffset(reference.toInstant()) + val offsetString = offset.id.replace(":", "") + + val standard = Standard().apply { + add(TzName(offset.id)) + add(TzOffsetFrom(offsetString)) + add(TzOffsetTo(offsetString)) + + val dtStart = DtStart(reference.withYear(1970).toLocalDateTime()) + add(dtStart) + } + + return VTimeZone().apply { + add(TzId(zone.id)) + add(standard) + } + } + } From 385b13caf791a64f4bacf782b4a6cfe8a76eaed0 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Sun, 1 Mar 2026 11:52:14 +0700 Subject: [PATCH 02/22] =?UTF-8?q?build/qg-xxx:=20Spring=20Boot=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D1=91=D0=BD=20=D0=B4=D0=BE=203.5?= =?UTF-8?q?.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index b316362b..99a749ba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,7 +7,7 @@ dependencyResolutionManagement { create("libs") { // plugin versions val kotlinVersion = version("kotlin", "2.2.10") - val springBootVersion = version("springBoot", "3.5.6") + val springBootVersion = version("springBoot", "3.5.11") val springDependencyManagementVersion = version("springDependencyManagement", "1.1.7") val koverVersion = version("kover", "0.9.2") val gitPropertiesVersion = version("gitProperties", "2.5.3") From c5feffeb9ab96a9cf66b97310816932636e41fa8 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 18:16:06 +0700 Subject: [PATCH 03/22] =?UTF-8?q?build/qg-xxx:=20Spring=20Boot=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D1=91=D0=BD=20=D0=B4=D0=BE=204.0?= =?UTF-8?q?.3=20(=D0=B1=D0=B5=D0=B7=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=86=D0=B8?= =?UTF-8?q?=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 99a749ba..70883f89 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,7 +7,7 @@ dependencyResolutionManagement { create("libs") { // plugin versions val kotlinVersion = version("kotlin", "2.2.10") - val springBootVersion = version("springBoot", "3.5.11") + val springBootVersion = version("springBoot", "4.0.3") val springDependencyManagementVersion = version("springDependencyManagement", "1.1.7") val koverVersion = version("kover", "0.9.2") val gitPropertiesVersion = version("gitProperties", "2.5.3") From c03fe583152f0ff5f9197fc7d5ab313b8d480c3f Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Sun, 1 Mar 2026 11:57:57 +0700 Subject: [PATCH 04/22] =?UTF-8?q?build/qg-xxx:=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D1=91=D0=BD=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82?= =?UTF-8?q?=20org.springframework.data.core.TypeInformation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../file_storage/internal/FilesMetaDataRepo.kt | 2 +- .../azhidkov/platform/spring/jdbc/RowMapperExt.kt | 12 ++++++------ .../pro/azhidkov/platform/spring/sdj/PageExt.kt | 2 +- .../spring/sdj/converters/ObjectToJsonbConverters.kt | 2 +- .../qyoga/core/users/therapists/TherapistsRepo.kt | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/pro/azhidkov/platform/file_storage/internal/FilesMetaDataRepo.kt b/app/src/main/kotlin/pro/azhidkov/platform/file_storage/internal/FilesMetaDataRepo.kt index 6f0b14da..c59856c2 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/file_storage/internal/FilesMetaDataRepo.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/file_storage/internal/FilesMetaDataRepo.kt @@ -1,10 +1,10 @@ package pro.azhidkov.platform.file_storage.internal +import org.springframework.data.core.TypeInformation import org.springframework.data.jdbc.core.JdbcAggregateOperations import org.springframework.data.jdbc.core.convert.JdbcConverter import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository import org.springframework.data.mapping.model.BasicPersistentEntity -import org.springframework.data.util.TypeInformation import org.springframework.stereotype.Repository import pro.azhidkov.platform.file_storage.api.FileMetaData diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt index 8bf5831a..9229ec50 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt @@ -9,19 +9,19 @@ import pro.azhidkov.platform.spring.sdj.converters.StringToSecretChars import pro.azhidkov.platform.spring.sdj.converters.UuidToAggregateReferenceConverter -inline fun rowMapperFor(objectMapper: ObjectMapper, columnName: String? = null) = RowMapper { rs, _ -> +inline fun rowMapperFor(objectMapper: ObjectMapper, columnName: String? = null) = RowMapper { rs, _ -> val json: String? = if (columnName != null) { rs.getString(columnName) } else { rs.getString(1) } - json?.let { objectMapper.readValue(it, T::class.java) } + requireNotNull(json) { "Expected JSON value for ${T::class.java.simpleName}" } + objectMapper.readValue(json, T::class.java) } -inline fun taDataClassRowMapper() = DataClassRowMapper.newInstance(T::class.java).apply { - conversionService = DefaultConversionService().apply { +inline fun taDataClassRowMapper() = + DataClassRowMapper.newInstance(T::class.java, DefaultConversionService().apply { addConverter(PGIntervalToDurationConverter()) addConverter(UuidToAggregateReferenceConverter) addConverter(StringToSecretChars()) - } -} + }) diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/PageExt.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/PageExt.kt index 4b213dc9..65b99153 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/PageExt.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/PageExt.kt @@ -4,6 +4,6 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.PageRequest -fun Page.mapContent(f: (MutableList) -> List): Page { +fun Page.mapContent(f: (List) -> List): Page { return PageImpl(f(this.content), PageRequest.of(this.number, this.size), this.totalElements) } diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt index d8a7210e..af4ace7a 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt @@ -12,7 +12,7 @@ import kotlin.reflect.KClass fun interface ObjectToJsonbWriter : Converter @ReadingConverter -fun interface JsonbToObjectReader : Converter +fun interface JsonbToObjectReader : Converter abstract class JacksonObjectToJsonbWriter( private val objectMapper: ObjectMapper diff --git a/app/src/main/kotlin/pro/qyoga/core/users/therapists/TherapistsRepo.kt b/app/src/main/kotlin/pro/qyoga/core/users/therapists/TherapistsRepo.kt index d8a74eb2..c3fec988 100644 --- a/app/src/main/kotlin/pro/qyoga/core/users/therapists/TherapistsRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/core/users/therapists/TherapistsRepo.kt @@ -1,10 +1,10 @@ package pro.qyoga.core.users.therapists +import org.springframework.data.core.TypeInformation import org.springframework.data.jdbc.core.JdbcAggregateTemplate import org.springframework.data.jdbc.core.convert.JdbcConverter import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository import org.springframework.data.mapping.model.BasicPersistentEntity -import org.springframework.data.util.TypeInformation import org.springframework.stereotype.Repository @@ -16,4 +16,4 @@ class TherapistsRepo( jdbcAggregateTemplate, BasicPersistentEntity(TypeInformation.of(Therapist::class.java)), jdbcConverter -) \ No newline at end of file +) From 9b7f74f89d6327dfbb26faf7300277295df814d5 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 12:37:28 +0700 Subject: [PATCH 05/22] =?UTF-8?q?build/qg-xxx:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D0=B8,=20=D0=B2=D1=8B=D0=B7=D0=B2=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=82=D0=B5=D0=BC,=20=D1=87=D1=82=D0=BE=20?= =?UTF-8?q?=D0=B2=20Spring=20=D1=8F=D0=B2=D0=BD=D0=BE=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=B2=D0=B8=D0=BB=D0=B8=20=D0=BD=D1=83=D0=BB?= =?UTF-8?q?=D0=BB=D0=B0=D0=B1=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=20=D0=90=D0=9F=D0=98=20=D0=B8=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=BE?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=81=D1=82=D0=B0=D0=BB=D0=BE=20not=20null=20?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=20=D1=87=D0=B5=D0=B3=D0=BE=20=D1=81=D0=B8=D0=B3=D0=BD?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=80=D0=B0=20=D1=87=D0=B0=D1=81=D1=82=D0=B8?= =?UTF-8?q?=20=D0=90=D0=9F=D0=98=20=D0=BF=D0=BE=D0=BC=D0=B5=D0=BD=D1=8F?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=8C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В частности: 1. CrudRepository.save(e: E): E : nullable -> not-nullable 2. PasswordEncoder.encode(plainPassword: ChartSequence): String? : platform -> explicitly nullable 3. WebMvcConfigurer.addArgumentResolvers(argumentResolvers: MutableList not-nullable 4. MultiValueMap.fromSingleValue(Map map): MultiValueMap : nullable -> not-null 5. AppicationContext.getBean(clazz: Class): T : nullable - not-null 6. ServerProperties.port: Int? : platform -> explicitly nullable --- .../azhidkov/platform/spring/jdbc/RowMapperExt.kt | 13 +++++++------ .../sdj/converters/ObjectToJsonbConverters.kt | 2 +- .../pro/qyoga/core/calendar/api/CalendarItem.kt | 2 +- .../pro/qyoga/core/clients/cards/ClientsRepo.kt | 2 +- .../core/clients/journals/JournalEntriesRepo.kt | 4 ++-- .../therapeutic_tasks/TherapeuticTasksRepo.kt | 7 ++++--- .../pro/qyoga/core/users/auth/UsersFactory.kt | 4 ++-- .../kotlin/pro/qyoga/core/users/auth/UsersRepo.kt | 4 ++-- .../qyoga/i9ns/calendars/ical/model/ICalEventId.kt | 5 +++-- .../main/kotlin/pro/qyoga/infra/web/WebConfig.kt | 4 ++-- .../cases/app/publc/surveys/SubmitSurveyTest.kt | 4 ++-- .../tests/assertions/MultiValueMapAssertions.kt | 4 ++-- .../tests/infra/test_config/spring/TestsConfig.kt | 4 ++-- .../pro/qyoga/tests/infra/web/QYogaAppBaseKoTest.kt | 6 +++--- .../pro/qyoga/tests/infra/web/QYogaAppBaseTest.kt | 8 ++++---- .../context/ConfigurableApplicationContextExt.kt | 2 +- 16 files changed, 39 insertions(+), 36 deletions(-) diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt index 9229ec50..9c6fb2a0 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt @@ -9,19 +9,20 @@ import pro.azhidkov.platform.spring.sdj.converters.StringToSecretChars import pro.azhidkov.platform.spring.sdj.converters.UuidToAggregateReferenceConverter -inline fun rowMapperFor(objectMapper: ObjectMapper, columnName: String? = null) = RowMapper { rs, _ -> +inline fun rowMapperFor(objectMapper: ObjectMapper, columnName: String? = null) = RowMapper { rs, _ -> val json: String? = if (columnName != null) { rs.getString(columnName) } else { rs.getString(1) } - requireNotNull(json) { "Expected JSON value for ${T::class.java.simpleName}" } - objectMapper.readValue(json, T::class.java) + json?.let { objectMapper.readValue(it, T::class.java) } } -inline fun taDataClassRowMapper() = - DataClassRowMapper.newInstance(T::class.java, DefaultConversionService().apply { +inline fun taDataClassRowMapper(): DataClassRowMapper = + DataClassRowMapper.newInstance(T::class.java).apply { + conversionService = DefaultConversionService().apply { addConverter(PGIntervalToDurationConverter()) addConverter(UuidToAggregateReferenceConverter) addConverter(StringToSecretChars()) - }) + } + } diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt index af4ace7a..c6082786 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt @@ -12,7 +12,7 @@ import kotlin.reflect.KClass fun interface ObjectToJsonbWriter : Converter @ReadingConverter -fun interface JsonbToObjectReader : Converter +fun interface JsonbToObjectReader : Converter abstract class JacksonObjectToJsonbWriter( private val objectMapper: ObjectMapper diff --git a/app/src/main/kotlin/pro/qyoga/core/calendar/api/CalendarItem.kt b/app/src/main/kotlin/pro/qyoga/core/calendar/api/CalendarItem.kt index ee0cf0d1..0a3ea9f4 100644 --- a/app/src/main/kotlin/pro/qyoga/core/calendar/api/CalendarItem.kt +++ b/app/src/main/kotlin/pro/qyoga/core/calendar/api/CalendarItem.kt @@ -55,7 +55,7 @@ interface CalendarItemId { .buildAndExpand(type.name) .toUri() - fun toMap(): Map + fun toMap(): Map } diff --git a/app/src/main/kotlin/pro/qyoga/core/clients/cards/ClientsRepo.kt b/app/src/main/kotlin/pro/qyoga/core/clients/cards/ClientsRepo.kt index 80d57b02..ab18e245 100644 --- a/app/src/main/kotlin/pro/qyoga/core/clients/cards/ClientsRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/core/clients/cards/ClientsRepo.kt @@ -38,7 +38,7 @@ class ClientsRepo( val topFiveByLastName = PageRequest.of(0, 5, sortBy(Client::lastName)) } - override fun save(instance: S & Any): S & Any { + override fun save(instance: S): S { return saveAndMapDuplicatedKeyException(instance) { ex -> DuplicatedPhoneException(instance, ex) } diff --git a/app/src/main/kotlin/pro/qyoga/core/clients/journals/JournalEntriesRepo.kt b/app/src/main/kotlin/pro/qyoga/core/clients/journals/JournalEntriesRepo.kt index 09ab57da..d4bd4705 100644 --- a/app/src/main/kotlin/pro/qyoga/core/clients/journals/JournalEntriesRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/core/clients/journals/JournalEntriesRepo.kt @@ -33,7 +33,7 @@ class JournalEntriesRepo( ) { @Transactional - override fun save(instance: S & Any): S & Any { + override fun save(instance: S): S { return saveAndMapDuplicatedKeyException(instance) { ex -> DuplicatedDate(instance, ex) } @@ -60,4 +60,4 @@ class JournalEntriesRepo( } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/core/therapy/therapeutic_tasks/TherapeuticTasksRepo.kt b/app/src/main/kotlin/pro/qyoga/core/therapy/therapeutic_tasks/TherapeuticTasksRepo.kt index add1f3b5..95466547 100644 --- a/app/src/main/kotlin/pro/qyoga/core/therapy/therapeutic_tasks/TherapeuticTasksRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/core/therapy/therapeutic_tasks/TherapeuticTasksRepo.kt @@ -1,5 +1,6 @@ package pro.qyoga.core.therapy.therapeutic_tasks +import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.data.domain.Slice import org.springframework.data.jdbc.core.JdbcAggregateOperations @@ -31,12 +32,12 @@ class TherapeuticTasksRepo( ) { object Page { - val topFiveByName = Pageable.ofSize(5).withSortBy(TherapeuticTask::name) - val topTenByName = Pageable.ofSize(10).withSortBy(TherapeuticTask::name) + val topFiveByName: PageRequest = Pageable.ofSize(5).withSortBy(TherapeuticTask::name) + val topTenByName: PageRequest = Pageable.ofSize(10).withSortBy(TherapeuticTask::name) } @Transactional - override fun save(instance: S & Any): S & Any { + override fun save(instance: S): S { return saveAndMapDuplicatedKeyException(instance) { ex -> DuplicatedTherapeuticTaskName(instance, ex) } diff --git a/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt b/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt index 7b1225f9..7dd33a09 100644 --- a/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt +++ b/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt @@ -12,8 +12,8 @@ class UsersFactory( ) { fun createUser(email: String, plainPassword: CharSequence, roles: Set): User { - val passwordHash = passwordEncoder.encode(plainPassword) + val passwordHash = passwordEncoder.encode(plainPassword)!! return User(email, passwordHash, roles.toTypedArray(), enabled = true) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersRepo.kt b/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersRepo.kt index 85b03282..bbc3cc41 100644 --- a/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersRepo.kt @@ -26,7 +26,7 @@ class UsersRepo( ) { @Transactional - override fun save(instance: S & Any): S & Any { + override fun save(instance: S): S { return saveAndMapDuplicatedKeyException(instance) { ex -> DuplicatedEmailException(instance, ex) } @@ -38,4 +38,4 @@ class UsersRepo( } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/model/ICalEventId.kt b/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/model/ICalEventId.kt index c844724a..08a85139 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/model/ICalEventId.kt +++ b/app/src/main/kotlin/pro/qyoga/i9ns/calendars/ical/model/ICalEventId.kt @@ -1,5 +1,6 @@ package pro.qyoga.i9ns.calendars.ical.model +import pro.azhidkov.platform.kotlin.mapOfNotNull import pro.qyoga.core.calendar.api.CalendarItemId import pro.qyoga.core.calendar.api.CalendarType @@ -11,9 +12,9 @@ data class ICalEventId( override val type: CalendarType = ICalCalendar.Type - override fun toMap(): Map = mapOf( + override fun toMap(): Map = mapOfNotNull( "uid" to uid, - "rid" to recurrenceId + recurrenceId?.let { "rid" to recurrenceId } ) } diff --git a/app/src/main/kotlin/pro/qyoga/infra/web/WebConfig.kt b/app/src/main/kotlin/pro/qyoga/infra/web/WebConfig.kt index 14238c6e..a7fc12e0 100644 --- a/app/src/main/kotlin/pro/qyoga/infra/web/WebConfig.kt +++ b/app/src/main/kotlin/pro/qyoga/infra/web/WebConfig.kt @@ -10,10 +10,10 @@ class WebConfig( private val handlerMethodArgumentResolvers: List ) : WebMvcConfigurer { - override fun addArgumentResolvers(argumentResolvers: MutableList) { + override fun addArgumentResolvers(argumentResolvers: MutableList) { handlerMethodArgumentResolvers.forEach { argumentResolvers.add(it) } } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt index f042764c..e617c802 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt @@ -217,7 +217,7 @@ class SubmitSurveyTest : QYogaAppIntegrationBaseKoTest({ // Проверка errorResponse.status shouldBe HttpStatus.CONFLICT.value() - errorResponse.type.path shouldBe InvalidSurveyException.SURVEY_SETTINGS_NOT_FOUND_FOR_ADMIN_EMAIL + errorResponse.type?.path shouldBe InvalidSurveyException.SURVEY_SETTINGS_NOT_FOUND_FOR_ADMIN_EMAIL } }) @@ -243,4 +243,4 @@ private infix fun String?.shouldContainAllCustomAnswersFrom(surveyRqJson: JsonNo .forAll { this shouldContain it.value.asText() } -} \ No newline at end of file +} diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/assertions/MultiValueMapAssertions.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/assertions/MultiValueMapAssertions.kt index 44c952c7..30e7ecb1 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/assertions/MultiValueMapAssertions.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/assertions/MultiValueMapAssertions.kt @@ -5,7 +5,7 @@ import io.kotest.matchers.maps.shouldContainKey import org.springframework.util.MultiValueMap -fun MultiValueMap.shouldContainValue(key: K, value: V) { +fun MultiValueMap.shouldContainValue(key: K, value: V) { this shouldContainKey key this[key]!! shouldContain value -} \ No newline at end of file +} diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/TestsConfig.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/TestsConfig.kt index 4a25b877..8c110d2b 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/TestsConfig.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/TestsConfig.kt @@ -1,7 +1,7 @@ package pro.qyoga.tests.infra.test_config.spring -import org.springframework.boot.autoconfigure.web.ServerProperties import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.boot.web.server.autoconfigure.ServerProperties import org.springframework.context.ApplicationContext import org.springframework.context.ConfigurableApplicationContext import org.springframework.context.annotation.AnnotationConfigApplicationContext @@ -53,4 +53,4 @@ val sdjContext by lazy { class TestsConfig @Import(SdjConfig::class, TestDataSourceConfig::class) -class SdjTestsConfig \ No newline at end of file +class SdjTestsConfig diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseKoTest.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseKoTest.kt index c7c59570..7256fe67 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseKoTest.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseKoTest.kt @@ -18,10 +18,10 @@ abstract class QYogaAppBaseKoTest(body: QYogaAppBaseKoTest.() -> Unit = {}) : Fr val presets: PresetsConf = context.getBean(PresetsConf::class.java) - inline fun getBean(): T = + inline fun getBean(): T = context.getBean(T::class.java) - inline fun getBean(name: String): T = + inline fun getBean(name: String): T = context.getBean(name, T::class.java) init { @@ -35,4 +35,4 @@ abstract class QYogaAppBaseKoTest(body: QYogaAppBaseKoTest.() -> Unit = {}) : Fr body() } -} \ No newline at end of file +} diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseTest.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseTest.kt index 2fa36d03..e84374c5 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseTest.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/QYogaAppBaseTest.kt @@ -1,7 +1,7 @@ package pro.qyoga.tests.infra.web import org.junit.jupiter.api.BeforeEach -import org.springframework.boot.autoconfigure.web.ServerProperties +import org.springframework.boot.web.server.autoconfigure.ServerProperties import pro.qyoga.tests.fixture.backgrounds.Backgrounds import pro.qyoga.tests.fixture.data.resetFaker import pro.qyoga.tests.fixture.presets.PresetsConf @@ -15,13 +15,13 @@ open class QYogaAppBaseTest { private val dataSource: DataSource = context.getBean(DataSource::class.java) - protected val port: Int = context.getBean(ServerProperties::class.java).port + protected val port: Int = requireNotNull(context.getBean(ServerProperties::class.java).port) protected val backgrounds: Backgrounds = context.getBean(Backgrounds::class.java) protected val presets: PresetsConf = context.getBean(PresetsConf::class.java) - inline fun getBean(): T = + inline fun getBean(): T = context.getBean(T::class.java) @BeforeEach @@ -31,4 +31,4 @@ open class QYogaAppBaseTest { WireMock.reset() } -} \ No newline at end of file +} diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/context/ConfigurableApplicationContextExt.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/context/ConfigurableApplicationContextExt.kt index 1c7857ab..73dbbd82 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/context/ConfigurableApplicationContextExt.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/context/ConfigurableApplicationContextExt.kt @@ -3,5 +3,5 @@ package pro.qyoga.tests.platform.spring.context import org.springframework.beans.factory.BeanFactory -inline fun BeanFactory.getBean(): T = +inline fun BeanFactory.getBean(): T = this.getBean(T::class.java) From 95ae9355a9ff4cf4c280a4c31fdaad08b8131ec3 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Sun, 1 Mar 2026 12:03:17 +0700 Subject: [PATCH 06/22] =?UTF-8?q?build/qg-xxx:=20testcontainers=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B4=D0=BE?= =?UTF-8?q?=202.0.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 ++-- .../kotlin/pro/qyoga/tests/infra/db/Containers.kt | 4 ++-- .../kotlin/pro/qyoga/tests/infra/SelenideContainer.kt | 6 ++---- settings.gradle.kts | 10 +++++++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1b1e336b..a5ae3f6b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -78,8 +78,8 @@ dependencies { testFixturesImplementation("org.springframework.boot:spring-boot-starter-test") testFixturesImplementation("org.springframework.security:spring-security-test") testFixturesImplementation("org.springframework.boot:spring-boot-starter-webflux") - testFixturesImplementation("org.testcontainers:junit-jupiter") - testFixturesImplementation("org.testcontainers:postgresql") + testFixturesImplementation("org.testcontainers:testcontainers-junit-jupiter") + testFixturesImplementation("org.testcontainers:testcontainers-postgresql") testFixturesImplementation(testLibs.testcontainers.minio) testImplementation(testFixtures(project(":app"))) diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/db/Containers.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/db/Containers.kt index 0fd22c12..5da23332 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/db/Containers.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/db/Containers.kt @@ -1,10 +1,10 @@ package pro.qyoga.tests.infra.db import org.testcontainers.containers.MinIOContainer -import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.postgresql.PostgreSQLContainer import org.testcontainers.utility.MountableFile -val pgContainer: PostgreSQLContainer<*> by lazy { +val pgContainer: PostgreSQLContainer by lazy { PostgreSQLContainer("postgres:15.2") .withExposedPorts(5432) .withUsername("postgres") diff --git a/e2e-tests/src/test/kotlin/pro/qyoga/tests/infra/SelenideContainer.kt b/e2e-tests/src/test/kotlin/pro/qyoga/tests/infra/SelenideContainer.kt index 46e8449b..a098e383 100644 --- a/e2e-tests/src/test/kotlin/pro/qyoga/tests/infra/SelenideContainer.kt +++ b/e2e-tests/src/test/kotlin/pro/qyoga/tests/infra/SelenideContainer.kt @@ -1,13 +1,11 @@ package pro.qyoga.tests.infra -import org.openqa.selenium.chrome.ChromeOptions -import org.testcontainers.containers.BrowserWebDriverContainer +import org.testcontainers.selenium.BrowserWebDriverContainer import org.testcontainers.utility.DockerImageName -val container: BrowserWebDriverContainer<*> by lazy { +val container: BrowserWebDriverContainer by lazy { BrowserWebDriverContainer(chromeImage()) - .withCapabilities(ChromeOptions()) .withAccessToHost(true) .apply { start() diff --git a/settings.gradle.kts b/settings.gradle.kts index 70883f89..940daf33 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,13 +70,15 @@ dependencyResolutionManagement { create("testLibs") { val selenideVersion = version("selenide", "7.10.1") - val testContainersVersion = version("testcontainers", "1.21.3") + val testContainersVersion = version("testcontainers", "2.0.3") val restAssuredVersion = version("restAssured", "5.5.6") val kotestVersion = version("kotest", "5.9.1") val wiremockVersion = version("wiremock", "3.13.1") library("selenide-proxy", "com.codeborne", "selenide-proxy").versionRef(selenideVersion) - library("testcontainers-selenium", "org.testcontainers", "selenium").versionRef(testContainersVersion) + library("testcontainers-selenium", "org.testcontainers", "testcontainers-selenium").versionRef( + testContainersVersion + ) library("kotest-assertions", "io.kotest", "kotest-assertions-core").versionRef(kotestVersion) library("kotest-runner", "io.kotest", "kotest-runner-junit5").versionRef(kotestVersion) @@ -95,7 +97,9 @@ dependencyResolutionManagement { library("datafaker", "net.datafaker", "datafaker").version("2.5.1") library("greenmail", "com.icegreen", "greenmail-junit5").version("2.1.6") - library("testcontainers-minio", "org.testcontainers", "minio").versionRef(testContainersVersion) + library("testcontainers-minio", "org.testcontainers", "testcontainers-minio").versionRef( + testContainersVersion + ) library("mockito-kotlin", "org.mockito.kotlin", "mockito-kotlin").version("6.1.0") library("archunit", "com.tngtech.archunit", "archunit").version("1.4.1") From 4c009232c0a75ca355c43d38196fd7cb9206e6e1 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 18:57:00 +0700 Subject: [PATCH 07/22] =?UTF-8?q?build/qg-xxx:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=81=20=D1=80=D0=B0=D1=81=D0=BF=D0=B8=D0=BB=D0=BE?= =?UTF-8?q?=D0=BC=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B5=D0=B7=D0=B4?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=B0=D0=B2=D1=82=D0=BE=D0=B3=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20=D0=B2=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D0=B8/=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qyoga/i9ns/calendars/google/client/GoogleCalendarsClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/calendars/google/client/GoogleCalendarsClient.kt b/app/src/main/kotlin/pro/qyoga/i9ns/calendars/google/client/GoogleCalendarsClient.kt index e8edd196..678f937f 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/calendars/google/client/GoogleCalendarsClient.kt +++ b/app/src/main/kotlin/pro/qyoga/i9ns/calendars/google/client/GoogleCalendarsClient.kt @@ -11,7 +11,7 @@ import com.google.auth.http.HttpCredentialsAdapter import com.google.auth.oauth2.UserCredentials import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties import org.springframework.cache.annotation.Cacheable import org.springframework.http.HttpStatus import org.springframework.stereotype.Component From bd6d3cda22fb3a45b49784ae187555c09d562f05 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 18:59:54 +0700 Subject: [PATCH 08/22] =?UTF-8?q?build/qg-xxx:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=20=D0=BF=D0=BE=D1=87=D0=B5=D0=BC=D1=83-?= =?UTF-8?q?=D1=82=D0=BE=20=D1=81=D0=BB=D0=BE=D0=BC=D0=B0=D0=B2=D1=88=D0=B8?= =?UTF-8?q?=D0=BC=D1=81=D1=8F=20=D1=81=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=BC=20=D1=81=D0=B0=D1=85?= =?UTF-8?q?=D0=B0=D1=80=D0=BE=D0=BC=20=D0=B4=D0=BB=D1=8F=20setter-=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt b/app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt index 8cff83cd..206e4fca 100644 --- a/app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt +++ b/app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt @@ -116,7 +116,10 @@ class WebSecurityConfig( @Bean fun tokenRepository(): PersistentTokenRepository { val jdbcTokenRepositoryImpl = JdbcTokenRepositoryImpl() - jdbcTokenRepositoryImpl.dataSource = dataSource + // JdbcDaoSupport хоть и помечен для удаления, но иного способа датасорс сейчас нет + // property access syntax почему-то перестал комплироваться после переезда на Spring Boot 4 + @Suppress("removal", "UsePropertyAccessSyntax") + jdbcTokenRepositoryImpl.setDataSource(dataSource) return jdbcTokenRepositoryImpl } From 22d1febcf3358cc7b8a649ccdf7a95eed26d6c7e Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 19:00:29 +0700 Subject: [PATCH 09/22] =?UTF-8?q?build/qg-xxx:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B8=D0=B7-=D0=B7=D0=B0=20=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B8=D0=BC=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2?= =?UTF-8?q?=20API=20DaoAuthenticationProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_config/spring/auth/TestPasswordEncoderConfig.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt index 29142f98..5856c8ec 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt @@ -24,9 +24,9 @@ class TestPasswordEncoderConfig( @Bean fun daoAuthenticationProvider(): DaoAuthenticationProvider { - val daoAuthenticationProvider = DaoAuthenticationProvider(fastPasswordEncoder()) - daoAuthenticationProvider.setUserDetailsService(userDetailsService) + val daoAuthenticationProvider = DaoAuthenticationProvider(userDetailsService) + daoAuthenticationProvider.setPasswordEncoder(fastPasswordEncoder()) return daoAuthenticationProvider } -} \ No newline at end of file +} From 490c4ca40325ac7123b4790194c9edeb8ace7ed4 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Sun, 1 Mar 2026 12:22:22 +0700 Subject: [PATCH 10/22] =?UTF-8?q?build/qg-xxx:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20Spring=20Boot=20Flyway=20Starter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Так как в 4-ом буте без него не выполняется автоконфигурация/запуск Flyway --- app/build.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ae3f6b..5e39ead5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,9 +29,10 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-cache") + implementation("org.springframework.boot:spring-boot-starter-flyway") implementation(libs.caffeine) - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("tools.jackson.module:jackson-module-kotlin") implementation("org.flywaydb:flyway-database-postgresql") implementation(libs.jackarta.validation) implementation(libs.thymeleaf.extras.java8time) @@ -71,7 +72,7 @@ dependencies { testFixturesImplementation("org.springframework.boot:spring-boot-starter-web") testFixturesImplementation("org.springframework.boot:spring-boot-starter-security") testFixturesImplementation("org.springframework.boot:spring-boot-starter-oauth2-client") - testFixturesImplementation("com.fasterxml.jackson.core:jackson-databind") + testFixturesImplementation("tools.jackson.core:jackson-databind") testFixturesImplementation(libs.minio) testFixturesImplementation(libs.ical4j) From 855c61e4350c89a0893e3da7fd1259f42faaf6e7 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Sun, 1 Mar 2026 17:06:07 +0700 Subject: [PATCH 11/22] =?UTF-8?q?build/qg-xxx:=20=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D0=B4=D1=91?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=B0=20Jackson=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 13 +++++++++ .../platform/spring/jdbc/RowMapperExt.kt | 2 +- .../sdj/AggregateReferenceDeserializer.kt | 15 +++++------ .../platform/spring/sdj/ErgoSdjConfig.kt | 6 ++--- .../sdj/converters/ObjectToJsonbConverters.kt | 2 +- .../therapy/exercises/model/ExerciseType.kt | 9 +++---- .../core/therapy/programs/ProgramsRepo.kt | 4 +-- .../pushes/web/WebPushSubscriptionsRepo.kt | 2 +- .../qyoga/i9ns/pushes/web/WebPushesConf.kt | 2 +- .../pro/qyoga/infra/web/ThymeleafConfig.kt | 4 +-- .../qyoga/tests/cases/app/auth/AuthTests.kt | 2 +- .../app/publc/surveys/SubmitSurveyTest.kt | 27 ++++++++++--------- .../appointments/core/SchedulePageTest.kt | 2 +- .../therapy/programs/EditProgramPageTest.kt | 4 +-- .../pro/qyoga/tests/clients/OpsClient.kt | 4 +-- .../tests/clients/api/TherapistProgramsApi.kt | 6 ++--- .../SurveyFormsSettingsComponent.kt | 6 ++--- .../pro/qyoga/tests/platform/html/Script.kt | 6 ++--- e2e-tests/build.gradle.kts | 8 +++++- 19 files changed, 71 insertions(+), 53 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5e39ead5..313d3a02 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,6 +19,16 @@ plugins { group = "pro.qyoga" version = "0.0.1-SNAPSHOT" +configurations.matching { it.name == "compileClasspath" }.all { + exclude(group = "com.fasterxml.jackson.core", module = "jackson-core") + exclude(group = "com.fasterxml.jackson.core", module = "jackson-databind") +} + +configurations.matching { it.name in setOf("testCompileClasspath", "testFixturesCompileClasspath") }.all { + exclude(group = "com.fasterxml.jackson") + exclude(group = "com.fasterxml.jackson.core") +} + dependencies { implementation(kotlin("reflect")) implementation("org.springframework.boot:spring-boot-starter-data-jdbc") @@ -50,6 +60,9 @@ dependencies { implementation(libs.bouncycastle) developmentOnly("org.springframework.boot:spring-boot-docker-compose") + runtimeOnly(platform("com.fasterxml.jackson:jackson-bom:2.20.2")) + runtimeOnly("com.fasterxml.jackson.core:jackson-core") + runtimeOnly("com.fasterxml.jackson.core:jackson-databind") testFixturesApi("org.springframework.boot:spring-boot-testcontainers") testFixturesApi(testLibs.kotest.assertions) diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt index 9c6fb2a0..18af3090 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/jdbc/RowMapperExt.kt @@ -1,12 +1,12 @@ package pro.azhidkov.platform.spring.jdbc -import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.core.convert.support.DefaultConversionService import org.springframework.jdbc.core.DataClassRowMapper import org.springframework.jdbc.core.RowMapper import pro.azhidkov.platform.spring.sdj.converters.PGIntervalToDurationConverter import pro.azhidkov.platform.spring.sdj.converters.StringToSecretChars import pro.azhidkov.platform.spring.sdj.converters.UuidToAggregateReferenceConverter +import tools.jackson.databind.ObjectMapper inline fun rowMapperFor(objectMapper: ObjectMapper, columnName: String? = null) = RowMapper { rs, _ -> diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/AggregateReferenceDeserializer.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/AggregateReferenceDeserializer.kt index 7fc076e6..e3786d57 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/AggregateReferenceDeserializer.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/AggregateReferenceDeserializer.kt @@ -1,22 +1,21 @@ package pro.azhidkov.platform.spring.sdj -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.* -import com.fasterxml.jackson.databind.deser.ContextualDeserializer -import com.fasterxml.jackson.databind.type.TypeFactory import org.springframework.data.jdbc.core.mapping.AggregateReference import pro.azhidkov.platform.spring.sdj.ergo.hydration.AggregateReferenceTarget import pro.azhidkov.platform.spring.sdj.ergo.hydration.Identifiable +import tools.jackson.core.JsonParser +import tools.jackson.databind.* +import tools.jackson.databind.type.TypeFactory import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.jvmErasure class AggregateReferenceDeserializer( private var type: JavaType = TypeFactory.unknownType() -) : JsonDeserializer>(), ContextualDeserializer { +) : ValueDeserializer>() { override fun deserialize(parser: JsonParser, context: DeserializationContext): AggregateReference<*, *>? { - val node = parser.codec.readTree(parser) + val node = parser.readValueAsTree() val propertyNames = node.properties().map { it.key }.toSet() return if (propertyNames == setOf(ID_FIELD_NAME)) { val idType = getIdType(type) @@ -28,7 +27,7 @@ class AggregateReferenceDeserializer( } } - override fun createContextual(ctx: DeserializationContext, property: BeanProperty): JsonDeserializer<*> { + override fun createContextual(ctx: DeserializationContext, property: BeanProperty): ValueDeserializer<*> { return AggregateReferenceDeserializer(property.type.containedType(0)) } @@ -44,4 +43,4 @@ private fun getIdType(type: JavaType): Class = .first { it.name == AggregateReferenceDeserializer.ID_FIELD_NAME } .returnType .jvmErasure - .java \ No newline at end of file + .java diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt index f28d1ddb..9cd25511 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt @@ -1,11 +1,11 @@ package pro.azhidkov.platform.spring.sdj -import com.fasterxml.jackson.databind.module.SimpleModule import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.data.jdbc.core.mapping.AggregateReference import pro.azhidkov.platform.spring.sdj.ergo.ErgoPersistenceExceptionTranslator -import com.fasterxml.jackson.databind.Module as JacksonModule +import tools.jackson.databind.JacksonModule +import tools.jackson.databind.module.SimpleModule @Configuration @@ -25,4 +25,4 @@ class ErgoSdjConfig { fun persistenceExceptionTranslator() = ErgoPersistenceExceptionTranslator() -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt index c6082786..9dd02bf1 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/converters/ObjectToJsonbConverters.kt @@ -1,10 +1,10 @@ package pro.azhidkov.platform.spring.sdj.converters -import com.fasterxml.jackson.databind.ObjectMapper import org.postgresql.util.PGobject import org.springframework.core.convert.converter.Converter import org.springframework.data.convert.ReadingConverter import org.springframework.data.convert.WritingConverter +import tools.jackson.databind.ObjectMapper import kotlin.reflect.KClass diff --git a/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/model/ExerciseType.kt b/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/model/ExerciseType.kt index 1b6f029c..0f67391a 100644 --- a/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/model/ExerciseType.kt +++ b/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/model/ExerciseType.kt @@ -2,9 +2,8 @@ package pro.qyoga.core.therapy.exercises.model import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonFormat -import com.fasterxml.jackson.databind.JsonNode import pro.azhidkov.platform.kotlin.LabeledEnum -import pro.qyoga.core.therapy.exercises.model.ExerciseType.entries +import tools.jackson.databind.JsonNode @JsonFormat(shape = JsonFormat.Shape.OBJECT) @@ -22,11 +21,11 @@ enum class ExerciseType(override val label: String) : LabeledEnum { @JsonCreator @JvmStatic fun ofName(node: JsonNode): ExerciseType? { - require(node.isTextual || node.isObject) { "Создать экземпляр ExerciseType можно только из строки или объекта" } - val name = if (node.isTextual) node.textValue() else node["name"].asText() + require(node.isString || node.isObject) { "Создать экземпляр ExerciseType можно только из строки или объекта" } + val name = if (node.isString) node.stringValue() else node["name"].stringValue() return entries.find { it.name == name } } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/core/therapy/programs/ProgramsRepo.kt b/app/src/main/kotlin/pro/qyoga/core/therapy/programs/ProgramsRepo.kt index 9a29ae7a..fc4965a3 100644 --- a/app/src/main/kotlin/pro/qyoga/core/therapy/programs/ProgramsRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/core/therapy/programs/ProgramsRepo.kt @@ -1,6 +1,5 @@ package pro.qyoga.core.therapy.programs -import com.fasterxml.jackson.databind.ObjectMapper import org.intellij.lang.annotations.Language import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest @@ -16,6 +15,7 @@ import pro.qyoga.core.therapy.programs.dtos.ProgramsSearchFilter import pro.qyoga.core.therapy.programs.model.DocxProgram import pro.qyoga.core.therapy.programs.model.Program import pro.qyoga.core.therapy.programs.views.ProgramSummaryView +import tools.jackson.databind.ObjectMapper import kotlin.reflect.KProperty1 @Repository @@ -112,4 +112,4 @@ fun ProgramsRepo.getSummaryById(programId: Long): ProgramSummaryView? { """.trimIndent() return findOne(query, mapOf("id" to programId), programSummaryViewRowMapper) -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushSubscriptionsRepo.kt b/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushSubscriptionsRepo.kt index a591748d..560dedd2 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushSubscriptionsRepo.kt +++ b/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushSubscriptionsRepo.kt @@ -1,12 +1,12 @@ package pro.qyoga.i9ns.pushes.web -import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.data.jdbc.core.JdbcAggregateTemplate import org.springframework.jdbc.core.simple.JdbcClient import org.springframework.stereotype.Repository import pro.azhidkov.platform.spring.sdj.query.query import pro.qyoga.core.users.therapists.TherapistRef import pro.qyoga.i9ns.pushes.web.model.TherapistWebPushSubscription +import tools.jackson.databind.ObjectMapper @Repository diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushesConf.kt b/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushesConf.kt index cc762ac5..69313639 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushesConf.kt +++ b/app/src/main/kotlin/pro/qyoga/i9ns/pushes/web/WebPushesConf.kt @@ -1,6 +1,5 @@ package pro.qyoga.i9ns.pushes.web -import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan @@ -8,6 +7,7 @@ import org.springframework.context.annotation.Configuration import pro.azhidkov.platform.spring.sdj.converters.ModuleConverters import pro.azhidkov.platform.spring.sdj.converters.ObjectToJsonbConverters.convertersFor import pro.qyoga.i9ns.pushes.web.model.WebPushSubscription +import tools.jackson.databind.ObjectMapper @Configuration diff --git a/app/src/main/kotlin/pro/qyoga/infra/web/ThymeleafConfig.kt b/app/src/main/kotlin/pro/qyoga/infra/web/ThymeleafConfig.kt index 4a165a84..7b7f5c5b 100644 --- a/app/src/main/kotlin/pro/qyoga/infra/web/ThymeleafConfig.kt +++ b/app/src/main/kotlin/pro/qyoga/infra/web/ThymeleafConfig.kt @@ -1,12 +1,12 @@ package pro.qyoga.infra.web -import com.fasterxml.jackson.databind.ObjectMapper import jakarta.annotation.PostConstruct import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Lazy import org.thymeleaf.spring6.SpringTemplateEngine import org.thymeleaf.standard.StandardDialect import org.thymeleaf.standard.serializer.IStandardJavaScriptSerializer +import tools.jackson.databind.ObjectMapper @Configuration @@ -29,4 +29,4 @@ class ThymeleafConfig( } } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/auth/AuthTests.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/auth/AuthTests.kt index 46fd6246..2032c345 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/auth/AuthTests.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/auth/AuthTests.kt @@ -1,6 +1,5 @@ package pro.qyoga.tests.cases.app.auth -import com.fasterxml.jackson.databind.JsonNode import io.kotest.matchers.shouldNotBe import io.kotest.matchers.types.shouldBeInstanceOf import io.restassured.http.ContentType @@ -27,6 +26,7 @@ import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_PASSWORD import pro.qyoga.tests.infra.web.QYogaAppIntegrationBaseTest import pro.qyoga.tests.pages.publc.LoginPage import pro.qyoga.tests.pages.therapist.clients.ClientsListPage +import tools.jackson.databind.JsonNode import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt index e617c802..75b1a001 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt @@ -1,8 +1,5 @@ package pro.qyoga.tests.cases.app.publc.surveys -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.kotest.core.annotation.DisplayName import io.kotest.inspectors.forAll import io.kotest.matchers.shouldBe @@ -23,6 +20,9 @@ import pro.qyoga.tests.fixture.object_mothers.therapists.THE_ADMIN_LOGIN import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_REF import pro.qyoga.tests.infra.web.QYogaAppIntegrationBaseKoTest import pro.qyoga.tests.infra.web.mainWebTestClient +import tools.jackson.databind.JsonNode +import tools.jackson.databind.node.ObjectNode +import tools.jackson.module.kotlin.jacksonObjectMapper @DisplayName("Операция создания анкеты") @@ -34,7 +34,7 @@ class SubmitSurveyTest : QYogaAppIntegrationBaseKoTest({ // Сетап val entranceSurveyJson = jacksonObjectMapper().readTree(entranceSurveyJsonStr) - val yandexAdminEmail = entranceSurveyJson["yandexAdminEmail"].textValue() + val yandexAdminEmail = entranceSurveyJson["yandexAdminEmail"].stringValue() backgrounds.settingsBackgrounds.createSurveyFormsSettings( THE_THERAPIST_REF, SurveyFormsSettingsObjectMother.aSurveyFromsSettingsFrom(yandexAdminEmail) @@ -55,7 +55,7 @@ class SubmitSurveyTest : QYogaAppIntegrationBaseKoTest({ } "заполнять поле 'Жалобы' ответом из соответствующего поля" { - client?.complaints shouldContain entranceSurveyJson["survey"]["answer"]["data"][Survey.COMPLAINTS_FIELD]["value"].textValue() + client?.complaints shouldContain entranceSurveyJson["survey"]["answer"]["data"][Survey.COMPLAINTS_FIELD]["value"].stringValue() } "заполнять поле 'Анамнез' карточки клиента корректным строковым представлением ответов на пользовательские вопросы" { @@ -68,7 +68,7 @@ class SubmitSurveyTest : QYogaAppIntegrationBaseKoTest({ val phoneAndNameOnlySurveyJsonStr = phoneAndNameOnlySurveyJsonStr(THE_ADMIN_LOGIN) val phoneAndNameOnlyEntranceSurveyJson = jacksonObjectMapper().readTree(phoneAndNameOnlySurveyJsonStr) - val yandexAdminEmail = phoneAndNameOnlyEntranceSurveyJson["yandexAdminEmail"].textValue() + val yandexAdminEmail = phoneAndNameOnlyEntranceSurveyJson["yandexAdminEmail"].stringValue() backgrounds.settingsBackgrounds.createSurveyFormsSettings( THE_THERAPIST_REF, SurveyFormsSettingsObjectMother.aSurveyFromsSettingsFrom(yandexAdminEmail) @@ -226,12 +226,12 @@ private infix fun Client?.shouldMatch(surveyRqJson: JsonNode) { this shouldNotBe null this!! val answerData = surveyRqJson["survey"]["answer"]["data"] - phoneNumber shouldBe PhoneNumber.of(answerData[Survey.PHONE_NUMBER_FIELD]["value"].textValue()) - firstName shouldBe answerData[Survey.FIRST_NAME_FIELD]["value"].textValue() - lastName shouldBe answerData[Survey.LAST_NAME_FIELD]["value"].textValue() - middleName shouldBe answerData[Survey.MIDDLE_NAME_FIELD]?.get("value")?.textValue() - birthDate?.toString() shouldBe answerData[Survey.BIRTH_DATE_FIELD]?.get("value")?.textValue() - address shouldBe answerData[Survey.LOCATION_FIELD]?.get("value")?.get(0)?.get("text")?.textValue() + phoneNumber shouldBe PhoneNumber.of(answerData[Survey.PHONE_NUMBER_FIELD]["value"].stringValue()) + firstName shouldBe answerData[Survey.FIRST_NAME_FIELD]["value"].stringValue() + lastName shouldBe answerData[Survey.LAST_NAME_FIELD]["value"].stringValue() + middleName shouldBe answerData[Survey.MIDDLE_NAME_FIELD]?.get("value")?.stringValue() + birthDate?.toString() shouldBe answerData[Survey.BIRTH_DATE_FIELD]?.get("value")?.stringValue() + address shouldBe answerData[Survey.LOCATION_FIELD]?.get("value")?.get(0)?.get("text")?.stringValue() } private infix fun String?.shouldContainAllCustomAnswersFrom(surveyRqJson: JsonNode) { @@ -241,6 +241,7 @@ private infix fun String?.shouldContainAllCustomAnswersFrom(surveyRqJson: JsonNo answerData.properties() .filter { it.key !in Survey.standardFields } .forAll { - this shouldContain it.value.asText() + val answerValue = it.value["value"].stringValue() + this shouldContain answerValue } } diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/appointments/core/SchedulePageTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/appointments/core/SchedulePageTest.kt index 1e43dbee..5ee78ab9 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/appointments/core/SchedulePageTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/appointments/core/SchedulePageTest.kt @@ -1,6 +1,5 @@ package pro.qyoga.tests.cases.app.therapist.appointments.core -import com.fasterxml.jackson.core.type.TypeReference import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe import org.junit.jupiter.api.DisplayName @@ -24,6 +23,7 @@ import pro.qyoga.tests.pages.therapist.appointments.CalendarPage import pro.qyoga.tests.pages.therapist.appointments.appointmentCards import pro.qyoga.tests.pages.therapist.appointments.shouldMatch import pro.qyoga.tests.platform.instancio.KSelect.Companion.field +import tools.jackson.core.type.TypeReference import java.time.LocalDate diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt index 0cb74707..a0be03af 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt @@ -1,6 +1,5 @@ package pro.qyoga.tests.cases.app.therapist.therapy.programs -import com.fasterxml.jackson.core.type.TypeReference import io.kotest.inspectors.forAll import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe @@ -24,6 +23,7 @@ import pro.qyoga.tests.pages.therapist.therapy.programs.EditProgramPage import pro.qyoga.tests.pages.therapist.therapy.programs.PROGRAM_FORM_SCRIPT import pro.qyoga.tests.pages.therapist.therapy.programs.ProgramPage.ProgramFormScript import pro.qyoga.tests.pages.therapist.therapy.programs.ProgramsListPage +import tools.jackson.core.type.TypeReference class EditProgramPageTest : QYogaAppIntegrationBaseTest() { @@ -125,4 +125,4 @@ class EditProgramPageTest : QYogaAppIntegrationBaseTest() { document shouldBePage GenericErrorPage } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/OpsClient.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/OpsClient.kt index d6228c97..0e1e8d57 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/OpsClient.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/OpsClient.kt @@ -1,12 +1,12 @@ package pro.qyoga.tests.clients -import com.fasterxml.jackson.databind.JsonNode import io.restassured.http.ContentType import io.restassured.module.kotlin.extensions.Extract import io.restassured.module.kotlin.extensions.Given import io.restassured.module.kotlin.extensions.Then import io.restassured.module.kotlin.extensions.When import org.springframework.http.HttpStatus +import tools.jackson.databind.JsonNode val actuatorPath = "ops/actuator" @@ -32,4 +32,4 @@ class OpsClient( } } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistProgramsApi.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistProgramsApi.kt index 28136c8b..235d9f69 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistProgramsApi.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistProgramsApi.kt @@ -1,8 +1,5 @@ package pro.qyoga.tests.clients.api -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.module.kotlin.convertValue import io.restassured.http.ContentType import io.restassured.http.Cookie import io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath @@ -24,6 +21,9 @@ import pro.qyoga.tests.pages.therapist.therapy.programs.CreateProgramForm import pro.qyoga.tests.pages.therapist.therapy.programs.CreateProgramPage import pro.qyoga.tests.pages.therapist.therapy.programs.EditProgramPage import pro.qyoga.tests.pages.therapist.therapy.programs.ProgramsListPage +import tools.jackson.databind.ObjectMapper +import tools.jackson.databind.node.ObjectNode +import tools.jackson.module.kotlin.convertValue class TherapistProgramsApi(override val authCookie: Cookie) : AuthorizedApi { diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/survey_forms/SurveyFormsSettingsComponent.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/survey_forms/SurveyFormsSettingsComponent.kt index f1f0275f..6ea0c3f1 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/survey_forms/SurveyFormsSettingsComponent.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/survey_forms/SurveyFormsSettingsComponent.kt @@ -1,7 +1,5 @@ package pro.qyoga.tests.pages.therapist.survey_forms -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper import io.kotest.matchers.Matcher import org.jsoup.nodes.Element import pro.qyoga.app.therapist.survey_forms.settings.SurveyFormsSettingsComponentController @@ -10,6 +8,8 @@ import pro.qyoga.tests.assertions.isTag import pro.qyoga.tests.infra.test_config.spring.context import pro.qyoga.tests.platform.html.* import pro.qyoga.tests.platform.kotest.all +import tools.jackson.databind.JsonNode +import tools.jackson.databind.ObjectMapper object SurveyFormsSettingsComponent : Component { @@ -35,7 +35,7 @@ object SurveyFormsSettingsComponent : Component { val formEl = element.select("#$id").single() val formModelJson = formEl.attr("x-data").replace("'", "\"") val formModel = mapper.readValue(formModelJson, JsonNode::class.java) - return formModel.get(yandexAdminEmail.name).asText() + return formModel.get(yandexAdminEmail.name).asString() } } diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/html/Script.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/html/Script.kt index f9bbd1c7..110997cf 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/html/Script.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/html/Script.kt @@ -1,12 +1,12 @@ package pro.qyoga.tests.platform.html -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.ObjectMapper import io.kotest.matchers.Matcher import io.kotest.matchers.compose.all import org.jsoup.nodes.Element import pro.qyoga.tests.assertions.htmlMatch import pro.qyoga.tests.infra.test_config.spring.context +import tools.jackson.core.type.TypeReference +import tools.jackson.databind.ObjectMapper import kotlin.reflect.KClass @@ -45,4 +45,4 @@ abstract class Script( return Matcher.all(*varMatchers.toTypedArray()) } -} \ No newline at end of file +} diff --git a/e2e-tests/build.gradle.kts b/e2e-tests/build.gradle.kts index 613769fa..11af63d6 100644 --- a/e2e-tests/build.gradle.kts +++ b/e2e-tests/build.gradle.kts @@ -1,6 +1,12 @@ group = "pro.qyoga.e2e-tests" version = "0.0.1-SNAPSHOT" +configurations.matching { it.name in setOf("compileClasspath", "testCompileClasspath", "testFixturesCompileClasspath") } + .all { + exclude(group = "com.fasterxml.jackson") + exclude(group = "com.fasterxml.jackson.core") + } + dependencies { implementation(platform("org.springframework.boot:spring-boot-dependencies:${libs.versions.springBoot.get()}")) @@ -16,4 +22,4 @@ tasks { named("test", Test::class) { systemProperties["selenide.browser"] = "chrome" } -} \ No newline at end of file +} From 2e8ae85af03c8afbc7a9d599f4a7d08ca78fec08 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Sun, 1 Mar 2026 17:07:58 +0700 Subject: [PATCH 12/22] =?UTF-8?q?build/qg-xxx:=20RestAssured=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D1=91=D0=BD=20=D0=B4=D0=BE=206.0?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5.* с Spring Boot 4 (и, видимо какой-то версией Groovy) NPE-шился каким-то непонятным образом --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 940daf33..191be3b0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -71,7 +71,7 @@ dependencyResolutionManagement { create("testLibs") { val selenideVersion = version("selenide", "7.10.1") val testContainersVersion = version("testcontainers", "2.0.3") - val restAssuredVersion = version("restAssured", "5.5.6") + val restAssuredVersion = version("restAssured", "6.0.0") val kotestVersion = version("kotest", "5.9.1") val wiremockVersion = version("wiremock", "3.13.1") From e8735baa99077cafa5893447938b49ce2e0e740d Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 13:37:16 +0700 Subject: [PATCH 13/22] =?UTF-8?q?build/qg-xxx:=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BB=D0=B0=D1=82=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D1=8B=20Jetty=20=D0=B2=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D1=85?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=84=D0=B8=D0=BA=D1=81=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=BD=D0=B0=2012.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Потому как Spring Boot переехал на неё, а самая актальная на данный момент версия WireMock - нет, и они бинарно не совместимы - без этого WireMock валится с: Caused by: java.lang.ExceptionInInitializerError: Exception java.lang.NoSuchMethodError: 'org.eclipse.jetty.util.component.Environment org.eclipse.jetty.util.component.Environment.ensure(java.lang.String)' [in thread "main @coroutine#3"] at org.eclipse.jetty.ee10.servlet.ServletContextHandler.(ServletContextHandler.java:135) --- app/build.gradle.kts | 4 ++++ settings.gradle.kts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 313d3a02..21122a3b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -85,6 +85,8 @@ dependencies { testFixturesImplementation("org.springframework.boot:spring-boot-starter-web") testFixturesImplementation("org.springframework.boot:spring-boot-starter-security") testFixturesImplementation("org.springframework.boot:spring-boot-starter-oauth2-client") + testFixturesImplementation(enforcedPlatform(testLibs.jetty)) + testFixturesImplementation(enforcedPlatform(testLibs.jetty.ee10)) testFixturesImplementation("tools.jackson.core:jackson-databind") testFixturesImplementation(libs.minio) testFixturesImplementation(libs.ical4j) @@ -97,6 +99,8 @@ dependencies { testFixturesImplementation(testLibs.testcontainers.minio) testImplementation(testFixtures(project(":app"))) + testImplementation(enforcedPlatform(testLibs.jetty)) + testImplementation(enforcedPlatform(testLibs.jetty.ee10)) testImplementation(testLibs.bundles.restassured) testImplementation(testLibs.archunit) testImplementation(testLibs.mockito.kotlin) diff --git a/settings.gradle.kts b/settings.gradle.kts index 191be3b0..629af1d5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -74,6 +74,7 @@ dependencyResolutionManagement { val restAssuredVersion = version("restAssured", "6.0.0") val kotestVersion = version("kotest", "5.9.1") val wiremockVersion = version("wiremock", "3.13.1") + val jettyVersion = version("jetty", "12.1.0") library("selenide-proxy", "com.codeborne", "selenide-proxy").versionRef(selenideVersion) library("testcontainers-selenium", "org.testcontainers", "testcontainers-selenium").versionRef( @@ -107,6 +108,8 @@ dependencyResolutionManagement { library("wiremock", "org.wiremock", "wiremock").versionRef(wiremockVersion) library("wiremock-jetty12", "org.wiremock", "wiremock-jetty12").versionRef(wiremockVersion) + library("jetty", "org.eclipse.jetty", "jetty-bom").versionRef(jettyVersion) + library("jetty-ee10", "org.eclipse.jetty.ee10", "jetty-ee10-bom").versionRef(jettyVersion) } } } From aaaeb2ac925bc65f8a5057f6fe154a3aeace4ccc Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 13:42:31 +0700 Subject: [PATCH 14/22] =?UTF-8?q?build/qg-xxx:=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D1=81=D1=82=D1=8B=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=B2=D1=8B=D1=82=D0=B0=D1=81=D0=BA=D0=B8=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20DataAccessException=20=D0=B8=D0=B7=20DbActionExe?= =?UTF-8?q?ctionException?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В Spring Data Jdbc 4 https://github.com/spring-projects/spring-data-relational/pull/1956[удалили DbActionExecutionException]. --- .../azhidkov/platform/spring/sdj/ErgoSdjConfig.kt | 5 ----- .../ergo/ErgoPersistenceExceptionTranslator.kt | 15 --------------- .../platform/spring/sdj/ergo/ErgoRepository.kt | 5 ++--- 3 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoPersistenceExceptionTranslator.kt diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt index 9cd25511..33a01b78 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ErgoSdjConfig.kt @@ -3,7 +3,6 @@ package pro.azhidkov.platform.spring.sdj import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.data.jdbc.core.mapping.AggregateReference -import pro.azhidkov.platform.spring.sdj.ergo.ErgoPersistenceExceptionTranslator import tools.jackson.databind.JacksonModule import tools.jackson.databind.module.SimpleModule @@ -21,8 +20,4 @@ class ErgoSdjConfig { fun aggregateReferenceBindingAdvice() = AggregateReferenceBindingAdvice() - @Bean - fun persistenceExceptionTranslator() = - ErgoPersistenceExceptionTranslator() - } diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoPersistenceExceptionTranslator.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoPersistenceExceptionTranslator.kt deleted file mode 100644 index 09828d98..00000000 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoPersistenceExceptionTranslator.kt +++ /dev/null @@ -1,15 +0,0 @@ -package pro.azhidkov.platform.spring.sdj.ergo - -import org.springframework.dao.DataAccessException -import org.springframework.dao.support.PersistenceExceptionTranslator -import org.springframework.data.relational.core.conversion.DbActionExecutionException -import org.springframework.stereotype.Component - - -@Component -class ErgoPersistenceExceptionTranslator : PersistenceExceptionTranslator { - - override fun translateExceptionIfPossible(ex: RuntimeException): DataAccessException? = - (ex as? DbActionExecutionException)?.cause as? DataAccessException - -} \ No newline at end of file diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt index adc50d1f..53693d2d 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt @@ -6,7 +6,6 @@ import org.springframework.data.jdbc.core.JdbcAggregateOperations import org.springframework.data.jdbc.core.convert.EntityRowMapper import org.springframework.data.jdbc.core.convert.JdbcConverter import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository -import org.springframework.data.relational.core.conversion.DbActionExecutionException import org.springframework.data.relational.core.mapping.RelationalMappingContext import org.springframework.data.relational.core.mapping.RelationalPersistentEntity import org.springframework.data.relational.core.query.Query @@ -48,7 +47,7 @@ class ErgoRepository( protected fun saveAndMapDuplicatedKeyException(aggregate: T, map: (DuplicateKeyException) -> Throwable): S { val res = runCatching { super.save(aggregate) } - when (val ex = (res.exceptionOrNull() as? DbActionExecutionException)?.cause as? DuplicateKeyException) { + when (val ex = res.exceptionOrNull() as? DuplicateKeyException) { is DuplicateKeyException -> throw map(ex) } @@ -263,4 +262,4 @@ class ErgoRepository( @Suppress("UNCHECKED_CAST") private fun RelationalMappingContext.getRelationPersistentEntity( type: KClass -): RelationalPersistentEntity = getRequiredPersistentEntity(type.java) as RelationalPersistentEntity \ No newline at end of file +): RelationalPersistentEntity = getRequiredPersistentEntity(type.java) as RelationalPersistentEntity From 8e7bbd1379c7b271f55ea92dd8a6971f93f2a8b2 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Mon, 2 Mar 2026 17:25:03 +0700 Subject: [PATCH 15/22] =?UTF-8?q?build/qg-xxx:=20=D0=B2=D0=BE=D1=81=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D1=80=D0=B0=D1=82=20=D0=BF=D1=83=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=82=D0=B5=D0=BB=D0=B0=20=D1=81=D0=BE=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=BC=20413=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B5=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20mulitipart/f?= =?UTF-8?q?ile-data=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B5=2010=20=D0=9C=D0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В Spring Boot 4 интеграция с OAuth2-клиентом стала триггерить в этом случае SizeLimitExceededException: org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (10485956) exceeds the configured maximum (10485760) at org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl$1.raiseError(FileItemIteratorImpl.java:171) at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:64) at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:160) at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:263) at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:328) at java.base/java.io.FilterInputStream.read(FilterInputStream.java:119) at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:157) at java.base/java.io.FilterInputStream.read(FilterInputStream.java:95) at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:119) at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:468) at org.apache.catalina.connector.Request.parseParts(Request.java:2576) at org.apache.catalina.connector.Request.doParseParameters(Request.java:2927) at org.apache.catalina.connector.Request.parseParameters(Request.java:2864) at org.apache.catalina.connector.Request.getParameterNames(Request.java:1122) at org.apache.catalina.connector.Request.getParameterMap(Request.java:1107) at org.apache.catalina.connector.RequestFacade.getParameterMap(RequestFacade.java:170) at jakarta.servlet.ServletRequestWrapper.getParameterMap(ServletRequestWrapper.java:169) at org.springframework.security.web.firewall.StrictHttpFirewall$StrictFirewalledRequest.getParameterMap(StrictHttpFirewall.java:788) at jakarta.servlet.ServletRequestWrapper.getParameterMap(ServletRequestWrapper.java:169) at jakarta.servlet.ServletRequestWrapper.getParameterMap(ServletRequestWrapper.java:169) at org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter.matchesAuthorizationResponse(OAuth2AuthorizationCodeGrantFilter.java:187) --- .../MultipartSizeLimitExceptionFilter.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 app/src/main/kotlin/pro/qyoga/app/infra/MultipartSizeLimitExceptionFilter.kt diff --git a/app/src/main/kotlin/pro/qyoga/app/infra/MultipartSizeLimitExceptionFilter.kt b/app/src/main/kotlin/pro/qyoga/app/infra/MultipartSizeLimitExceptionFilter.kt new file mode 100644 index 00000000..d5de6001 --- /dev/null +++ b/app/src/main/kotlin/pro/qyoga/app/infra/MultipartSizeLimitExceptionFilter.kt @@ -0,0 +1,57 @@ +package pro.qyoga.app.infra + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException +import org.slf4j.LoggerFactory +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.multipart.MaxUploadSizeExceededException + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +class MultipartSizeLimitExceptionFilter : OncePerRequestFilter() { + + private val log = LoggerFactory.getLogger(javaClass) + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + filterChain.doFilter(request, response) + } catch (ex: Exception) { + if (!isMultipartSizeLimitExceeded(ex)) { + throw ex + } + + log.info("Rejected oversized multipart request: {} {}", request.method, request.requestURI, ex) + + if (response.isCommitted) { + throw ex + } + + response.resetBuffer() + response.status = HttpStatus.PAYLOAD_TOO_LARGE.value() + } + } + + private fun isMultipartSizeLimitExceeded(ex: Throwable): Boolean { + var current: Throwable? = ex + + while (current != null) { + if (current is MaxUploadSizeExceededException || current is SizeLimitExceededException) { + current.printStackTrace() + return true + } + current = current.cause + } + + return false + } +} From 106509ddbbe11ee8e248480c86a8281e9925a5ed Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 13:21:33 +0700 Subject: [PATCH 16/22] =?UTF-8?q?build/qg-xxx:=20=D0=B7=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE=D1=82=20api?= =?UTF-8?q?=20jackarta=20validation=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=B0=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D0=B9=20?= =?UTF-8?q?Spring=20Boot=20Starter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Не связано с переездом на SB 4 - валидация и раньше де-факто не работала --- app/build.gradle.kts | 2 +- settings.gradle.kts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 21122a3b..14bb716d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { implementation("tools.jackson.module:jackson-module-kotlin") implementation("org.flywaydb:flyway-database-postgresql") - implementation(libs.jackarta.validation) + implementation("org.springframework.boot:spring-boot-starter-validation") implementation(libs.thymeleaf.extras.java8time) implementation(libs.postgres) implementation(libs.minio) diff --git a/settings.gradle.kts b/settings.gradle.kts index 629af1d5..686982aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,7 +32,6 @@ dependencyResolutionManagement { plugin("detekt", "io.gitlab.arturbosch.detekt").versionRef(detektVersion) // libs - library("jackarta-validation", "jakarta.validation", "jakarta.validation-api").version("3.1.1") library( "thymeleaf-extras-java8time", "org.thymeleaf.extras", From c6c34afe2971c4a30221a7cf7709be10672667e0 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 13:29:06 +0700 Subject: [PATCH 17/22] =?UTF-8?q?build/qg-xxx:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20Authenticati?= =?UTF-8?q?onProvider-=D0=B0=20PasswordEncoder-=D0=BE=D0=BC=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Без этого SB ругался: WARN o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer - Global AuthenticationManager configured with an AuthenticationProvider bean. UserDetailsService beans will not be used by Spring Security for automatically configuring username/password login. Consider removing the AuthenticationProvider bean. Alternatively, consider using the UserDetailsService in a manually instantiated DaoAuthenticationProvider. If the current configuration is intentional, to turn off this warning, increase the logging level of 'org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer' to ERROR Не связано с миграцией на SB 4 --- .../spring/auth/TestPasswordEncoderConfig.kt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt index 5856c8ec..28939061 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/test_config/spring/auth/TestPasswordEncoderConfig.kt @@ -5,16 +5,12 @@ package pro.qyoga.tests.infra.test_config.spring.auth import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Primary -import org.springframework.security.authentication.dao.DaoAuthenticationProvider -import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.password.NoOpPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder @TestConfiguration -class TestPasswordEncoderConfig( - private val userDetailsService: UserDetailsService -) { +class TestPasswordEncoderConfig { // Стандартный BCryptPasswordEncoder кодирует пароли по 300мс, что слишком расточительно для тестов @Suppress("DEPRECATION") @@ -22,11 +18,4 @@ class TestPasswordEncoderConfig( @Bean fun fastPasswordEncoder(): PasswordEncoder = NoOpPasswordEncoder.getInstance() - @Bean - fun daoAuthenticationProvider(): DaoAuthenticationProvider { - val daoAuthenticationProvider = DaoAuthenticationProvider(userDetailsService) - daoAuthenticationProvider.setPasswordEncoder(fastPasswordEncoder()) - return daoAuthenticationProvider - } - } From d20ebb8c2daeb7ded8f715c09ea1e2dcb9d37b5f Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 14:45:16 +0700 Subject: [PATCH 18/22] =?UTF-8?q?tests/qg-xxx:=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20RestTe?= =?UTF-8?q?stClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Так как он проще в отладке и чуть быстрее --- .../app/publc/surveys/SubmitSurveyTest.kt | 4 +-- .../GoogleAuthorizationIntegrationTest.kt | 2 +- .../qyoga/tests/clients/TherapistClient.kt | 8 +++--- .../qyoga/tests/clients/YandexFormsClient.kt | 15 ++++++----- .../qyoga/tests/clients/api/AuthorizedApi.kt | 6 ++--- .../tests/clients/api/NotificationsApi.kt | 12 ++++----- .../TherapistGoogleCalendarIntegrationApi.kt | 25 +++++++++---------- .../tests/clients/api/WebPushesPublicApi.kt | 15 ++++++----- .../clients/api/WebPushesTherapistApi.kt | 11 ++++---- .../qyoga/tests/infra/web/RestTestClient.kt | 22 ++++++++++++++++ .../qyoga/tests/infra/web/WebTestClient.kt | 14 ----------- .../rest_test_client/ResponseSpecExt.kt | 14 +++++++++++ .../spring/web_test_client/ResponseSpecExt.kt | 19 -------------- 13 files changed, 83 insertions(+), 84 deletions(-) create mode 100644 app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/RestTestClient.kt delete mode 100644 app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/WebTestClient.kt create mode 100644 app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/rest_test_client/ResponseSpecExt.kt delete mode 100644 app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/web_test_client/ResponseSpecExt.kt diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt index 75b1a001..5076bfc4 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/publc/surveys/SubmitSurveyTest.kt @@ -19,7 +19,7 @@ import pro.qyoga.tests.fixture.object_mothers.survey_forms.SurveyFormsSettingsOb import pro.qyoga.tests.fixture.object_mothers.therapists.THE_ADMIN_LOGIN import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_REF import pro.qyoga.tests.infra.web.QYogaAppIntegrationBaseKoTest -import pro.qyoga.tests.infra.web.mainWebTestClient +import pro.qyoga.tests.infra.web.mainRestTestClient import tools.jackson.databind.JsonNode import tools.jackson.databind.node.ObjectNode import tools.jackson.module.kotlin.jacksonObjectMapper @@ -28,7 +28,7 @@ import tools.jackson.module.kotlin.jacksonObjectMapper @DisplayName("Операция создания анкеты") class SubmitSurveyTest : QYogaAppIntegrationBaseKoTest({ - val yandexFormsClient by lazy { YandexFormsClient(mainWebTestClient) } + val yandexFormsClient by lazy { YandexFormsClient(mainRestTestClient) } "при отправке новым клиентом корректного запроса со всеми значениями карточки должна" - { // Сетап diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/calendars/google/GoogleAuthorizationIntegrationTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/calendars/google/GoogleAuthorizationIntegrationTest.kt index 55e6fa19..bafe85fa 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/calendars/google/GoogleAuthorizationIntegrationTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/calendars/google/GoogleAuthorizationIntegrationTest.kt @@ -20,7 +20,7 @@ import pro.qyoga.tests.fixture.wiremocks.MockGoogleOAuthServer import pro.qyoga.tests.infra.test_config.spring.context import pro.qyoga.tests.infra.web.QYogaAppIntegrationBaseKoTest import pro.qyoga.tests.infra.wiremock.WireMock -import pro.qyoga.tests.platform.spring.web_test_client.redirectLocation +import pro.qyoga.tests.platform.spring.rest_test_client.redirectLocation @DisplayName("Интеграция с Google OAuth") diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/TherapistClient.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/TherapistClient.kt index dfb6a1f2..e1c3a18c 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/TherapistClient.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/TherapistClient.kt @@ -8,21 +8,21 @@ import io.restassured.module.kotlin.extensions.When import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.springframework.http.HttpStatus -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.client.RestTestClient import pro.qyoga.tests.clients.api.* import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_LOGIN import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_PASSWORD -import pro.qyoga.tests.infra.web.mainWebTestClient +import pro.qyoga.tests.infra.web.mainRestTestClient class TherapistClient( val authCookie: Cookie, - webTestClient: WebTestClient = mainWebTestClient + restTestClient: RestTestClient = mainRestTestClient ) { // Work val appointments = TherapistAppointmentsApi(authCookie) - val googleCalendarIntegration = TherapistGoogleCalendarIntegrationApi(authCookie, webTestClient) + val googleCalendarIntegration = TherapistGoogleCalendarIntegrationApi(authCookie, restTestClient) val clients = TherapistClientsApi(authCookie) val clientJournal = TherapistClientJournalApi(authCookie) val clientFiles = TherapistClientFilesApi(authCookie) diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/YandexFormsClient.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/YandexFormsClient.kt index d76a7e34..3c253933 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/YandexFormsClient.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/YandexFormsClient.kt @@ -3,14 +3,14 @@ package pro.qyoga.tests.clients import org.springframework.http.HttpStatus import org.springframework.http.HttpStatusCode import org.springframework.http.ResponseEntity -import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.test.web.reactive.server.returnResult +import org.springframework.test.web.servlet.client.RestTestClient +import org.springframework.test.web.servlet.client.returnResult import pro.azhidkov.platform.spring.http.ErrorResponse import pro.qyoga.app.publc.surverys.SurveysController class YandexFormsClient( - private val client: WebTestClient + private val client: RestTestClient ) { fun createSurvey(surveyJsonStr: String): HttpStatusCode { @@ -26,16 +26,15 @@ class YandexFormsClient( return createSurveyForResponse(entranceSurveyJsonStr) .expectStatus().isEqualTo(expectedStatus) .returnResult(ErrorResponse::class.java) - .responseBody - .blockFirst()!! + .responseBody!! } - fun createSurveyForResponse(entranceSurveyJsonStr: String): WebTestClient.ResponseSpec { + fun createSurveyForResponse(entranceSurveyJsonStr: String): RestTestClient.ResponseSpec { return client .post() .uri(SurveysController.PATH) - .bodyValue(entranceSurveyJsonStr) + .body(entranceSurveyJsonStr) .exchange() } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/api/AuthorizedApi.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/api/AuthorizedApi.kt index 2e491dd3..ec3a4288 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/api/AuthorizedApi.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/api/AuthorizedApi.kt @@ -2,7 +2,7 @@ package pro.qyoga.tests.clients.api import io.restassured.http.Cookie import io.restassured.specification.RequestSpecification -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.client.RestTestClient interface AuthorizedApi { @@ -13,8 +13,8 @@ interface AuthorizedApi { return cookie(authCookie) } - fun WebTestClient.RequestHeadersSpec<*>.authorized(): WebTestClient.RequestHeadersSpec<*> { + fun RestTestClient.RequestHeadersSpec<*>.authorized(): RestTestClient.RequestHeadersSpec<*> { return cookie(authCookie.name, authCookie.value) } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/api/NotificationsApi.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/api/NotificationsApi.kt index 2e7c7608..10f69dab 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/api/NotificationsApi.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/api/NotificationsApi.kt @@ -3,16 +3,16 @@ package pro.qyoga.tests.clients.api import io.restassured.http.Cookie import org.jsoup.Jsoup import org.jsoup.nodes.Document -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.client.RestTestClient import pro.qyoga.app.therapist.appointments.core.schedule.settings.NotificationsSettingsController -import pro.qyoga.tests.infra.web.mainWebTestClient -import pro.qyoga.tests.platform.spring.web_test_client.getBodyAsString +import pro.qyoga.tests.infra.web.mainRestTestClient +import pro.qyoga.tests.platform.spring.rest_test_client.getBodyAsString object NotificationsApiFactory { fun therapistApi( principal: Cookie, - ) = NotificationsTherapistApi(principal, mainWebTestClient) + ) = NotificationsTherapistApi(principal, mainRestTestClient) } @@ -22,11 +22,11 @@ val TrainerAdvisorApis.Notifications class NotificationsTherapistApi( override val authCookie: Cookie, - private val webTestClient: WebTestClient + private val restTestClient: RestTestClient ) : AuthorizedApi { fun getNotificationsSettings(): Document { - return webTestClient.get() + return restTestClient.get() .uri(NotificationsSettingsController.PATH) .authorized() .exchange() diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistGoogleCalendarIntegrationApi.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistGoogleCalendarIntegrationApi.kt index f8ade129..afd2803b 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistGoogleCalendarIntegrationApi.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/api/TherapistGoogleCalendarIntegrationApi.kt @@ -4,21 +4,20 @@ import io.restassured.http.Cookie import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse -import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.web.reactive.function.BodyInserters.fromValue +import org.springframework.test.web.servlet.client.RestTestClient import pro.qyoga.app.therapist.appointments.core.schedule.settings.GoogleCalendarSettingsController import pro.qyoga.i9ns.calendars.google.model.GoogleAccountRef -import pro.qyoga.tests.platform.spring.web_test_client.getBodyAsString -import pro.qyoga.tests.platform.spring.web_test_client.redirectLocation +import pro.qyoga.tests.platform.spring.rest_test_client.getBodyAsString +import pro.qyoga.tests.platform.spring.rest_test_client.redirectLocation import java.net.URI class TherapistGoogleCalendarIntegrationApi( override val authCookie: Cookie, - private val webTestClient: WebTestClient + private val restTestClient: RestTestClient ) : AuthorizedApi { fun authorizeInGoogle(): URI { - val response = webTestClient.get() + val response = restTestClient.get() .uri("/oauth2/authorization/google") .authorized() .exchange() @@ -33,8 +32,8 @@ class TherapistGoogleCalendarIntegrationApi( // scope=https://www.googleapis.com/auth/calendar.readonly' \ fun handleOAuthCallbackForResponse( authResponse: OAuth2AuthorizationResponse - ): WebTestClient.ResponseSpec { - return webTestClient.get() + ): RestTestClient.ResponseSpec { + return restTestClient.get() .uri { it.path("/therapist/oauth2/google/callback") .queryParam("state", authResponse.state) @@ -45,15 +44,15 @@ class TherapistGoogleCalendarIntegrationApi( .exchange() } - fun finalizeOAuthCallbackForResponse(): WebTestClient.ResponseSpec { - return webTestClient.get() + fun finalizeOAuthCallbackForResponse(): RestTestClient.ResponseSpec { + return restTestClient.get() .uri("/therapist/oauth2/google/callback") .authorized() .exchange() } fun getGoogleCalendarComponent(): Document { - return webTestClient.get() + return restTestClient.get() .uri(GoogleCalendarSettingsController.PATH) .authorized() .exchange() @@ -67,9 +66,9 @@ class TherapistGoogleCalendarIntegrationApi( "shouldBeShown" to shouldBeShown ) - webTestClient.patch() + restTestClient.patch() .uri(GoogleCalendarSettingsController.updateCalendarSettingsPath(googleAccount, calendarId)) - .body(fromValue(body)) + .body(body) .authorized() .exchange() .expectStatus().isNoContent diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesPublicApi.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesPublicApi.kt index 0477555f..7eb6f1ca 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesPublicApi.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesPublicApi.kt @@ -1,19 +1,19 @@ package pro.qyoga.tests.clients.api import io.restassured.http.Cookie -import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.test.web.reactive.server.returnResult +import org.springframework.test.web.servlet.client.RestTestClient +import org.springframework.test.web.servlet.client.returnResult import pro.qyoga.app.publc.pushes.web.PushesPublicKeyController -import pro.qyoga.tests.infra.web.mainWebTestClient +import pro.qyoga.tests.infra.web.mainRestTestClient object WebPushesApiFactory { - val publicApi = WebPushesPublicApi(mainWebTestClient) + val publicApi = WebPushesPublicApi(mainRestTestClient) fun therapistApi( principal: Cookie, - ) = WebPushesTherapistApi(principal, mainWebTestClient) + ) = WebPushesTherapistApi(principal, mainRestTestClient) } @@ -22,7 +22,7 @@ val TrainerAdvisorApis.WebPushes get() = WebPushesApiFactory class WebPushesPublicApi( - private val client: WebTestClient + private val client: RestTestClient ) { fun getPublicKey(): String { @@ -31,8 +31,7 @@ class WebPushesPublicApi( .exchange() .expectStatus().isOk .returnResult() - .responseBody - .blockFirst() ?: "" + .responseBody ?: "" } } diff --git a/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesTherapistApi.kt b/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesTherapistApi.kt index 9622ac5e..b795b9e5 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesTherapistApi.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/clients/api/WebPushesTherapistApi.kt @@ -1,27 +1,26 @@ package pro.qyoga.tests.clients.api import io.restassured.http.Cookie -import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.web.reactive.function.BodyInserters.fromValue +import org.springframework.test.web.servlet.client.RestTestClient import pro.qyoga.app.pushes.web.WebPushesController import pro.qyoga.i9ns.pushes.web.model.WebPushSubscription class WebPushesTherapistApi( override val authCookie: Cookie, - private val webTestClient: WebTestClient + private val restTestClient: RestTestClient ) : AuthorizedApi { fun createSubscription(subscription: WebPushSubscription) { - webTestClient.post() + restTestClient.post() .uri(WebPushesController.PATH) - .body(fromValue(subscription)) + .body(subscription) .authorized() .exchange() .expectStatus().isNoContent } fun deleteSubscription(p256dh: String) { - webTestClient.delete() + restTestClient.delete() .uri(WebPushesController.DELETE_SUBSCRIPTION_PATH, p256dh) .authorized() .exchange() diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/RestTestClient.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/RestTestClient.kt new file mode 100644 index 00000000..c1a29edb --- /dev/null +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/RestTestClient.kt @@ -0,0 +1,22 @@ +package pro.qyoga.tests.infra.web + +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory +import org.springframework.test.web.servlet.client.RestTestClient +import pro.qyoga.tests.infra.test_config.spring.baseUrl + + +val mainRestTestClient: RestTestClient by lazy { createRestTestClient() } + +fun createRestTestClient(context: ConfigurableApplicationContext = pro.qyoga.tests.infra.test_config.spring.context): RestTestClient = + RestTestClient.bindToServer( + HttpComponentsClientHttpRequestFactory( + HttpClients.custom() + .disableRedirectHandling() + .build() + ) + ) + .baseUrl(context.baseUrl) + .defaultHeader("Content-Type", "application/json;charset=UTF-8") + .build() diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/WebTestClient.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/WebTestClient.kt deleted file mode 100644 index 1a185a42..00000000 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/infra/web/WebTestClient.kt +++ /dev/null @@ -1,14 +0,0 @@ -package pro.qyoga.tests.infra.web - -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.test.web.reactive.server.WebTestClient -import pro.qyoga.tests.infra.test_config.spring.baseUrl - - -val mainWebTestClient: WebTestClient by lazy { createWebTestClient() } - -fun createWebTestClient(context: ConfigurableApplicationContext = pro.qyoga.tests.infra.test_config.spring.context): WebTestClient = - WebTestClient.bindToServer() - .baseUrl(context.baseUrl) - .defaultHeader("Content-Type", "application/json;charset=UTF-8") - .build() \ No newline at end of file diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/rest_test_client/ResponseSpecExt.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/rest_test_client/ResponseSpecExt.kt new file mode 100644 index 00000000..76f68e46 --- /dev/null +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/rest_test_client/ResponseSpecExt.kt @@ -0,0 +1,14 @@ +package pro.qyoga.tests.platform.spring.rest_test_client + +import org.springframework.test.web.servlet.client.RestTestClient +import java.net.URI + + +fun RestTestClient.ResponseSpec.getBodyAsString(): String = + String(this.returnResult().responseBodyContent) + +fun RestTestClient.ResponseSpec.redirectLocation(): URI = + this + .expectStatus().isFound.returnResult() + .responseHeaders + .location!! diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/web_test_client/ResponseSpecExt.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/web_test_client/ResponseSpecExt.kt deleted file mode 100644 index 0aa22851..00000000 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/platform/spring/web_test_client/ResponseSpecExt.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pro.qyoga.tests.platform.spring.web_test_client - -import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.test.web.reactive.server.returnResult -import java.net.URI - - -fun WebTestClient.ResponseSpec.getBodyAsString(): String = - this.returnResult(String::class.java) - .responseBody - .collectList() - .block()!! - .joinToString("\n") - -fun WebTestClient.ResponseSpec.redirectLocation(): URI = - this - .expectStatus().isFound.returnResult() - .responseHeaders - .location!! \ No newline at end of file From df5201fef9b17eece3a6f5a9ca40a38cd3209464 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 14:46:11 +0700 Subject: [PATCH 19/22] =?UTF-8?q?tests/qg-xxx:=20=D0=BF=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B0=20=D1=81=D1=82=D0=B0=D0=B1=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BC=D0=B8=D0=B3=D0=B0?= =?UTF-8?q?=D1=8E=D1=89=D0=B5=D0=B3=D0=BE=20=D1=82=D0=B5=D1=81=D1=82=D0=B0?= =?UTF-8?q?=20ical-=D0=BA=D0=B0=D0=BB=D0=B5=D0=BD=D0=B4=D0=B0=D1=80=D0=B5?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/cases/i9ns/calendars/ical/ICalCalendarTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/i9ns/calendars/ical/ICalCalendarTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/i9ns/calendars/ical/ICalCalendarTest.kt index b5a653bc..e711327a 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/i9ns/calendars/ical/ICalCalendarTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/i9ns/calendars/ical/ICalCalendarTest.kt @@ -14,6 +14,7 @@ import pro.qyoga.i9ns.calendars.ical.model.findById import pro.qyoga.i9ns.calendars.ical.model.vEvents import pro.qyoga.tests.fixture.object_mothers.calendars.ical.ICalCalendarsObjectMother.aICalCalendar import java.time.Duration +import java.time.ZoneId import java.time.ZonedDateTime @@ -25,7 +26,10 @@ class ICalCalendarTest : FreeSpec({ val ical = aICalCalendar(singleWeeklyEvent) "при запросе событий за период включающим это событие" - { - val interval = Interval.of(ZonedDateTime.now(), Duration.ofDays(7)) + val interval = Interval.of( + ZonedDateTime.of(2025, 3, 26, 0, 0, 0, 0, ZoneId.of("Asia/Novosibirsk")), + Duration.ofDays(7) + ) val events = ical.calendarItemsIn(interval)!! "должен вернуть одно событие" { From 838b51ba848785ee4c85327485f46c051e498c5a Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 14:50:35 +0700 Subject: [PATCH 20/22] =?UTF-8?q?build/qg-xxx:=20Kotlin=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D1=91=D0=BD=20=D0=B4=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D0=B8=202.3.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 686982aa..e8925e01 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,7 +6,7 @@ dependencyResolutionManagement { versionCatalogs { create("libs") { // plugin versions - val kotlinVersion = version("kotlin", "2.2.10") + val kotlinVersion = version("kotlin", "2.3.10") val springBootVersion = version("springBoot", "4.0.3") val springDependencyManagementVersion = version("springDependencyManagement", "1.1.7") val koverVersion = version("kover", "0.9.2") From 7d1a2beb39e71086d48258d726b9f1b5f599117e Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 16:46:04 +0700 Subject: [PATCH 21/22] =?UTF-8?q?chore/qg-xxx:=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D1=81=D0=B5=20?= =?UTF-8?q?=D0=B2=D0=B0=D1=80=D0=BD=D0=B8=D0=BD=D0=B3=D0=B8=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=B8=D0=BB=D1=8F=D1=82=D0=BE=D1=80=D0=B0=20=D0=B8?= =?UTF-8?q?=20Gradle-=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- .../spring/sdj/ergo/ErgoRepository.kt | 20 ++++++++++------ .../spring/sdj/ergo/hydration/Hydration.kt | 23 ++++++++++++------- .../spring/tx/TransactionTemplateExt.kt | 6 ++--- .../core/edit/ops/UpdateAppointmentOp.kt | 4 ++-- .../core/appointments/core/Transformations.kt | 4 ++-- .../core/clients/files/ClientFilesService.kt | 6 ++--- .../settings/model/SurveyFormsSettings.kt | 4 ++-- .../therapy/exercises/ExercisesServiceImpl.kt | 6 ++--- .../pro/qyoga/core/users/auth/model/User.kt | 4 ++-- .../therapy/programs/EditProgramPageTest.kt | 2 +- .../backgrounds/AppointmentsBackgrounds.kt | 4 ++-- .../appointments/AppointmentsObjectMother.kt | 2 +- .../therapist/appointments/AppointmentForm.kt | 2 +- build.gradle.kts | 9 +++++--- config/detekt/detekt.yml | 17 ++++---------- .../tests/platform/selenide/SelenideExt.kt | 13 +++++++++-- settings.gradle.kts | 4 ++-- 18 files changed, 74 insertions(+), 58 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 14bb716d..04d75772 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -238,7 +238,7 @@ configurations.matching { it.name == "detekt" }.all { resolutionStrategy.eachDependency { if (requested.group == "org.jetbrains.kotlin") { @Suppress("UnstableApiUsage") - useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion()) + useVersion(dev.detekt.gradle.plugin.getSupportedKotlinVersion()) } } } diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt index 53693d2d..cca05b6c 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/ErgoRepository.kt @@ -161,13 +161,19 @@ class ErgoRepository( queryBuilder: QueryBuilder.() -> Unit = {} ): Page { val query = query(queryBuilder) - val res = jdbcAggregateTemplate.findAll(query, relationalPersistentEntity.type, pageRequest) - return res.mapContent { - jdbcAggregateTemplate.hydrate( - res, - FetchSpec(fetch) - ) - } + + val content = jdbcAggregateTemplate.findAll(query.with(pageRequest), relationalPersistentEntity.type) + .let { jdbcAggregateTemplate.hydrate(it, FetchSpec(fetch)) } + + val total = jdbcAggregateTemplate.count(query, relationalPersistentEntity.type) + + val page = PageImpl( + content, + pageRequest, + total + ) + + return page } fun findSlice( diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/hydration/Hydration.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/hydration/Hydration.kt index 225587f1..148332f8 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/hydration/Hydration.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/sdj/ergo/hydration/Hydration.kt @@ -11,7 +11,7 @@ import kotlin.reflect.jvm.jvmErasure data class PropertyFetchSpec( val property: KProperty1, - val fetchSpec: FetchSpec<*> = FetchSpec( + val fetchSpec: FetchSpec = FetchSpec( emptyList>() ) ) @@ -42,15 +42,14 @@ fun JdbcAggregateOperations.hydrate( return (entities as? List) ?: entities.toList() } - val refs: Map?>, Map> = + val refs: Map, Map> = fetchSpec.propertyFetchSpecs.filter { detectRefType( it.property ) != null } .associate { it: PropertyFetchSpec -> - val property = it.property as KProperty1<*, AggregateReference<*, *>?> - property to fetchPropertyRefs(entities, it) + it.property to fetchPropertyRefs(entities, it) } if (refs.isEmpty()) { @@ -69,11 +68,19 @@ private fun JdbcAggregateOperations.fetchPropertyRefs( val property = propertyFetchSpec.property val ids = fetchIds(entities, property) val targetType = (property.returnType.arguments[0].type!!.classifier!! as KClass<*>).java - val refs = hydrate(this.findAllById(ids, targetType), propertyFetchSpec.fetchSpec as FetchSpec) + val refs = hydrateAny(this.findAllById(ids, targetType), propertyFetchSpec.fetchSpec) .associateBy { (it as Identifiable<*>).id } return refs } +@Suppress("UNCHECKED_CAST") +private fun JdbcAggregateOperations.hydrateAny( + entities: Iterable<*>, + fetchSpec: FetchSpec +): List { + return hydrate(entities as Iterable, fetchSpec as FetchSpec) +} + private fun fetchIds( entities: Iterable, property: KProperty1 @@ -87,12 +94,12 @@ private fun fetchIds( else -> error("Unsupported property type: $property") } -private fun hydrateEntity(entity: T, refs: Map?>, Map>): T { +private fun hydrateEntity(entity: T, refs: Map, Map>): T { val constructorParams = entity::class.primaryConstructor!!.parameters val paramValues = constructorParams.associateWith { param -> val prop = - entity::class.memberProperties.find { prop -> param.name == prop.name }!! as KProperty1?> - val currentValue: Any? = prop.invoke(entity) + entity::class.memberProperties.find { prop -> param.name == prop.name }!! + val currentValue = prop.getter.call(entity) val newValue = if (prop in refs) { val id = (currentValue as AggregateReference<*, *>?)?.id if (id != null) { diff --git a/app/src/main/kotlin/pro/azhidkov/platform/spring/tx/TransactionTemplateExt.kt b/app/src/main/kotlin/pro/azhidkov/platform/spring/tx/TransactionTemplateExt.kt index 27568727..bdec826b 100644 --- a/app/src/main/kotlin/pro/azhidkov/platform/spring/tx/TransactionTemplateExt.kt +++ b/app/src/main/kotlin/pro/azhidkov/platform/spring/tx/TransactionTemplateExt.kt @@ -7,11 +7,9 @@ interface TransactionalService { val transactionTemplate: TransactionTemplate fun transaction(body: () -> T): T { - val res = transactionTemplate.execute { + return transactionTemplate.execute { body() } - @Suppress("UNCHECKED_CAST") - return res as T } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/app/therapist/appointments/core/edit/ops/UpdateAppointmentOp.kt b/app/src/main/kotlin/pro/qyoga/app/therapist/appointments/core/edit/ops/UpdateAppointmentOp.kt index b747b68d..590d5101 100644 --- a/app/src/main/kotlin/pro/qyoga/app/therapist/appointments/core/edit/ops/UpdateAppointmentOp.kt +++ b/app/src/main/kotlin/pro/qyoga/app/therapist/appointments/core/edit/ops/UpdateAppointmentOp.kt @@ -38,9 +38,9 @@ class UpdateAppointmentOp( val typeRef = appointmentTypesRepo.createTypeIfNew(therapistRef, editAppointmentRequest) - return appointmentsRepo.updateById(appointmentRef.id!!) { appointment -> + return appointmentsRepo.updateById(appointmentRef.id) { appointment -> appointment.patchBy(editAppointmentRequest, typeRef) } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/core/appointments/core/Transformations.kt b/app/src/main/kotlin/pro/qyoga/core/appointments/core/Transformations.kt index 7d6d32df..7b32add8 100644 --- a/app/src/main/kotlin/pro/qyoga/core/appointments/core/Transformations.kt +++ b/app/src/main/kotlin/pro/qyoga/core/appointments/core/Transformations.kt @@ -38,7 +38,7 @@ fun Appointment.toEditRequest(resolveTimeZone: (ZoneId) -> LocalizedTimeZone?) = clientRef, clientRef.resolveOrNull()?.fullName() ?: clientRef.id.toString(), typeRef, - typeRef.resolveOrNull()?.name ?: typeRef.id?.toString() ?: "", + typeRef.resolveOrNull()?.name ?: typeRef.id.toString(), therapeuticTaskRef, therapeuticTaskRef?.resolveOrNull()?.name ?: therapeuticTaskRef?.id?.toString() ?: "", wallClockDateTime, @@ -50,4 +50,4 @@ fun Appointment.toEditRequest(resolveTimeZone: (ZoneId) -> LocalizedTimeZone?) = payed, status, comment -) \ No newline at end of file +) diff --git a/app/src/main/kotlin/pro/qyoga/core/clients/files/ClientFilesService.kt b/app/src/main/kotlin/pro/qyoga/core/clients/files/ClientFilesService.kt index 4c3c3851..d213b948 100644 --- a/app/src/main/kotlin/pro/qyoga/core/clients/files/ClientFilesService.kt +++ b/app/src/main/kotlin/pro/qyoga/core/clients/files/ClientFilesService.kt @@ -40,7 +40,7 @@ class ClientFilesService( val clientFile = clientFilesRepo.findFile(clientId, fileId) ?: return null - return clientFilesStorage.findByIdOrNull(clientFile.fileRef.id!!) + return clientFilesStorage.findByIdOrNull(clientFile.fileRef.id) } fun deleteFile(clientId: UUID, fileId: Long): ClientFile? { @@ -50,7 +50,7 @@ class ClientFilesService( clientFilesRepo.deleteById(clientFile.id) try { - clientFilesStorage.deleteById(clientFile.fileRef.id!!) + clientFilesStorage.deleteById(clientFile.fileRef.id) } catch (ex: Exception) { log.warn("Client file deletion failed", ex) } @@ -58,4 +58,4 @@ class ClientFilesService( return clientFile } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/core/survey_forms/settings/model/SurveyFormsSettings.kt b/app/src/main/kotlin/pro/qyoga/core/survey_forms/settings/model/SurveyFormsSettings.kt index 64b1ba4a..bcf979c9 100644 --- a/app/src/main/kotlin/pro/qyoga/core/survey_forms/settings/model/SurveyFormsSettings.kt +++ b/app/src/main/kotlin/pro/qyoga/core/survey_forms/settings/model/SurveyFormsSettings.kt @@ -18,7 +18,7 @@ data class SurveyFormsSettings( ) : Identifiable { @Transient - override val id: UUID = therapistRef.id!! + override val id: UUID = therapistRef.id companion object { @@ -30,4 +30,4 @@ data class SurveyFormsSettings( } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/ExercisesServiceImpl.kt b/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/ExercisesServiceImpl.kt index ef7b0e27..6cbe3d48 100644 --- a/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/ExercisesServiceImpl.kt +++ b/app/src/main/kotlin/pro/qyoga/core/therapy/exercises/ExercisesServiceImpl.kt @@ -40,7 +40,7 @@ class ExercisesService( val stepIdxToStepImageId = exerciseStepsImagesStorage.uploadAllStepImages(stepImages) .mapValues { it.value.id } - val exercise = Exercise.of(createExerciseRequest, stepIdxToStepImageId, therapistRef.id!!) + val exercise = Exercise.of(createExerciseRequest, stepIdxToStepImageId, therapistRef.id) return exercisesRepo.save(exercise) } @@ -86,7 +86,7 @@ class ExercisesService( val imageId = exercise.steps[stepIdx].imageId ?: return null - return exerciseStepsImagesStorage.findByIdOrNull(imageId.id!!) + return exerciseStepsImagesStorage.findByIdOrNull(imageId.id) } fun deleteById(exerciseId: Long) { @@ -121,4 +121,4 @@ fun FilesStorage.uploadAllStepImages(stepImages: Map): Map stepIdx to persistedImage } return stepIdxToStepImage -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/core/users/auth/model/User.kt b/app/src/main/kotlin/pro/qyoga/core/users/auth/model/User.kt index a9a4eafd..f2124e4c 100644 --- a/app/src/main/kotlin/pro/qyoga/core/users/auth/model/User.kt +++ b/app/src/main/kotlin/pro/qyoga/core/users/auth/model/User.kt @@ -13,7 +13,7 @@ import java.util.* typealias UserRef = AggregateReference -fun UserRef(therapistRef: TherapistRef): UserRef = AggregateReference.to(therapistRef.id!!) +fun UserRef(therapistRef: TherapistRef): UserRef = AggregateReference.to(therapistRef.id) @Table("users") data class User( @@ -66,4 +66,4 @@ data class User( return result } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt index a0be03af..42196222 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/therapy/programs/EditProgramPageTest.kt @@ -99,7 +99,7 @@ class EditProgramPageTest : QYogaAppIntegrationBaseTest() { // When val document = therapist.programs.updateProgramForError( program.id, - CreateProgramRequest(program.title, program.exercises.map { it.exerciseRef.id!! }), + CreateProgramRequest(program.title, program.exercises.map { it.exerciseRef.id }), notExistingTherapeuticTask ) diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/AppointmentsBackgrounds.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/AppointmentsBackgrounds.kt index d18c63fe..ccf6d376 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/AppointmentsBackgrounds.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/AppointmentsBackgrounds.kt @@ -59,7 +59,7 @@ class AppointmentsBackgrounds( appointmentStatus: AppointmentStatus = AppointmentStatus.entries.randomElement(), comment: String? = randomSentence(), therapist: TherapistRef = THE_THERAPIST_REF, - therapeuticTaskRef: TherapeuticTaskRef? = therapeuticTasksBackgrounds.createTherapeuticTask(therapist.id!!) + therapeuticTaskRef: TherapeuticTaskRef? = therapeuticTasksBackgrounds.createTherapeuticTask(therapist.id) .ref(), ): Appointment { return create(dateTime, timeZone, duration, place, cost, payed, appointmentStatus, comment, therapist, therapeuticTaskRef) @@ -77,7 +77,7 @@ class AppointmentsBackgrounds( therapist: TherapistRef = THE_THERAPIST_REF, therapeuticTaskRef: TherapeuticTaskRef? = null, ): Appointment { - val clientRef = clientsBackgrounds.createClients(1, therapist.id!!).single().ref() + val clientRef = clientsBackgrounds.createClients(1, therapist.id).single().ref() val appointment = createAppointment( therapist, AppointmentsObjectMother.randomEditAppointmentRequest( diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/appointments/AppointmentsObjectMother.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/appointments/AppointmentsObjectMother.kt index 243f523e..d09fccc8 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/appointments/AppointmentsObjectMother.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/appointments/AppointmentsObjectMother.kt @@ -28,7 +28,7 @@ object AppointmentsObjectMother { appointmentStatus: AppointmentStatus = AppointmentStatus.entries.randomElement(), ): LocalizedAppointmentSummary { return LocalizedAppointmentSummary( - aAppointmentId().id!!, + aAppointmentId().id, client.resolveOrThrow().fullName(), typeTitle, dateTime, diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/appointments/AppointmentForm.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/appointments/AppointmentForm.kt index b00bd729..afca45ee 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/appointments/AppointmentForm.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/pages/therapist/appointments/AppointmentForm.kt @@ -88,7 +88,7 @@ object EditAppointmentForm : AppointmentForm(FormAction.hxPut("$PATH/{appointmen fun statusFromXData(element: Element): String? { val formElement = element.select("form[x-data]").single() - val xDataValue = formElement?.attr("x-data") ?: return null + val xDataValue = formElement.attr("x-data") val regex = """'status':\s'(.*)'""".toRegex() val matchResult = regex.find(xDataValue) diff --git a/build.gradle.kts b/build.gradle.kts index d7f5862a..182e8cc7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -26,7 +27,7 @@ subprojects { } } - configure { + configure { toolVersion = detektPlugin.version.requiredVersion config.setFrom(file(rootProject.rootDir.resolve("config/detekt/detekt.yml"))) @@ -35,8 +36,10 @@ subprojects { tasks.withType { compilerOptions { - freeCompilerArgs = listOf("-Xjsr305=strict", "-Xjvm-default=all", "-Xwhen-guards") + freeCompilerArgs = listOf("-Xjsr305=strict", "-Xwhen-guards") jvmTarget.set(JvmTarget.JVM_21) + jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY) + allWarningsAsErrors = true } } @@ -45,4 +48,4 @@ subprojects { useJUnitPlatform() } -} \ No newline at end of file +} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 1ae7b19a..48f5af4a 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -1,21 +1,14 @@ -build: - maxIssues: 0 - excludeCorrectable: false - weights: - # complexity: 2 - # LongParameterList: 1 - # style: 1 - # comments: 1 - style: - UnusedImports: + UnusedImport: active: true UnusedPrivateClass: active: true - UnusedPrivateMember: + UnusedPrivateFunction: + active: true + UnusedPrivateProperty: active: true complexity: CognitiveComplexMethod: active: true - threshold: 15 \ No newline at end of file + allowedComplexity: 15 diff --git a/e2e-tests/src/test/kotlin/pro/qyoga/tests/platform/selenide/SelenideExt.kt b/e2e-tests/src/test/kotlin/pro/qyoga/tests/platform/selenide/SelenideExt.kt index e1752a22..85e330cf 100644 --- a/e2e-tests/src/test/kotlin/pro/qyoga/tests/platform/selenide/SelenideExt.kt +++ b/e2e-tests/src/test/kotlin/pro/qyoga/tests/platform/selenide/SelenideExt.kt @@ -4,6 +4,7 @@ import com.codeborne.selenide.Condition.attribute import com.codeborne.selenide.Condition.visible import com.codeborne.selenide.Selenide import com.codeborne.selenide.Selenide.`$` +import com.codeborne.selenide.Selenide.executeJavaScript import com.codeborne.selenide.SelenideElement import com.codeborne.selenide.TypeOptions import pro.qyoga.tests.platform.html.ComboBox @@ -63,16 +64,24 @@ fun open(page: HtmlPageCompat) { fun click(component: Component) { `$`(component.selector()) - .scrollIntoView("{behavior: \"instant\", block: \"center\", inline: \"center\"}") + .scrollToCenter() .click() } fun SelenideElement.click(selector: String) { `$`(selector) - .scrollIntoView("{behavior: \"instant\", block: \"center\", inline: \"center\"}") + .scrollToCenter() .click() } +private fun SelenideElement.scrollToCenter(): SelenideElement { + executeJavaScript( + "arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'})", + this + ) + return this +} + fun await(page: HtmlPageCompat) { requireNotNull(page.title) { "Невозможно дождаться страницы без названия" } `$`("title").shouldHave(attribute("text", page.title!!)) diff --git a/settings.gradle.kts b/settings.gradle.kts index e8925e01..738ce6cd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,7 +11,7 @@ dependencyResolutionManagement { val springDependencyManagementVersion = version("springDependencyManagement", "1.1.7") val koverVersion = version("kover", "0.9.2") val gitPropertiesVersion = version("gitProperties", "2.5.3") - val detektVersion = version("dekekt", "1.23.8") + val detektVersion = version("dekekt", "2.0.0-alpha.1") // lib versions val poiVersion = version("poi", "5.4.1") @@ -29,7 +29,7 @@ dependencyResolutionManagement { plugin("kover", "org.jetbrains.kotlinx.kover").versionRef(koverVersion) plugin("gitProperties", "com.gorylenko.gradle-git-properties").versionRef(gitPropertiesVersion) - plugin("detekt", "io.gitlab.arturbosch.detekt").versionRef(detektVersion) + plugin("detekt", "dev.detekt").versionRef(detektVersion) // libs library( From 1455473648e324709bcce83acaf30f84eec0b6c3 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Tue, 3 Mar 2026 16:46:26 +0700 Subject: [PATCH 22/22] =?UTF-8?q?build/qg-xxx:=20Gradle=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D1=91=D0=BD=20=D0=B4=D0=BE=209.3.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 +---- gradlew.bat | 3 +-- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55baabb587c669f562ae36f953de2481846..8bdaf60c75ab801e22807dde59e12a8735a34077 100644 GIT binary patch delta 37256 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 delta 35551 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e111328..37f78a6a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 23d15a93..adff685a 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index db3a6ac2..c4bdd3ab 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell