mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Clean up minor warnings and formatting in tests (#1049)
This commit is contained in:
parent
3211e902d4
commit
2fd3eac6ee
27 changed files with 245 additions and 257 deletions
|
@ -30,7 +30,7 @@ import retrofit2.create
|
|||
class IdentityServiceTest : BaseServiceTest() {
|
||||
|
||||
private val identityApi: IdentityApi = retrofit.create()
|
||||
private val deviceModelProvider = mockk<DeviceModelProvider>() {
|
||||
private val deviceModelProvider = mockk<DeviceModelProvider> {
|
||||
every { deviceModel } returns "Test Device"
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class AuthDiskSourceExtensionsTest {
|
|||
"userId2" to mockk<AccountJson>(),
|
||||
"userId3" to mockk<AccountJson>(),
|
||||
)
|
||||
val userStateJson = mockk<UserStateJson>() {
|
||||
val userStateJson = mockk<UserStateJson> {
|
||||
every { accounts } returns mockAccounts
|
||||
}
|
||||
authDiskSource.apply {
|
||||
|
@ -88,7 +88,7 @@ class AuthDiskSourceExtensionsTest {
|
|||
"userId2" to mockk<AccountJson>(),
|
||||
"userId3" to mockk<AccountJson>(),
|
||||
)
|
||||
val userStateJson = mockk<UserStateJson>() {
|
||||
val userStateJson = mockk<UserStateJson> {
|
||||
every { accounts } returns mockAccounts
|
||||
}
|
||||
authDiskSource.apply {
|
||||
|
|
|
@ -30,6 +30,7 @@ class AutofillActivityManagerTest {
|
|||
}
|
||||
|
||||
// We will construct an instance here just to hook the various dependencies together internally
|
||||
@Suppress("unused")
|
||||
private val autofillActivityManager: AutofillActivityManager = AutofillActivityManagerImpl(
|
||||
autofillManager = autofillManager,
|
||||
appForegroundManager = appForegroundManager,
|
||||
|
@ -39,7 +40,6 @@ class AutofillActivityManagerTest {
|
|||
|
||||
private var isAutofillEnabledAndSupported = false
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `changes in app foreground status should update the AutofillEnabledManager as necessary`() =
|
||||
runTest {
|
||||
|
|
|
@ -47,7 +47,7 @@ class FilledDataExtensionsTest {
|
|||
every { this@mockk.resources } returns res
|
||||
}
|
||||
private val dataset: Dataset = mockk()
|
||||
private val filledItem: FilledItem = mockk() {
|
||||
private val filledItem: FilledItem = mockk {
|
||||
every { autofillId } returns mockk()
|
||||
}
|
||||
private val filledItemPlaceholder = FilledItem(
|
||||
|
@ -83,7 +83,7 @@ class FilledDataExtensionsTest {
|
|||
isVaultLocked = false,
|
||||
)
|
||||
private val mockIntentSender: IntentSender = mockk()
|
||||
private val pendingIntent: PendingIntent = mockk() {
|
||||
private val pendingIntent: PendingIntent = mockk {
|
||||
every { intentSender } returns mockIntentSender
|
||||
}
|
||||
private val presentations: Presentations = mockk()
|
||||
|
|
|
@ -47,4 +47,4 @@ private enum class TestEnum {
|
|||
}
|
||||
|
||||
private class TestEnumSerializer :
|
||||
BaseEnumeratedIntSerializer<TestEnum>(values = TestEnum.values())
|
||||
BaseEnumeratedIntSerializer<TestEnum>(values = TestEnum.entries.toTypedArray())
|
||||
|
|
|
@ -90,7 +90,7 @@ class EnvironmentRepositoryTest {
|
|||
@Test
|
||||
fun `environment should pull from and update EnvironmentDiskSource`() {
|
||||
val environmentUrlDataJson = mockk<EnvironmentUrlDataJson>()
|
||||
val environment = mockk<Environment>() {
|
||||
val environment = mockk<Environment> {
|
||||
every { environmentUrlData } returns environmentUrlDataJson
|
||||
}
|
||||
every { environmentUrlDataJson.toEnvironmentUrls() } returns environment
|
||||
|
@ -125,7 +125,7 @@ class EnvironmentRepositoryTest {
|
|||
@Test
|
||||
fun `environmentStateFow should react to changes in environment`() = runTest {
|
||||
val environmentUrlDataJson = mockk<EnvironmentUrlDataJson>()
|
||||
val environment = mockk<Environment>() {
|
||||
val environment = mockk<Environment> {
|
||||
every { environmentUrlData } returns environmentUrlDataJson
|
||||
}
|
||||
every { environmentUrlDataJson.toEnvironmentUrls() } returns environment
|
||||
|
|
|
@ -105,7 +105,6 @@ class VaultSdkSourceTest {
|
|||
verify { sdkClientManager.getOrCreateClient(userId = userId) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `derivePinProtectedUserKey should call SDK and return a Result with the correct data`() =
|
||||
runBlocking {
|
||||
|
@ -179,7 +178,7 @@ class VaultSdkSourceTest {
|
|||
clientCrypto.initializeUserCrypto(
|
||||
req = mockInitCryptoRequest,
|
||||
)
|
||||
} returns Unit
|
||||
} just runs
|
||||
val result = vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
request = mockInitCryptoRequest,
|
||||
|
@ -258,7 +257,7 @@ class VaultSdkSourceTest {
|
|||
clientCrypto.initializeOrgCrypto(
|
||||
req = mockInitCryptoRequest,
|
||||
)
|
||||
} returns Unit
|
||||
} just runs
|
||||
val result = vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = mockInitCryptoRequest,
|
||||
|
@ -759,33 +758,32 @@ class VaultSdkSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `generateTotp should call SDK and return a Result with correct data`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val totpResponse = TotpResponse("TestCode", 30u)
|
||||
coEvery { clientVault.generateTotp(any(), any()) } returns totpResponse
|
||||
fun `generateTotp should call SDK and return a Result with correct data`() = runTest {
|
||||
val userId = "userId"
|
||||
val totpResponse = TotpResponse("TestCode", 30u)
|
||||
coEvery { clientVault.generateTotp(any(), any()) } returns totpResponse
|
||||
|
||||
val time = DateTime.now()
|
||||
val result = vaultSdkSource.generateTotp(
|
||||
userId = userId,
|
||||
totp = "Totp",
|
||||
val time = DateTime.now()
|
||||
val result = vaultSdkSource.generateTotp(
|
||||
userId = userId,
|
||||
totp = "Totp",
|
||||
time = time,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Result.success(totpResponse),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
clientVault.generateTotp(
|
||||
key = "Totp",
|
||||
time = time,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
Result.success(totpResponse),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
clientVault.generateTotp(
|
||||
key = "Totp",
|
||||
time = time,
|
||||
)
|
||||
}
|
||||
|
||||
verify { sdkClientManager.getOrCreateClient(userId = userId) }
|
||||
}
|
||||
|
||||
verify { sdkClientManager.getOrCreateClient(userId = userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePassword should call SDK and a Result with correct data`() = runTest {
|
||||
val userId = "userId"
|
||||
|
|
|
@ -54,7 +54,7 @@ fun createMockSdkCipher(number: Int): Cipher =
|
|||
)
|
||||
|
||||
/**
|
||||
* Create a mock [SecureNote] with a given [number].
|
||||
* Create a mock [SecureNote].
|
||||
*/
|
||||
fun createMockSdkSecureNote(): SecureNote =
|
||||
SecureNote(
|
||||
|
|
|
@ -64,8 +64,7 @@ class VaultLockManagerTest {
|
|||
}
|
||||
private val mutableVaultTimeoutStateFlow =
|
||||
MutableStateFlow<VaultTimeout>(VaultTimeout.ThirtyMinutes)
|
||||
private val mutableVaultTimeoutActionStateFlow =
|
||||
MutableStateFlow<VaultTimeoutAction>(VaultTimeoutAction.LOCK)
|
||||
private val mutableVaultTimeoutActionStateFlow = MutableStateFlow(VaultTimeoutAction.LOCK)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { getVaultTimeoutStateFlow(any()) } returns mutableVaultTimeoutStateFlow
|
||||
every { getVaultTimeoutActionStateFlow(any()) } returns mutableVaultTimeoutActionStateFlow
|
||||
|
|
|
@ -2241,7 +2241,7 @@ class VaultRepositoryTest {
|
|||
userId = userId,
|
||||
cipher = createMockCipher(number = 1),
|
||||
)
|
||||
} returns Unit
|
||||
} just runs
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
mockkStatic(Instant::class)
|
||||
every { Instant.now() } returns fixedInstant
|
||||
|
@ -2328,7 +2328,7 @@ class VaultRepositoryTest {
|
|||
userId = userId,
|
||||
cipher = createMockCipher(number = 1),
|
||||
)
|
||||
} returns Unit
|
||||
} just runs
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
mockkStatic(Instant::class)
|
||||
every { Instant.now() } returns fixedInstant
|
||||
|
@ -2405,7 +2405,7 @@ class VaultRepositoryTest {
|
|||
userId = userId,
|
||||
cipher = createMockCipher(number = 1),
|
||||
)
|
||||
} returns Unit
|
||||
} just runs
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
mockkStatic(Instant::class)
|
||||
every { Instant.now() } returns fixedInstant
|
||||
|
|
|
@ -365,7 +365,6 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `ContinueWithBreachedPasswordClick should call repository with checkDataBreaches false`() {
|
||||
val repo = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
|
|
|
@ -180,27 +180,24 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `LogInClick with invalid organization should show error dialog`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(EnterpriseSignOnAction.LogInClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
message = R.string.validation_field_required.asText(
|
||||
R.string.org_identifier.asText(),
|
||||
),
|
||||
fun `LogInClick with invalid organization should show error dialog`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(EnterpriseSignOnAction.LogInClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
message = R.string.validation_field_required.asText(
|
||||
R.string.org_identifier.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `LogInClick with no Internet should show error dialog`() = runTest {
|
||||
val viewModel = createViewModel(isNetworkConnected = false)
|
||||
|
@ -591,120 +588,124 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OrganizationDomainSsoDetails failure should make a request, hide the dialog, and update the org input based on the remembered org`() = runTest {
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns OrganizationDomainSsoDetailsResult.Failure
|
||||
fun `OrganizationDomainSsoDetails failure should make a request, hide the dialog, and update the org input based on the remembered org`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns OrganizationDomainSsoDetailsResult.Failure
|
||||
|
||||
coEvery {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
authRepository.rememberedOrgIdentifier
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
authRepository.rememberedOrgIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OrganizationDomainSsoDetails success with no SSO available should make a request, hide the dialog, and update the org input based on the remembered org`() = runTest {
|
||||
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
|
||||
isSsoAvailable = false,
|
||||
organizationIdentifier = "Bitwarden without SSO",
|
||||
)
|
||||
fun `OrganizationDomainSsoDetails success with no SSO available should make a request, hide the dialog, and update the org input based on the remembered org`() =
|
||||
runTest {
|
||||
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
|
||||
isSsoAvailable = false,
|
||||
organizationIdentifier = "Bitwarden without SSO",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns orgDetails
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns orgDetails
|
||||
|
||||
coEvery {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden"),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
authRepository.rememberedOrgIdentifier
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
authRepository.rememberedOrgIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OrganizationDomainSsoDetails success with blank identifier should make a request, show the error dialog, and update the org input based on the remembered org`() = runTest {
|
||||
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
|
||||
isSsoAvailable = true,
|
||||
organizationIdentifier = "",
|
||||
)
|
||||
fun `OrganizationDomainSsoDetails success with blank identifier should make a request, show the error dialog, and update the org input based on the remembered org`() =
|
||||
runTest {
|
||||
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
|
||||
isSsoAvailable = true,
|
||||
organizationIdentifier = "",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns orgDetails
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns orgDetails
|
||||
|
||||
coEvery {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
message = R.string.organization_sso_identifier_required.asText(),
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
message = R.string.organization_sso_identifier_required.asText(),
|
||||
),
|
||||
orgIdentifierInput = "Bitwarden",
|
||||
),
|
||||
orgIdentifierInput = "Bitwarden",
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
authRepository.rememberedOrgIdentifier
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
authRepository.rememberedOrgIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OrganizationDomainSsoDetails success with valid organization should make a request then attempt to login`() = runTest {
|
||||
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
|
||||
isSsoAvailable = true,
|
||||
organizationIdentifier = "Bitwarden with SSO",
|
||||
)
|
||||
fun `OrganizationDomainSsoDetails success with valid organization should make a request then attempt to login`() =
|
||||
runTest {
|
||||
val orgDetails = OrganizationDomainSsoDetailsResult.Success(
|
||||
isSsoAvailable = true,
|
||||
organizationIdentifier = "Bitwarden with SSO",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns orgDetails
|
||||
coEvery {
|
||||
authRepository.getOrganizationDomainSsoDetails(any())
|
||||
} returns orgDetails
|
||||
|
||||
// Just hang on this request; login is tested elsewhere
|
||||
coEvery {
|
||||
authRepository.prevalidateSso(any())
|
||||
} just awaits
|
||||
// Just hang on this request; login is tested elsewhere
|
||||
coEvery {
|
||||
authRepository.prevalidateSso(any())
|
||||
} just awaits
|
||||
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
orgIdentifierInput = "Bitwarden with SSO",
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
message = R.string.logging_in.asText(),
|
||||
val viewModel = createViewModel(dismissInitialDialog = false)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
orgIdentifierInput = "Bitwarden with SSO",
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
message = R.string.logging_in.asText(),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.getOrganizationDomainSsoDetails(DEFAULT_EMAIL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createViewModel(
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.landing
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
|
@ -17,7 +16,6 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
|
@ -55,8 +53,6 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
private val resources
|
||||
get() = ApplicationProvider.getApplicationContext<Application>().resources
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
|
|
|
@ -183,7 +183,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on AddAccountClick should send NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
|
|
@ -20,7 +20,6 @@ class MasterPasswordHintViewModelTest : BaseViewModelTest() {
|
|||
private val authRepository: AuthRepository = mockk()
|
||||
private val networkConnectionManager: NetworkConnectionManager = mockk()
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -29,7 +28,6 @@ class MasterPasswordHintViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SubmitClick with valid email should show success dialog`() = runTest {
|
||||
val validEmail = "test@example.com"
|
||||
|
|
|
@ -31,7 +31,7 @@ import org.junit.Test
|
|||
|
||||
class TwoFactorLoginScreenTest : BaseComposeTest() {
|
||||
private val intentManager = mockk<IntentManager>(relaxed = true) {
|
||||
every { launchUri(any()) } returns Unit
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
private val nfcManager: NfcManager = mockk {
|
||||
every { start() } just runs
|
||||
|
|
|
@ -175,32 +175,31 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `CodeInputChanged should update input and disable button if code is blank`() =
|
||||
runTest {
|
||||
val input = "123456"
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
// Set it to true.
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.CodeInputChanged(input))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
codeInput = input,
|
||||
isContinueButtonEnabled = true,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
fun `CodeInputChanged should update input and disable button if code is blank`() = runTest {
|
||||
val input = "123456"
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
// Set it to true.
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.CodeInputChanged(input))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
codeInput = input,
|
||||
isContinueButtonEnabled = true,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
// Set it to false.
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.CodeInputChanged(""))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
codeInput = "",
|
||||
isContinueButtonEnabled = false,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
// Set it to false.
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.CodeInputChanged(""))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
codeInput = "",
|
||||
isContinueButtonEnabled = false,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick login returns success should update loadingDialogState`() = runTest {
|
||||
|
@ -251,64 +250,66 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ContinueButtonClick login should emit NavigateToDuo when auth method is Duo and authUrl is non-null`() = runTest {
|
||||
val authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.DUO to JsonObject(
|
||||
mapOf("AuthUrl" to JsonPrimitive("bitwarden.com")),
|
||||
),
|
||||
)
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = authMethodsData,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
every { authRepository.twoFactorResponse } returns response
|
||||
val mockkUri = mockk<Uri>()
|
||||
val viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.DUO,
|
||||
),
|
||||
)
|
||||
every { Uri.parse("bitwarden.com") } returns mockkUri
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
TwoFactorLoginEvent.NavigateToDuo(mockkUri),
|
||||
awaitItem(),
|
||||
fun `ContinueButtonClick login should emit NavigateToDuo when auth method is Duo and authUrl is non-null`() =
|
||||
runTest {
|
||||
val authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.DUO to JsonObject(
|
||||
mapOf("AuthUrl" to JsonPrimitive("bitwarden.com")),
|
||||
),
|
||||
)
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = authMethodsData,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
every { authRepository.twoFactorResponse } returns response
|
||||
val mockkUri = mockk<Uri>()
|
||||
val viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.DUO,
|
||||
),
|
||||
)
|
||||
every { Uri.parse("bitwarden.com") } returns mockkUri
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
TwoFactorLoginEvent.NavigateToDuo(mockkUri),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify {
|
||||
Uri.parse("bitwarden.com")
|
||||
}
|
||||
}
|
||||
verify {
|
||||
Uri.parse("bitwarden.com")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ContinueButtonClick login should emit ShowToast when auth method is Duo and authUrl is null`() = runTest {
|
||||
val authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.DUO to JsonObject(
|
||||
mapOf("Nothing" to JsonPrimitive("Nothing")),
|
||||
),
|
||||
)
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = authMethodsData,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
every { authRepository.twoFactorResponse } returns response
|
||||
val viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.DUO,
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
TwoFactorLoginEvent.ShowToast(R.string.generic_error_message.asText()),
|
||||
awaitItem(),
|
||||
fun `ContinueButtonClick login should emit ShowToast when auth method is Duo and authUrl is null`() =
|
||||
runTest {
|
||||
val authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.DUO to JsonObject(
|
||||
mapOf("Nothing" to JsonPrimitive("Nothing")),
|
||||
),
|
||||
)
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = authMethodsData,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
every { authRepository.twoFactorResponse } returns response
|
||||
val viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.DUO,
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
TwoFactorLoginEvent.ShowToast(R.string.generic_error_message.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
|
||||
|
|
|
@ -95,7 +95,6 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserState updates with a non-null unlocked account should not update the state`() {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -180,7 +179,6 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on AddAccountClick should set hasPendingAccountAddition to true on the AuthRepository`() {
|
||||
val viewModel = createViewModel()
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.base
|
|||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
|
||||
abstract class BaseViewModelTest {
|
||||
@Suppress("unused")
|
||||
@RegisterExtension
|
||||
protected open val mainDispatcherExtension = MainDispatcherExtension()
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ import org.junit.jupiter.api.extension.AfterAllCallback
|
|||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeAllCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
|
||||
/**
|
||||
* JUnit 5 Extension for automatically setting a [testDispatcher] as the "main" dispatcher.
|
||||
|
@ -20,7 +22,7 @@ import org.junit.jupiter.api.extension.ExtensionContext
|
|||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainDispatcherExtension(
|
||||
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
|
||||
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
|
||||
) : AfterAllCallback,
|
||||
AfterEachCallback,
|
||||
BeforeAllCallback,
|
||||
|
|
|
@ -4,7 +4,9 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
@ -17,7 +19,7 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||
fun `on about row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) } returns Unit
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) } just runs
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
|
@ -40,7 +42,7 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||
every { eventFlow } returns emptyFlow()
|
||||
every {
|
||||
trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
|
||||
} returns Unit
|
||||
} just runs
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
|
@ -61,7 +63,7 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||
fun `on appearance row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) } returns Unit
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) } just runs
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
|
@ -82,7 +84,7 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||
fun `on auto-fill row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) } returns Unit
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) } just runs
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
|
@ -103,7 +105,7 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||
fun `on other row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) } returns Unit
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) } just runs
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
|
@ -124,7 +126,7 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||
fun `on vault row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) } returns Unit
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) } just runs
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
|
|
|
@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test
|
|||
class AutoFillViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableIsAutofillEnabledStateFlow = MutableStateFlow(false)
|
||||
private val settingsRepository: SettingsRepository = mockk() {
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { isInlineAutofillEnabled } returns true
|
||||
every { isInlineAutofillEnabled = any() } just runs
|
||||
every { isAutoCopyTotpDisabled } returns true
|
||||
|
|
|
@ -12,7 +12,9 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
|||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertTrue
|
||||
|
@ -31,7 +33,7 @@ class VaultSettingsScreenTest : BaseComposeTest() {
|
|||
),
|
||||
)
|
||||
private val intentManager: IntentManager = mockk(relaxed = true) {
|
||||
every { launchUri(any()) } returns Unit
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
|
||||
val viewModel = mockk<VaultSettingsViewModel>(relaxed = true) {
|
||||
|
@ -54,7 +56,7 @@ class VaultSettingsScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `on back click should send BackClick`() {
|
||||
every { viewModel.trySendAction(VaultSettingsAction.BackClick) } returns Unit
|
||||
every { viewModel.trySendAction(VaultSettingsAction.BackClick) } just runs
|
||||
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||
verify { viewModel.trySendAction(VaultSettingsAction.BackClick) }
|
||||
}
|
||||
|
|
|
@ -158,7 +158,7 @@ class PasswordHistoryScreenTest : BaseComposeTest() {
|
|||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = PasswordHistoryState.ViewState.Content(
|
||||
viewState = PasswordHistoryState.ViewState.Content(
|
||||
passwords = passwords,
|
||||
),
|
||||
)
|
||||
|
@ -166,8 +166,4 @@ class PasswordHistoryScreenTest : BaseComposeTest() {
|
|||
|
||||
composeTestRule.onNodeWithText("Password1").assertIsDisplayed()
|
||||
}
|
||||
|
||||
private fun updateState(state: PasswordHistoryState) {
|
||||
mutableStateFlow.value = state
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1939,7 +1939,6 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
//region Helper functions
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateLoginType(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.ItemType.Login.() ->
|
||||
|
@ -1963,7 +1962,6 @@ private fun updateLoginType(
|
|||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateIdentityType(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.ItemType.Identity.() ->
|
||||
|
@ -1987,7 +1985,6 @@ private fun updateIdentityType(
|
|||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateCardType(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.ItemType.Card.() ->
|
||||
|
@ -2011,7 +2008,6 @@ private fun updateCardType(
|
|||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateCommonContent(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.Common.()
|
||||
|
|
|
@ -90,7 +90,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
|
||||
private val vaultRepository: VaultRepository =
|
||||
mockk {
|
||||
every { vaultFilterType = any() } returns Unit
|
||||
every { vaultFilterType = any() } just runs
|
||||
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
|
||||
every { sync() } just runs
|
||||
every { syncIfNecessary() } just runs
|
||||
|
|
|
@ -15,7 +15,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `initials should return the starting letters of the first two words for a multi-word name`() {
|
||||
assertEquals(
|
||||
"FS",
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { name } returns "First Second Third"
|
||||
}
|
||||
.initials,
|
||||
|
@ -26,7 +26,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `initials should return the first two letters of the name for a single word name`() {
|
||||
assertEquals(
|
||||
"FI",
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { name } returns "First"
|
||||
}
|
||||
.initials,
|
||||
|
@ -38,7 +38,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `initials should return the first two letters of the user's email if the name is not present`() {
|
||||
assertEquals(
|
||||
"TE",
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { name } returns null
|
||||
every { email } returns "test@bitwarden.com"
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `iconRes returns a checkmark for active accounts`() {
|
||||
assertEquals(
|
||||
R.drawable.ic_check_mark,
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.ACTIVE
|
||||
}
|
||||
.iconRes,
|
||||
|
@ -61,7 +61,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `iconRes returns a locked lock for locked accounts`() {
|
||||
assertEquals(
|
||||
R.drawable.ic_locked,
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.LOCKED
|
||||
}
|
||||
.iconRes,
|
||||
|
@ -72,7 +72,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `iconRes returns a locked lock for logged out accounts`() {
|
||||
assertEquals(
|
||||
R.drawable.ic_locked,
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.LOGGED_OUT
|
||||
}
|
||||
.iconRes,
|
||||
|
@ -83,7 +83,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `iconRes returns an unlocked lock for unlocked accounts`() {
|
||||
assertEquals(
|
||||
R.drawable.ic_unlocked,
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.UNLOCKED
|
||||
}
|
||||
.iconRes,
|
||||
|
@ -93,7 +93,7 @@ class AccountSummaryExtensionsTest {
|
|||
@Test
|
||||
fun `supportingTextResOrNull returns a null for active accounts`() {
|
||||
assertNull(
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.ACTIVE
|
||||
}
|
||||
.supportingTextResOrNull,
|
||||
|
@ -104,7 +104,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `supportingTextResOrNull returns Locked locked accounts`() {
|
||||
assertEquals(
|
||||
R.string.account_locked,
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.LOCKED
|
||||
}
|
||||
.supportingTextResOrNull,
|
||||
|
@ -115,7 +115,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `supportingTextResOrNull returns Logged Out for logged out accounts`() {
|
||||
assertEquals(
|
||||
R.string.account_logged_out,
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.LOGGED_OUT
|
||||
}
|
||||
.supportingTextResOrNull,
|
||||
|
@ -126,7 +126,7 @@ class AccountSummaryExtensionsTest {
|
|||
fun `supportingTextResOrNull returns Unlocked for unlocked accounts`() {
|
||||
assertEquals(
|
||||
R.string.account_unlocked,
|
||||
mockk<AccountSummary>() {
|
||||
mockk<AccountSummary> {
|
||||
every { status } returns AccountSummary.Status.UNLOCKED
|
||||
}
|
||||
.supportingTextResOrNull,
|
||||
|
|
Loading…
Add table
Reference in a new issue