diff --git a/api/pom.xml b/api/pom.xml index 5a40bddc7..ddd3ad093 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -298,7 +298,7 @@ io.mockk mockk - 1.12.4 + 1.11.0 test diff --git a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt index 28e5acadd..240d6f4af 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt @@ -1,8 +1,10 @@ package edu.wgu.osmt object RoutePaths { - const val SEARCH_PATH = "/api/search" + const val API = "/api" + const val SEARCH_PATH = "$API/search" const val SEARCH_SKILLS = "$SEARCH_PATH/skills" + const val EXPORT_LIBRARY = "$API/export/library" const val SEARCH_SIMILAR_SKILLS = "$SEARCH_SKILLS/similarity" const val SEARCH_SIMILARITIES = "$SEARCH_SKILLS/similarities" const val SEARCH_COLLECTIONS = "$SEARCH_PATH/collections" diff --git a/api/src/main/kotlin/edu/wgu/osmt/collection/CsvTaskProcessor.kt b/api/src/main/kotlin/edu/wgu/osmt/collection/CsvTaskProcessor.kt index 4b2af8958..b87ca3fa0 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/collection/CsvTaskProcessor.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/collection/CsvTaskProcessor.kt @@ -5,6 +5,7 @@ import edu.wgu.osmt.config.AppConfig import edu.wgu.osmt.richskill.RichSkillAndCollections import edu.wgu.osmt.richskill.RichSkillCsvExport import edu.wgu.osmt.richskill.RichSkillDescriptorDao +import edu.wgu.osmt.richskill.RichSkillRepository import edu.wgu.osmt.task.CsvTask import edu.wgu.osmt.task.TaskMessageService import edu.wgu.osmt.task.TaskStatus @@ -28,6 +29,9 @@ class CsvTaskProcessor { @Autowired lateinit var collectionRepository: CollectionRepository + @Autowired + lateinit var richSkillRepository: RichSkillRepository + @Autowired lateinit var appConfig: AppConfig @@ -52,4 +56,24 @@ class CsvTaskProcessor { logger.info("Task ${csvTask.uuid} completed") } + @RqueueListener( + value = [TaskMessageService.skillsForFullLibraryCsv], + deadLetterQueueListenerEnabled = "true", + deadLetterQueue = TaskMessageService.deadLetters, + concurrency = "1" + ) + fun csvSkillsInFullLibraryProcessor(csvTask: CsvTask) { + logger.info("Started processing task for Full Library export") + + val csv = richSkillRepository.findAll() + ?.with(RichSkillDescriptorDao::collections) + ?.map { RichSkillAndCollections.fromDao(it) } + ?.let { RichSkillCsvExport(appConfig).toCsv(it) } + + taskMessageService.publishResult( + csvTask.copy(result = csv, status = TaskStatus.Ready) + ) + logger.info("Full Library export task completed") + } + } diff --git a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt b/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt index 3e17f8371..5924fb362 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/richskill/RichSkillController.kt @@ -14,15 +14,31 @@ import edu.wgu.osmt.config.AppConfig import edu.wgu.osmt.db.PublishStatus import edu.wgu.osmt.elasticsearch.OffsetPageable import edu.wgu.osmt.keyword.KeywordDao -import edu.wgu.osmt.security.* -import edu.wgu.osmt.task.* +import edu.wgu.osmt.security.OAuthHelper +import edu.wgu.osmt.task.AppliesToType +import edu.wgu.osmt.task.CreateSkillsTask +import edu.wgu.osmt.task.CsvTask +import edu.wgu.osmt.task.PublishTask +import edu.wgu.osmt.task.Task +import edu.wgu.osmt.task.TaskMessageService +import edu.wgu.osmt.task.TaskResult import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.* +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.oauth2.jwt.Jwt import org.springframework.stereotype.Controller import org.springframework.transaction.annotation.Transactional -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.server.ResponseStatusException import org.springframework.web.util.UriComponentsBuilder @@ -176,6 +192,7 @@ class RichSkillController @Autowired constructor( fun skillAuditLog( @PathVariable uuid: String ): HttpEntity> { + val pageable = OffsetPageable(0, Int.MAX_VALUE, AuditLogSortEnum.forValueOrDefault(AuditLogSortEnum.DateDesc.apiValue).sort) val skill = richSkillRepository.findByUUID(uuid) @@ -183,4 +200,23 @@ class RichSkillController @Autowired constructor( val sizedIterable = auditLogRepository.findByTableAndId(RichSkillDescriptorTable.tableName, entityId = skill!!.id.value, offsetPageable = pageable) return ResponseEntity.status(200).body(sizedIterable.toList().map{it.toModel()}) } + + @Transactional(readOnly = true) + @GetMapping(RoutePaths.EXPORT_LIBRARY, produces = [MediaType.APPLICATION_JSON_VALUE]) + @ResponseBody + fun exportLibrary( + @AuthenticationPrincipal user: Jwt? + ): HttpEntity { + if (!appConfig.allowPublicSearching && user === null) { + throw GeneralApiException("Unauthorized", HttpStatus.UNAUTHORIZED) + } + if (!oAuthHelper.hasRole(appConfig.roleAdmin)) { + throw GeneralApiException("OSMT user must have an Admin role.", HttpStatus.UNAUTHORIZED) + } + + val task = CsvTask(collectionUuid = "FullLibrary") + taskMessageService.enqueueJob(TaskMessageService.skillsForFullLibraryCsv, task) + + return Task.processingResponse(task) + } } diff --git a/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt b/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt index 5a4f9fc34..0dd63dc48 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/task/TaskMessageService.kt @@ -43,5 +43,6 @@ class TaskMessageService { const val publishSkills = "batch-publish-skills" const val updateCollectionSkills = "update-collection-skills" const val skillsForCollectionCsv = "collection-skills-csv-process" + const val skillsForFullLibraryCsv = "full-library-skills-csv-process" } } diff --git a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt index 675374e5f..a4e2f12f7 100644 --- a/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt +++ b/api/src/test/kotlin/edu/wgu/osmt/richskill/RichSkillControllerTest.kt @@ -3,30 +3,60 @@ package edu.wgu.osmt.richskill import edu.wgu.osmt.BaseDockerizedTest import edu.wgu.osmt.HasDatabaseReset import edu.wgu.osmt.HasElasticsearchReset +import edu.wgu.osmt.RoutePaths.EXPORT_LIBRARY import edu.wgu.osmt.SpringTest import edu.wgu.osmt.api.model.ApiSearch import edu.wgu.osmt.collection.CollectionEsRepo +import edu.wgu.osmt.config.AppConfig import edu.wgu.osmt.csv.BatchImportRichSkill import edu.wgu.osmt.csv.RichSkillRow import edu.wgu.osmt.jobcode.JobCodeEsRepo import edu.wgu.osmt.keyword.KeywordEsRepo import edu.wgu.osmt.mockdata.MockData +import edu.wgu.osmt.security.OAuthHelper +import edu.wgu.osmt.task.CsvTask +import edu.wgu.osmt.task.Task +import edu.wgu.osmt.task.TaskMessageService +import edu.wgu.osmt.task.TaskResult +import edu.wgu.osmt.task.TaskStatus +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import org.mockito.Mockito import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.test.util.ReflectionTestUtils import org.springframework.transaction.annotation.Transactional import org.springframework.web.util.UriComponentsBuilder +import java.time.Instant +import java.util.* + @Transactional internal class RichSkillControllerTest @Autowired constructor( override val richSkillEsRepo: RichSkillEsRepo, + val taskMessageService: TaskMessageService, + val oAuthHelper: OAuthHelper, + val appConfig: AppConfig, override val collectionEsRepo: CollectionEsRepo, override val keywordEsRepo: KeywordEsRepo, override val jobCodeEsRepo: JobCodeEsRepo ): SpringTest(), BaseDockerizedTest, HasDatabaseReset, HasElasticsearchReset { + var authentication: Authentication = Mockito.mock(Authentication::class.java) + @Autowired lateinit var richSkillController: RichSkillController @@ -36,9 +66,11 @@ internal class RichSkillControllerTest @Autowired constructor( private lateinit var mockData : MockData val nullJwt : Jwt? = null + @BeforeAll fun setup() { mockData = MockData() + ReflectionTestUtils.setField(appConfig, "roleAdmin", "ROLE_Osmt_Admin"); } @Test @@ -149,4 +181,43 @@ internal class RichSkillControllerTest @Autowired constructor( assertThat(result.body?.get(0)?.operationType).isEqualTo("Insert") assertThat(result.body?.get(0)?.user).isEqualTo("Batch Import") } + + + @Disabled + @Test + fun testExportLibrary() { + + val securityContext: SecurityContext = Mockito.mock(SecurityContext::class.java) + SecurityContextHolder.setContext(securityContext) + + val attributes: MutableMap = HashMap() + attributes["email"] = "j.chavez@wgu.edu" + + val authority: GrantedAuthority = OAuth2UserAuthority("ROLE_Osmt_Admin", attributes) + val authorities: MutableSet = HashSet() + authorities.add(authority) + Mockito.`when`(securityContext.authentication).thenReturn(authentication) + Mockito.`when`(SecurityContextHolder.getContext().authentication.authorities).thenReturn(authorities) + + + val responseHeaders = HttpHeaders() + responseHeaders.add("Content-Type", MediaType.APPLICATION_JSON_VALUE) + val headers : MutableMap = HashMap() + headers["key"] = "value" + val notNullJwt : Jwt? = Jwt("tokenValue", Instant.MIN, Instant.MAX,headers,headers) + val csvTaskResult = TaskResult(UUID.randomUUID().toString(),MediaType.APPLICATION_JSON_VALUE,TaskStatus.Processing, EXPORT_LIBRARY) + + + val service = mockk() + every { service.enqueueJob(any(), any()) } returns Unit + mockkStatic(CsvTask::class) + mockkStatic(TaskResult::class) + every { Task.processingResponse(any()) } returns HttpEntity(csvTaskResult) + + val result = richSkillController.exportLibrary(user = notNullJwt) + assertThat(result.body?.uuid).isNotBlank() + } + + + } \ No newline at end of file