diff --git a/.github/workflows/alpha-server.yml b/.github/workflows/alpha-server.yml index 857462a..96ad7e7 100644 --- a/.github/workflows/alpha-server.yml +++ b/.github/workflows/alpha-server.yml @@ -38,7 +38,7 @@ jobs: - name: Cache Docker images. uses: ScribeMD/docker-cache@0.5.0 with: - key: docker-${{ runner.os }}-${{ hashFiles('docker-compose.yaml') }} + key: docker-${{ runner.os }}-${{ hashFiles('Dockerfile', 'docker-image-in-test') }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build mini service diff --git a/backend/src/main/kotlin/com/storyteller_f/Backend.kt b/backend/src/main/kotlin/com/storyteller_f/Backend.kt index b17b323..22a6c4d 100644 --- a/backend/src/main/kotlin/com/storyteller_f/Backend.kt +++ b/backend/src/main/kotlin/com/storyteller_f/Backend.kt @@ -1,8 +1,8 @@ package com.storyteller_f -import com.storyteller_f.index.ElasticTopicDocumentService -import com.storyteller_f.index.LuceneTopicDocumentService -import com.storyteller_f.index.TopicDocumentService +import com.storyteller_f.index.ElasticTopicSearchService +import com.storyteller_f.index.LuceneTopicSearchService +import com.storyteller_f.index.TopicSearchService import com.storyteller_f.media.FileSystemMediaService import com.storyteller_f.media.MediaService import com.storyteller_f.media.MinIoMediaService @@ -20,7 +20,7 @@ import java.util.* class Backend( val config: Config, - val topicDocumentService: TopicDocumentService, + val topicSearchService: TopicSearchService, val mediaService: MediaService, val nameService: NameService ) @@ -36,8 +36,8 @@ data class ElasticConnection(val url: String, val certFile: String, val name: St data class MinIoConnection(val url: String, val user: String, val pass: String) data class DatabaseConnection(val uri: String, val driver: String, val user: String, val password: String) -fun readEnv(): Map { - val map = ClassLoader.getSystemClassLoader().getResourceAsStream(".env")?.use { +fun readEnv(envResFile: String = ".env"): Map { + val map = ClassLoader.getSystemClassLoader().getResourceAsStream(envResFile)?.use { Properties().apply { load(it) } @@ -98,14 +98,14 @@ private fun mediaService(map: Map): MediaService { private fun topicDocumentService( map: Map, -): TopicDocumentService { +): TopicSearchService { return when (val type = map["SEARCH_SERVICE"]) { "elastic" -> { val certFile = map["CERT_FILE"] as String val url = map["ELASTIC_URL"] as String val name = map["ELASTIC_NAME"] as String val pass = map["ELASTIC_PASSWORD"] as String - ElasticTopicDocumentService(ElasticConnection(url, certFile, name, pass)) + ElasticTopicSearchService(ElasticConnection(url, certFile, name, pass)) } "lucene" -> { @@ -114,7 +114,7 @@ private fun topicDocumentService( Napier.i { "lucene path $path" } - LuceneTopicDocumentService(path) + LuceneTopicSearchService(path) } else -> throw UnsupportedOperationException("unsupported search service type [$type]") diff --git a/backend/src/main/kotlin/com/storyteller_f/index/ElasticTopicDocumentService.kt b/backend/src/main/kotlin/com/storyteller_f/index/ElasticTopicSearchService.kt similarity index 92% rename from backend/src/main/kotlin/com/storyteller_f/index/ElasticTopicDocumentService.kt rename to backend/src/main/kotlin/com/storyteller_f/index/ElasticTopicSearchService.kt index f7b3410..e1a08d1 100644 --- a/backend/src/main/kotlin/com/storyteller_f/index/ElasticTopicDocumentService.kt +++ b/backend/src/main/kotlin/com/storyteller_f/index/ElasticTopicSearchService.kt @@ -30,8 +30,13 @@ import org.elasticsearch.client.RestClient import java.io.File import java.io.FileInputStream import java.net.ConnectException +import javax.net.ssl.SSLContext + +class ElasticTopicSearchService(private val connection: ElasticConnection) : TopicSearchService { + companion object { + var INJECTED_SSL_CONTEXT: SSLContext? = null + } -class ElasticTopicDocumentService(private val connection: ElasticConnection) : TopicDocumentService { override suspend fun saveDocument(topics: List): Result { return useElasticClient(connection) { topics.map { topic -> @@ -173,12 +178,15 @@ private suspend fun useElasticClient( elasticConnection: ElasticConnection, block: suspend ElasticsearchAsyncClient.() -> T ): Result { - val crtStream = withContext(Dispatchers.IO) { - Napier.i(message = "cert path ${File(elasticConnection.certFile).canonicalPath}") - FileInputStream(elasticConnection.certFile) + val sslContext = if (elasticConnection.certFile.isNotBlank()) { + val crtStream = withContext(Dispatchers.IO) { + Napier.i(message = "cert path ${File(elasticConnection.certFile).canonicalPath}") + FileInputStream(elasticConnection.certFile) + } + TransportUtils.sslContextFromHttpCaCrt(crtStream) + } else { + null } - val sslContext = TransportUtils - .sslContextFromHttpCaCrt(crtStream) val credsProv = BasicCredentialsProvider() credsProv.setCredentials( diff --git a/backend/src/main/kotlin/com/storyteller_f/index/LuceneTopicDocumentService.kt b/backend/src/main/kotlin/com/storyteller_f/index/LuceneTopicSearchService.kt similarity index 98% rename from backend/src/main/kotlin/com/storyteller_f/index/LuceneTopicDocumentService.kt rename to backend/src/main/kotlin/com/storyteller_f/index/LuceneTopicSearchService.kt index 118a1db..a1159be 100644 --- a/backend/src/main/kotlin/com/storyteller_f/index/LuceneTopicDocumentService.kt +++ b/backend/src/main/kotlin/com/storyteller_f/index/LuceneTopicSearchService.kt @@ -18,7 +18,7 @@ import org.apache.lucene.search.* import org.apache.lucene.store.FSDirectory import java.nio.file.Path -class LuceneTopicDocumentService(private val path: Path) : TopicDocumentService { +class LuceneTopicSearchService(private val path: Path) : TopicSearchService { private val analyzer = StandardAnalyzer() override suspend fun saveDocument(topics: List): Result { diff --git a/backend/src/main/kotlin/com/storyteller_f/index/TopicDocument.kt b/backend/src/main/kotlin/com/storyteller_f/index/TopicDocument.kt index 320a855..577b3f7 100644 --- a/backend/src/main/kotlin/com/storyteller_f/index/TopicDocument.kt +++ b/backend/src/main/kotlin/com/storyteller_f/index/TopicDocument.kt @@ -30,7 +30,7 @@ data class TopicDocument( } } -interface TopicDocumentService { +interface TopicSearchService { suspend fun saveDocument(topics: List): Result suspend fun getDocument(idList: List): Result> diff --git a/backend/src/main/kotlin/com/storyteller_f/media/FileSystemMediaService.kt b/backend/src/main/kotlin/com/storyteller_f/media/FileSystemMediaService.kt index 2e2ca5b..c639451 100644 --- a/backend/src/main/kotlin/com/storyteller_f/media/FileSystemMediaService.kt +++ b/backend/src/main/kotlin/com/storyteller_f/media/FileSystemMediaService.kt @@ -1,7 +1,7 @@ package com.storyteller_f.media class FileSystemMediaService : MediaService { - override fun upload(bucketName: String, list: List>): Result = Result.success(Unit) + override fun upload(bucketName: String, list: List): Result = Result.success(Unit) override fun get(bucketName: String, objList: List): Result> { return Result.success(objList.map { diff --git a/backend/src/main/kotlin/com/storyteller_f/media/MediaService.kt b/backend/src/main/kotlin/com/storyteller_f/media/MediaService.kt index fb02320..bfe77b8 100644 --- a/backend/src/main/kotlin/com/storyteller_f/media/MediaService.kt +++ b/backend/src/main/kotlin/com/storyteller_f/media/MediaService.kt @@ -1,7 +1,11 @@ package com.storyteller_f.media +import java.io.File + +data class UploadPack(val name: String, val path: File) + interface MediaService { - fun upload(bucketName: String, list: List>): Result + fun upload(bucketName: String, list: List): Result fun get(bucketName: String, objList: List): Result> diff --git a/backend/src/main/kotlin/com/storyteller_f/media/MinIoMediaService.kt b/backend/src/main/kotlin/com/storyteller_f/media/MinIoMediaService.kt index eb26a60..ea02274 100644 --- a/backend/src/main/kotlin/com/storyteller_f/media/MinIoMediaService.kt +++ b/backend/src/main/kotlin/com/storyteller_f/media/MinIoMediaService.kt @@ -4,6 +4,7 @@ import com.storyteller_f.MinIoConnection import io.minio.* import io.minio.http.Method import java.util.concurrent.TimeUnit +import kotlin.Result class MinIoMediaService(private val connection: MinIoConnection) : MediaService { override fun clean(bucketName: String): kotlin.Result { @@ -22,14 +23,16 @@ class MinIoMediaService(private val connection: MinIoConnection) : MediaService } } - override fun upload(bucketName: String, list: List>): kotlin.Result { + override fun upload(bucketName: String, list: List): Result { return useMinIoClient(connection) { if (!bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()) } list.forEach { (objName, picFullPath) -> uploadObject( - UploadObjectArgs.builder().bucket(bucketName).`object`(objName).filename(picFullPath).build() + UploadObjectArgs.builder().bucket( + bucketName + ).`object`(objName).filename(picFullPath.absolutePath).build() ) } } diff --git a/cli/src/main/kotlin/com/storyteller_f/Add.kt b/cli/src/main/kotlin/com/storyteller_f/Add.kt index 68c790c..c429716 100644 --- a/cli/src/main/kotlin/com/storyteller_f/Add.kt +++ b/cli/src/main/kotlin/com/storyteller_f/Add.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.readValue import com.perraco.utils.SnowflakeFactory import com.storyteller_f.index.TopicDocument +import com.storyteller_f.media.UploadPack import com.storyteller_f.shared.* import com.storyteller_f.shared.obj.PresetTopic import com.storyteller_f.shared.obj.PresetValue @@ -84,7 +85,7 @@ class Add : Subcommand("add", "add entry") { Triple(it, null, id) } else { val p = "icon/${Uuid.random()}" - backend.mediaService.upload("apic", listOf(p to File(parentDir, icon).absolutePath)) + backend.mediaService.upload("apic", listOf(UploadPack(p, File(parentDir, icon)))) Triple(it, p, id) } } @@ -93,16 +94,12 @@ class Add : Subcommand("add", "add entry") { it.users + it.admin }.distinct().map { User.wrapRow(findUserByAId(it)!!) - }.associate { - it.aid to it - } + }.associateBy { it.aid } val communityMap = l.mapNotNull { it.community }.distinct().map { Community.wrapRow(findCommunityByAId(it)!!) - }.associate { - it.aid to it - } + }.associateBy { it.aid } val idList = Rooms.batchInsert(data) { (it, p, id) -> this[Rooms.id] = id this[Rooms.aid] = it.id @@ -132,9 +129,7 @@ class Add : Subcommand("add", "add entry") { it.author }.distinct().map { User.wrapRow(findUserByAId(it)!!) - }.associate { - it.aid!! to it - } + }.associateBy { it.aid!! } data.groupBy { it.community != null }.forEach { (t, u) -> @@ -147,15 +142,12 @@ class Add : Subcommand("add", "add entry") { }.getOrThrow() } - @OptIn(ExperimentalUnsignedTypes::class) private suspend fun addTopicsIntoRoom(u: List, userList: Map, parentDir: File) { val roomList = u.mapNotNull { it.room }.distinct().map { Room.wrapRow(Room.findRoomByAId(it).firstOrNull()!!) - }.associate { - it.aid to it - } + }.associateBy { it.aid } val ids = insertRoomTopicBaseLevel(u, userList, roomList) // 检查聊天室是属于社区的还是私有的 val roomIsPrivate = roomList.mapValues { (_, value) -> @@ -166,7 +158,6 @@ class Add : Subcommand("add", "add entry") { insertUnEncryptedTopic(u, roomIsPrivate, parentDir, ids, roomList, userList) } - @OptIn(ExperimentalUnsignedTypes::class) private suspend fun insertRoomTopicBaseLevel( u: List, userList: Map, @@ -210,7 +201,6 @@ class Add : Subcommand("add", "add entry") { return ids } - @OptIn(ExperimentalUnsignedTypes::class) private suspend fun insertUnEncryptedTopic( presetTopicList: List, roomIsPrivate: Map, @@ -226,7 +216,7 @@ class Add : Subcommand("add", "add entry") { null } } - backend.topicDocumentService.saveDocument( + backend.topicSearchService.saveDocument( topicsPublic.map { (first, second) -> val content = getTopicContent(first, parentDir) val level = first.level @@ -246,7 +236,6 @@ class Add : Subcommand("add", "add entry") { ) } - @OptIn(ExperimentalUnsignedTypes::class) private suspend fun insertEncryptedTopic( roomIsPrivate: Map, parentDir: File, @@ -274,9 +263,7 @@ class Add : Subcommand("add", "add entry") { }.map { it.publicKey to it.id } - }.associate { - it.first to it - } + }.associateBy { it.first } val encrypted = topicsPrivate.map { (addTopic, index) -> val (first, aesBytes) = encrypt(getTopicContent(addTopic, parentDir)) Tuple4(index, first, aesBytes, addTopic) @@ -299,7 +286,6 @@ class Add : Subcommand("add", "add entry") { } } - @OptIn(ExperimentalUnsignedTypes::class) private suspend fun addTopicsIntoCommunity(u: List, userList: Map, parentDir: File) { val communityList = u.mapNotNull { it.community @@ -308,15 +294,15 @@ class Add : Subcommand("add", "add entry") { if (rowCommunity == null) { error("$it not found") } else { - Community.wrapRow(rowCommunity).let { - it.aid to it.id + Community.wrapRow(rowCommunity).let { community -> + community.aid to community.id } } }.associate { it.first to it.second } val ids = insertCommunityTopicTopLevel(u, userList, communityList) - backend.topicDocumentService.saveDocument( + backend.topicSearchService.saveDocument( u.mapIndexedNotNull { index, topic -> if (topic.community != null) { val content = getTopicContent(topic, parentDir) @@ -408,8 +394,7 @@ class Add : Subcommand("add", "add entry") { Tuple5(it, null, derPublicKey, ad, id) } else { val p = "icon/${Uuid.random()}" - val absolutePath = File(parentDir, icon).absolutePath - backend.mediaService.upload("apic", listOf(p to absolutePath)) + backend.mediaService.upload("apic", listOf(UploadPack(p, File(parentDir, icon)))) Tuple5(it, null, derPublicKey, ad, id) } } @@ -436,7 +421,7 @@ class Add : Subcommand("add", "add entry") { Triple(it, null, id) } else { val p = "icon/${Uuid.random()}" - backend.mediaService.upload("apic", listOf(p to File(parentDir, icon).absolutePath)) + backend.mediaService.upload("apic", listOf(UploadPack(p, File(parentDir, icon)))) Triple(it, p, id) } } diff --git a/cli/src/main/kotlin/com/storyteller_f/CleanCommand.kt b/cli/src/main/kotlin/com/storyteller_f/CleanCommand.kt index 7f2adb3..5cc47a6 100644 --- a/cli/src/main/kotlin/com/storyteller_f/CleanCommand.kt +++ b/cli/src/main/kotlin/com/storyteller_f/CleanCommand.kt @@ -14,7 +14,7 @@ class CleanCommand : Subcommand("clean", "clean all data") { } backend.mediaService.clean("apic") runBlocking { - backend.topicDocumentService.clean() + backend.topicSearchService.clean() } Napier.i { "clean done." diff --git a/docker-image-in-test b/docker-image-in-test new file mode 100644 index 0000000..0459d66 --- /dev/null +++ b/docker-image-in-test @@ -0,0 +1,2 @@ +docker.elastic.co/elasticsearch/elasticsearch:7.9.2 +minio/minio:RELEASE.2023-09-04T19-57-37Z \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb70822..fd8ecbd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ kotlinxDatetime = "0.6.1" ktomlCore = "0.5.1" lifecycleViewmodelCompose = "2.8.7" luceneCore = "10.0.0" +minioVersion = "1.20.0" mosaicRuntime = "0.14.0" multiplatformCryptoLibsodiumBindings = "0.9.2" multiplatformMarkdownRenderer = "0.27.0" @@ -60,6 +61,7 @@ kover = "0.7.4" [libraries] desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } +elasticsearch = { module = "org.testcontainers:elasticsearch", version.ref = "minioVersion" } emoji-compose-m3 = { module = "org.kodein.emoji:emoji-compose-m3", version.ref = "emojiKt" } emoji-kt = { module = "org.kodein.emoji:emoji-kt", version.ref = "emojiKt" } geoip2 = { module = "com.maxmind.geoip2:geoip2", version.ref = "geoip2" } @@ -155,6 +157,7 @@ postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4jSimple" } sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" } sunny-chung-composable-table = { module = "io.github.sunny-chung:composable-table", version.ref = "composableTableVersion" } +testcontainers-minio = { module = "org.testcontainers:minio", version.ref = "minioVersion" } uri-kmp = { module = "com.eygraber:uri-kmp", version.ref = "uriKmp" } [plugins] diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 589c7ad..ab04e96 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -30,7 +30,8 @@ dependencies { testImplementation(libs.ktor.server.test.host) testImplementation(libs.kotlin.test) testImplementation(libs.h2) - testImplementation(libs.jimfs) + testImplementation(libs.testcontainers.minio) + testImplementation(libs.elasticsearch) implementation(libs.pdfbox) implementation(libs.napier) implementation(libs.emoji.reader.jvm) diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/Websocket.kt b/server/src/main/kotlin/com/storyteller_f/a/server/Websocket.kt index 41712dd..3a0a62e 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/Websocket.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/Websocket.kt @@ -180,7 +180,7 @@ private suspend fun savePlainTopicContent( ): Result { return DatabaseFactory.dbQuery { Topic.new(topic) - backend.topicDocumentService.saveDocument( + backend.topicSearchService.saveDocument( listOf(TopicDocument.fromTopic(topic, content)) ).getOrThrow() diff --git a/server/src/main/kotlin/com/storyteller_f/a/server/service/Topic.kt b/server/src/main/kotlin/com/storyteller_f/a/server/service/Topic.kt index 991bdbb..4c0f2e1 100644 --- a/server/src/main/kotlin/com/storyteller_f/a/server/service/Topic.kt +++ b/server/src/main/kotlin/com/storyteller_f/a/server/service/Topic.kt @@ -21,16 +21,16 @@ import com.storyteller_f.types.PaginationResult import io.ktor.resources.* import io.ktor.server.request.* import io.ktor.server.routing.* -import org.apache.fontbox.ttf.OTFParser -import org.apache.pdfbox.io.RandomAccessReadBufferedFile import org.apache.pdfbox.pdmodel.PDDocument import org.apache.pdfbox.pdmodel.PDPage import org.apache.pdfbox.pdmodel.PDPageContentStream +import org.apache.pdfbox.pdmodel.font.FontMappers import org.apache.pdfbox.pdmodel.font.PDType0Font import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SqlExpressionBuilder import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.selectAll +import java.awt.GraphicsEnvironment import java.io.File @Resource("/topics") @@ -93,7 +93,7 @@ suspend fun RoutingContext.addTopicAtCommunity(uid: PrimaryKey, backend: Backend ) DatabaseFactory.dbQuery { val newTopicId = Topic.new(topic) - backend.topicDocumentService.saveDocument( + backend.topicSearchService.saveDocument( listOf( TopicDocument( newTopicId, @@ -136,47 +136,54 @@ private suspend fun getTopicSnapshot( backend: Backend ): Result { val topicId = topicInfo.id - return backend.topicDocumentService.getDocument(listOf(topicId)).map { value -> value.firstOrNull() } + return backend.topicSearchService.getDocument(listOf(topicId)).map { value -> value.firstOrNull() } .mapResultNotNull { documents -> DatabaseFactory.first(User::toUserInfo, User::wrapRow) { User.findById(topicInfo.author) }.mapNotNull { authorInfo -> - PDDocument().use { document -> - val firstPage = PDPage() - PDPageContentStream(document, firstPage).use { stream -> - stream.beginText() - val otf = OTFParser().parse( - RandomAccessReadBufferedFile( - File( - "~/DIN-Regular.otf".replace( - "~", - System.getProperty("user.home") - ) - ) - ) - ) - stream.setFont(PDType0Font.load(document, otf, false), 12f) - stream.newLineAtOffset(100F, 700F) - stream.setLeading(14.5f) - stream.showText(if (authorInfo.aid == null) authorInfo.address else authorInfo.aid) - stream.newLine() - stream.showText(documents.content) - stream.newLine() - stream.showText(if (creatorInfo.aid == null) creatorInfo.address else creatorInfo.aid) - stream.newLine() - stream.showText(topicInfo.createdTime.toString()) - stream.newLine() - stream.showText(now().toString()) - stream.endText() - } - document.addPage(firstPage) - document.save("/tmp/1.pdf") - } + generateSnapshot(authorInfo, documents, creatorInfo, topicInfo) File("/tmp/1.pdf") } } } +private fun generateSnapshot( + authorInfo: UserInfo, + documents: TopicDocument, + creatorInfo: UserInfo, + topicInfo: TopicInfo +) { + val graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment() + PDDocument().use { document -> + val firstPage = PDPage() + PDPageContentStream(document, firstPage).use { stream -> + stream.beginText() + // 创建字体文件 + val fontNames = graphicsEnvironment.availableFontFamilyNames + val firstFontName = fontNames.firstOrNull() + + // 使用 PDFBox 加载字体 + val pdFont = + PDType0Font.load(document, FontMappers.instance().getTrueTypeFont(firstFontName, null).font, true) + stream.setFont(pdFont, 12f) + stream.newLineAtOffset(100F, 700F) + stream.setLeading(14.5f) + stream.showText(if (authorInfo.aid == null) authorInfo.address else authorInfo.aid) + stream.newLine() + stream.showText(documents.content) + stream.newLine() + stream.showText(if (creatorInfo.aid == null) creatorInfo.address else creatorInfo.aid) + stream.newLine() + stream.showText(topicInfo.createdTime.toString()) + stream.newLine() + stream.showText(now().toString()) + stream.endText() + } + document.addPage(firstPage) + document.save("/tmp/1.pdf") + } +} + suspend fun getTopic( topicId: PrimaryKey, uid: PrimaryKey?, @@ -200,7 +207,7 @@ suspend fun getTopic( } } } else { - backend.topicDocumentService.getDocument(listOf(topicId)).map { value -> + backend.topicSearchService.getDocument(listOf(topicId)).map { value -> value.firstOrNull()?.content?.let { info.copy(content = TopicContent.Plain(it)) } @@ -238,7 +245,7 @@ suspend fun getTopics( size, fillHasCommented ).mapResult { (data, count) -> - backend.topicDocumentService.getDocument(data.map { + backend.topicSearchService.getDocument(data.map { it.id }).map { it.mapNotNull { @@ -383,7 +390,7 @@ suspend fun searchPublicTopics( backend: Backend, uid: PrimaryKey? ): Result?> { - return backend.topicDocumentService.searchDocument( + return backend.topicSearchService.searchDocument( search.word, size, nextTopicId, @@ -430,7 +437,7 @@ suspend fun recommendTopics( size, fillHasCommented ).mapResult { (data, count) -> - backend.topicDocumentService.getDocument(data.map { + backend.topicSearchService.getDocument(data.map { it.id }).map { value -> PaginationResult(data.mapIndexed { i, t -> diff --git a/server/src/test/kotlin/TestBuilder.kt b/server/src/test/kotlin/TestBuilder.kt index 324cf66..eadcdf6 100644 --- a/server/src/test/kotlin/TestBuilder.kt +++ b/server/src/test/kotlin/TestBuilder.kt @@ -1,15 +1,9 @@ import com.perraco.utils.SnowflakeFactory import com.storyteller_f.DatabaseFactory import com.storyteller_f.a.client_lib.* -import com.storyteller_f.a.client_lib.ClientSession -import com.storyteller_f.a.client_lib.LoginViewModel -import com.storyteller_f.a.client_lib.getData -import com.storyteller_f.a.client_lib.signUp import com.storyteller_f.buildBackendFromEnv import com.storyteller_f.readEnv import com.storyteller_f.shared.* -import com.storyteller_f.shared.finalData -import com.storyteller_f.shared.signature import com.storyteller_f.shared.type.PrimaryKey import com.storyteller_f.shared.type.Tuple4 import com.storyteller_f.shared.type.Tuple5 @@ -18,9 +12,9 @@ import io.ktor.server.application.* import io.ktor.server.config.* import io.ktor.server.routing.* import io.ktor.server.testing.* -import java.nio.file.Paths -import kotlin.io.path.ExperimentalPathApi -import kotlin.io.path.deleteRecursively +import org.testcontainers.containers.MinIOContainer +import org.testcontainers.elasticsearch.ElasticsearchContainer +import org.testcontainers.utility.DockerImageName @Suppress("unused") fun Application.module() { @@ -28,33 +22,61 @@ fun Application.module() { } } -@OptIn(ExperimentalPathApi::class) fun test(block: suspend (HttpClient) -> Unit) { SnowflakeFactory.setMachine(0) + addProvider() testApplication { - addProvider() - val path = Paths.get("../deploy/lucene_data/index") - path.deleteRecursively() - val backend = buildBackendFromEnv(readEnv()) - DatabaseFactory.clean(backend.config.databaseConnection) - DatabaseFactory.init(backend.config.databaseConnection) - environment { - config = MapApplicationConfig( - "ktor.application.modules.0" to "TestBuilderKt.module", - "ktor.application.modules.1" to "com.storyteller_f.a.server.ApplicationKt.module", - "ktor.application.modules.size" to "2" - ) + val env = readEnv(".env").toMutableMap() + ElasticsearchContainer( + DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:7.9.2") + ).use { elasticClient -> + elasticClient.start() + env["SEARCH_SERVICE"] = "elastic" + env["ELASTIC_NAME"] = "elastic" + env["ELASTIC_PASSWORD"] = "changeme" + env["ELASTIC_URL"] = "https://${elasticClient.httpHostAddress}" + MinIOContainer("minio/minio:RELEASE.2023-09-04T19-57-37Z").use { minioContainer -> + minioContainer.start() + env["MEDIA_SERVICE"] = "minio" + env["MINIO_URL"] = minioContainer.s3URL + env["MINIO_NAME"] = minioContainer.userName + env["MINIO_PASS"] = minioContainer.password + doTest(env, block) + minioContainer.stop() + } + elasticClient.stop() } - val client = createClient { - defaultClientConfigure() - } - block(client) - path.deleteRecursively() - DatabaseFactory.clean() } + testApplication { + val env = readEnv(".env") + doTest(env, block) + } +} + +private suspend fun ApplicationTestBuilder.doTest( + env: Map, + block: suspend (HttpClient) -> Unit +) { + val backend = buildBackendFromEnv(env) + backend.topicSearchService.clean() + DatabaseFactory.clean(backend.config.databaseConnection) + DatabaseFactory.init(backend.config.databaseConnection) + environment { + config = MapApplicationConfig( + "ktor.application.modules.0" to "TestBuilderKt.module", + "ktor.application.modules.1" to "com.storyteller_f.a.server.ApplicationKt.module", + "ktor.application.modules.size" to "2" + ) + } + val client1 = createClient { + defaultClientConfigure() + } + block(client1) + backend.topicSearchService.clean() + DatabaseFactory.clean() } -suspend fun attachSession( +suspend fun attachSession( client: HttpClient, block: suspend (Tuple4) -> R ): Tuple5 { @@ -74,7 +96,7 @@ suspend fun attachSession( return Tuple5(priKey, pubKey, address, userInfo.id, r) } -suspend fun loginSession( +suspend fun loginSession( client: HttpClient, session: Tuple5, block: suspend (Tuple4) -> R2 diff --git a/server/src/test/kotlin/TopicTest.kt b/server/src/test/kotlin/TopicTest.kt index 562fa6d..2095edb 100644 --- a/server/src/test/kotlin/TopicTest.kt +++ b/server/src/test/kotlin/TopicTest.kt @@ -1,25 +1,12 @@ import com.perraco.utils.SnowflakeFactory import com.storyteller_f.DatabaseFactory -import com.storyteller_f.a.client_lib.addReaction -import com.storyteller_f.a.client_lib.createNewTopic -import com.storyteller_f.a.client_lib.deleteReaction -import com.storyteller_f.a.client_lib.getReactions -import com.storyteller_f.a.client_lib.getTopicSnapshot -import com.storyteller_f.a.client_lib.joinCommunity -import com.storyteller_f.a.client_lib.searchTopics +import com.storyteller_f.a.client_lib.* import com.storyteller_f.shared.model.TopicInfo import com.storyteller_f.shared.type.DEFAULT_PRIMARY_KEY import com.storyteller_f.shared.type.ObjectType import com.storyteller_f.shared.utils.now import com.storyteller_f.tables.Community import io.ktor.client.call.body -import org.apache.fontbox.ttf.OTFParser -import org.apache.pdfbox.io.RandomAccessReadBufferedFile -import org.apache.pdfbox.pdmodel.PDDocument -import org.apache.pdfbox.pdmodel.PDPage -import org.apache.pdfbox.pdmodel.PDPageContentStream -import org.apache.pdfbox.pdmodel.font.PDType0Font -import java.io.File import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -65,35 +52,6 @@ class TopicTest { } } - @Test - fun `test generate pdf`() { - PDDocument().use { document -> - val firstPage = PDPage() - PDPageContentStream(document, firstPage).use { stream -> - stream.beginText() - val otf = OTFParser().parse( - RandomAccessReadBufferedFile( - File( - "~/DIN-Regular.otf".replace( - "~", - System.getProperty("user.home") - ) - ) - ) - ) - stream.setFont(PDType0Font.load(document, otf, false), 12f) - stream.newLineAtOffset(100F, 700F) - stream.setLeading(14.5f) - stream.showText("hello") - stream.newLine() - stream.showText("world") - stream.endText() - } - document.addPage(firstPage) - document.save("/tmp/test.pdf") - } - } - @Test fun `test reaction`() { test { client -> diff --git a/server/src/test/resources/http_ca.crt b/server/src/test/resources/http_ca.crt new file mode 100644 index 0000000..704e5a3 --- /dev/null +++ b/server/src/test/resources/http_ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIVAPVLz0Dwvzl66ZVpyJY/ntos+qQZMA0GCSqGSIb3DQEB +CwUAMDwxOjA4BgNVBAMTMUVsYXN0aWNzZWFyY2ggc2VjdXJpdHkgYXV0by1jb25m +aWd1cmF0aW9uIEhUVFAgQ0EwHhcNMjIwNDE0MDcyOTA4WhcNMjUwNDEzMDcyOTA4 +WjA8MTowOAYDVQQDEzFFbGFzdGljc2VhcmNoIHNlY3VyaXR5IGF1dG8tY29uZmln +dXJhdGlvbiBIVFRQIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +ufHmvM04dPKNF6AulX3HrjNfcYy62pBO2oJIKhScetDzuRupv/qiXkO1gzGT3/jk +8uk57rzylDFoCgFNUyWdYQAXD+qsy5vbAytGVNCHOGSuWlNm1bbDYwZNTXjZTK6C +CKIY31lbFn9a4oM+Jp1kvIr8GSMQAYDisq+yrVloDLkRs1SPImnoaXsq8epxloBf +En0vo5V8PtOh+xQFpPP21pd5QohCSB+jMaxaizScWX+k7BijEaS1LCsc2w2PNvwn +/bEvtW7w9w+HnzRGZW2nVlt8eji1PHMmfM5Zugn4HAxsSvdI9VRFGAeT2moNiSLN +zxOWmEvQVl2MxRWiTkM1EM7CFDN40hLHtHej8UddeXTDDXyLoUjY3FmaB45rLKdE +k/rszepc3lmptEMh74iUowKaYZTS5jRqT0yIDzevP5je3JP+pe4aNkx7lWMhTReQ +6fs97nd71PfJBuMcHPEA14zwSzSRb/8mNqqaQLBb5H1DpDcZtzbBxb4wFSAa7Dd0 +pVl4A04iB4PS3DaWg/im2C5a83nUTld7Lvy+I6cO1MTaXdkzn6EWXtxkHj7P4VXX +sFTr+Z2A6g/novqderirQzq8aD87MBp2hLBgG59lVB3IXA+CJTzBRBZipU+FOSs2 +1enMlaEa84d8cb+GSpDkmsvamPMhhLeMw6QLmhQXoWMCAwEAAaNTMFEwHQYDVR0O +BBYEFOt9JC+RiqfFdGf/UT2vfmnV8f+kMB8GA1UdIwQYMBaAFOt9JC+RiqfFdGf/ +UT2vfmnV8f+kMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBALFX +r2CKtu2DcLFQaZft9LeRK67xU0rjN8w1+0MN9otSkU9avd8atPOI9p6r67HZyoTv +LDA47ajitzPo4zAXiy4GUXFVCjx3iPOQ4TTCm0YEf9IudLHLqGTbK+Pup6XlfAMb +RejkztcXkxw3Cy6SjIRq99xU8J6Y1jAggB162hqnp3u42nA1PgOJgo/biUYeVtu7 +Y01gKPdCEkiqmSFxfLiRPv5Z4WyYoKge6UyDYFHu0zyMY7zQ2hzrPMFsEhI4+g5D +W7ihilcOijhvfeeWIxcP5lRn2pGbf2GtwqtA7Bt9YKp+NQIBPzK7D1ymS0v/CAWh +3Bv1rqkqDK8TlBybZitTTB6MgGtXOBccouTPmBFBXSWydvW4GW6Dag/ogHHE2vG2 +xlXY6EC1QEzExcM5FZNJI6SOaK0nl+WKAv060U/1ZqcRIkhyctYdkrK4449n1JMy +wjtwcDW7QxhQspHp8GEXztLctokqGjnuMcgPjVoFdiF3w/IV0UUvVeFK4Oms0YbH +uFr3q44Fu/Fol68/1CUk1ytgLUS5anf0Q0WlJsmMUX156ATA29dVBfloJN63EYd7 +01uwbjoMJce7MiwTaLIetW75fxxZHlQK9TMNhaQwKUO8SRaNuE4wKURFoKPg/Dqu +yPhx9adseStlJ3oV6ziEWMwjOK2JmJf0bmIqQ7KR +-----END CERTIFICATE-----