Skip to content

Commit a219170

Browse files
Add unit test to compare library against Top 4 GitHub TOTP libraries
1 parent 136f781 commit a219170

File tree

2 files changed

+84
-1
lines changed

2 files changed

+84
-1
lines changed

build.gradle.kts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import java.net.URI
2-
import kotlin.math.sign
32

43
plugins {
54
`java-library`
@@ -61,6 +60,16 @@ dependencies {
6160
testImplementation("org.junit.jupiter:junit-jupiter-params:$jUnitVersion")
6261
testImplementation("org.junit.jupiter:junit-jupiter-api:$jUnitVersion")
6362
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jUnitVersion")
63+
64+
testImplementation("com.github.bastiaanjansen:otp-java:1.3.0") {
65+
because("For `OtherLibrariesComparisonTest`")
66+
}
67+
testImplementation("com.eatthepath:java-otp:0.3.1") {
68+
because("For `OtherLibrariesComparisonTest`")
69+
}
70+
testImplementation("com.j256.two-factor-auth:two-factor-auth:1.3") {
71+
because("For `OtherLibrariesComparisonTest`")
72+
}
6473
}
6574

6675
tasks.withType<Test> {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package dev.turingcomplete.kotlinonetimepassword
2+
3+
import com.bastiaanjansen.otp.HMACAlgorithm
4+
import com.bastiaanjansen.otp.TOTP
5+
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil
6+
import org.apache.commons.codec.binary.Base32
7+
import org.junit.jupiter.api.Assertions
8+
import org.junit.jupiter.api.DisplayName
9+
import org.junit.jupiter.params.ParameterizedTest
10+
import org.junit.jupiter.params.provider.CsvSource
11+
import java.time.Duration
12+
import java.time.Instant
13+
import java.util.concurrent.TimeUnit
14+
import javax.crypto.spec.SecretKeySpec
15+
16+
class OtherLibrariesComparisonTest {
17+
// -- Companion Object -------------------------------------------------------------------------------------------- //
18+
19+
companion object {
20+
private val SECRET_PLAIN = "#ug0sEABk,}@anh&<ozWM6,#Nq/<NC3s"
21+
private val SECRET_BASE_32 = Base32().encodeToString(SECRET_PLAIN.toByteArray()).toString()
22+
}
23+
24+
// -- Properties -------------------------------------------------------------------------------------------------- //
25+
// -- Initialization ---------------------------------------------------------------------------------------------- //
26+
// -- Exposed Methods --------------------------------------------------------------------------------------------- //
27+
28+
@ParameterizedTest
29+
@DisplayName("com.eatthepath:java-otp (https://github.com/jchambers/java-otp)")
30+
@CsvSource(value = ["15, 6", "15, 8", "30, 6", "30, 8", "45, 6", "45, 8"])
31+
fun testComparetoEtthapath(timeStepSeconds: Long, digits: Int) {
32+
val currentTime = System.currentTimeMillis()
33+
val expectedCode = createExpectedCode(timeStepSeconds, digits, currentTime)
34+
35+
// Library only supports 6-8 digits
36+
val eatthepath = com.eatthepath.otp.TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(timeStepSeconds), digits, com.eatthepath.otp.TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA1)
37+
val eatthepathCode = eatthepath.generateOneTimePasswordString(SecretKeySpec(SECRET_PLAIN.toByteArray(), "RAW"), Instant.ofEpochMilli(currentTime))
38+
Assertions.assertEquals(expectedCode, eatthepathCode)
39+
}
40+
41+
@ParameterizedTest
42+
@DisplayName("com.github.bastiaanjansen:otp-java (https://github.com/BastiaanJansen/otp-java)")
43+
@CsvSource(value = ["15, 6", "15, 8", "30, 6", "30, 8", "45, 6", "45, 8"])
44+
fun testCompareToBastiaanJansen(timeStepSeconds: Long, digits: Int) {
45+
val currentTime = System.currentTimeMillis()
46+
val expectedCode = createExpectedCode(timeStepSeconds, digits, currentTime)
47+
48+
// Library only supports 6-8 digits
49+
val bastiaanjansen = TOTP.Builder(SECRET_BASE_32.toByteArray()).withAlgorithm(HMACAlgorithm.SHA1).withPasswordLength(digits).withPeriod(Duration.ofSeconds(timeStepSeconds)).build()
50+
val bastiaanjansenCode = bastiaanjansen.at(Instant.ofEpochMilli(currentTime))
51+
Assertions.assertEquals(expectedCode, bastiaanjansenCode)
52+
}
53+
54+
@ParameterizedTest
55+
@DisplayName("com.j256.two-factor-auth:two-factor-auth (https://github.com/j256/two-factor-auth)")
56+
@CsvSource(value = ["15, 6", "15, 8", "30, 6", "30, 8", "45, 6", "45, 8"])
57+
fun testCompareToJ256(timeStepSeconds: Long, digits: Int) {
58+
val currentTime = System.currentTimeMillis()
59+
val expectedCode = createExpectedCode(timeStepSeconds, digits, currentTime)
60+
61+
val j256Code = TimeBasedOneTimePasswordUtil.generateNumberString(SECRET_BASE_32, System.currentTimeMillis(), timeStepSeconds.toInt(), digits)
62+
Assertions.assertEquals(expectedCode, j256Code)
63+
}
64+
65+
// -- Private Methods --------------------------------------------------------------------------------------------- //
66+
67+
private fun createExpectedCode(timeStepSeconds: Long, digits: Int, currentTime: Long): String {
68+
val config = TimeBasedOneTimePasswordConfig(timeStep = timeStepSeconds, timeStepUnit = TimeUnit.SECONDS, codeDigits = digits, hmacAlgorithm = HmacAlgorithm.SHA1)
69+
val generator = TimeBasedOneTimePasswordGenerator(SECRET_PLAIN.toByteArray(), config)
70+
return generator.generate(currentTime)
71+
}
72+
73+
// -- Inner Type -------------------------------------------------------------------------------------------------- //
74+
}

0 commit comments

Comments
 (0)