Skip to content

Commit

Permalink
file attachment business logic
Browse files Browse the repository at this point in the history
  • Loading branch information
fractalwrench committed Jan 13, 2025
1 parent 62f04fb commit 03333fa
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.embrace.android.embracesdk.internal.logs.attachments

import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.ATTACHMENT_TOO_LARGE
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.OVER_MAX_ATTACHMENTS
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.UNKNOWN
import io.embrace.android.embracesdk.internal.utils.toNonNullMap
import java.util.UUID

/**
* Holds attributes that describe an attachment to a log record.
*/
internal sealed class Attachment(
val size: Long,
val id: String,
val counter: AttachmentCounter,
) {

internal companion object {
const val ATTR_KEY_SIZE = "emb.attachment_size"
const val ATTR_KEY_URL = "emb.attachment_url"
const val ATTR_KEY_ID = "emb.attachment_id"
const val ATTR_KEY_ERR_CODE = "emb.attachment_error_code"
private const val LIMIT_MB = 1 * 1024 * 1024
}

abstract val attributes: Map<String, String>

protected fun constructAttributes(
size: Long,
id: String,
errorCode: AttachmentErrorCode? = null

Check warning on line 31 in embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/logs/attachments/Attachment.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/logs/attachments/Attachment.kt#L31

Added line #L31 was not covered by tests
) = mapOf(
ATTR_KEY_SIZE to size.toString(),
ATTR_KEY_ID to id,
ATTR_KEY_ERR_CODE to errorCode?.name
).toNonNullMap()

/**
* An attachment that is uploaded to Embrace's backend.
*/
class EmbraceHosted(
val bytes: ByteArray,
counter: AttachmentCounter,
) : Attachment(
bytes.size.toLong(),
UUID.randomUUID().toString(),
counter
) {

private val errorCode: AttachmentErrorCode? = when {
!counter.incrementAndCheckAttachmentLimit() -> OVER_MAX_ATTACHMENTS
size > LIMIT_MB -> ATTACHMENT_TOO_LARGE
else -> null
}

override val attributes: Map<String, String> = constructAttributes(size, id, errorCode)
}

/**
* An attachment that is uploaded to a user-supplied backend.
*/
class UserHosted(
size: Long,
id: String,
val url: String,
counter: AttachmentCounter,
) : Attachment(size, id, counter) {

private val errorCode: AttachmentErrorCode? = when {
!counter.incrementAndCheckAttachmentLimit() -> OVER_MAX_ATTACHMENTS
size < 0 -> UNKNOWN
url.isEmpty() -> UNKNOWN
isNotUuid() -> UNKNOWN
else -> null
}

override val attributes: Map<String, String> =
constructAttributes(size, id, errorCode).plus(
ATTR_KEY_URL to url
)

private fun isNotUuid(): Boolean = try {
UUID.fromString(id)
false
} catch (e: IllegalArgumentException) {
true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.embrace.android.embracesdk.internal.logs.attachments

import io.embrace.android.embracesdk.internal.session.MemoryCleanerListener
import java.util.concurrent.atomic.AtomicInteger

/**
* Counts the number of attachments that should be added to log records.
*/
class AttachmentCounter(private val limit: Int = 5) : MemoryCleanerListener {

private val count: AtomicInteger = AtomicInteger(0)

override fun cleanCollections() = count.set(0)

/**
* Increments the counter of attachments for this session and returns true if an attachment can be uploaded.
*/
fun incrementAndCheckAttachmentLimit(): Boolean = count.incrementAndGet() <= limit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.embrace.android.embracesdk.internal.logs.attachments

/**
* Enumerates the states where an attachment could not be added to a log record.
*/
internal enum class AttachmentErrorCode {
ATTACHMENT_TOO_LARGE,
UNSUCCESSFUL_UPLOAD,
OVER_MAX_ATTACHMENTS,
UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package io.embrace.android.embracesdk.internal.logs.attachments

import io.embrace.android.embracesdk.internal.logs.attachments.Attachment.Companion.ATTR_KEY_ERR_CODE
import io.embrace.android.embracesdk.internal.logs.attachments.Attachment.Companion.ATTR_KEY_ID
import io.embrace.android.embracesdk.internal.logs.attachments.Attachment.Companion.ATTR_KEY_SIZE
import io.embrace.android.embracesdk.internal.logs.attachments.Attachment.Companion.ATTR_KEY_URL
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.ATTACHMENT_TOO_LARGE
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.OVER_MAX_ATTACHMENTS
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.UNKNOWN
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import java.util.UUID

internal class AttachmentTest {

private companion object {
private const val URL = "https://example.com/my-attachment"
private const val SIZE = 2L
private const val LIMIT_MB = 1 * 1024 * 1024
private val ID = UUID.randomUUID().toString()
private val BYTES = "{}".toByteArray()
}

private val counter = AttachmentCounter(limit = Int.MAX_VALUE)

@Test
fun `create embrace hosted attachment`() {
val attachment = Attachment.EmbraceHosted(BYTES, counter)
attachment.assertEmbraceHostedAttributesMatch()
}

@Test
fun `embrace hosted attachment empty byte array`() {
val attachment = Attachment.EmbraceHosted(ByteArray(0), counter)
attachment.assertEmbraceHostedAttributesMatch(size = 0)
}

@Test
fun `embrace hosted attachment at max size`() {
val attachment = Attachment.EmbraceHosted(ByteArray(LIMIT_MB), counter)
attachment.assertEmbraceHostedAttributesMatch(size = LIMIT_MB.toLong())
}

@Test
fun `embrace hosted attachment obeys max size constraints`() {
val size = LIMIT_MB + 1
val attachment = Attachment.EmbraceHosted(ByteArray(size), counter)
attachment.assertEmbraceHostedAttributesMatch(size = size.toLong(), errorCode = ATTACHMENT_TOO_LARGE)
}

@Test
fun `embrace hosted attachment exceeds session limit`() {
val smallCounter = AttachmentCounter(1)
val attachment = Attachment.EmbraceHosted(BYTES, smallCounter)
attachment.assertEmbraceHostedAttributesMatch()

val size = LIMIT_MB + 1L
val bytes = ByteArray(size.toInt())
val limitedAttachment = Attachment.EmbraceHosted(bytes, smallCounter)
limitedAttachment.assertEmbraceHostedAttributesMatch(size = size, errorCode = OVER_MAX_ATTACHMENTS)
}

@Test
fun `create user hosted attachment`() {
val attachment = Attachment.UserHosted(SIZE, ID, URL, counter)
attachment.assertUserHostedAttributesMatch()
}

@Test
fun `user hosted attachment empty size`() {
val size: Long = 0
val attachment = Attachment.UserHosted(size, ID, URL, counter)
attachment.assertUserHostedAttributesMatch(size = size)
}

@Test
fun `user hosted attachment invalid size`() {
val size: Long = -1
val attachment = Attachment.UserHosted(size, ID, URL, counter)
attachment.assertUserHostedAttributesMatch(size = size, errorCode = UNKNOWN)
}

@Test
fun `user hosted attachment invalid url`() {
val url = ""
val attachment = Attachment.UserHosted(SIZE, ID, url, counter)
attachment.assertUserHostedAttributesMatch(url = url, errorCode = UNKNOWN)
}

@Test
fun `user hosted attachment invalid ID`() {
val id = "my-id"
val attachment = Attachment.UserHosted(SIZE, id, URL, counter)
attachment.assertUserHostedAttributesMatch(id = id, errorCode = UNKNOWN)
}

@Test
fun `user hosted attachment has no max size constraints`() {
val size = 5000000L // 50MiB
val attachment = Attachment.UserHosted(size, ID, URL, counter)
attachment.assertUserHostedAttributesMatch(size = size)
}

@Test
fun `user hosted attachment exceeds session limit`() {
val smallCounter = AttachmentCounter(1)
val attachment = Attachment.UserHosted(SIZE, ID, URL, smallCounter)
attachment.assertUserHostedAttributesMatch()

val size = -1L
val limitedAttachment = Attachment.UserHosted(size, ID, URL, smallCounter)
limitedAttachment.assertUserHostedAttributesMatch(size = size, errorCode = OVER_MAX_ATTACHMENTS)
}

private fun Attachment.assertEmbraceHostedAttributesMatch(
size: Long = SIZE,
errorCode: AttachmentErrorCode? = null,
) {
val observedId = checkNotNull(attributes[ATTR_KEY_ID])
assertNotNull(UUID.fromString(observedId))
assertEquals(size, checkNotNull(attributes[ATTR_KEY_SIZE]).toLong())
assertEquals(errorCode?.toString(), attributes[ATTR_KEY_ERR_CODE])
}

private fun Attachment.assertUserHostedAttributesMatch(
size: Long = SIZE,
url: String = URL,
id: String = ID,
errorCode: AttachmentErrorCode? = null,
) {
assertEquals(size, checkNotNull(attributes[ATTR_KEY_SIZE]).toLong())
assertEquals(id, checkNotNull(attributes[ATTR_KEY_ID]))
assertEquals(errorCode?.toString(), attributes[ATTR_KEY_ERR_CODE])
assertEquals(url, checkNotNull(attributes[ATTR_KEY_URL]))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.embrace.android.embracesdk.internal.logs.attachments

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

internal class AttachmentCounterTest {

@Test
fun `limit exceeded`() {
val counter = AttachmentCounter()
assertLimitRespected(counter)

counter.cleanCollections()
assertLimitRespected(counter)
}

private fun assertLimitRespected(counter: AttachmentCounter) {
repeat(5) {
assertTrue(counter.incrementAndCheckAttachmentLimit())
}
assertFalse(counter.incrementAndCheckAttachmentLimit())
}
}

0 comments on commit 03333fa

Please sign in to comment.