Skip to content

Commit 93825de

Browse files
committed
feat/qg-253: WIP: добавлена обработка ошибки получения Google Calendars
1 parent 37a25f6 commit 93825de

File tree

13 files changed

+240
-70
lines changed

13 files changed

+240
-70
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package pro.azhidkov.platform.spring.sdj.converters
2+
3+
import org.springframework.core.convert.converter.Converter
4+
import org.springframework.data.convert.ReadingConverter
5+
import org.springframework.data.convert.WritingConverter
6+
7+
data class SecretChars(val value: CharArray) {
8+
override fun equals(other: Any?): Boolean {
9+
if (this === other) return true
10+
if (javaClass != other?.javaClass) return false
11+
12+
other as SecretChars
13+
14+
return value.contentEquals(other.value)
15+
}
16+
17+
override fun hashCode(): Int {
18+
return value.contentHashCode()
19+
}
20+
}
21+
22+
@WritingConverter
23+
class SecretCharsToString : Converter<SecretChars, String> {
24+
override fun convert(source: SecretChars) = String(source.value)
25+
}
26+
27+
@ReadingConverter
28+
class StringToSecretChars : Converter<String, SecretChars> {
29+
override fun convert(source: String) = SecretChars(source.toCharArray())
30+
}

app/src/main/kotlin/pro/qyoga/app/therapist/oauth2/GoogleCallbackController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class GoogleOAuthController(
4747
val picture = response["picture"] as? String?
4848

4949
googleCalendarsService.addGoogleAccount(
50-
GoogleAccount(therapistId, email, authorizedClient.refreshToken!!.tokenValue.toCharArray())
50+
GoogleAccount(therapistId, email, authorizedClient.refreshToken!!.tokenValue)
5151
)
5252

5353
// Греем кэш, чтобы улучшить UX пользователя при возврате на страницу расписания

app/src/main/kotlin/pro/qyoga/core/calendar/google/GoogleAccount.kt

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pro.qyoga.core.calendar.google
33
import org.springframework.data.annotation.Id
44
import org.springframework.data.jdbc.core.mapping.AggregateReference
55
import org.springframework.data.relational.core.mapping.Table
6+
import pro.azhidkov.platform.spring.sdj.converters.SecretChars
67
import pro.azhidkov.platform.spring.sdj.ergo.hydration.Identifiable
78
import pro.azhidkov.platform.uuid.UUIDv7
89
import pro.qyoga.core.users.therapists.TherapistRef
@@ -15,7 +16,7 @@ typealias GoogleAccountRef = AggregateReference<GoogleAccount, UUID>
1516
data class GoogleAccount(
1617
val ownerRef: TherapistRef,
1718
val email: String,
18-
val refreshToken: CharArray,
19+
val refreshToken: SecretChars,
1920

2021
@Id override val id: UUID = UUIDv7.randomUUID()
2122
) : Identifiable<UUID> {
@@ -24,28 +25,6 @@ data class GoogleAccount(
2425
ownerRef: TherapistRef,
2526
email: String,
2627
refreshToken: String
27-
) : this(ownerRef, email, refreshToken.toCharArray())
28-
29-
override fun equals(other: Any?): Boolean {
30-
if (this === other) return true
31-
if (javaClass != other?.javaClass) return false
32-
33-
other as GoogleAccount
34-
35-
if (ownerRef != other.ownerRef) return false
36-
if (email != other.email) return false
37-
if (!refreshToken.contentEquals(other.refreshToken)) return false
38-
if (id != other.id) return false
39-
40-
return true
41-
}
42-
43-
override fun hashCode(): Int {
44-
var result = ownerRef.hashCode()
45-
result = 31 * result + email.hashCode()
46-
result = 31 * result + refreshToken.contentHashCode()
47-
result = 31 * result + id.hashCode()
48-
return result
49-
}
28+
) : this(ownerRef, email, SecretChars(refreshToken.toCharArray()))
5029

5130
}

app/src/main/kotlin/pro/qyoga/core/calendar/google/GoogleCalendarsClient.kt

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import org.springframework.cache.annotation.Cacheable
1212
import org.springframework.stereotype.Component
1313
import pro.azhidkov.platform.java.time.Interval
1414
import pro.qyoga.core.users.therapists.TherapistRef
15+
import java.io.IOException
1516
import java.net.URI
1617
import java.time.Duration
1718
import java.time.Instant
1819
import java.time.ZoneId
1920
import java.time.ZonedDateTime
21+
import kotlin.Result.Companion.failure
22+
import kotlin.Result.Companion.success
2023

2124

2225
@Component
@@ -81,21 +84,27 @@ class GoogleCalendarsClient(
8184
fun getAccountCalendars(
8285
therapist: TherapistRef,
8386
account: GoogleAccount
84-
): List<GoogleCalendar> {
87+
): Result<List<GoogleCalendar>> {
8588
log.info("Fetching calendars for therapist {} using {}", therapist, account)
8689
val service = servicesCache.getValue(account)
8790

88-
return service.CalendarList().list()
89-
.execute().items.map {
90-
GoogleCalendar(therapist, it.id, it.summary)
91-
}
91+
val getCalendarsListRequest = service.CalendarList().list()
92+
93+
val calendarListDto = tryExecute { getCalendarsListRequest.execute() }
94+
.getOrElse { return failure(it) }
95+
96+
val calendarsList = calendarListDto.items.map {
97+
GoogleCalendar(therapist, it.id, it.summary)
98+
}
99+
100+
return success(calendarsList)
92101
}
93102

94103
private fun createCalendarService(account: GoogleAccount): Calendar {
95104
val credentials = UserCredentials.newBuilder()
96105
.setClientId(googleOAuthProps.registration["google"]!!.clientId)
97106
.setClientSecret(googleOAuthProps.registration["google"]!!.clientSecret)
98-
.setRefreshToken(String(account.refreshToken))
107+
.setRefreshToken(String(account.refreshToken.value))
99108
.setTokenServerUri(tokenUri)
100109
.build()
101110

@@ -106,4 +115,11 @@ class GoogleCalendarsClient(
106115
return service
107116
}
108117

109-
}
118+
}
119+
120+
private fun <T> tryExecute(eventsRequest: () -> T): Result<T> =
121+
try {
122+
success(eventsRequest())
123+
} catch (e: IOException) {
124+
failure(e)
125+
}

app/src/main/kotlin/pro/qyoga/core/calendar/google/GoogleCalendarsService.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,47 @@ data class GoogleCalendarView(
2525
val shouldBeShown: Boolean
2626
)
2727

28+
private const val DEFAULT_CALENDAR_VISIBILITY = false
29+
30+
sealed interface GoogleAccountContentView {
31+
data class Calendars(val calendars: List<GoogleCalendarView>) : GoogleAccountContentView
32+
data object Error : GoogleAccountContentView
33+
34+
companion object {
35+
operator fun invoke(
36+
calendars: Result<List<GoogleCalendar>>,
37+
calendarSettings: Map<String, GoogleCalendarSettings>
38+
): GoogleAccountContentView =
39+
if (calendars.isSuccess) {
40+
Calendars(calendars.getOrThrow().map {
41+
GoogleCalendarView(
42+
it.externalId,
43+
it.name,
44+
calendarSettings[it.externalId]?.shouldBeShown ?: DEFAULT_CALENDAR_VISIBILITY
45+
)
46+
})
47+
} else {
48+
Error
49+
}
50+
}
51+
}
52+
2853
data class GoogleAccountCalendarsView(
2954
val id: UUID,
3055
val email: String,
31-
val calendars: List<GoogleCalendarView>
56+
val content: GoogleAccountContentView
3257
) {
3358

3459
companion object {
3560

3661
fun of(
3762
account: GoogleAccount,
38-
calendars: List<GoogleCalendar>,
63+
calendars: Result<List<GoogleCalendar>>,
3964
calendarSettings: Map<String, GoogleCalendarSettings>
4065
): GoogleAccountCalendarsView = GoogleAccountCalendarsView(
4166
account.id,
4267
account.email,
43-
calendars.map {
44-
GoogleCalendarView(it.externalId, it.name, calendarSettings[it.externalId]?.shouldBeShown ?: false)
45-
}
68+
GoogleAccountContentView(calendars, calendarSettings)
4669
)
4770
}
4871

app/src/main/kotlin/pro/qyoga/infra/db/SdjConfig.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class SdjConfig(
1818
PGIntervalToDurationConverter(),
1919
URLToStringConverter(),
2020
StringToURLConverter(),
21+
SecretCharsToString(),
22+
StringToSecretChars(),
2123
*modulesConverters.flatMap { it.converters() }.toTypedArray()
2224
)
2325
}

app/src/main/resources/templates/therapist/appointments/google-settings-component.html

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,55 @@ <h6>Google Calendar</h6>
66
</div>
77

88
<div th:if="${hasAccounts}">
9-
<div th:each="account : ${accounts}">
10-
<div th:text="${account.email}">email@example.com</div>
11-
<ul class="list-unstyled ms-3 my-1">
12-
<li class="d-flex justify-content-between align-items-center py-1"
13-
th:each="cal : ${account.calendars}">
14-
<span th:text="${cal.title}">Calendar name</span>
15-
<div class="form-check form-switch m-0">
16-
<input aria-label="Показывать календарь" class="form-check-input"
17-
th:checked="${cal.shouldBeShown}"
18-
hx-ext="json-enc"
19-
hx-swap="none"
20-
hx-trigger="change"
21-
hx-vals='js:{ "shouldBeShown": !!event.target.checked }'
22-
name="shouldBeShown"
23-
th:attr="hx-patch=@{/therapist/schedule/settings/google-calendar/{accountId}/calendars/{id}(accountId=${account.id},id=${cal.id})}"
24-
type="checkbox">
25-
</div>
26-
</li>
27-
</ul>
9+
<div class="mb-2 google-account-item" th:each="account : ${accounts}">
10+
<div class=" border-bottom pb-1 mb-2 text-body-secondary small" th:text="${account.email}">
11+
email@example.com
12+
</div>
13+
<div th:if="${account.content instanceof T(pro.qyoga.core.calendar.google.GoogleAccountContentView$Calendars)}">
14+
<ul class="list-unstyled ms-3 my-1">
15+
<li class="d-flex justify-content-between align-items-center py-2"
16+
th:each="cal : ${account.content.calendars}">
17+
<span th:text="${cal.title}">Calendar name</span>
18+
<div class="form-check form-switch m-0">
19+
<input
20+
aria-label="Показывать календарь"
21+
class="form-check-input"
22+
hx-ext="json-enc"
23+
hx-swap="none"
24+
hx-trigger="change"
25+
hx-vals='js:{ "shouldBeShown": !!event.target.checked }'
26+
name="shouldBeShown"
27+
style="height: 22px;width: 44px;"
28+
th:attr="hx-patch=@{/therapist/schedule/settings/google-calendar/{accountId}/calendars/{id}(accountId=${account.id},id=${cal.id})}"
29+
th:checked="${cal.shouldBeShown}"
30+
type="checkbox"
31+
>
32+
</div>
33+
</li>
34+
</ul>
35+
</div>
36+
<div
37+
class="text-danger ms-3 my-1 google-account-error-content d-inline-flex align-items-center"
38+
th:if="${account.content instanceof T(pro.qyoga.core.calendar.google.GoogleAccountContentView$Error)}"
39+
>
40+
<svg aria-hidden="true" class="me-2 align-middle" height="14" style="fill: currentColor;"
41+
viewBox="0 0 16 16" width="14" xmlns="http://www.w3.org/2000/svg">
42+
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
43+
<path d="M7.002 11a1 1 0 1 0 2 0 1 1 0 0 0-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.555.555 0 0 1-1.1 0z"/>
44+
</svg>
45+
Ошибка получения календарей аккаунта
46+
</div>
2847
</div>
2948
</div>
3049
</div>
31-
<a class="btn btn-outline-secondary" href="/oauth2/authorization/google" id="connect-google-calendar">
50+
<a
51+
class="btn btn-outline-secondary"
52+
href="/oauth2/authorization/google"
53+
id="connect-google-calendar"
54+
>
3255
<img alt="G"
33-
class="me-2" src="https://www.gstatic.com/marketing-cms/assets/images/d5/dc/cfe9ce8b4425b410b49b7f2dd3f3/g.webp=s48-fcrop64=1,00000000ffffffff-rw" width="20"> Добавить аккаунт
56+
class="me-2"
57+
src="https://www.gstatic.com/marketing-cms/assets/images/d5/dc/cfe9ce8b4425b410b49b7f2dd3f3/g.webp=s48-fcrop64=1,00000000ffffffff-rw"
58+
width="20"> Добавить аккаунт
3459
</a>
3560
</div>

app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/calendars/google/GetGoogleCalendarsSettingsEndpointTest.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@ package pro.qyoga.tests.cases.app.therapist.calendars.google
22

33
import io.kotest.core.annotation.DisplayName
44
import pro.qyoga.core.calendar.google.GoogleAccountCalendarsView
5+
import pro.qyoga.core.calendar.google.GoogleAccountContentView
56
import pro.qyoga.tests.assertions.shouldHaveComponent
67
import pro.qyoga.tests.clients.TherapistClient
8+
import pro.qyoga.tests.fixture.data.faker
9+
import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_REF
10+
import pro.qyoga.tests.fixture.presets.googleCalendarFixturePresets
11+
import pro.qyoga.tests.infra.test_config.spring.context
712
import pro.qyoga.tests.infra.web.QYogaAppIntegrationBaseKoTest
813
import pro.qyoga.tests.pages.therapist.appointments.GoogleCalendarSettingsComponent
14+
import java.util.*
915

1016

1117
@DisplayName("Эндпоинт получения компонента настройки интеграции с Google Calendar")
1218
class GetGoogleCalendarsSettingsEndpointTest : QYogaAppIntegrationBaseKoTest({
1319

20+
val googleCalendarsFixturePresets = context.googleCalendarFixturePresets()
21+
1422
"должен возвращать пустой список аккаунтов для терапевта без настроенной интеграции" {
1523
// Сетап
1624
val therapist = TherapistClient.loginAsTheTherapist()
@@ -23,4 +31,30 @@ class GetGoogleCalendarsSettingsEndpointTest : QYogaAppIntegrationBaseKoTest({
2331
res shouldHaveComponent GoogleCalendarSettingsComponent(accounts)
2432
}
2533

34+
"в случае если запрос каленадрей в гугле возвращает ошибку" - {
35+
// Сетап
36+
val therapist = TherapistClient.loginAsTheTherapist()
37+
val accessToken = "accessToken"
38+
val account = googleCalendarsFixturePresets.setupCalendar(THE_THERAPIST_REF)
39+
googleCalendarsFixturePresets.mockGoogleCalendar.OnGetCalendars(accessToken)
40+
.returnsForbidden()
41+
val accounts = listOf(
42+
GoogleAccountCalendarsView(
43+
UUID.fromString(faker.internet().uuid()),
44+
account.email,
45+
GoogleAccountContentView.Error
46+
)
47+
)
48+
49+
"при запросе настроек" - {
50+
// Действие
51+
val res = therapist.googleCalendarIntegration.getGoogleCalendarComponent()
52+
53+
"должен корректно вернуть компонент, в котором у аккаунта вместо списка календарей выведена ошибка" {
54+
// Проверка
55+
res shouldHaveComponent GoogleCalendarSettingsComponent(accounts)
56+
}
57+
}
58+
59+
}
2660
})

app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/calendars/google/GoogleAuthorizationIntegrationTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.kotest.matchers.shouldBe
55
import org.springframework.core.env.get
66
import pro.qyoga.app.therapist.appointments.core.schedule.SchedulePageController
77
import pro.qyoga.app.therapist.oauth2.GoogleOAuthController
8+
import pro.qyoga.core.calendar.google.GoogleAccountContentView
89
import pro.qyoga.core.calendar.google.GoogleCalendar
910
import pro.qyoga.core.calendar.google.GoogleCalendarsService
1011
import pro.qyoga.tests.assertions.shouldBeRedirectToGoogleOAuth
@@ -93,7 +94,7 @@ class GoogleAuthorizationIntegrationTest : QYogaAppIntegrationBaseKoTest({
9394

9495
"обеспечивать возможность дальнейших запросов к Google Calendar" {
9596
val gotCalendars = googleCalendarsService.findGoogleAccountCalendars(THE_THERAPIST_REF)
96-
gotCalendars.single().calendars shouldBe calendars
97+
(gotCalendars.single().content as GoogleAccountContentView.Calendars).calendars shouldBe calendars
9798
}
9899

99100
}

app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/calendars/google/SetCalendarShouldBeShownTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pro.qyoga.tests.cases.app.therapist.calendars.google
33
import io.kotest.core.annotation.DisplayName
44
import io.kotest.matchers.shouldBe
55
import pro.azhidkov.platform.spring.sdj.ergo.hydration.ref
6+
import pro.qyoga.core.calendar.google.GoogleAccountContentView
67
import pro.qyoga.tests.clients.TherapistClient.Companion.loginAsTheTherapist
78
import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_REF
89
import pro.qyoga.tests.fixture.presets.GoogleCalendarFixturePresets
@@ -33,7 +34,7 @@ class SetCalendarShouldBeShownTest : QYogaAppIntegrationBaseKoTest({
3334

3435
// Проверка
3536
val settings = googleCalendarsTestApi.getGoogleCalendarsSettings(THE_THERAPIST_REF)
36-
settings.single().calendars.single { it.id == calendarId }.shouldBeShown shouldBe true
37+
(settings.single().content as GoogleAccountContentView.Calendars).calendars.single { it.id == calendarId }.shouldBeShown shouldBe true
3738
}
3839

3940
})

0 commit comments

Comments
 (0)