Skip to content

Commit

Permalink
Improve UT for SR Obfuscators
Browse files Browse the repository at this point in the history
  • Loading branch information
xgouchet committed May 13, 2024
1 parent 3d0a4d2 commit 803d45b
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 269 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import com.datadog.android.sessionreplay.internal.recorder.obfuscator.StringObfu
*/
@RequiresApi(Build.VERSION_CODES.N)
class AndroidNStringObfuscator : StringObfuscator {

override fun obfuscate(stringValue: String): String {
return buildString {
return buildString(stringValue.length) {
stringValue.codePoints().forEach {
if (Character.isWhitespace(it)) {
// I don't think we should log this case here. I could not even reproduce it
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.internal.recorder.obfuscator

import fr.xgouchet.elmyr.annotation.IntForgery
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.annotation.StringForgeryType
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream

internal abstract class AbstractObfuscatorTest {

lateinit var testedObfuscator: StringObfuscator

@StringForgery(StringForgeryType.ALPHA_NUMERICAL)
lateinit var fakeAlphaNumChunks: List<String>

@IntForgery(1, 10)
var fakeSeparatorLength: Int = 1

@IntForgery(1, 10)
var fakePrefixLength: Int = 1

@IntForgery(1, 10)
var fakePostfixLength: Int = 1

@IntForgery(1, 10)
var fakeEmojiRepeat: Int = 1

@ParameterizedTest(name = "{index} (char:{0})")
@MethodSource("whitespacesUseCases")
fun `M mask non whitespace chars W obfuscate`(
whitespaceSeparator: Char
) {
// Given
val input = fakeAlphaNumChunks
.joinToString(
separator = CharArray(fakeSeparatorLength) { whitespaceSeparator }.concatToString(),
prefix = CharArray(fakePrefixLength) { whitespaceSeparator }.concatToString(),
postfix = CharArray(fakePostfixLength) { whitespaceSeparator }.concatToString()
)
val expectedOutput = fakeAlphaNumChunks
.joinToString(
separator = CharArray(fakeSeparatorLength) { whitespaceSeparator }.concatToString(),
prefix = CharArray(fakePrefixLength) { whitespaceSeparator }.concatToString(),
postfix = CharArray(fakePostfixLength) { whitespaceSeparator }.concatToString()
) { CharArray(it.length) { 'x' }.concatToString() }

// When
val output = testedObfuscator.obfuscate(input)

// Then
assertThat(output).isEqualTo(expectedOutput)
}

companion object {

@JvmStatic
fun emojiUseCases(): Stream<Arguments> {
val emojiChars = mutableListOf<String>()

// First set of emojis
for (emojiCodePoint in 0x1F600 until 0x1F64F) {
emojiChars.add(
Character.toChars(emojiCodePoint).concatToString()
)
}

for (emojiCodePoint in 0x1F680 until 0x1F6FC) {
emojiChars.add(
Character.toChars(emojiCodePoint).concatToString()
)
}

for (emojiCodePoint in 0x1F90C until 0x1F9FF) {
emojiChars.add(
Character.toChars(emojiCodePoint).concatToString()
)
}

for (emojiCodePoint in 0x1FA70 until 0x1FAD6) {
emojiChars.add(
Character.toChars(emojiCodePoint).concatToString()
)
}

return emojiChars.map { Arguments.of(it) }.stream()
}

@JvmStatic
fun whitespacesUseCases(): Stream<Arguments> {
val whitespaceChars = mutableListOf<Char>()

// ASCII whitespace character
whitespaceChars.add('\u0009') // Horizontal Tab = '\t'
whitespaceChars.add('\u000A') // Line Feed = '\n'
whitespaceChars.add('\u000B') // Vertical Tab
whitespaceChars.add('\u000C') // Form Feed
whitespaceChars.add('\u000D') // Carriage Return = '\r'
whitespaceChars.add('\u001C') // File Separator
whitespaceChars.add('\u001D') // Group Separator
whitespaceChars.add('\u001E') // Record Separator
whitespaceChars.add('\u001F') // Unit Separator
whitespaceChars.add('\u0020') // plain old whitespace ' '

// Non ASCII
whitespaceChars.add('\u1680') // Ogham (Old Irish alphabet) Space Mark
// whitespaceChars.add('\u180e') // Mongolian Vowel Separator - this works in Android, but not in the JDK

// General Punctuation Block : spaces of varying width
whitespaceChars.add('\u2000') // en quad = ½ em space (fixed width)
whitespaceChars.add('\u2001') // em quad = 1 em space (fixed width)
whitespaceChars.add('\u2002') // en space = ½ em space (width can vary depending on font)
whitespaceChars.add('\u2003') // em space = 1 em space (width can vary depending on font)
whitespaceChars.add('\u2004') // 3-per-em space = ⅓ em space (width can vary depending on font)
whitespaceChars.add('\u2005') // 4-per-em space = ¼ em space (width can vary depending on font)
whitespaceChars.add('\u2006') // 6-per-em space = ⅙ em space (width can vary depending on font)
whitespaceChars.add('\u2008') // punctuation space (space as wide as a period '.')
whitespaceChars.add('\u2009') // thin space (between a ⅙ and ¼ em space)
whitespaceChars.add('\u200a') // hair space (narrower than a thin space, usually the thinnest space)
whitespaceChars.add('\u2028') // line separator
whitespaceChars.add('\u2029') // paragraph separator
whitespaceChars.add('\u205f') // Medium mathematical space (four-eighteenths of an em, because why not)
whitespaceChars.add('\u3000') // Ideographic space

return whitespaceChars.map { Arguments.of(it) }.stream()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ package com.datadog.android.sessionreplay.internal.recorder.obfuscator

import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import com.datadog.tools.unit.extensions.ApiLevelExtension
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.quality.Strictness
import java.util.LinkedList
import java.util.stream.Stream

@Extensions(
ExtendWith(MockitoExtension::class),
Expand All @@ -28,152 +29,35 @@ import java.util.LinkedList
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(ForgeConfigurator::class)
internal class AndroidNStringObfuscatorTest {

lateinit var testedObfuscator: StringObfuscator
internal class AndroidNStringObfuscatorTest : AbstractObfuscatorTest() {

@BeforeEach
fun `set up`() {
testedObfuscator = AndroidNStringObfuscator()
}

// region Android N and above

@Test
fun `M mask String W obfuscate(){string with newline, Android N}`(
forge: Forge
@ParameterizedTest(name = "{index} (emojis:{0})")
@MethodSource("emojiUseCases")
fun `M mask emoji chars W obfuscate`(
emojiChars: String
) {
// Given
val fakeExpectedChunk1 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' }
val fakeExpectedChunk2 = forge.aString { 'x' }
val fakeExpectedChunk3 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' }
val fakeExpectedChunk4 = forge.aString { 'x' }
val fakeExpectedChunk5 = forge.aString(size = forge.anInt(1, max = 10)) { '\n' }
val fakeExpectedText = (
fakeExpectedChunk1 +
fakeExpectedChunk2 +
fakeExpectedChunk3 +
fakeExpectedChunk4 +
fakeExpectedChunk5
)
val fakeText = (
fakeExpectedChunk1 +
forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } +
fakeExpectedChunk3 +
forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } +
fakeExpectedChunk5
)
val emojiRepeat = List(fakeEmojiRepeat) { emojiChars }
val input = emojiRepeat.joinToString(" ")
val expectedOutput = emojiRepeat.joinToString(" ") { "x" }

// When
val obfuscatedText = testedObfuscator.obfuscate(fakeText)
val output = testedObfuscator.obfuscate(input)

// Then
assertThat(obfuscatedText).isEqualTo(fakeExpectedText)
assertThat(output).isEqualTo(expectedOutput)
}

@Test
fun `M mask String W obfuscate(){string with carriage return character, Android N}`(
forge: Forge
) {
// Given
val fakeExpectedChunk1 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' }
val fakeExpectedChunk2 = forge.aString { 'x' }
val fakeExpectedChunk3 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' }
val fakeExpectedChunk4 = forge.aString { 'x' }
val fakeExpectedChunk5 = forge.aString(size = forge.anInt(1, max = 10)) { '\r' }
val fakeExpectedText = (
fakeExpectedChunk1 +
fakeExpectedChunk2 +
fakeExpectedChunk3 +
fakeExpectedChunk4 +
fakeExpectedChunk5
)
val fakeText = (
fakeExpectedChunk1 +
forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } +
fakeExpectedChunk3 +
forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } +
fakeExpectedChunk5
)

// When
val obfuscatedText = testedObfuscator.obfuscate(fakeText)
companion object {

// Then
assertThat(obfuscatedText).isEqualTo(fakeExpectedText)
}

@Test
fun `M mask String W obfuscate(){string with whitespace character, Android N}`(
forge: Forge
) {
// Given
val fakeExpectedChunk1 = forge.aWhitespaceString()
val fakeExpectedChunk2 = forge.aString { 'x' }
val fakeExpectedChunk3 = forge.aWhitespaceString()
val fakeExpectedChunk4 = forge.aString { 'x' }
val fakeExpectedChunk5 = forge.aWhitespaceString()
val fakeExpectedText = (
fakeExpectedChunk1 +
fakeExpectedChunk2 +
fakeExpectedChunk3 +
fakeExpectedChunk4 +
fakeExpectedChunk5
)
val fakeText = (
fakeExpectedChunk1 +
forge.aString(fakeExpectedChunk2.length) { forge.anAlphaNumericalChar() } +
fakeExpectedChunk3 +
forge.aString(fakeExpectedChunk4.length) { forge.anAlphaNumericalChar() } +
fakeExpectedChunk5
)

// When
val obfuscatedText = testedObfuscator.obfuscate(fakeText)

// Then
assertThat(obfuscatedText).isEqualTo(fakeExpectedText)
}

@Test
fun `M mask String W obfuscate(){string with whitespace character and emoji, Android N}`(
forge: Forge
) {
// Given
val fakeChunk1 = forge.aStringWithEmoji()
val fakeChunk2 = forge.aWhitespaceString()
val fakeChunk3 = forge.aStringWithEmoji()
val fakeChunk4 = forge.aWhitespaceString()
val fakeText = fakeChunk1 + fakeChunk2 + fakeChunk3 + fakeChunk4

// the real size of an emoji chunk is chunk.length/2 as one emoji contains 2 chars.
// In our current code for >= Android N we are treating this case correctly so expected
// obfuscated string length is chunk.length/2
val fakeExpectedChunk1 = String(CharArray(fakeChunk1.length / 2) { 'x' })
val fakeExpectedChunk3 = String(CharArray(fakeChunk3.length / 2) { 'x' })
val fakeExpectedText = fakeExpectedChunk1 + fakeChunk2 + fakeExpectedChunk3 + fakeChunk4

// When
val obfuscatedText = testedObfuscator.obfuscate(fakeText)

// Then
assertThat(obfuscatedText).isEqualTo(fakeExpectedText)
}

// endregion

// region Internal

private fun Forge.aStringWithEmoji(): String {
val charsList = LinkedList<Char>()
val stringSize = anInt(min = 10, max = 100)
repeat(stringSize) {
val emojiCodePoint = anInt(min = 0x1f600, max = 0x1f60A)
val chars = Character.toChars(emojiCodePoint).toList()
charsList.addAll(chars)
@JvmStatic
fun emojiUseCases(): Stream<Arguments> {
return AbstractObfuscatorTest.emojiUseCases()
}
return String(charsList.toCharArray())
}

// endregion
}
Loading

0 comments on commit 803d45b

Please sign in to comment.