From d96369c7e335ba150ec66ad58a59afe63401a316 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Sun, 27 Sep 2026 22:22:54 +0300 Subject: [PATCH] Test: Testcontainers integration tests for distribution and content import --- .../content/ContentImportIntegrationTest.kt | 87 +++++++++++++++++++ .../ProgressDistributionIntegrationTest.kt | 82 +++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 services/academic-service/src/test/kotlin/ru/ysu/lms/academic/content/ContentImportIntegrationTest.kt create mode 100644 services/progress-service/src/test/kotlin/ru/ysu/lms/progress/ProgressDistributionIntegrationTest.kt diff --git a/services/academic-service/src/test/kotlin/ru/ysu/lms/academic/content/ContentImportIntegrationTest.kt b/services/academic-service/src/test/kotlin/ru/ysu/lms/academic/content/ContentImportIntegrationTest.kt new file mode 100644 index 0000000..675ca6a --- /dev/null +++ b/services/academic-service/src/test/kotlin/ru/ysu/lms/academic/content/ContentImportIntegrationTest.kt @@ -0,0 +1,87 @@ +package ru.ysu.lms.academic.content + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.postgresql.PostgreSQLContainer +import ru.ysu.lms.academic.repo.ContentArticleRepository +import ru.ysu.lms.academic.repo.ContentChapterRepository +import ru.ysu.lms.academic.repo.ContentSectionRepository +import java.io.ByteArrayOutputStream +import java.nio.file.Files +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * Интеграционный тест импорта материалов: реальная PostgreSQL (Testcontainers) + Flyway. + * Проверяет, что повторный импорт не плодит дубли (сопоставление по названию). + */ +@Testcontainers +@SpringBootTest +class ContentImportIntegrationTest { + companion object { + @Container + @ServiceConnection + @JvmStatic + val postgres = PostgreSQLContainer("postgres:18") + + @JvmStatic + @DynamicPropertySource + fun assetsDir(registry: DynamicPropertyRegistry) { + registry.add("lms.content.assets-dir") { Files.createTempDirectory("lms-assets").toString() } + } + + private fun articleZip(): ByteArray { + val out = ByteArrayOutputStream() + ZipOutputStream(out).use { zos -> + zos.putNextEntry(ZipEntry("chapters/05-Тестовая/01-Раздел/01-Статья.md")) + zos.write( + "---\ntitle: \"Тестовая статья\"\nslug: \"test-article\"\n---\nТело статьи".toByteArray(), + ) + zos.closeEntry() + } + return out.toByteArray() + } + } + + @Autowired + private lateinit var importer: ContentImporter + + @Autowired + private lateinit var chapters: ContentChapterRepository + + @Autowired + private lateinit var sections: ContentSectionRepository + + @Autowired + private lateinit var articles: ContentArticleRepository + + @Test + fun `re-import does not duplicate chapter, section or article`() { + val zip = articleZip() + + val first = importer.importZip("content.zip", zip, sync = false) + assertThat(first.chapters).isEqualTo(1) + + val chapter = chapters.findByTitleIgnoreCase("Тестовая") + assertThat(chapter).isNotNull + val section = sections.findByChapterIdAndTitleIgnoreCase(chapter!!.id, "Раздел") + assertThat(section).isNotNull + val article = articles.findBySectionIdAndTitleIgnoreCase(section!!.id, "Тестовая статья") + assertThat(article).isNotNull + assertThat(article!!.slug).isEqualTo("test-article") + + // повторный импорт того же архива обновляет существующие записи, а не создаёт новые + importer.importZip("content.zip", zip, sync = false) + + assertThat(chapters.findByTitleIgnoreCase("Тестовая")!!.id).isEqualTo(chapter.id) + assertThat(sections.findByChapterIdOrderByPositionAscIdAsc(chapter.id)).hasSize(1) + assertThat(articles.findBySectionIdOrderByPositionAscIdAsc(section.id)).hasSize(1) + } +} diff --git a/services/progress-service/src/test/kotlin/ru/ysu/lms/progress/ProgressDistributionIntegrationTest.kt b/services/progress-service/src/test/kotlin/ru/ysu/lms/progress/ProgressDistributionIntegrationTest.kt new file mode 100644 index 0000000..bb44e9a --- /dev/null +++ b/services/progress-service/src/test/kotlin/ru/ysu/lms/progress/ProgressDistributionIntegrationTest.kt @@ -0,0 +1,82 @@ +package ru.ysu.lms.progress + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.Mockito.`when` +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.postgresql.PostgreSQLContainer +import ru.ysu.lms.progress.repo.VariantAssignmentRepository +import java.util.UUID + +/** + * Интеграционный тест распределения вариантов: реальная PostgreSQL (Testcontainers) + Flyway, + * внешние сервисы (identity/academic) подменены заглушками. + */ +@Testcontainers +@SpringBootTest +class ProgressDistributionIntegrationTest { + companion object { + @Container + @ServiceConnection + @JvmStatic + val postgres = PostgreSQLContainer("postgres:18") + } + + @Autowired + private lateinit var service: ProgressService + + @Autowired + private lateinit var assignments: VariantAssignmentRepository + + @MockitoBean + private lateinit var academic: AcademicClient + + @MockitoBean + private lateinit var identity: IdentityClient + + @Test + fun `distributes variants to every student of the selected group`() { + val labId = UUID.randomUUID() + val variantA = UUID.randomUUID().toString() + val variantB = UUID.randomUUID().toString() + val student1 = UUID.randomUUID().toString() + val student2 = UUID.randomUUID().toString() + + `when`(identity.listGroups("t")).thenReturn(listOf(mapOf("id" to "g1"))) + `when`(identity.getGroup("g1", "t")).thenReturn( + GroupInfo(id = "g1", name = "Группа", students = listOf(StudentInfo(student1), StudentInfo(student2))), + ) + `when`(academic.getLab(labId.toString(), "t")).thenReturn( + LabInfo( + id = labId.toString(), + levels = + listOf( + LevelInfo( + id = "lvl", + code = "basic", + title = "Основная", + variants = + listOf( + VariantInfo(id = variantA, number = "1"), + VariantInfo(id = variantB, number = "2"), + ), + ), + ), + ), + ) + + val result = service.distribute(DistributeRequest(labIds = listOf(labId.toString()), groupIds = listOf("g1")), "t") + + assertThat(result.assigned).isEqualTo(2) + assertThat(result.labs).isEqualTo(1) + val saved = assignments.findByLabId(labId) + assertThat(saved).hasSize(2) + assertThat(saved.map { it.studentId.toString() }).containsExactlyInAnyOrder(student1, student2) + assertThat(saved.map { it.variantId.toString() }).containsAnyOf(variantA, variantB) + } +}