Add tests for IndividualSendJobV2.

This commit is contained in:
Greyson Parrelli 2026-05-20 11:43:56 -04:00 committed by Cody Henthorne
parent a5359e05a3
commit 15e7b30fa1

View File

@ -0,0 +1,684 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import arrow.core.left
import arrow.core.right
import assertk.assertThat
import assertk.assertions.isTrue
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.models.ServiceId.ACI
import org.signal.libsignal.net.ChallengeOption
import org.signal.libsignal.protocol.UntrustedIdentityException
import org.signal.network.service.MessageService
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageSendLogTables
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.PendingPniSignatureMessageTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.keyvalue.MiscellaneousValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.service.ExpiringMessageManager
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
import org.thoughtcrime.securesms.testutil.MockSignalStoreRule
import org.thoughtcrime.securesms.util.DataMessageError
import org.thoughtcrime.securesms.util.MessageUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.toDataMessage
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.PniSignatureMessage
import java.io.IOException
import java.util.Optional
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
/**
* Unit tests for [IndividualSendJobV2]. Drives the job's [doRun] via [Job.run] (which delegates to the
* coroutine entry point) with all of its collaborators mocked: [SignalStore], [SignalDatabase] tables,
* [AppDependencies] (including [MessageService]), [Recipient] statics, and the various static helpers
* the job depends on ([SealedSenderAccessUtil], [RecipientUtil], [PushSendJob], etc.).
*
* Tests verify both the [Job.Result] returned and the side effects on the database / job manager,
* including for every [MessageService.SendError] variant.
*/
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class IndividualSendJobV2Test {
@get:Rule
val signalStore = MockSignalStoreRule()
@get:Rule
val appDependencies = MockAppDependenciesRule()
private val messageId: Long = 42L
private val threadId: Long = 17L
private val sentTime: Long = 1_700_000_000_000L
private val selfAci: ACI = ACI.from(UUID.fromString("00000000-0000-4000-8000-000000000001"))
private val recipientAci: ACI = ACI.from(UUID.fromString("00000000-0000-4000-8000-000000000002"))
private val selfRecipientId: RecipientId = RecipientId.from(1L)
private val recipientId: RecipientId = RecipientId.from(2L)
private lateinit var misc: MiscellaneousValues
private lateinit var recipient: Recipient
private lateinit var self: Recipient
private lateinit var outgoingMessage: OutgoingMessage
private lateinit var messageRecord: MessageRecord
private lateinit var dataMessage: DataMessage
private lateinit var messages: MessageTable
private lateinit var messageLog: MessageSendLogTables
private lateinit var threads: ThreadTable
private lateinit var recipientsTable: RecipientTable
private lateinit var attachments: AttachmentTable
private lateinit var pendingPniSignatureMessages: PendingPniSignatureMessageTable
private lateinit var expiringMessageManager: ExpiringMessageManager
private lateinit var messageService: MessageService
@Before
fun setUp() {
misc = mockk(relaxUnitFun = true)
every { misc.isClientDeprecated } returns false
every { SignalStore.misc } returns misc
every { signalStore.account.aci } returns selfAci
every { signalStore.account.requireAci() } returns selfAci
every { signalStore.account.isMultiDevice } returns false
every { signalStore.account.aciPreKeys.lastSignedPreKeyRotationTime } returns System.currentTimeMillis()
every { signalStore.account.pniPreKeys.lastSignedPreKeyRotationTime } returns System.currentTimeMillis()
messages = mockk(relaxed = true)
messageLog = mockk(relaxed = true)
threads = mockk(relaxed = true)
recipientsTable = mockk(relaxed = true)
attachments = mockk(relaxed = true)
pendingPniSignatureMessages = mockk(relaxed = true)
mockkObject(SignalDatabase.Companion)
every { SignalDatabase.messages } returns messages
every { SignalDatabase.messageLog } returns messageLog
every { SignalDatabase.threads } returns threads
every { SignalDatabase.recipients } returns recipientsTable
every { SignalDatabase.attachments } returns attachments
every { SignalDatabase.pendingPniSignatureMessages } returns pendingPniSignatureMessages
every { messageLog.insertIfPossible(any<RecipientId>(), any<Long>(), any(), any(), any<MessageId>(), any<Boolean>()) } returns 1L
recipient = mockRecipient(recipientAci, isSelf = false)
self = mockRecipient(selfAci, isSelf = true)
every { self.isRegistered } returns true
outgoingMessage = mockk {
every { threadRecipient } returns recipient
every { sentTimeMillis } returns sentTime
every { body } returns "hello"
every { messageToEdit } returns 0L
every { expiresIn } returns 0L
every { isExpirationUpdate } returns false
every { isViewOnce } returns false
every { attachments } returns emptyList<Attachment>()
}
messageRecord = mockk(relaxed = true)
every { messageRecord.threadId } returns threadId
every { messageRecord.dateSent } returns sentTime
every { messageRecord.type } returns 0L
every { messages.getOutgoingMessageOrNull(messageId) } returns outgoingMessage
every { messages.getMessageRecordOrNull(messageId) } returns messageRecord
every { threads.getRecipientForThreadId(threadId) } returns recipient
dataMessage = DataMessage(timestamp = sentTime)
mockkStatic("org.thoughtcrime.securesms.util.SignalServiceTransformExtensionsKt")
every { outgoingMessage.toDataMessage() } returns dataMessage.right()
mockkObject(Recipient.Companion)
every { Recipient.self() } returns self
mockkStatic(RecipientUtil::class)
every { RecipientUtil.shareProfileIfFirstSecureMessage(any()) } just runs
mockkStatic(SealedSenderAccessUtil::class)
every { SealedSenderAccessUtil.getSealedSenderAccessFor(any<Recipient>()) } returns mockk(relaxed = true)
every { SealedSenderAccessUtil.getSealedSenderCertificate() } returns mockk(relaxed = true)
mockkObject(RemoteConfig)
every { RemoteConfig.defaultMaxBackoff } returns 60_000L
mockkObject(PushSendJob.Companion)
every { PushSendJob.markAttachmentsUploaded(any<Long>(), any<OutgoingMessage>()) } just runs
every { PushSendJob.notifyMediaMessageDeliveryFailed(any(), any<Long>()) } just runs
mockkObject(ConversationShortcutRankingUpdateJob.Companion)
every { ConversationShortcutRankingUpdateJob.enqueueForOutgoingIfNecessary(any()) } just runs
mockkStatic(ProofRequiredExceptionHandler::class)
messageService = AppDependencies.messageService
expiringMessageManager = AppDependencies.expiringMessageManager
}
@After
fun tearDown() {
unmockkAll()
}
// region — Pre-send guards
@Test
fun `Given client is deprecated, when run, then return failure and skip send`() {
every { SignalStore.misc.isClientDeprecated } returns true
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
coVerify(exactly = 0) { messageService.sendMessage(serviceId = any(), envelopeContent = any(), timestamp = any(), sealedSenderAccess = any(), story = any(), isOnline = any(), urgent = any(), onEncrypted = any()) }
}
@Test
fun `Given signed prekey age is too old, when run, then trigger PreKeysSyncJob and continue on success`() {
val staleTime = System.currentTimeMillis() - 30L * 24L * 60L * 60L * 1000L
every { signalStore.account.aciPreKeys.lastSignedPreKeyRotationTime } returns staleTime
stubSuccessfulSend()
every {
AppDependencies.jobManager.runSynchronously(any(), any())
} returns Optional.of(JobTracker.JobState.SUCCESS)
val result = createAndRunJob()
assertThat(result.isSuccess).isTrue()
verify { AppDependencies.jobManager.runSynchronously(match { it is PreKeysSyncJob }, any()) }
}
@Test
fun `Given signed prekey age is too old and refresh fails, when run, then retry`() {
val staleTime = System.currentTimeMillis() - 30L * 24L * 60L * 60L * 1000L
every { signalStore.account.pniPreKeys.lastSignedPreKeyRotationTime } returns staleTime
every {
AppDependencies.jobManager.runSynchronously(any(), any())
} returns Optional.empty()
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
coVerify(exactly = 0) { messageService.sendMessage(serviceId = any(), envelopeContent = any(), timestamp = any(), sealedSenderAccess = any(), story = any(), isOnline = any(), urgent = any(), onEncrypted = any()) }
}
@Test
fun `Given self is not registered, when run, then return failure`() {
every { self.isRegistered } returns false
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given outgoing message is missing, when run, then return failure`() {
every { messages.getOutgoingMessageOrNull(messageId) } returns null
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given message record is missing, when run, then return failure`() {
every { messages.getMessageRecordOrNull(messageId) } returns null
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given message is already sent, when run, then return success without sending`() {
every { messageRecord.type } returns MessageTypes.BASE_SENT_TYPE
val result = createAndRunJob()
assertThat(result.isSuccess).isTrue()
coVerify(exactly = 0) { messageService.sendMessage(serviceId = any(), envelopeContent = any(), timestamp = any(), sealedSenderAccess = any(), story = any(), isOnline = any(), urgent = any(), onEncrypted = any()) }
}
@Test
fun `Given body exceeds inline size limit, when run, then return failure`() {
val tooLargeBody = "x".repeat(MessageUtil.MAX_INLINE_BODY_SIZE_BYTES + 1)
every { outgoingMessage.body } returns tooLargeBody
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
coVerify(exactly = 0) { messageService.sendMessage(serviceId = any(), envelopeContent = any(), timestamp = any(), sealedSenderAccess = any(), story = any(), isOnline = any(), urgent = any(), onEncrypted = any()) }
}
@Test
fun `Given recipient is unregistered, when run, then return failure`() {
every { recipient.isUnregistered } returns true
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given recipient has no service id, when run, then return failure`() {
every { recipient.hasServiceId } returns false
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given toDataMessage fails, when run, then return failure`() {
every { outgoingMessage.toDataMessage() } returns DataMessageError.MissingAttachmentRemoteFields.left()
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
coVerify(exactly = 0) { messageService.sendMessage(serviceId = any(), envelopeContent = any(), timestamp = any(), sealedSenderAccess = any(), story = any(), isOnline = any(), urgent = any(), onEncrypted = any()) }
}
// endregion
// region — Successful send
@Test
fun `Given a successful send, when run, then mark sent, write to log, update thread, return success`() {
stubSuccessfulSend(sentSealedSender = true)
val result = createAndRunJob()
assertThat(result.isSuccess).isTrue()
verify { messages.markAsSent(messageId, true) }
verify { threads.updateSilently(threadId, false) }
verify { messageLog.insertIfPossible(recipientId, sentTime, any(), ContentHint.RESENDABLE, MessageId(messageId), any()) }
verify { ConversationShortcutRankingUpdateJob.enqueueForOutgoingIfNecessary(recipient) }
}
@Test
fun `Given a successful send and an expiring message, when run, then start expiration timer`() {
every { outgoingMessage.expiresIn } returns 60_000L
every { outgoingMessage.isExpirationUpdate } returns false
stubSuccessfulSend()
createAndRunJob()
verify { messages.markExpireStarted(messageId, any()) }
verify { expiringMessageManager.scheduleDeletion(messageId, true, 60_000L) }
}
@Test
fun `Given a successful send and a view-once message, when run, then delete attachment files`() {
every { outgoingMessage.isViewOnce } returns true
stubSuccessfulSend()
createAndRunJob()
verify { attachments.deleteAttachmentFilesForViewOnceMessage(messageId) }
}
@Test
fun `Given a UD send to unknown sealed-sender-mode recipient with no profile key, when run, then mark UNRESTRICTED`() {
every { recipient.sealedSenderAccessMode } returns SealedSenderAccessMode.UNKNOWN
every { recipient.profileKey } returns null
stubSuccessfulSend(sentSealedSender = true)
createAndRunJob()
verify { recipientsTable.setSealedSenderAccessMode(recipientId, SealedSenderAccessMode.UNRESTRICTED) }
}
@Test
fun `Given a UD send to unknown sealed-sender-mode recipient with profile key, when run, then mark ENABLED`() {
every { recipient.sealedSenderAccessMode } returns SealedSenderAccessMode.UNKNOWN
every { recipient.profileKey } returns ByteArray(32)
stubSuccessfulSend(sentSealedSender = true)
createAndRunJob()
verify { recipientsTable.setSealedSenderAccessMode(recipientId, SealedSenderAccessMode.ENABLED) }
}
@Test
fun `Given a non-UD send to an enabled recipient, when run, then mark DISABLED`() {
every { recipient.sealedSenderAccessMode } returns SealedSenderAccessMode.ENABLED
stubSuccessfulSend(sentSealedSender = false)
createAndRunJob()
verify { recipientsTable.setSealedSenderAccessMode(recipientId, SealedSenderAccessMode.DISABLED) }
}
@Test
fun `Given multi-device, when send succeeds, then also send a sync transcript`() {
every { signalStore.account.isMultiDevice } returns true
stubSuccessfulSend()
createAndRunJob()
coVerify(exactly = 1) {
messageService.sendMessage(
serviceId = any(),
envelopeContent = any(),
timestamp = any(),
sealedSenderAccess = any(),
story = any(),
isOnline = any(),
urgent = any(),
onEncrypted = any()
)
}
coVerify(exactly = 1) {
messageService.sendSyncMessage(
timestamp = any(),
envelopeContent = any(),
urgent = any(),
onEncrypted = any()
)
}
}
@Test
fun `Given a send needs PNI signature, when send succeeds, then write a pending PNI signature record`() {
every { recipient.needsPniSignature } returns true
every { AppDependencies.signalServiceMessageSender.createPniSignatureMessage() } returns PniSignatureMessage()
stubSuccessfulSend()
createAndRunJob()
verify { pendingPniSignatureMessages.insertIfNecessary(recipientId, sentTime, any()) }
}
@Test
fun `Given a self-send, when run, then wrap content in a SyncMessage and use ContentHint IMPLICIT`() {
every { outgoingMessage.threadRecipient } returns self
every { messages.getMessageRecordOrNull(messageId) } returns messageRecord
stubSuccessfulSend(sentSealedSender = false)
createAndRunJob()
// Self-send signals success via the certificate fallback, not the network-reported flag.
verify { messages.markAsSent(messageId, true) }
}
// endregion
// region — SendError variants
@Test
fun `Given IdentityMismatch, when run, then record mismatched identity and mark failed, return success`() {
val untrusted = mockk<UntrustedIdentityException>(relaxed = true)
val external = mockRecipient(recipientAci, isSelf = false)
every { Recipient.external(recipientAci.toString()) } returns external
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.IdentityMismatch(
serviceId = recipientAci,
exception = untrusted
).left()
val result = createAndRunJob()
assertThat(result.isSuccess).isTrue()
verify { messages.addMismatchedIdentity(messageId, recipientId, any()) }
verify { messages.markAsSentFailed(messageId) }
}
@Test
fun `Given NotRegistered, when run, then mark failed and enqueue DirectoryRefreshJob, return success`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.NotRegistered().left()
val result = createAndRunJob()
assertThat(result.isSuccess).isTrue()
verify { messages.markAsSentFailed(messageId) }
verify { AppDependencies.jobManager.add(match { it is DirectoryRefreshJob }) }
}
@Test
fun `Given Unauthorized, when run, then return failure`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.Unauthorized().left()
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given ChallengeRequired and handler says retry now, when run, then retry with zero backoff`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.ChallengeRequired(
token = "tok",
options = setOf(ChallengeOption.CAPTCHA),
retryAfter = 1.seconds
).left()
every {
ProofRequiredExceptionHandler.handle(any(), any(), any(), any(), any())
} returns ProofRequiredExceptionHandler.Result.RETRY_NOW
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given ChallengeRequired and handler says retry later, when run, then retry with backoff`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.ChallengeRequired(
token = "tok",
options = emptySet(),
retryAfter = null
).left()
every {
ProofRequiredExceptionHandler.handle(any(), any(), any(), any(), any())
} returns ProofRequiredExceptionHandler.Result.RETRY_LATER
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given ServerRejected, when run, then return failure`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.ServerRejected().left()
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given ContentTooLarge, when run, then return failure`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.ContentTooLarge(size = 9_999L, maxAllowed = 256L).left()
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given SessionAttemptsExhausted, when run, then return retry`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.SessionAttemptsExhausted().left()
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given PreKeyUnavailable, when run, then return retry`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.PreKeyUnavailable("no signed prekey").left()
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given RateLimited, when run, then return retry`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.RateLimited(retryAfter = 600.seconds).left()
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given NetworkError, when run, then return retry`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.NetworkError(IOException("boom")).left()
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
}
@Test
fun `Given ApplicationError with a RuntimeException, when run, then return fatal failure`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.ApplicationError(IllegalStateException("nope")).left()
val result = createAndRunJob()
assertThat(result.isFailure).isTrue()
}
@Test
fun `Given ApplicationError with a non-RuntimeException, when run, then return retry`() {
coEvery {
messageService.sendMessage(any(), any(), any(), any(), any(), any(), any(), any())
} returns MessageService.SendError.ApplicationError(Exception("eventually retryable")).left()
val result = createAndRunJob()
assertThat(result.isRetry).isTrue()
}
// endregion
// region — Helpers
private fun createAndRunJob(): Job.Result {
val job = IndividualSendJobV2(messageId, recipient, hasMedia = false, isScheduledSend = false)
job.setContext(ApplicationProvider.getApplicationContext())
return job.run()
}
private fun stubSuccessfulSend(sentSealedSender: Boolean = false) {
val envelopeContent = EnvelopeContent.encrypted(Content(dataMessage = dataMessage), ContentHint.RESENDABLE, Optional.empty())
coEvery {
messageService.sendMessage(
serviceId = any(),
envelopeContent = any(),
timestamp = any(),
sealedSenderAccess = any(),
story = any(),
isOnline = any(),
urgent = any(),
onEncrypted = any()
)
} returns MessageService.SendSuccess(envelopeContent = envelopeContent, sentSealedSender = sentSealedSender, devices = listOf(1)).right()
coEvery {
messageService.sendSyncMessage(
timestamp = any(),
envelopeContent = any(),
urgent = any(),
onEncrypted = any()
)
} returns MessageService.SendSuccess(envelopeContent = envelopeContent, sentSealedSender = sentSealedSender, devices = listOf(1)).right()
}
/**
* Builds a [Recipient] mock with the minimum stubs the job touches. [fresh] returns the mock itself
* so the job's `recipient.fresh()` calls don't hit the live cache.
*/
private fun mockRecipient(aci: ACI, isSelf: Boolean): Recipient {
val rid = if (isSelf) selfRecipientId else recipientId
val mock = mockk<Recipient>(relaxed = true)
every { mock.id } returns rid
every { mock.hasServiceId } returns true
every { mock.isUnregistered } returns false
every { mock.isRegistered } returns true
every { mock.isGroup } returns false
every { mock.isSelf } returns isSelf
every { mock.serviceId } returns Optional.of(aci)
every { mock.requireServiceId() } returns aci
every { mock.e164 } returns Optional.empty()
every { mock.needsPniSignature } returns false
every { mock.profileKey } returns null
every { mock.sealedSenderAccessMode } returns SealedSenderAccessMode.DISABLED
every { mock.fresh() } returns mock
return mock
}
// endregion
}