diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 86ebe4c..3e29a08 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,7 +1,10 @@ + + - + \ No newline at end of file diff --git a/src/bld/java/net/thauvin/erik/AkismetBuild.java b/src/bld/java/net/thauvin/erik/AkismetBuild.java index bb5b600..8ac1a3d 100644 --- a/src/bld/java/net/thauvin/erik/AkismetBuild.java +++ b/src/bld/java/net/thauvin/erik/AkismetBuild.java @@ -37,7 +37,6 @@ import rife.bld.extension.*; import rife.bld.extension.dokka.LoggingLevel; import rife.bld.extension.dokka.OutputFormat; import rife.bld.extension.dokka.SourceSet; -import rife.bld.extension.kotlin.CompileOptions; import rife.bld.extension.kotlin.CompilerPlugin; import rife.bld.operations.exceptions.ExitStatusException; import rife.bld.publish.PomBuilder; @@ -83,7 +82,7 @@ public class AkismetBuild extends Project { scope(provided) .include(dependency("jakarta.servlet", "jakarta.servlet-api", version(6, 1, 0))); scope(test) - .include(dependency("org.mockito", "mockito-core", version(5, 17, 0))) + .include(dependency("org.mockito.kotlin", "mockito-kotlin", version(5, 4, 0))) .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin)) .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 2))) .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 2))) @@ -139,11 +138,10 @@ public class AkismetBuild extends Project { @Override public void compile() throws Exception { genver(); - var options = new CompileOptions().verbose(true).jvmOptions("--enable-native-access=ALL-UNNAMED"); var op = new CompileKotlinOperation() .fromProject(this) - .compileOptions(options) .plugins(CompilerPlugin.KOTLIN_SERIALIZATION); + op.compileOptions().verbose(true); op.execute(); } diff --git a/src/test/kotlin/net/thauvin/erik/akismet/AkismetTest.kt b/src/test/kotlin/net/thauvin/erik/akismet/AkismetTests.kt similarity index 83% rename from src/test/kotlin/net/thauvin/erik/akismet/AkismetTest.kt rename to src/test/kotlin/net/thauvin/erik/akismet/AkismetTests.kt index 3d047b3..676158c 100644 --- a/src/test/kotlin/net/thauvin/erik/akismet/AkismetTest.kt +++ b/src/test/kotlin/net/thauvin/erik/akismet/AkismetTests.kt @@ -1,5 +1,5 @@ /* - * AkismetTest.kt + * AkismetTests.kt * * Copyright 2019-2024 Erik C. Thauvin (erik@thauvin.net) * @@ -43,12 +43,13 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.mockito.Mockito -import org.mockito.Mockito.`when` +import org.mockito.kotlin.whenever import java.io.File import java.io.FileInputStream import java.time.LocalDateTime import java.time.ZoneId import java.util.* +import java.util.concurrent.atomic.AtomicBoolean import java.util.logging.ConsoleHandler import java.util.logging.Level import kotlin.test.assertEquals @@ -56,32 +57,12 @@ import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertTrue - -fun getKey(key: String): String { - var value = System.getenv(key) ?: "" - if (value.isBlank()) { - val localProps = File("local.properties") - if (localProps.exists()) - localProps.apply { - if (exists()) { - FileInputStream(this).use { fis -> - Properties().apply { - load(fis) - value = getProperty(key, "") - } - } - } - } - } - return value -} - /** * [Akismet] Tests * * `AKISMET_API_KEY` and `AKISMET_BLOG` should be set in env vars or `local.properties` */ -class AkismetTest { +class AkismetTests { private val emptyFormBody = FormBody.Builder().build() companion object { @@ -111,6 +92,7 @@ class AkismetTest { .isTest(true) .build() private val mockComment: AkismetComment = AkismetComment(request = getMockRequest()) + private val isFirstRun = AtomicBoolean(true) init { with(comment) { @@ -150,25 +132,45 @@ class AkismetTest { @JvmStatic @BeforeClass fun beforeClass() { - with(akismet.logger) { - addHandler(ConsoleHandler().apply { level = Level.FINE }) - level = Level.FINE + if (isFirstRun.getAndSet(false)) { + with(akismet.logger) { + addHandler(ConsoleHandler().apply { level = Level.FINE }) + level = Level.FINE + } } akismet.logger.info(comment.toString()) akismet.logger.info(mockComment.toJson()) } + private fun getKey(key: String): String { + var value = System.getenv(key) ?: "" + if (value.isBlank()) { + val localProps = File("local.properties") + localProps.apply { + if (exists()) { + FileInputStream(this).use { fis -> + Properties().apply { + load(fis) + value = getProperty(key, "") + } + } + } + } + } + return value + } + private fun getMockRequest(): HttpServletRequest { val request = Mockito.mock(HttpServletRequest::class.java) with(request) { - `when`(remoteAddr).thenReturn(comment.userIp) - `when`(requestURI).thenReturn("/blog/post=1") - `when`(getHeader("referer")).thenReturn(REFERER) - `when`(getHeader("Cookie")).thenReturn("name=value; name2=value2; name3=value3") - `when`(getHeader("User-Agent")).thenReturn(comment.userAgent) - `when`(getHeader("Accept-Encoding")).thenReturn("gzip") - `when`(headerNames).thenReturn( + whenever(remoteAddr).thenReturn(comment.userIp) + whenever(requestURI).thenReturn("/blog/post=1") + whenever(getHeader("referer")).thenReturn(REFERER) + whenever(getHeader("Cookie")).thenReturn("name=value; name2=value2; name3=value3") + whenever(getHeader("User-Agent")).thenReturn(comment.userAgent) + whenever(getHeader("Accept-Encoding")).thenReturn("gzip") + whenever(headerNames).thenReturn( Collections.enumeration(listOf("User-Agent", "referer", "Cookie", "Accept-Encoding", "Null")) ) } @@ -197,6 +199,8 @@ class AkismetTest { assertThat(akismet::response).isEqualTo("true") assertThat(akismet::httpStatusCode).isEqualTo(200) + + comment.userRole = AkismetComment.ADMIN_ROLE } } @@ -264,22 +268,6 @@ class AkismetTest { } } - @Test - fun jsonComment() { - val jsonComment = Akismet.jsonComment(mockComment.toJson()) - - assertEquals(jsonComment, mockComment, "jsonComment = mockComment") - assertEquals(jsonComment.hashCode(), mockComment.hashCode(), "jsonComment.hashCode = mockComment.hashcode") - - assertNotEquals(jsonComment, comment, "json") - assertNotEquals(jsonComment.hashCode(), comment.hashCode(), "jsonComment.hashCode != mockComment.hashcode") - - jsonComment.recheckReason = "" - assertNotEquals(jsonComment, mockComment, "jsonComment != jsonComment") - - assertThat(this, "this != comment").isNotEqualTo(comment) - } - @Test fun mockComment() { assertThat(mockComment, "mockComment").all { @@ -346,13 +334,57 @@ class AkismetTest { } @Test - fun dateToGmtTest() { + fun dateToGmt() { val localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()) val utcDate = Akismet.dateToGmt(date) assertEquals(Akismet.dateToGmt(localDateTime), utcDate, "dateGmt(localDateTime)") assertThat(comment::dateGmt).isEqualTo(utcDate) } + @Nested + @DisplayName("JSON Comment Tests") + inner class JsonCommentTest { + @Test + fun jsonCommentEqualHashCode() { + val jsonComment = Akismet.jsonComment(mockComment.toJson()) + assertEquals( + jsonComment.hashCode(), + mockComment.hashCode(), + "jsonComment.hashCode = mockComment.hashcode" + ) + } + + @Test + fun jsonCommentEqualsMockComment() { + val jsonComment = Akismet.jsonComment(mockComment.toJson()) + assertEquals(jsonComment, mockComment, "jsonComment = mockComment") + } + + @Test + fun jsonCommentNotEqualsComment() { + val jsonComment = Akismet.jsonComment(mockComment.toJson()) + assertNotEquals(jsonComment, comment, "json") + assertNotEquals( + jsonComment.hashCode(), + comment.hashCode(), + "jsonComment.hashCode != mockComment.hashcode" + ) + } + + @Test + fun jsonCommentNotEqualsMockComment() { + val jsonComment = Akismet.jsonComment(mockComment.toJson()) + jsonComment.recheckReason = "" + assertNotEquals(jsonComment, mockComment, "jsonComment != jsonComment") + } + + @Test + fun jsonCommentNotEqualsThis() { + Akismet.jsonComment(mockComment.toJson()) + assertThat(this, "this != comment").isNotEqualTo(comment) + } + } + @Nested @DisplayName("Response Tests") inner class ResponseTests { @@ -417,15 +449,14 @@ class AkismetTest { } @Test - fun proTipResponse() { + fun proTip() { assertFalse( akismet.executeMethod( "https://postman-echo.com/response-headers?x-akismet-pro-tip=discard".toHttpUrl(), emptyFormBody ) ) - - assertThat(akismet, "executeMethod(pro-tip)").all { + assertThat(akismet, "executeMethod(x-akismet-pro-tip=discard)").all { prop(Akismet::proTip).isEqualTo("discard") prop(Akismet::isDiscard).isTrue() } @@ -445,20 +476,49 @@ class AkismetTest { @Test fun submitHam() { assertTrue(akismet.submitHam(comment), "submitHam") + } + + @Test + fun submitHamMocked() { assertTrue(akismet.submitHam(mockComment), "submitHam(mock)") } @Test fun submitSpam() { assertTrue(akismet.submitSpam(comment), "submitHam") + } + + @Test + fun submitSpamMocked() { assertTrue(akismet.submitSpam(mockComment), "submitHam(mock)") } } + @Nested + @DisplayName("User Agent Tests") + inner class UserAgentTests { + val libAgent = "${GeneratedVersion.PROJECT}/${GeneratedVersion.VERSION}" + + @Test + fun userAgentCustom() { + akismet.appUserAgent = "My App/1.0" + + assertEquals( + akismet.buildUserAgent(), + "${akismet.appUserAgent} | $libAgent", + "buildUserAgent(My App/1.0)" + ) + } + + @Test + fun userAgentDefault() { + assertEquals(akismet.buildUserAgent(), libAgent, "buildUserAgent($libAgent)") + } + } + @Nested @DisplayName("Validation Tests") inner class ValidationTests { - @Test fun blogProperty() { assertThrows(IllegalArgumentException::class.java) { @@ -470,7 +530,7 @@ class AkismetTest { @Test fun validateConfig() { - assertThat(AkismetComment(config) == comment).isTrue() + assertThat(AkismetComment(config)).isEqualTo(comment) } @Test @@ -489,16 +549,9 @@ class AkismetTest { prop(Akismet::httpStatusCode).isEqualTo(0) } - assertThat(Akismet("123456789012"), "akismet(123456789012)").prop(Akismet::verifyKey).isFalse() - } - - @Test - fun userAgent() { - val libAgent = "${GeneratedVersion.PROJECT}/${GeneratedVersion.VERSION}" - assertEquals(akismet.buildUserAgent(), libAgent, "buildUserAgent($libAgent)") - - akismet.appUserAgent = "My App/1.0" - assertEquals(akismet.buildUserAgent(), "${akismet.appUserAgent} | $libAgent", "buildUserAgent(My App/1.0)") + assertThat(Akismet("123456789012"), "akismet(123456789012)") + .prop(Akismet::verifyKey) + .isFalse() } } } diff --git a/src/test/kotlin/net/thauvin/erik/akismet/CommentConfigTests.kt b/src/test/kotlin/net/thauvin/erik/akismet/CommentConfigTests.kt new file mode 100644 index 0000000..6f81ba5 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/akismet/CommentConfigTests.kt @@ -0,0 +1,165 @@ +/* + * CommentConfigTests.kt + * + * Copyright 2019-2025 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.akismet + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class CommentConfigTests { + @Test + fun `test default optional fields`() { + val commentConfig = CommentConfig.Builder("192.168.0.1", "DefaultAgent").build() + + assertEquals("", commentConfig.referrer) + assertEquals("", commentConfig.permalink) + assertEquals(CommentType.NONE, commentConfig.type) + assertEquals("", commentConfig.author) + assertEquals("", commentConfig.authorEmail) + assertEquals("", commentConfig.authorUrl) + assertEquals("", commentConfig.content) + assertEquals("", commentConfig.dateGmt) + assertEquals("", commentConfig.postModifiedGmt) + assertEquals("", commentConfig.blogLang) + assertEquals("", commentConfig.blogCharset) + assertEquals("", commentConfig.userRole) + assertEquals(false, commentConfig.isTest) + assertEquals("", commentConfig.recheckReason) + assertTrue(commentConfig.serverEnv.isEmpty()) + } + + @Test + fun `test empty server environment`() { + val commentConfig = CommentConfig.Builder("127.0.0.1", "TestUserAgent") + .serverEnv(emptyMap()) + .build() + assertTrue(commentConfig.serverEnv.isEmpty()) + } + + + @Test + fun `test invalid inputs for mandatory fields`() { + try { + CommentConfig.Builder("", "UserAgent").build() + } catch (e: IllegalArgumentException) { + assertEquals("User IP cannot be empty", e.message) + } + + try { + CommentConfig.Builder("127.0.0.1", "").build() + } catch (e: IllegalArgumentException) { + assertEquals("User Agent cannot be empty", e.message) + } + } + + + @Nested + @DisplayName("Builder Tests") + inner class BuilderTests { + @Test + fun `test builder with all optional fields`() { + val builder = CommentConfig.Builder("127.0.0.1", "TestUserAgent") + .referrer("http://example.com") + .permalink("http://example.com/post") + .type(CommentType.COMMENT) + .author("John Doe") + .authorEmail("john.doe@example.com") + .authorUrl("http://johndoe.com") + .content("This is a test comment.") + .dateGmt("2025-05-28T00:00:00Z") + .postModifiedGmt("2025-05-28T01:00:00Z") + .blogLang("en") + .blogCharset("UTF-8") + .userRole("admin") + .isTest(true) + .recheckReason("manual recheck") + .serverEnv(mapOf("key" to "value")) + + val commentConfig = builder.build() + + assertEquals("127.0.0.1", commentConfig.userIp) + assertEquals("TestUserAgent", commentConfig.userAgent) + assertEquals("http://example.com", commentConfig.referrer) + assertEquals("http://example.com/post", commentConfig.permalink) + assertEquals(CommentType.COMMENT, commentConfig.type) + assertEquals("John Doe", commentConfig.author) + assertEquals("john.doe@example.com", commentConfig.authorEmail) + assertEquals("http://johndoe.com", commentConfig.authorUrl) + assertEquals("This is a test comment.", commentConfig.content) + assertEquals("2025-05-28T00:00:00Z", commentConfig.dateGmt) + assertEquals("2025-05-28T01:00:00Z", commentConfig.postModifiedGmt) + assertEquals("en", commentConfig.blogLang) + assertEquals("UTF-8", commentConfig.blogCharset) + assertEquals("admin", commentConfig.userRole) + assertEquals(true, commentConfig.isTest) + assertEquals("manual recheck", commentConfig.recheckReason) + assertEquals(mapOf("key" to "value"), commentConfig.serverEnv) + } + + @Test + fun `test builder with mandatory fields only`() { + val builder = CommentConfig.Builder("127.0.0.1", "TestUserAgent") + val commentConfig = builder.build() + + assertEquals("127.0.0.1", commentConfig.userIp) + assertEquals("TestUserAgent", commentConfig.userAgent) + assertEquals("", commentConfig.referrer) + assertEquals("", commentConfig.permalink) + assertEquals(CommentType.NONE, commentConfig.type) + assertEquals("", commentConfig.author) + assertEquals("", commentConfig.authorEmail) + assertEquals("", commentConfig.authorUrl) + assertEquals("", commentConfig.content) + assertEquals("", commentConfig.dateGmt) + assertEquals("", commentConfig.postModifiedGmt) + assertEquals("", commentConfig.blogLang) + assertEquals("", commentConfig.blogCharset) + assertEquals("", commentConfig.userRole) + assertEquals(false, commentConfig.isTest) + assertEquals("", commentConfig.recheckReason) + assertTrue(commentConfig.serverEnv.isEmpty()) + } + + @Test + fun `test builder with modified mandatory fields`() { + val builder = CommentConfig.Builder("127.0.0.1", "TestUserAgent") + .userIp("192.168.1.1") + .userAgent("ModifiedUserAgent") + val commentConfig = builder.build() + + assertEquals("192.168.1.1", commentConfig.userIp) + assertEquals("ModifiedUserAgent", commentConfig.userAgent) + } + } +} diff --git a/src/test/kotlin/net/thauvin/erik/akismet/CommentTypeTest.kt b/src/test/kotlin/net/thauvin/erik/akismet/CommentTypeTest.kt new file mode 100644 index 0000000..a3101cb --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/akismet/CommentTypeTest.kt @@ -0,0 +1,103 @@ +/* + * CommentTypeTest.kt + * + * Copyright 2019-2025 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.akismet + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class CommentTypeTest { + @Test + fun `verify BLOG_POST value`() { + val commentType = CommentType.BLOG_POST + assertEquals("blog-post", commentType.value) + } + + @Test + fun `verify COMMENT value`() { + val commentType = CommentType.COMMENT + assertEquals("comment", commentType.value) + } + + @Test + fun `verify CONTACT_FORM value`() { + val commentType = CommentType.CONTACT_FORM + assertEquals("contact-form", commentType.value) + } + + @Test + fun `verify FORUM_POST value`() { + val commentType = CommentType.FORUM_POST + assertEquals("forum-post", commentType.value) + } + + @Test + fun `verify MESSAGE value`() { + val commentType = CommentType.MESSAGE + assertEquals("message", commentType.value) + } + + @Test + fun `verify NONE value`() { + val commentType = CommentType.NONE + assertEquals("", commentType.value) + } + + @Test + fun `verify PINGBACK value`() { + val commentType = CommentType.PINGBACK + assertEquals("pingback", commentType.value) + } + + @Test + fun `verify REPLY value`() { + val commentType = CommentType.REPLY + assertEquals("reply", commentType.value) + } + + @Test + fun `verify SIGNUP value`() { + val commentType = CommentType.SIGNUP + assertEquals("signup", commentType.value) + } + + @Test + fun `verify TRACKBACK value`() { + val commentType = CommentType.TRACKBACK + assertEquals("trackback", commentType.value) + } + + @Test + fun `verify TWEET value`() { + val commentType = CommentType.TWEET + assertEquals("tweet", commentType.value) + } +}