Implemented errorMessage and JSON (de)serialization.

This commit is contained in:
Erik C. Thauvin 2019-09-22 12:03:53 -07:00
parent 139048d7c4
commit 8a20a5cdb2
5 changed files with 154 additions and 43 deletions

View file

@ -16,6 +16,7 @@ plugins {
id("net.thauvin.erik.gradle.semver") version "1.0.4"
id("org.jetbrains.dokka") version "0.9.18"
id("org.jetbrains.kotlin.kapt").version("1.3.50")
id("org.jetbrains.kotlin.plugin.serialization").version("1.3.50")
id("org.jmailen.kotlinter") version "2.1.1"
id("org.sonarqube") version "2.7.1"
}
@ -59,6 +60,7 @@ dependencies {
compile("com.squareup.okhttp3:logging-interceptor:4.2.0")
compile(kotlin("stdlib"))
compile("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.13.0")
testImplementation("org.mockito:mockito-core:3.0.0")
testImplementation("org.testng:testng:7.0.0")

View file

@ -2,12 +2,15 @@
<SmellBaseline>
<Blacklist></Blacklist>
<Whitelist>
<ID>ComplexMethod:Akismet.kt$Akismet$private fun buildFormBody( userIp: String, userAgent: String, referrer: String, permalink: String, type: String, author: String, authorEmail: String, authorUrl: String, content: String, dateGmt: String, postModifiedGmt: String, blogLang: String, blogCharset: String, userRole: String, isTest: Boolean, recheckReason: String, other: Map&lt;String, String&gt; ): FormBody</ID>
<ID>LongMethod:Akismet.kt$Akismet$private fun buildFormBody( userIp: String, userAgent: String, referrer: String, permalink: String, type: String, author: String, authorEmail: String, authorUrl: String, content: String, dateGmt: String, postModifiedGmt: String, blogLang: String, blogCharset: String, userRole: String, isTest: Boolean, recheckReason: String, other: Map&lt;String, String&gt; ): FormBody</ID>
<ID>ComplexMethod:Akismet.kt$Akismet$ @JvmOverloads fun executeMethod(apiUrl: HttpUrl?, formBody: FormBody, trueOnError: Boolean = false): Boolean</ID>
<ID>ComplexMethod:Akismet.kt$Akismet$private fun buildFormBody(comment: AkismetComment): FormBody</ID>
<ID>ComplexMethod:AkismetComment.kt$AkismetComment$ @Suppress("DuplicatedCode") override fun equals(other: Any?): Boolean</ID>
<ID>MagicNumber:Akismet.kt$Akismet$12</ID>
<ID>MagicNumber:Akismet.kt$Akismet.&lt;no name provided&gt;$8</ID>
<ID>NestedBlockDepth:Akismet.kt$Akismet$ @Suppress("MemberVisibilityCanBePrivate") protected fun executeMethod(apiUrl: HttpUrl?, formBody: FormBody): Boolean</ID>
<ID>NestedBlockDepth:AkismetTest.kt$fun getApiKey(): String</ID>
<ID>MaxLineLength:Akismet.kt$Akismet$/** * The _x-akismet-pro-tip_ header from the last operation, if any. * * If the _x-akismet-pro-tip_ header is set to discard, then Akismet has determined that the comment is blatant spam, * and you can safely discard it without saving it in any spam queue. * * Read more about this feature in this * [Akismet blog post](https://blog.akismet.com/2014/04/23/theres-a-ninja-in-your-akismet/). * * @see [Akismet.isDiscard] */ @Suppress("MemberVisibilityCanBePrivate") var proTip: String = "" private set</ID>
<ID>NestedBlockDepth:Akismet.kt$Akismet$ @JvmOverloads fun executeMethod(apiUrl: HttpUrl?, formBody: FormBody, trueOnError: Boolean = false): Boolean</ID>
<ID>NestedBlockDepth:AkismetTest.kt$fun getKey(key: String): String</ID>
<ID>ReturnCount:Akismet.kt$Akismet$ @JvmOverloads fun executeMethod(apiUrl: HttpUrl?, formBody: FormBody, trueOnError: Boolean = false): Boolean</ID>
<ID>TooManyFunctions:Akismet.kt$Akismet</ID>
</Whitelist>
</SmellBaseline>

View file

@ -31,6 +31,8 @@
*/
package net.thauvin.erik.akismet
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import net.thauvin.erik.semver.Version
import okhttp3.FormBody
import okhttp3.HttpUrl
@ -108,6 +110,16 @@ open class Akismet(apiKey: String) {
var response: String = ""
private set
/**
* The error message.
*
* The error (IO, empty response from Akismet, etc.) message is also logged as a warning.
*
* @see [Akismet.checkComment]
*/
var errorMessage: String = ""
private set
/**
* The _x-akismet-pro-tip_ header from the last operation, if any.
*
@ -269,6 +281,15 @@ open class Akismet(apiKey: String) {
return executeMethod(buildApiUrl("submit-ham"), buildFormBody(comment))
}
/**
* (Re)Create a [comment][AkismetComment] from a JSON string.
*
* @see [AkismetComment.toString]
*/
fun jsonComment(json: String): AkismetComment {
return Json(JsonConfiguration.Stable).parse(AkismetComment.serializer(), json)
}
/**
* Convert a date to a UTC timestamp. (ISO 8601)
*
@ -304,7 +325,11 @@ open class Akismet(apiKey: String) {
fun executeMethod(apiUrl: HttpUrl?, formBody: FormBody, trueOnError: Boolean = false): Boolean {
reset()
if (apiUrl != null) {
val request = Request.Builder().url(apiUrl).post(formBody).header("User-Agent", buildUserAgent()).build()
val request = if (formBody.size == 0) {
Request.Builder().url(apiUrl).header("User-Agent", buildUserAgent()).build()
} else {
Request.Builder().url(apiUrl).post(formBody).header("User-Agent", buildUserAgent()).build()
}
try {
val result = client.newCall(request).execute()
httpStatusCode = result.code
@ -317,26 +342,28 @@ open class Akismet(apiKey: String) {
if (response == "valid" || response == "true" || response.startsWith("Thanks")) {
return true
} else if (response != "false" && response != "invalid") {
logger.warning("Unexpected response: $body")
return trueOnError
errorMessage = "Unexpected response: " + if (body.isBlank()) "(0-byte body)" else body
}
} else {
val message = "An empty response was received from Akismet."
if (debugHelp.isNotBlank()) {
logger.warning("$message: $debugHelp")
errorMessage = if (debugHelp.isNotBlank()) {
"$message: $debugHelp"
} else {
logger.warning(message)
message
}
return trueOnError
}
} catch (e: IOException) {
logger.log(Level.SEVERE, "An IO error occurred while communicating with the Akismet service.", e)
return trueOnError
errorMessage = "An IO error occurred while communicating with the Akismet service."
}
} else {
logger.severe("Invalid API end point URL.")
errorMessage = "Invalid API end point URL."
}
if (errorMessage.isNotEmpty()) {
logger.warning(errorMessage)
return trueOnError
}
return false
}
@ -345,6 +372,7 @@ open class Akismet(apiKey: String) {
*/
fun reset() {
debugHelp = ""
errorMessage = ""
httpStatusCode = 0
isDiscard = false
isVerifiedKey = false

View file

@ -32,6 +32,9 @@
package net.thauvin.erik.akismet
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import javax.servlet.http.HttpServletRequest
/**
@ -50,6 +53,7 @@ import javax.servlet.http.HttpServletRequest
* @param userIp IP address of the comment submitter.
* @param userAgent User agent string of the web browser submitting the comment.
*/
@Serializable
open class AkismetComment(val userIp: String, val userAgent: String) {
@Suppress("unused")
companion object {
@ -171,27 +175,68 @@ open class AkismetComment(val userIp: String, val userAgent: String) {
}
/**
* Returns a string representation of the comment.
* Returns a JSON representation of the comment.
*
* @see [Akismet.jsonComment]
*/
override fun toString(): String {
return this.javaClass.simpleName +
"(userIp=$userIp" +
", userAgent=$userAgent" +
", referrer=$referrer" +
", permalink=$permalink" +
", type=$type" +
", author=$author" +
", authorEmail=$authorEmail" +
", authorUrl=$authorUrl" +
", content=$content" +
", dateGmt=$dateGmt" +
", postModifiedGmt=$postModifiedGmt" +
", blogLang=$blogLang" +
", blogCharset=$blogCharset" +
", userRole=$userRole" +
", isTest=$isTest" +
", recheckReason=$recheckReason" +
", serverEnv=$serverEnv)"
return Json(JsonConfiguration.Stable).stringify(serializer(), this)
}
/**
* Indicates whether some other object is _equal to_ this one.
*/
@Suppress("DuplicatedCode")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AkismetComment
if (userIp != other.userIp) return false
if (userAgent != other.userAgent) return false
if (referrer != other.referrer) return false
if (permalink != other.permalink) return false
if (type != other.type) return false
if (author != other.author) return false
if (authorEmail != other.authorEmail) return false
if (authorUrl != other.authorUrl) return false
if (content != other.content) return false
if (dateGmt != other.dateGmt) return false
if (postModifiedGmt != other.postModifiedGmt) return false
if (blogLang != other.blogLang) return false
if (blogCharset != other.blogCharset) return false
if (userRole != other.userRole) return false
if (isTest != other.isTest) return false
if (recheckReason != other.recheckReason) return false
if (serverEnv != other.serverEnv) return false
return true
}
/**
* Returns a hash code value for the object.
*/
@Suppress("DuplicatedCode")
override fun hashCode(): Int {
var result = userIp.hashCode()
result = 31 * result + userAgent.hashCode()
result = 31 * result + referrer.hashCode()
result = 31 * result + permalink.hashCode()
result = 31 * result + type.hashCode()
result = 31 * result + author.hashCode()
result = 31 * result + authorEmail.hashCode()
result = 31 * result + authorUrl.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + dateGmt.hashCode()
result = 31 * result + postModifiedGmt.hashCode()
result = 31 * result + blogLang.hashCode()
result = 31 * result + blogCharset.hashCode()
result = 31 * result + userRole.hashCode()
result = 31 * result + isTest.hashCode()
result = 31 * result + recheckReason.hashCode()
result = 31 * result + serverEnv.hashCode()
return result
}
}

View file

@ -37,6 +37,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.mockito.Mockito
import org.testng.Assert.assertEquals
import org.testng.Assert.assertFalse
import org.testng.Assert.assertNotEquals
import org.testng.Assert.assertTrue
import org.testng.Assert.expectThrows
import org.testng.annotations.BeforeClass
@ -179,12 +180,37 @@ class AkismetTest {
}
}
@Test
fun testEmptyResponse() {
assertTrue(
akismet.executeMethod(
"https://postman-echo.com/status/200".toHttpUrlOrNull(), FormBody.Builder().build(), true
)
)
val expected = "{\"status\":200}"
assertEquals(akismet.response, expected, expected)
assertTrue(akismet.errorMessage.contains(expected), "errorMessage contains $expected")
}
@Test
fun testProTipResponse() {
assertFalse(
akismet.executeMethod(
"https://postman-echo.com/response-headers?x-akismet-pro-tip=test".toHttpUrlOrNull(),
FormBody.Builder().build()
)
)
assertEquals(akismet.proTip, "test")
}
@Test
fun resetTest() {
akismet.reset()
//@TODO fix
assertTrue(
akismet.debugHelp == "" && akismet.httpStatusCode == 0 && !akismet.isDiscard &&
!akismet.isVerifiedKey && akismet.proTip == "" && akismet.response == ""
akismet.debugHelp == "" && akismet.errorMessage == "" && akismet.httpStatusCode == 0 &&
!akismet.isDiscard && !akismet.isVerifiedKey && akismet.proTip == "" && akismet.response == ""
)
}
@ -225,17 +251,24 @@ class AkismetTest {
assertTrue(akismet.submitSpam(mockComment), "submitHam(request)")
}
@Test
fun testJsonComment() {
val jsonComment = akismet.jsonComment(mockComment.toString())
assertEquals(jsonComment, mockComment, "equals")
assertEquals(jsonComment.hashCode(), mockComment.hashCode(), "hashcode")
assertNotEquals(jsonComment, comment, "json is different")
assertNotEquals(jsonComment.hashCode(), comment.hashCode(), "json hashcode is different")
}
@Test
fun testBuildUserAgent() {
val libAgent = "${GeneratedVersion.PROJECT}/${GeneratedVersion.VERSION}"
assertEquals(
akismet.buildUserAgent(), libAgent, "libAgent"
)
akismet.applicationName = "My App"
assertEquals(akismet.buildUserAgent(), libAgent, "libAgent")
assertEquals(
akismet.buildUserAgent(), libAgent, "libAgent, no app"
)
akismet.applicationName = "My App"
assertEquals(akismet.buildUserAgent(), libAgent, "libAgent, no app")
akismet.applicationVersion = "1.0-test"
assertEquals(