Merge pull request #246 from usbharu/feature/post-decoration

Feature/post decoration
This commit is contained in:
usbharu 2024-01-25 12:26:04 +09:00 committed by GitHub
commit 99081d6483
25 changed files with 341 additions and 36 deletions

View File

@ -215,6 +215,8 @@ dependencies {
implementation("org.flywaydb:flyway-core")
implementation("dev.usbharu:emoji-kt:2.0.0")
implementation("org.jsoup:jsoup:1.17.2")
implementation("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20220608.1")
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")

View File

@ -21,6 +21,8 @@ insert into relationships (actor_id, target_actor_id, following, blocking, mutin
ignore_follow_request)
VALUES (9, 8, true, false, false, false, false);
insert into POSTS (ID, ACTOR_ID, OVERVIEW, TEXT, CREATED_AT, VISIBILITY, URL, REPLY_ID, REPOST_ID, SENSITIVE, AP_ID)
VALUES (1239, 8, null, 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239', null, null, false,
insert into POSTS (ID, ACTOR_ID, OVERVIEW, CONTENT, TEXT, CREATED_AT, VISIBILITY, URL, REPLY_ID, REPOST_ID, SENSITIVE,
AP_ID)
VALUES (1239, 8, null, '<p>test post</p>', 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239',
null, null, false,
'https://example.com/users/test-user8/posts/1239');

View File

@ -21,7 +21,9 @@ insert into relationships (actor_id, target_actor_id, following, blocking, mutin
ignore_follow_request)
VALUES (5, 4, true, false, false, false, false);
insert into POSTS (ID, "actor_id", OVERVIEW, TEXT, "created_at", VISIBILITY, URL, "repost_id", "reply_id", SENSITIVE,
insert into POSTS (ID, "actor_id", OVERVIEW, CONTENT, TEXT, "created_at", VISIBILITY, URL, "repost_id", "reply_id",
SENSITIVE,
AP_ID)
VALUES (1237, 4, null, 'test post', 12345680, 0, 'https://example.com/users/test-user4/posts/1237', null, null, false,
VALUES (1237, 4, null, '<p>test post</p>', 'test post', 12345680, 0, 'https://example.com/users/test-user4/posts/1237',
null, null, false,
'https://example.com/users/test-user4/posts/1237');

View File

@ -21,7 +21,9 @@ insert into relationships (actor_id, target_actor_id, following, blocking, mutin
ignore_follow_request)
VALUES (7, 6, true, false, false, false, false);
insert into POSTS (ID, "actor_ID", OVERVIEW, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID", SENSITIVE,
insert into POSTS (ID, "actor_ID", OVERVIEW, CONTENT, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID",
SENSITIVE,
AP_ID)
VALUES (1238, 6, null, 'test post', 12345680, 1, 'https://example.com/users/test-user6/posts/1238', null, null, false,
VALUES (1238, 6, null, '<p>test post</p>', 'test post', 12345680, 1, 'https://example.com/users/test-user6/posts/1238',
null, null, false,
'https://example.com/users/test-user6/posts/1238');

View File

@ -9,9 +9,11 @@ VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is te
'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following',
'https://example.com/users/test-user11/followers', null, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false,
VALUES (1242, 11, null, '<p>test post</p>', 'test post', 12345680, 0,
'https://example.com/users/test-user11/posts/1242', null, null, false,
'https://example.com/users/test-user11/posts/1242', false);
insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE, DESCRIPTION)

View File

@ -9,9 +9,12 @@ VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is te
'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following',
'https://example.com/users/test-user10/followers', null, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1240, 10, null, 'test post', 12345680, 0, 'https://example.com/users/test-user10/posts/1240', null, null, false,
VALUES (1240, 10, null, '<p>test post</p>', 'test post', 12345680, 0,
'https://example.com/users/test-user10/posts/1240', null, null, false,
'https://example.com/users/test-user10/posts/1240', false),
(1241, 10, null, 'test post', 12345680, 0, 'https://example.com/users/test-user10/posts/1241', null, 1240, false,
(1241, 10, null, '<p>test post</p>', 'test post', 12345680, 0,
'https://example.com/users/test-user10/posts/1241', null, 1240, false,
'https://example.com/users/test-user10/posts/1241', false);

View File

@ -9,7 +9,9 @@ VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test
'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following',
'https://example.com/users/test-user3/followers', null, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1236, 3, null, 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236', null, null, false,
VALUES (1236, 3, null, '<p>test post</p>', 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236',
null, null, false,
'https://example.com/users/test-user3/posts/1236', false)

View File

@ -9,7 +9,9 @@ VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test us
'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following',
'https://example.com/users/test-users/followers', null, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1234, 1, null, 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234', null, null, false,
VALUES (1234, 1, null, '<p>test post</p>', 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234',
null, null, false,
'https://example.com/users/test-user/posts/1234', false)

View File

@ -9,7 +9,9 @@ VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
'https://example.com/users/test-user2/followers', null, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1235, 2, null, 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235', null, null, false,
VALUES (1235, 2, null, '<p>test post</p>', 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235',
null, null, false,
'https://example.com/users/test-user2/posts/1235', false)

View File

@ -1,3 +1,4 @@
insert into posts (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id)
VALUES (1, 1, null, 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false,
insert into posts (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id)
VALUES (1, 1, null, '<p>test post</p>', 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false,
'https://users/1/posts/1');

View File

@ -128,7 +128,7 @@ class APNoteServiceImpl(
postBuilder.of(
id = postRepository.generateId(),
actorId = person.second.id,
text = note.content,
content = note.content,
createdAt = Instant.parse(note.published).toEpochMilli(),
visibility = visibility,
url = note.id,

View File

@ -0,0 +1,21 @@
package dev.usbharu.hideout.application.config
import org.owasp.html.HtmlPolicyBuilder
import org.owasp.html.PolicyFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class HtmlSanitizeConfig {
@Bean
fun policy(): PolicyFactory {
return HtmlPolicyBuilder()
.allowElements("p")
.allowElements("a")
.allowElements("br")
.allowAttributes("href").onElements("a")
.allowUrlProtocols("http", "https")
.allowElements({ _, _ -> return@allowElements "p" }, "h1", "h2", "h3", "h4", "h5", "h6")
.toFactory()
}
}

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.core.domain.model.post
import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.core.service.post.PostContentFormatter
import org.springframework.stereotype.Component
import java.time.Instant
@ -8,6 +9,7 @@ data class Post private constructor(
val id: Long,
val actorId: Long,
val overview: String? = null,
val content: String,
val text: String,
val createdAt: Long,
val visibility: Visibility,
@ -22,13 +24,16 @@ data class Post private constructor(
) {
@Component
class PostBuilder(private val characterLimit: CharacterLimit) {
class PostBuilder(
private val characterLimit: CharacterLimit,
private val postContentFormatter: PostContentFormatter
) {
@Suppress("FunctionMinLength", "LongParameterList")
fun of(
id: Long,
actorId: Long,
overview: String? = null,
text: String,
content: String,
createdAt: Long,
visibility: Visibility,
url: String,
@ -49,12 +54,14 @@ data class Post private constructor(
overview
}
val limitedText = if (text.length >= characterLimit.post.text) {
text.substring(0, characterLimit.post.text)
val limitedText = if (content.length >= characterLimit.post.text) {
content.substring(0, characterLimit.post.text)
} else {
text
content
}
val (html, content1) = postContentFormatter.format(limitedText)
require(url.isNotBlank()) { "url must contain non-blank characters" }
require(url.length <= characterLimit.general.url) {
"url must not exceed ${characterLimit.general.url} characters."
@ -67,7 +74,8 @@ data class Post private constructor(
id = id,
actorId = actorId,
overview = limitedOverview,
text = limitedText,
content = html,
text = content1,
createdAt = createdAt,
visibility = visibility,
url = url,
@ -94,6 +102,7 @@ data class Post private constructor(
id = id,
actorId = 0,
overview = null,
content = "",
text = "",
createdAt = Instant.EPOCH.toEpochMilli(),
visibility = visibility,
@ -113,6 +122,7 @@ data class Post private constructor(
id = this.id,
actorId = 0,
overview = null,
content = "",
text = "",
createdAt = Instant.EPOCH.toEpochMilli(),
visibility = visibility,

View File

@ -25,7 +25,7 @@ class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRow
id = resultRow[Posts.id],
actorId = resultRow[Posts.actorId],
overview = resultRow[Posts.overview],
text = resultRow[Posts.text],
content = resultRow[Posts.text],
createdAt = resultRow[Posts.createdAt],
visibility = Visibility.values().first { visibility -> visibility.ordinal == resultRow[Posts.visibility] },
url = resultRow[Posts.url],

View File

@ -27,6 +27,7 @@ class PostRepositoryImpl(
it[id] = post.id
it[actorId] = post.actorId
it[overview] = post.overview
it[content] = post.content
it[text] = post.text
it[createdAt] = post.createdAt
it[visibility] = post.visibility.ordinal
@ -63,6 +64,7 @@ class PostRepositoryImpl(
Posts.update({ Posts.id eq post.id }) {
it[actorId] = post.actorId
it[overview] = post.overview
it[content] = post.content
it[text] = post.text
it[createdAt] = post.createdAt
it[visibility] = post.visibility.ordinal
@ -128,6 +130,7 @@ object Posts : Table() {
val id: Column<Long> = long("id")
val actorId: Column<Long> = long("actor_id").references(Actors.id)
val overview: Column<String?> = varchar("overview", 100).nullable()
val content = varchar("content", 5000)
val text: Column<String> = varchar("text", 3000)
val createdAt: Column<Long> = long("created_at")
val visibility: Column<Int> = integer("visibility").default(0)

View File

@ -0,0 +1,83 @@
package dev.usbharu.hideout.core.service.post
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import org.jsoup.select.Elements
import org.owasp.html.PolicyFactory
import org.springframework.stereotype.Service
@Service
class DefaultPostContentFormatter(private val policyFactory: PolicyFactory) : PostContentFormatter {
override fun format(content: String): FormattedPostContent {
// まず不正なHTMLを整形する
val document = Jsoup.parseBodyFragment(content)
val outputSettings = Document.OutputSettings()
outputSettings.prettyPrint(false)
document.outputSettings(outputSettings)
val unsafeElement = document.getElementsByTag("body").first() ?: return FormattedPostContent(
"",
""
)
// 文字だけのHTMLなどはここでpタグで囲む
val flattenHtml = unsafeElement.childNodes().mapNotNull {
if (it is Element) {
it
} else if (it is TextNode) {
Element("p").appendText(it.text())
} else {
null
}
}.filter { it.text().isNotBlank() }
// HTMLのサニタイズをする
val unsafeHtml = Elements(flattenHtml).outerHtml()
val safeHtml = policyFactory.sanitize(unsafeHtml)
val safeDocument =
Jsoup.parseBodyFragment(safeHtml).getElementsByTag("body").first() ?: return FormattedPostContent("", "")
val formattedHtml = mutableListOf<Element>()
// 連続するbrタグを段落に変換する
for (element in safeDocument.children()) {
var brCount = 0
var prevIndex = 0
val childNodes = element.childNodes()
for ((index, childNode) in childNodes.withIndex()) {
if (childNode is Element && childNode.tagName() == "br") {
brCount++
} else if (brCount >= 2) {
formattedHtml.add(Element("p").appendChildren(childNodes.subList(prevIndex, index - brCount)))
prevIndex = index
}
}
formattedHtml.add(Element("p").appendChildren(childNodes.subList(prevIndex, childNodes.size)))
}
val elements = Elements(formattedHtml)
return FormattedPostContent(elements.outerHtml().replace("\n", ""), printHtml(elements))
}
private fun printHtml(element: Elements): String {
return element.joinToString("\n\n") {
it.childNodes().joinToString("") {
if (it is Element && it.tagName() == "br") {
"\n"
} else if (it is Element) {
it.text()
} else if (it is TextNode) {
it.text()
} else {
""
}
}
}
}
}

View File

@ -0,0 +1,6 @@
package dev.usbharu.hideout.core.service.post
data class FormattedPostContent(
val html: String,
val content: String
)

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.service.post
interface PostContentFormatter {
fun format(content: String): FormattedPostContent
}

View File

@ -97,7 +97,7 @@ class PostServiceImpl(
id = id,
actorId = post.userId,
overview = post.overview,
text = post.text,
content = post.text,
createdAt = Instant.now().toEpochMilli(),
visibility = post.visibility,
url = "${user.url}/posts/$id",

View File

@ -90,6 +90,7 @@ create table if not exists posts
id bigint primary key,
actor_id bigint not null,
overview varchar(100) null,
content varchar(5000) not null,
text varchar(3000) not null,
created_at bigint not null,
visibility int default 0 not null,

View File

@ -12,10 +12,12 @@ import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter
import dev.usbharu.hideout.core.service.post.PostService
import io.ktor.client.*
import io.ktor.client.call.*
@ -42,7 +44,7 @@ import java.time.Instant
class APNoteServiceImplTest {
val postBuilder = Post.PostBuilder(CharacterLimit())
val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()))
@Test
fun `fetchNote(String,String) ートが既に存在する場合はDBから取得したものを返す`() = runTest {
@ -71,7 +73,10 @@ class APNoteServiceImplTest {
apUserService = mock(),
postService = mock(),
apResourceResolveService = mock(),
postBuilder = Post.PostBuilder(CharacterLimit()),
postBuilder = Post.PostBuilder(
CharacterLimit(),
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
),
noteQueryService = noteQueryService,
mock(),
mock()
@ -141,7 +146,10 @@ class APNoteServiceImplTest {
apUserService = apUserService,
postService = mock(),
apResourceResolveService = apResourceResolveService,
postBuilder = Post.PostBuilder(CharacterLimit()),
postBuilder = Post.PostBuilder(
CharacterLimit(),
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
),
noteQueryService = noteQueryService,
mock(),
mock { }
@ -190,7 +198,10 @@ class APNoteServiceImplTest {
apUserService = mock(),
postService = mock(),
apResourceResolveService = apResourceResolveService,
postBuilder = Post.PostBuilder(CharacterLimit()),
postBuilder = Post.PostBuilder(
CharacterLimit(),
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
),
noteQueryService = noteQueryService,
mock(),
mock()

View File

@ -0,0 +1,135 @@
package dev.usbharu.hideout.core.service.post
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class DefaultPostContentFormatterTest {
val defaultPostContentFormatter = DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
@Test
fun pタグがpタグになる() {
//language=HTML
val html = """<p>hoge</p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
}
@Test
fun hタグがpタグになる() {
//language=HTML
val html = """<h1>hoge</h1>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
}
@Test
fun pタグのネストは破棄される() {
//language=HTML
val html = """<p>hoge<p>fuga</p>piyo</p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p><p>fuga</p><p>piyo</p>", "hoge\n\nfuga\n\npiyo"))
}
@Test
fun spanタグは無視される() {
//language=HTML
val html = """<p><span>hoge</span></p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
}
@Test
fun `2連続改行は段落に変換される`() {
//language=HTML
val html = """<p>hoge<br><br>fuga</p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p><p>fuga</p>", "hoge\n\nfuga"))
}
@Test
fun iタグは無視される() {
//language=HTML
val html = """<p><i>hoge</i></p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
}
@Test
fun aタグはhrefの中身のみ引き継がれる() {
//language=HTML
val html = """<p><a href='https://example.com' class='u-url' target='_blank'>hoge</a></p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p><a href=\"https://example.com\">hoge</a></p>", "hoge"))
}
@Test
fun aタグの中のspanは無視される() {
//language=HTML
val html = """<p><a href='https://example.com'><span>hoge</span></a></p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p><a href=\"https://example.com\">hoge</a></p>", "hoge"))
}
@Test
fun brタグのコンテンツは改行になる() {
//language=HTML
val html = """<p>hoge<br>fuga</p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge<br> fuga</p>", "hoge\nfuga"))
}
@Test
fun いきなりテキストが来たらpタグで囲む() {
//language=HTML
val html = """hoge"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
}
@Test
fun bodyタグが含まれていた場合消す() {
//language=HTML
val html = """</body><p>hoge</p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
}
@Test
fun pタグの中のspanは無視される() {
//language=HTML
val html =
"""<p><span class="h-card" translate="no"><a href="https://test-hideout.usbharu.dev/users/testuser14" class="u-url mention">@<span>testuser14</span></a></span> tes</p>"""
val actual = defaultPostContentFormatter.format(html)
assertThat(actual).isEqualTo(
FormattedPostContent(
"<p><a href=\"https://test-hideout.usbharu.dev/users/testuser14\">@testuser14</a> tes</p>",
"@testuser14 tes"
)
)
}
}

View File

@ -3,6 +3,7 @@ package dev.usbharu.hideout.core.service.post
import dev.usbharu.hideout.activitypub.service.activity.create.ApSendCreateService
import dev.usbharu.hideout.activitypub.service.activity.delete.APSendDeleteService
import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.Post
@ -36,7 +37,11 @@ class PostServiceImplTest {
@Mock
private lateinit var timelineService: TimelineService
@Spy
private var postBuilder: Post.PostBuilder = Post.PostBuilder(CharacterLimit())
private var postBuilder: Post.PostBuilder = Post.PostBuilder(
CharacterLimit(), DefaultPostContentFormatter(
HtmlSanitizeConfig().policy()
)
)
@Mock
private lateinit var apSendCreateService: ApSendCreateService

View File

@ -4,9 +4,11 @@ package dev.usbharu.hideout.core.service.user
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@ -20,7 +22,7 @@ import kotlin.test.assertNull
class ActorServiceTest {
val actorBuilder = Actor.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com")))
val postBuilder = Post.PostBuilder(CharacterLimit())
val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()))
@Test
fun `createLocalUser ローカルユーザーを作成できる`() = runTest {

View File

@ -1,15 +1,18 @@
package utils
import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter
import kotlinx.coroutines.runBlocking
import java.time.Instant
object PostBuilder {
private val postBuilder = Post.PostBuilder(CharacterLimit())
private val postBuilder =
Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()))
private val idGenerator = TwitterSnowflakeIdGenerateService
@ -26,7 +29,7 @@ object PostBuilder {
id = id,
actorId = userId,
overview = overview,
text = text,
content = text,
createdAt = createdAt,
visibility = visibility,
url = url,