From 4f1dc92def111049d7c1098ec2b825ec65f911d1 Mon Sep 17 00:00:00 2001 From: Tom Winter Date: Mon, 9 Dec 2024 21:04:29 +0100 Subject: [PATCH] fix: pr feedback and some tests --- .../aam-backend-service/build.gradle.kts | 1 + .../changes/repository/SyncRepository.kt | 3 + .../skill/core/SqlSearchUserProfileUseCase.kt | 41 +- .../skill/core/UserProfileMatcher.kt | 10 - .../skill/core/UserProfileStorage.kt | 15 - .../skill/job/SyncSkillsJob.kt | 5 +- .../repository/SkillLabUserProfileEntity.kt | 2 + .../SkillLabUserProfileRepository.kt | 4 +- .../SkillLabUserProfileSyncEntity.kt | 5 +- .../skill/skilllab/SkillLabClient.kt | 2 - .../SkillLabFetchUserProfileUpdatesUseCase.kt | 28 +- .../SkillLabSyncUserProfileUseCase.kt | 23 +- .../skilllab/SkillLabUserProfileMatcher.kt | 13 - .../core/SqlSearchUserProfileUseCaseTest.kt | 463 ++++++++++++++++++ ...llLabFetchUserProfileUpdatesUseCaseTest.kt | 320 ++++++++++++ .../SkillLabSyncUserProfileUseCaseTest.kt | 266 ++++++++++ docs/api-specs/skill-api-v1.yaml | 2 +- 17 files changed, 1134 insertions(+), 69 deletions(-) delete mode 100644 application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileMatcher.kt delete mode 100644 application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileStorage.kt delete mode 100644 application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabUserProfileMatcher.kt create mode 100644 application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCaseTest.kt create mode 100644 application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCaseTest.kt create mode 100644 application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCaseTest.kt diff --git a/application/aam-backend-service/build.gradle.kts b/application/aam-backend-service/build.gradle.kts index 6d9ce0c..9e4ce7a 100644 --- a/application/aam-backend-service/build.gradle.kts +++ b/application/aam-backend-service/build.gradle.kts @@ -91,6 +91,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-testcontainers") testImplementation("org.springframework.security:spring-security-test") + testImplementation(kotlin("test")) } jacoco { diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/reporting/changes/repository/SyncRepository.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/reporting/changes/repository/SyncRepository.kt index e3ce92d..3141671 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/reporting/changes/repository/SyncRepository.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/reporting/changes/repository/SyncRepository.kt @@ -9,6 +9,9 @@ import org.springframework.data.repository.CrudRepository import org.springframework.stereotype.Repository import java.util.* +/** + * Store latest sync sequence for each database for change detection + */ @Entity(name = "couchdb_sync_entry") data class SyncEntry( @Id diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCase.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCase.kt index 4be7a50..f82fbd3 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCase.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCase.kt @@ -9,6 +9,7 @@ import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileRepo import org.springframework.data.domain.Example import org.springframework.data.domain.ExampleMatcher import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable /** @@ -26,7 +27,7 @@ class SqlSearchUserProfileUseCase( ) : SearchUserProfileUseCase() { companion object { - private val MATCHER_EMAIL = ExampleMatcher.matchingAny() + val MATCHER_EMAIL = ExampleMatcher.matchingAny() .withIgnorePaths( "id", "externalIdentifier", @@ -41,7 +42,7 @@ class SqlSearchUserProfileUseCase( .withIncludeNullValues() .withStringMatcher(ExampleMatcher.StringMatcher.EXACT) - private val MATCHER_PHONE = ExampleMatcher.matchingAny() + val MATCHER_PHONE = ExampleMatcher.matchingAny() .withIgnorePaths( "id", "externalIdentifier", @@ -56,7 +57,7 @@ class SqlSearchUserProfileUseCase( .withIncludeNullValues() .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) - private val MATCHER_NAME = ExampleMatcher.matchingAll() + val MATCHER_NAME = ExampleMatcher.matchingAll() .withIgnorePaths( "id", "externalIdentifier", @@ -73,7 +74,6 @@ class SqlSearchUserProfileUseCase( } override fun apply(request: SearchUserProfileRequest): UseCaseOutcome { - val userProfile = SkillLabUserProfileEntity( id = 0, externalIdentifier = "", @@ -96,7 +96,7 @@ class SqlSearchUserProfileUseCase( ) if (!searchResults.isEmpty) { - return asSuccessResponse(searchResults); + return asSuccessResponse(searchResults) } } @@ -107,7 +107,7 @@ class SqlSearchUserProfileUseCase( ) if (!searchResults.isEmpty) { - return asSuccessResponse(searchResults); + return asSuccessResponse(searchResults) } } @@ -120,7 +120,34 @@ class SqlSearchUserProfileUseCase( Page.empty() } - return asSuccessResponse(searchResults); + // search runs with just firstName/lastName + if (searchResults.isEmpty && !userProfile.fullName.isNullOrBlank()) { + val nameParts = userProfile.fullName?.split(" ") ?: listOf() + + if (nameParts.size < 2) { + return asSuccessResponse(searchResults) + } + + val searchNames = listOf(nameParts.first(), nameParts.last()) + val partNameSearchResults = mutableSetOf() + + searchNames.forEach { name -> + userProfile.fullName = name + partNameSearchResults.addAll( + userProfileRepository.findAll( + Example.of(userProfile, MATCHER_NAME), pageable + ).toList() + ) + } + + val result = partNameSearchResults.toList().take(request.pageSize) + + searchResults = PageImpl( + result, + ) + } + + return asSuccessResponse(searchResults) } private fun asSuccessResponse( diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileMatcher.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileMatcher.kt deleted file mode 100644 index 697989e..0000000 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileMatcher.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.aamdigital.aambackendservice.skill.core - -import com.aamdigital.aambackendservice.skill.domain.UserProfile - -interface UserProfileMatcher { - - fun findExternalUserProfiles( - userProfile: UserProfile, - ) -} diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileStorage.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileStorage.kt deleted file mode 100644 index 3fad42d..0000000 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/core/UserProfileStorage.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.aamdigital.aambackendservice.skill.core - -import com.aamdigital.aambackendservice.domain.DomainReference -import com.aamdigital.aambackendservice.error.AamException -import com.aamdigital.aambackendservice.skill.domain.UserProfile -import org.springframework.data.domain.Pageable - -interface UserProfileStorage { - - @Throws(AamException::class) - fun fetchUserProfile(externalIdentifier: DomainReference): UserProfile - - @Throws(AamException::class) - fun fetchUserProfiles(pageable: Pageable, updatedFrom: String?): List -} diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/job/SyncSkillsJob.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/job/SyncSkillsJob.kt index 86629fa..bebc2eb 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/job/SyncSkillsJob.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/job/SyncSkillsJob.kt @@ -8,6 +8,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.context.annotation.Configuration import org.springframework.scheduling.annotation.Scheduled +/** + * Configures cron job for scheduled sync all SkillLab changes + */ @Configuration @ConditionalOnProperty( prefix = "features.skill-api", @@ -28,7 +31,7 @@ class SyncSkillsJob( } @Scheduled(fixedDelay = (60000 * 10)) // every 10 minutes - fun checkForCouchDbChanges() { + fun checkForSkillLabChanges() { if (ERROR_COUNTER >= MAX_ERROR_COUNT) { logger.trace("${this.javaClass.name}: MAX_ERROR_COUNT reached. Not starting job again.") return diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileEntity.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileEntity.kt index dac9837..d8e9c72 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileEntity.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileEntity.kt @@ -39,10 +39,12 @@ data class SkillLabUserProfileEntity( @Column var updatedAt: String?, + /** latest update within our system */ @UpdateTimestamp(source = SourceType.DB) @Column(updatable = true) var latestSyncAt: OffsetDateTime? = null, + /** original date (first latestSyncAt) when added to our system */ @CreationTimestamp(source = SourceType.DB) @Column(updatable = true) var importedAt: OffsetDateTime? = null, diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileRepository.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileRepository.kt index 2f8a607..981d307 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileRepository.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileRepository.kt @@ -5,8 +5,8 @@ import org.springframework.data.repository.PagingAndSortingRepository /** - * CrudRepository analyses the function name to create SQL queries - * see: https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html + * JpaRepository and PagingAndSortingRepository interfaces provide all necessary query functions + * Check Docs for further information: https://docs.spring.io/spring-data/jpa/reference/jpa/getting-started.html */ interface SkillLabUserProfileRepository : JpaRepository, PagingAndSortingRepository { diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileSyncEntity.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileSyncEntity.kt index 30064ca..22c56cb 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileSyncEntity.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/repository/SkillLabUserProfileSyncEntity.kt @@ -8,13 +8,16 @@ import jakarta.persistence.Index import jakarta.persistence.Table import java.time.OffsetDateTime +/** + * Stores the latest successful SkillLabFetchUserProfileUpdateUseCase run date for each project. + */ @Entity @Table(indexes = [Index(columnList = "projectId", unique = true)]) data class SkillLabUserProfileSyncEntity( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) var id: Long = 0, - + var projectId: String, var latestSync: OffsetDateTime, ) diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabClient.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabClient.kt index d0913fe..e27795d 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabClient.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabClient.kt @@ -95,8 +95,6 @@ class SkillLabClient( .uri("/profiles/${externalIdentifier.id}") .accept(MediaType.APPLICATION_JSON) .exchange { _, clientResponse -> - // todo check for 4xx response first - val response = clientResponse.bodyTo(String::class.java) ?: throw ExternalSystemException( message = "Empty or invalid response from server", code = SkillLabUserProfileStorageAamErrorCode.EMPTY_RESPONSE diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCase.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCase.kt index 5276477..2d09bb3 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCase.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCase.kt @@ -22,7 +22,7 @@ enum class SkillLabFetchUserProfileUpdatesErrorCode : AamErrorCode { } /** - * Fetch latest changes for this SkillLab tenant and store it to database. + * Fetch latest changes for this SkillLab tenant and create SyncUserProfileEvents for each changed UserProfile */ class SkillLabFetchUserProfileUpdatesUseCase( private val skillLabClient: SkillLabClient, @@ -30,17 +30,20 @@ class SkillLabFetchUserProfileUpdatesUseCase( private val userProfileUpdatePublisher: UserProfileUpdatePublisher, ) : FetchUserProfileUpdatesUseCase() { + companion object { + private const val PAGE_SIZE = 50 + private const val MAX_RESULTS_LIMIT = 10_000 + } + override fun apply(request: FetchUserProfileUpdatesRequest): UseCaseOutcome { val results = mutableListOf() - var currentSync = skillLabUserProfileSyncRepository.findByProjectId(request.projectId).getOrNull() - var page = 1 do { val batch = try { fetchNextBatch( - pageable = Pageable.ofSize(50).withPage(page++), + pageable = Pageable.ofSize(PAGE_SIZE).withPage(page++), currentSync = currentSync, ) } catch (ex: AamException) { @@ -51,7 +54,7 @@ class SkillLabFetchUserProfileUpdatesUseCase( ) } results.addAll(batch) - } while (batch.isNotEmpty()) + } while (batch.size >= PAGE_SIZE && results.size < MAX_RESULTS_LIMIT) results.forEach { try { @@ -94,8 +97,15 @@ class SkillLabFetchUserProfileUpdatesUseCase( private fun fetchNextBatch( pageable: Pageable, currentSync: SkillLabUserProfileSyncEntity?, - ): List = skillLabClient.fetchUserProfiles( - pageable = pageable, - updatedFrom = currentSync?.latestSync?.toString() - ) + ): List = if (currentSync == null) { + skillLabClient.fetchUserProfiles( + pageable = pageable, + updatedFrom = null + ) + } else { + skillLabClient.fetchUserProfiles( + pageable = pageable, + updatedFrom = currentSync.latestSync.toString() + ) + } } diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCase.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCase.kt index 6fdc2d3..985b2ad 100644 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCase.kt +++ b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCase.kt @@ -16,6 +16,9 @@ enum class SkillLabSyncUserProfileErrorCode : AamErrorCode { IO_ERROR } +/** + * Will load a specific UserProfile from SkillLab, store it to the database and returns the profile afterwards + */ class SkillLabSyncUserProfileUseCase( private val skillLabClient: SkillLabClient, private val skillLabUserProfileRepository: SkillLabUserProfileRepository, @@ -29,11 +32,10 @@ class SkillLabSyncUserProfileUseCase( val allSkillsEntities = getSkillEntities(userProfile.profile) val userProfileEntity = fetchUserProfileEntity(userProfile.profile, allSkillsEntities) - if (!userProfileEntity.mobileNumber.isNullOrBlank()) { - userProfileEntity.mobileNumber = userProfileEntity.mobileNumber - ?.replace(" ", "") - ?.replace("-", "") - ?.trim() + userProfileEntity.mobileNumber.let { + if (!it.isNullOrBlank()) { + userProfileEntity.mobileNumber = formatMobileNumber(it) + } } try { @@ -62,17 +64,22 @@ class SkillLabSyncUserProfileUseCase( ) }, updatedAtExternalSystem = userProfileEntity.updatedAt, - importedAt = userProfileEntity.importedAt!!.toInstant(), - latestSyncAt = userProfileEntity.latestSyncAt!!.toInstant(), + importedAt = userProfileEntity.importedAt?.toInstant(), + latestSyncAt = userProfileEntity.latestSyncAt?.toInstant(), ) ) ) } + private fun formatMobileNumber(mobileNumber: String): String = mobileNumber + .replace(" ", "") + .replace("-", "") + .trim() + private fun fetchUserProfileEntity( userProfile: SkillLabProfileDto, allSkillsEntities: Set - ) = if (skillLabUserProfileRepository.existsByExternalIdentifier(userProfile.id)) { + ): SkillLabUserProfileEntity = if (skillLabUserProfileRepository.existsByExternalIdentifier(userProfile.id)) { val entity = skillLabUserProfileRepository.findByExternalIdentifier(userProfile.id) entity.fullName = userProfile.fullName entity.mobileNumber = userProfile.mobileNumber diff --git a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabUserProfileMatcher.kt b/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabUserProfileMatcher.kt deleted file mode 100644 index 1f6e15e..0000000 --- a/application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabUserProfileMatcher.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.aamdigital.aambackendservice.skill.skilllab - -import com.aamdigital.aambackendservice.skill.core.UserProfileMatcher -import com.aamdigital.aambackendservice.skill.domain.UserProfile -import org.springframework.web.client.RestClient - -class SkillLabUserProfileMatcher( - val http: RestClient -) : UserProfileMatcher { - override fun findExternalUserProfiles(userProfile: UserProfile) { - TODO("Not yet implemented") - } -} diff --git a/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCaseTest.kt b/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCaseTest.kt new file mode 100644 index 0000000..f186e05 --- /dev/null +++ b/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/core/SqlSearchUserProfileUseCaseTest.kt @@ -0,0 +1,463 @@ +package com.aamdigital.aambackendservice.skill.core + +import com.aamdigital.aambackendservice.domain.UseCaseOutcome +import com.aamdigital.aambackendservice.skill.core.SqlSearchUserProfileUseCase.Companion.MATCHER_EMAIL +import com.aamdigital.aambackendservice.skill.core.SqlSearchUserProfileUseCase.Companion.MATCHER_NAME +import com.aamdigital.aambackendservice.skill.core.SqlSearchUserProfileUseCase.Companion.MATCHER_PHONE +import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileEntity +import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileRepository +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.data.domain.Example +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +class SqlSearchUserProfileUseCaseTest { + private lateinit var service: SqlSearchUserProfileUseCase + + @Mock + private lateinit var userProfileRepository: SkillLabUserProfileRepository + + @BeforeEach + fun setup() { + reset(userProfileRepository) + service = SqlSearchUserProfileUseCase(userProfileRepository = userProfileRepository) + } + + + @Test + fun `should return exact match for email address`() { + // given + whenever( + userProfileRepository.findAll( + eq( + Example.of( + getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = "123", + email = "example@mail.local", + ), + MATCHER_EMAIL + ) + ), + any() + ) + ).thenReturn( + PageImpl( + listOf( + getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = null, + email = "example@mail.local", + ) + ) + ) + ) + + // when + val response = service.run( + SearchUserProfileRequest( + email = "example@mail.local", + fullName = "Max Muster", + phone = "123", + page = 1, + pageSize = 50, + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileRepository, + times(1) + ).findAll( + any>(), + any() + ) + + assertEquals(1, (response as UseCaseOutcome.Success).data.totalElements) + } + + @Test + fun `should return exact match for mobileNumber when email check returns no match`() { + val exampleEntity = getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = "123456789", + email = "foo@mail.local", + ) + + val responseEntity = getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = "123456789", + email = "example@mail.local", + ) + + // given + whenever( + userProfileRepository.findAll( + eq( + Example.of( + exampleEntity, + MATCHER_EMAIL + ) + ), + any() + ) + ).thenReturn(Page.empty()) + + whenever( + userProfileRepository.findAll( + eq( + Example.of( + exampleEntity, + MATCHER_PHONE + ) + ), + any() + ) + ).thenReturn( + PageImpl(listOf(responseEntity)) + ) + + // when + val response = service.run( + SearchUserProfileRequest( + email = "foo@mail.local", + fullName = "Max Muster", + phone = "123456789", + page = 1, + pageSize = 50, + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileRepository, + times(2) + ).findAll( + any>(), + any() + ) + + assertEquals(1, (response as UseCaseOutcome.Success).data.totalElements) + } + + @ParameterizedTest + @ValueSource( + strings = ["null", "", " "], + ) + fun `should return exact match for mobileNumber when email is nullOrBlank`(emailRawValue: String?) { + val emailValue = if (emailRawValue == "null") { + null + } else { + emailRawValue + } + + val exampleEntity = getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = "123456789", + email = emailValue, + ) + + val responseEntity = getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = "123456789", + email = "example@mail.local", + ) + + // given + whenever( + userProfileRepository.findAll( + eq( + Example.of( + exampleEntity, + MATCHER_PHONE + ) + ), + any() + ) + ).thenReturn( + PageImpl(listOf(responseEntity)) + ) + + // when + val response = service.run( + SearchUserProfileRequest( + email = emailValue, + fullName = "Max Muster", + phone = "123456789", + page = 1, + pageSize = 50, + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileRepository, + times(1) + ).findAll( + any>(), + any() + ) + + assertEquals(1, (response as UseCaseOutcome.Success).data.totalElements) + } + + @Test + fun `should return matches for fullName`() { + val exampleEntity = getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = null, + email = null, + ) + + val responseEntity = getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = "123456789", + email = "example@mail.local", + ) + + // given + whenever( + userProfileRepository.findAll( + eq( + Example.of( + exampleEntity, + MATCHER_NAME + ) + ), + any() + ) + ).thenReturn( + PageImpl(listOf(responseEntity)) + ) + + // when + val response = service.run( + SearchUserProfileRequest( + email = null, + fullName = "Max Muster", + phone = null, + page = 1, + pageSize = 50, + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileRepository, + times(1) + ).findAll( + any>(), + any() + ) + + assertEquals(1, (response as UseCaseOutcome.Success).data.totalElements) + } + + @Test + fun `should return matches for split fullName`() { + val responseEntity = getSkillLabUserProfileEntity( + fullName = "Max Muster", + mobileNumber = "123456789", + email = "example@mail.local", + ) + + // given + whenever( + userProfileRepository.findAll( + eq( + Example.of( + getSkillLabUserProfileEntity( + fullName = "Max Martin Muster", + mobileNumber = null, + email = null, + ), + MATCHER_NAME + ) + ), + any() + ) + ).thenReturn(Page.empty()) + + whenever( + userProfileRepository.findAll( + eq( + Example.of( + getSkillLabUserProfileEntity( + fullName = "Max", + mobileNumber = null, + email = null, + ), + MATCHER_NAME + ) + ), + any() + ) + ).thenReturn( + PageImpl(listOf(responseEntity)) + ) + + whenever( + userProfileRepository.findAll( + eq( + Example.of( + getSkillLabUserProfileEntity( + fullName = "Muster", + mobileNumber = null, + email = null, + ), + MATCHER_NAME + ) + ), + any() + ) + ).thenReturn( + PageImpl(listOf(responseEntity)) + ) + + // when + val response = service.run( + SearchUserProfileRequest( + email = null, + fullName = "Max Martin Muster", + phone = null, + page = 1, + pageSize = 50, + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileRepository, + times(3) + ).findAll( + any>(), + any() + ) + + assertEquals(1, (response as UseCaseOutcome.Success).data.totalElements) + } + + @Test + fun `should skip namePart search if not enough nameParts`() { + // given + whenever( + userProfileRepository.findAll( + eq( + Example.of( + getSkillLabUserProfileEntity( + fullName = "Mila", + mobileNumber = null, + email = null, + ), + MATCHER_NAME + ) + ), + any() + ) + ).thenReturn(Page.empty()) + + // when + val response = service.run( + SearchUserProfileRequest( + email = null, + fullName = "Mila", + phone = null, + page = 1, + pageSize = 50, + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileRepository, + times(1) + ).findAll( + any>(), + any() + ) + + assertEquals(0, (response as UseCaseOutcome.Success).data.totalElements) + } + + + @ParameterizedTest + @ValueSource( + strings = ["null", "", " "], + ) + fun `should return empty list when all search parameter are empty`(rawValue: String?) { + val emptyOrBlankValue = if (rawValue == "null") { + null + } else { + rawValue + } + + // when + val response = service.run( + SearchUserProfileRequest( + email = emptyOrBlankValue, + fullName = emptyOrBlankValue, + phone = emptyOrBlankValue, + page = 1, + pageSize = 50, + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileRepository, + times(0) + ).findAll( + any>(), + any() + ) + + assertEquals(0, (response as UseCaseOutcome.Success).data.totalElements) + } + + private fun getSkillLabUserProfileEntity( + externalIdentifier: String = "", + fullName: String, + mobileNumber: String?, + email: String?, + ): SkillLabUserProfileEntity = SkillLabUserProfileEntity( + id = 0, + externalIdentifier = externalIdentifier, + fullName = fullName, + mobileNumber = mobileNumber, + email = email, + skills = emptySet(), + updatedAt = "", + latestSyncAt = null, + importedAt = null, + ) +} diff --git a/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCaseTest.kt b/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCaseTest.kt new file mode 100644 index 0000000..bec35dc --- /dev/null +++ b/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabFetchUserProfileUpdatesUseCaseTest.kt @@ -0,0 +1,320 @@ +package com.aamdigital.aambackendservice.skill.skilllab + +import com.aamdigital.aambackendservice.domain.DomainReference +import com.aamdigital.aambackendservice.domain.TestErrorCode +import com.aamdigital.aambackendservice.domain.UseCaseOutcome +import com.aamdigital.aambackendservice.error.InternalServerException +import com.aamdigital.aambackendservice.queue.core.QueueMessage +import com.aamdigital.aambackendservice.skill.core.FetchUserProfileUpdatesRequest +import com.aamdigital.aambackendservice.skill.core.UserProfileUpdatePublisher +import com.aamdigital.aambackendservice.skill.core.event.UserProfileUpdateEvent +import com.aamdigital.aambackendservice.skill.di.UserProfileUpdateEventQueueConfiguration +import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileSyncEntity +import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileSyncRepository +import okio.IOException +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.data.domain.Pageable +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.* + +@ExtendWith(MockitoExtension::class) +class SkillLabFetchUserProfileUpdatesUseCaseTest { + + private lateinit var service: SkillLabFetchUserProfileUpdatesUseCase + + @Mock + lateinit var skillLabClient: SkillLabClient + + @Mock + lateinit var skillLabUserProfileSyncRepository: SkillLabUserProfileSyncRepository + + @Mock + lateinit var userProfileUpdatePublisher: UserProfileUpdatePublisher + + @BeforeEach + fun setup() { + reset( + skillLabClient, + skillLabUserProfileSyncRepository, + userProfileUpdatePublisher + ) + service = SkillLabFetchUserProfileUpdatesUseCase( + skillLabClient = skillLabClient, + skillLabUserProfileSyncRepository = skillLabUserProfileSyncRepository, + userProfileUpdatePublisher = userProfileUpdatePublisher + ) + } + + @Test + fun `should return Failure when SkillLabClient throws exception`() { + // given + whenever(skillLabClient.fetchUserProfiles(any(), anyOrNull())) + .thenAnswer { + throw InternalServerException( + message = "error", + code = TestErrorCode.TEST_EXCEPTION, + cause = null, + ) + } + + // when + val response = service.run( + FetchUserProfileUpdatesRequest( + projectId = "1", + ) + ) + + // then + + assertThat(response).isInstanceOf(UseCaseOutcome.Failure::class.java) + Assertions.assertEquals( + SkillLabFetchUserProfileUpdatesErrorCode.EXTERNAL_SYSTEM_ERROR, + (response as UseCaseOutcome.Failure).errorCode + ) + } + + @Test + fun `should publish UserProfileUpdateEvent for each UserProfile fetched from skillLabClient`() { + // given + `when`(skillLabClient.fetchUserProfiles(eq(Pageable.ofSize(50).withPage(1)), anyOrNull())).thenReturn( + listOf( + DomainReference("user-profile-1"), + DomainReference("user-profile-2"), + DomainReference("user-profile-3"), + ) + ) + + whenever(userProfileUpdatePublisher.publish(any(), any())).thenReturn( + getQueueMessage() + ) + + // when + val response = service.run( + FetchUserProfileUpdatesRequest( + projectId = "1", + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileUpdatePublisher, + times(1) + ).publish( + eq(UserProfileUpdateEventQueueConfiguration.USER_PROFILE_UPDATE_QUEUE), + eq( + UserProfileUpdateEvent( + projectId = "1", + userProfileId = "user-profile-1", + ) + ) + ) + + verify( + userProfileUpdatePublisher, + times(1) + ).publish( + eq(UserProfileUpdateEventQueueConfiguration.USER_PROFILE_UPDATE_QUEUE), + eq( + UserProfileUpdateEvent( + projectId = "1", + userProfileId = "user-profile-2", + ) + ) + ) + + verify( + userProfileUpdatePublisher, + times(1) + ).publish( + eq(UserProfileUpdateEventQueueConfiguration.USER_PROFILE_UPDATE_QUEUE), + eq( + UserProfileUpdateEvent( + projectId = "1", + userProfileId = "user-profile-3", + ) + ) + ) + } + + @Test + fun `should abort fetchNextBatch loop when reaching MAX_RESULTS_LIMIT`() { + val maxResultsLimit = 10_000 + val pageSize = 50 + + // given + whenever( + skillLabClient.fetchUserProfiles( + any(), + anyOrNull() + ) + ).thenReturn( + (1..pageSize).map { + DomainReference("user-profile-$it") + } + ) + + whenever(userProfileUpdatePublisher.publish(any(), any())).thenReturn( + getQueueMessage() + ) + + // when + val response = service.run( + FetchUserProfileUpdatesRequest( + projectId = "1", + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + userProfileUpdatePublisher, + times(maxResultsLimit) + ).publish( + eq(UserProfileUpdateEventQueueConfiguration.USER_PROFILE_UPDATE_QUEUE), + any() + ) + } + + @Test + fun `should return Failure when userProfileUpdatePublisher throws Exception`() { + // given + whenever( + skillLabClient.fetchUserProfiles( + any(), + anyOrNull() + ) + ).thenReturn( + listOf( + DomainReference("user-profile-1"), + DomainReference("user-profile-2") + ) + ) + + whenever(userProfileUpdatePublisher.publish(any(), any())).thenAnswer { + throw IOException("mock-error") + } + + // when + val response = service.run( + FetchUserProfileUpdatesRequest( + projectId = "1", + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Failure::class.java) + Assertions.assertEquals( + SkillLabFetchUserProfileUpdatesErrorCode.EVENT_PUBLISH_ERROR, + (response as UseCaseOutcome.Failure).errorCode + ) + Assertions.assertEquals("mock-error", response.errorMessage) + } + + @Test + fun `should store latestSyncEntity when SyncEntity exist for this projectId`() { + val syncEntity = SkillLabUserProfileSyncEntity( + id = 42L, + projectId = "1", + latestSync = OffsetDateTime.parse("2024-01-01T00:00:00Z"), + ) + + // given + whenever(skillLabUserProfileSyncRepository.findByProjectId(any())) + .thenReturn( + Optional.of( + syncEntity + ) + ) + + `when`(skillLabClient.fetchUserProfiles(eq(Pageable.ofSize(50).withPage(1)), anyOrNull())).thenReturn( + listOf( + DomainReference("user-profile-1"), + DomainReference("user-profile-2"), + DomainReference("user-profile-3"), + ) + ) + + whenever(userProfileUpdatePublisher.publish(any(), any())).thenReturn( + getQueueMessage() + ) + + // when + val response = service.run( + FetchUserProfileUpdatesRequest( + projectId = "1", + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + skillLabUserProfileSyncRepository, + times(1) + ).save( + eq(syncEntity) + ) + } + + @Test + fun `should store latestSyncEntity when no SyncEntity exist for this projectId`() { + // given + `when`(skillLabClient.fetchUserProfiles(eq(Pageable.ofSize(50).withPage(1)), anyOrNull())).thenReturn( + listOf( + DomainReference("user-profile-1"), + DomainReference("user-profile-2"), + DomainReference("user-profile-3"), + ) + ) + + whenever(userProfileUpdatePublisher.publish(any(), any())).thenReturn( + getQueueMessage() + ) + + // when + val response = service.run( + FetchUserProfileUpdatesRequest( + projectId = "1", + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + skillLabUserProfileSyncRepository, + times(1) + ).save( + any() + ) + } + + private fun getQueueMessage(): QueueMessage = QueueMessage( + id = UUID.fromString("00000000-0000-0000-0000-000000000000"), + eventType = "FOO", + event = UserProfileUpdateEvent( + projectId = "1", + userProfileId = "mock", + ), + createdAt = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + ) +} diff --git a/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCaseTest.kt b/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCaseTest.kt new file mode 100644 index 0000000..26de777 --- /dev/null +++ b/application/aam-backend-service/src/test/kotlin/com/aamdigital/aambackendservice/skill/skilllab/SkillLabSyncUserProfileUseCaseTest.kt @@ -0,0 +1,266 @@ +package com.aamdigital.aambackendservice.skill.skilllab + +import com.aamdigital.aambackendservice.domain.DomainReference +import com.aamdigital.aambackendservice.domain.UseCaseOutcome +import com.aamdigital.aambackendservice.skill.core.SyncUserProfileRequest +import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileEntity +import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileRepository +import com.aamdigital.aambackendservice.skill.repository.SkillReferenceEntity +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.io.IOException +import java.util.* + +@ExtendWith(MockitoExtension::class) +class SkillLabSyncUserProfileUseCaseTest { + private lateinit var service: SkillLabSyncUserProfileUseCase + + @Mock + lateinit var skillLabClient: SkillLabClient + + @Mock + lateinit var skillLabUserProfileRepository: SkillLabUserProfileRepository + + @BeforeEach + fun setUp() { + reset( + skillLabClient, + skillLabUserProfileRepository, + ) + service = SkillLabSyncUserProfileUseCase( + skillLabClient = skillLabClient, skillLabUserProfileRepository = skillLabUserProfileRepository + ) + } + + @Test + fun `should store new user profile and return Success`() { + // given + whenever(skillLabClient.fetchUserProfile(any())).thenReturn( + getSkillLabProfileResponseDto("user-profile-1", mobileNumber = "") + ) + whenever(skillLabUserProfileRepository.existsByExternalIdentifier(eq("user-profile-1"))).thenReturn(false) + + // when + val response = service.run( + SyncUserProfileRequest( + userProfile = DomainReference("user-profile-1"), + project = DomainReference("project-1"), + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + skillLabUserProfileRepository, + times(1) + ).save( + eq( + SkillLabUserProfileEntity( + id = 0L, + externalIdentifier = "user-profile-1", + fullName = "Max Muster", + mobileNumber = "", + email = "max.muster@fake.local", + skills = setOf( + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000001", + escoUri = "http://link-to-esco-skill-1", + usage = "always" + ), + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000002", + escoUri = "http://link-to-esco-skill-2", + usage = "always" + ), + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000003", + escoUri = "http://link-to-esco-skill-3", + usage = "always" + ) + ), + updatedAt = "2022-02-02T22:22Z", + latestSyncAt = null, + importedAt = null + ) + ) + ) + } + + @Test + fun `should update existing user profile and return Success`() { + val existingEntity = SkillLabUserProfileEntity( + id = 0L, + externalIdentifier = "user-profile-1", + fullName = "Max Muster", + mobileNumber = "+49123456789", + email = "max.muster@fake.local", + skills = setOf( + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000001", + escoUri = "http://link-to-esco-skill-1", + usage = "always" + ), + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000002", + escoUri = "http://link-to-esco-skill-2", + usage = "always" + ), + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000003", + escoUri = "http://link-to-esco-skill-3", + usage = "always" + ) + ), + updatedAt = "2022-02-02T22:22Z", + latestSyncAt = null, + importedAt = null + ) + + // given + whenever(skillLabClient.fetchUserProfile(any())).thenReturn( + getSkillLabProfileResponseDto("user-profile-1") + ) + whenever(skillLabUserProfileRepository.existsByExternalIdentifier(eq("user-profile-1"))).thenReturn(true) + whenever(skillLabUserProfileRepository.findByExternalIdentifier(eq("user-profile-1"))) + .thenReturn( + existingEntity + ) + + + // when + val response = service.run( + SyncUserProfileRequest( + userProfile = DomainReference("user-profile-1"), + project = DomainReference("project-1"), + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Success::class.java) + + verify( + skillLabUserProfileRepository, + times(1) + ).save( + eq(existingEntity) + ) + } + + @Test + fun `should and return Failure when database access throws Exception`() { + // given + whenever(skillLabClient.fetchUserProfile(any())).thenReturn( + getSkillLabProfileResponseDto("user-profile-1") + ) + whenever(skillLabUserProfileRepository.existsByExternalIdentifier(eq("user-profile-1"))).thenReturn(false) + + whenever( + skillLabUserProfileRepository.save( + eq( + SkillLabUserProfileEntity( + id = 0L, + externalIdentifier = "user-profile-1", + fullName = "Max Muster", + mobileNumber = "+49123456789", + email = "max.muster@fake.local", + skills = setOf( + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000001", + escoUri = "http://link-to-esco-skill-1", + usage = "always" + ), + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000002", + escoUri = "http://link-to-esco-skill-2", + usage = "always" + ), + SkillReferenceEntity( + externalIdentifier = "00000000-0000-0000-0000-000000000003", + escoUri = "http://link-to-esco-skill-3", + usage = "always" + ) + ), + updatedAt = "2022-02-02T22:22Z", + latestSyncAt = null, + importedAt = null + ) + ) + + ) + ).thenAnswer { + throw IOException("mock-error") + } + + // when + val response = service.run( + SyncUserProfileRequest( + userProfile = DomainReference("user-profile-1"), + project = DomainReference("project-1"), + ) + ) + + // then + assertThat(response).isInstanceOf(UseCaseOutcome.Failure::class.java) + Assertions.assertEquals( + SkillLabSyncUserProfileErrorCode.IO_ERROR, + (response as UseCaseOutcome.Failure).errorCode + ) + Assertions.assertEquals("mock-error", response.errorMessage) + } + + private fun getSkillLabProfileResponseDto( + id: String = "user-profile-1", + mobileNumber: String = " +49 1234-56789 ", + ): SkillLabProfileResponseDto = + SkillLabProfileResponseDto( + profile = SkillLabProfileDto( + id = id, + mobileNumber = mobileNumber, + city = "Berlin", + country = "DE", + projects = listOf("Project 1", "Project 2", "Project 3"), + fullName = "Max Muster", + email = "max.muster@fake.local", + streetAndHouseNumber = "", + arrivalInCountry = "", + nationality = "DE", + gender = "male", + birthday = "2000-01-01", + genderCustom = null, + experiences = listOf( + SkillLabExperienceDto( + experiencesSkills = mutableListOf( + SkillLabSkillDto( + id = UUID.fromString("00000000-0000-0000-0000-000000000001"), + externalId = "http://link-to-esco-skill-1", + choice = "always", + ), + SkillLabSkillDto( + id = UUID.fromString("00000000-0000-0000-0000-000000000002"), + externalId = "http://link-to-esco-skill-2", + choice = "always", + ), + SkillLabSkillDto( + id = UUID.fromString("00000000-0000-0000-0000-000000000003"), + externalId = "http://link-to-esco-skill-3", + choice = "always", + ) + ) + ) + ), + updatedAt = "2022-02-02T22:22Z", + ) + ) +} diff --git a/docs/api-specs/skill-api-v1.yaml b/docs/api-specs/skill-api-v1.yaml index 4ba531f..1db527e 100644 --- a/docs/api-specs/skill-api-v1.yaml +++ b/docs/api-specs/skill-api-v1.yaml @@ -99,7 +99,7 @@ paths: /sync/{projectId}: post: summary: Trigger project sync - description: Import all changes for this project from SkillProvider + description: Import all changes for this project from SkillProvider. You can choose to sync just the changes from an specific date (DELTA) or do a full import (FULL). tags: - admin parameters: