I am developing a app in flutter and I am having a problem with detecting a locks via tapkey mobile sdk. I am login users with Token Exchange method, I have created new Identity Providers. I am creating a new user via a cloud function (Owners/{ownerAccountId}/IdentityProviders/{ipId}/Users however i'm not adding the contact) using the Client Credentials that i have also added to my.tapkey.com as new user and assigned a lock to it.
I can successfully run logInAsync with the token i am receiving via Token Exchange however when i try to find a nearby locks i got {} as a response (i will mention that the lock is next to me).
my code:
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import com.tapkey.mobile.TapkeyAppContext
import com.tapkey.mobile.TapkeyEnvironmentConfigBuilder
import com.tapkey.mobile.TapkeyServiceFactory
import com.tapkey.mobile.TapkeyServiceFactoryBuilder
import com.tapkey.mobile.ble.BleLockScanner
import com.tapkey.mobile.concurrent.CancellationToken
import com.tapkey.mobile.concurrent.CancellationTokenSource
import com.tapkey.mobile.manager.UserManager
import io.flutter.app.FlutterApplication
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import net.tpky.mc.time.ServerClock
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
class TapKeyTest : FlutterApplication(), TapkeyAppContext {
private lateinit var tapkeyServiceFactory: TapkeyServiceFactory
lateinit var flutterEngine: FlutterEngine
companion object {
const val FLUTTER_ENGINE_NAME = "nps_flutter_engine_name"
}
override fun onCreate() {
super.onCreate()
flutterEngine = FlutterEngine(this)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache
.getInstance()
.put(FLUTTER_ENGINE_NAME, flutterEngine)
val serverClock = ServerClock()
val config = TapkeyEnvironmentConfigBuilder()
config.setBaseUri("https://my.tapkey.com/")
val b = TapkeyServiceFactoryBuilder(this )
.setServerClock(serverClock)
.setConfig(config.build())
val sf = b.build()
tapkeyServiceFactory = sf
}
override fun getTapkeyServiceFactory(): TapkeyServiceFactory {
return tapkeyServiceFactory
}
fun login(SECRET_TOKEN: String) {
val src = CancellationTokenSource()
val ct: CancellationToken = src.token
val userManager: UserManager = tapkeyServiceFactory.userManager
userManager.logInAsync(SECRET_TOKEN, ct)
.continueOnUi { userId -> scanLocks()}
.catchOnUi { asyncError -> println(asyncError.cause) } }
private fun scanLocks( ) {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_SCAN
) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
return
} else {
tapkeyServiceFactory.bleLockScanner.locksChangedObservable
.addObserver { locks -> println(locks)}
println( tapkeyServiceFactory.bleLockScanner.locks)
println("Permission granted")
}
}
}
Is there a step that i have missed? Also i can't find anywhere new users that i am creating.
It seems, that you don't start the scanning:
BleLockScanner scanner = tapkeyServiceFactory.getBleLockScanner();
bleScanObserverRegistration = scanner.startForegroundScan();
The app is written in Kotlin and Compose. I am testing the app using Espresso(JUnit4). I have tested the activities but I'm facing issues regarding testing ViewModels. I have attached the code written for the activities and ViewModel. Please help me in testing the project.
The code for FeedScreenViewModel is given below:-
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.amplitude.android.Amplitude
import com.kotlang.auth.login.UserProfileProto
import com.navachar.neptune.data.respository.FeedRepository
import com.navachar.neptune.data.db.entities.Post
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class FeedScreenViewModel #Inject constructor(
// TODO: Make this private
val amplitude: Amplitude,
private val feedRepository: FeedRepository,
): ViewModel()
{
companion object {
private const val TAG = "FeedScreenViewModel"
}
val profile = feedRepository.getProfile().stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = UserProfileProto.getDefaultInstance()
)
val feedPagingFlow = feedRepository.feedPagingFlow()
.cachedIn(viewModelScope)
/**
* Adds or removes like from the given post
*
* #param post the post to update like for
* #param isLiked the updated like state
*/
fun togglePostLike(post: Post, isLiked: Boolean) {
viewModelScope.launch {
feedRepository.setPostLike(post, isLiked)
}
}
}
The code for FeedScreen is:-
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.amplitude.android.Amplitude
import com.kotlang.auth.login.UserProfileProto
import com.navachar.neptune.R
import
com.navachar.neptune.presentation.screens.main.feed.actionscreens.post.PostActivity
import com.navachar.neptune.presentation.screens.main.feed.components.ErrorMessage
import com.navachar.neptune.presentation.screens.main.feed.components.PagingFooter
import com.navachar.neptune.presentation.screens.main.feed.components.Post
import com.navachar.neptune.presentation.screens.main.profile.ProfileActivity
import com.navachar.neptune.data.db.entities.Post
import com.navachar.neptune.data.db.entities.User
import com.navachar.neptune.data.db.entities.PostFull
const val TAG8="feed"
#Composable
fun FeedScreen(
modifier: Modifier = Modifier,
viewModel: FeedScreenViewModel = hiltViewModel()
) {
val context = LocalContext.current
val profile by viewModel.profile.collectAsState()
val pagingItems = viewModel.feedPagingFlow.collectAsLazyPagingItems()
FeedScreen(
modifier = modifier,
pagingItems = pagingItems,
openCreatePostScreen = {
context.startActivity(Intent(context, PostActivity::class.java))
},
toggleLike = viewModel::togglePostLike,
openPostDetails = { post ->
Intent(
context,
CommentActivity::class.java,
).apply {
putExtra("activity", "MainActivity")
putExtra("postId", post.id)
context.startActivity(this)
}
},
sharePost = {
shareContent(
it,
profile,
viewModel.amplitude,
context,
)
},
openProfile = { user ->
context.startActivity(
Intent(
context,
ProfileActivity::class.java
).apply {
putExtra("UserId", user.id)
}
)
},
onRequestOpenUrl = { url ->
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
)
}
#Composable
private fun FeedScreen(
modifier: Modifier,
pagingItems: LazyPagingItems<PostFull>,
openCreatePostScreen: () -> Unit,
toggleLike: (post: Post, isLiked: Boolean) -> Unit,
openPostDetails: (post: Post) -> Unit,
sharePost: (post: PostFull) -> Unit,
openProfile: (user: User) -> Unit,
onRequestOpenUrl: (url: String) -> Unit,
) {
Scaffold(
modifier = modifier,
content = {
Feed(
modifier = Modifier
.fillMaxSize()
.padding(it),
pagingItems = pagingItems,
toggleLike = toggleLike,
openPostDetails = openPostDetails,
sharePost = sharePost,
openProfile = openProfile,
onRequestOpenUrl = onRequestOpenUrl,
)
},
bottomBar = {
BottomBar(onClick = openCreatePostScreen)
}
)
}
#Composable
private fun Feed(
modifier: Modifier,
pagingItems: LazyPagingItems<PostFull>,
toggleLike: (post: Post, isLiked: Boolean) -> Unit,
openPostDetails: (post: Post) -> Unit,
sharePost: (post: PostFull) -> Unit,
openProfile: (user: User) -> Unit,
onRequestOpenUrl: (url: String) -> Unit,
) {
when (val refreshState = pagingItems.loadState.refresh) {
is LoadState.NotLoading -> {
FeedLazyList(
modifier = modifier.testTag(TAG8),
pagingItems = pagingItems,
openPostDetails = openPostDetails,
openProfile = openProfile,
sharePost = sharePost,
toggleLike = toggleLike,
onRequestOpenUrl = onRequestOpenUrl,
)
}
is LoadState.Loading -> {
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is LoadState.Error -> {
ErrorMessage(
modifier = modifier,
error = refreshState.error,
onRetry = pagingItems::retry,
)
}
}
}
#Composable
private fun FeedLazyList(
modifier: Modifier,
pagingItems: LazyPagingItems<PostFull>,
openPostDetails: (post: Post) -> Unit,
openProfile: (user: User) -> Unit,
sharePost: (post: PostFull) -> Unit,
toggleLike: (post: Post, isLiked: Boolean) -> Unit,
onRequestOpenUrl: (url: String) -> Unit,
) {
LazyColumn(
modifier = modifier
) {
items(pagingItems) { post ->
if (post != null) {
Post(
post = post,
openPostDetails = { openPostDetails(post.post) },
openProfile = {
if (post.author != null) {
openProfile(post.author)
}
},
onSharePost = { sharePost(post) },
onToggleLike = {
toggleLike(post.post, it)
},
onRequestOpenUrl = onRequestOpenUrl,
)
} else {
// TODO: Show a shimmer placeholder for post
}
}
item {
PagingFooter(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
pagingItems = pagingItems
)
}
}
}
#Composable
private fun BottomBar(
onClick: () -> Unit
) {
Row(
Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.medium
) {
Image(
modifier = Modifier
.size(24.dp)
.clip(CircleShape),
painter = painterResource(id = R.drawable.ic_account),
contentDescription = "",
contentScale = ContentScale.FillBounds
)
Spacer(modifier = Modifier.width(8.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.post_something),
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Start,
)
}
}
}
private fun shareContent(
post: PostFull,
profile: UserProfileProto,
amplitude: Amplitude,
context: Context
) {
// TODO: Separate the amplitude logic from UI
val eventName = "CTA_Post_Share"
val eventTime = System.currentTimeMillis()
val userId = profile.loginId
val loadTime = ""
val postTag = post.post.tags.joinToString()
val postContentType = if (post.media.isEmpty()) "Text" else "Media"
val postId = post.post.id
amplitude.track(
eventType = eventName,
eventProperties = mapOf(
Pair("Event_Time", eventTime),
Pair("Screen_Load_Time", loadTime),
Pair("User_ID", userId),
Pair("Post_Tag", postTag),
Pair("Post_Content_Type", postContentType),
Pair("Post_ID", postId)
)
)
// TODO: Localise the share content
val shareContent = "${post.author?.name} Posted at Urvar:\n\n" +
"${post.post.body.take(200)}...\n\n" +
"Read More: http://www.urvar.com/post/${post.post.id}"
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(
Intent.EXTRA_TEXT,
shareContent
)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
Please help me test this viewmodel.
The code generated by Test for this View model is:-
class FeedScreenViewModelTest {
#Before
fun setUp() {
}
#After
fun tearDown() {
}
#Test
fun getProfile() {
}
#Test
fun getFeedPagingFlow() {
}
#Test
fun togglePostLike() {
}
#Test
fun getAmplitude() {
}
}
Please tell me what to write inside these functions.
I am new to Kotlin and Java so bear with me but I just wrote a Kotlin test as follows:
package com.squareup.cash.transactiongraph.service.actions
import com.squareup.cash.transactiongraph.TransactionGraphTestingModule
import com.squareup.cash.transactiongraph.client.franklin.FakeFranklinClient
import com.squareup.cash.transactiongraph.dataloader.DataLoaderRegistryFactory
import com.squareup.cash.transactiongraph.graphql.GraphQLContextFactory
import com.squareup.cash.transactiongraph.graphql.TransactionGraphContextFactory
import com.squareup.cash.transactiongraph.service.TransactionGraphGraphqlModule
import com.squareup.graphql.dataloaders.FlowDataLoaderDispatcher
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import misk.testing.MiskTest
import misk.testing.MiskTestModule
import okhttp3.Headers
import org.junit.jupiter.api.Test
import org.assertj.core.api.Assertions.assertThat
#MiskTest(startService = true)
class CashCustomerTransactionsQueryTest {
#MiskTestModule
private val module = TransactionGraphTestingModule()
#Test
fun `returns an array of CashTransactions`() = runBlocking<Unit> {
val query = """
{
cashCustomerTransactions(customerToken: "customerToken") {
id
reasonCode
createdAt
}
}
""".trimIndent()
val result = execute(query)
assertThat(result["errors"]).isNull()
assertThat(result["data"]).isEqualTo(
mapOf(
"cashCustomerTransactions" to arrayOf(
mapOf(
"createdAt" to "2019-03-20T18:26:18Z",
"id" to "TOKEN",
"reasonCode" to "CARD_PRESENT_PURCHASE"
)
)
)
)
}
private suspend fun execute(query: String): Map<String, Any> {
val franklinClient = FakeFranklinClient()
val dataLoaderRegistryFactory = DataLoaderRegistryFactory()
val flowDataLoaderDispatcher = FlowDataLoaderDispatcher(dataLoaderRegistryFactory)
return flowDataLoaderDispatcher.run { registry ->
val contextFactory: GraphQLContextFactory =
TransactionGraphContextFactory(franklinClient)
TransactionGraphGraphqlModule().graphQL().executeAsync {
it
.query(query)
.context(contextFactory.build(Headers.Builder().build(), registry))
}.await().toSpecification()
}
}
}
Upon running the test it fails with the following error: expected: "{"cashCustomerTransactions"=[{"createdAt"="2019-03-20T18:26:18Z", "id"="TOKEN", "reasonCode"="CARD_PRESENT_PURCHASE"}]} (SingletonMap#58303289)" but was: "{"cashCustomerTransactions"=[{"createdAt"="2019-03-20T18:26:18Z", "id"="TOKEN", "reasonCode"="CARD_PRESENT_PURCHASE"}]} (LinkedHashMap#c32f16d)"
The following responses appear to be identical with the exception that one is a SingletonMap and one is a LinkedHashMap. I do not understand why the types are different. What am I doing incorrectly? Can someone please point me in the right direction. Thank you
Change arrayOf to listOf and the problem will be solved.
Accessing the metrics of an Alpakka PlainSource seems fairly straight forward, but how can I do the same thing with a CommittableSource?
I currently have a simple consumer, something like this:
class Consumer(implicit val ma: ActorMaterializer, implicit val ec: ExecutionContext) extends Actor {
private val settings = ConsumerSettings(
context.system,
new ByteArrayDeserializer,
new StringDeserializer)
.withProperties(...)
override def receive: Receive = Actor.emptyBehavior
RestartSource
.withBackoff(minBackoff = 2.seconds, maxBackoff = 20.seconds, randomFactor = 0.2)(consumer)
.runForeach { handleMessage }
private def consumer() = {
AkkaConsumer
.committableSource(settings, Subscriptions.topics(Set(topic)))
.log(getClass.getSimpleName)
.withAttributes(ActorAttributes.supervisionStrategy(_ => Supervision.Resume))
}
private def handleMessage(message: CommittableMessage[Array[Byte], String]): Unit = {
...
}
}
How can I get access to the consumer metrics in this case?
We are using the Java prometheus client and I solved my issue with a custom collector that fetches its metrics directly from JMX:
import java.lang.management.ManagementFactory
import java.util
import io.prometheus.client.Collector
import io.prometheus.client.Collector.MetricFamilySamples
import io.prometheus.client.CounterMetricFamily
import io.prometheus.client.GaugeMetricFamily
import javax.management.ObjectName
import scala.collection.JavaConverters._
import scala.collection.mutable
class ConsumerMetricsCollector(val labels: Map[String, String] = Map.empty) extends Collector {
val metrics: mutable.Map[String, MetricFamilySamples] = mutable.Map.empty
def collect: util.List[MetricFamilySamples] = {
val server = ManagementFactory.getPlatformMBeanServer
for {
attrType <- List("consumer-metrics", "consumer-coordinator-metrics", "consumer-fetch-manager-metrics")
name <- server.queryNames(new ObjectName(s"kafka.consumer:type=$attrType,client-id=*"), null).asScala
attrInfo <- server.getMBeanInfo(name).getAttributes.filter { _.getType == "double" }
} yield {
val attrName = attrInfo.getName
val metricLabels = attrName.split(",").map(_.split("=").toList).collect {
case "client-id" :: (id: String) :: Nil => ("client-id", id)
}.toList ++ labels
val metricName = "kafka_consumer_" + attrName.replaceAll(raw"""[^\p{Alnum}]+""", "_")
val labelKeys = metricLabels.map(_._1).asJava
val metric = metrics.getOrElseUpdate(metricName,
if(metricName.endsWith("_total") || metricName.endsWith("_sum")) {
new CounterMetricFamily(metricName, attrInfo.getDescription, labelKeys)
} else {
new GaugeMetricFamily(metricName, attrInfo.getDescription, labelKeys)
}: MetricFamilySamples
)
val metricValue = server.getAttribute(name, attrName).asInstanceOf[Double]
val labelValues = metricLabels.map(_._2).asJava
metric match {
case f: CounterMetricFamily => f.addMetric(labelValues, metricValue)
case f: GaugeMetricFamily => f.addMetric(labelValues, metricValue)
case _ =>
}
}
metrics.values.toList.asJava
}
}
I'm trying to mock newInstance() and call() in groovy.sql.Sql:
package com.sample
import grails.test.GrailsUnitTestCase
import groovy.mock.interceptor.MockFor
import groovy.sql.Sql
class MySampleTests extends GrailsUnitTestCase {
void testThat_SqlCall_IsInvokedWithexpectedQuery() {
def mockSql = new MockFor(Sql.class)
mockSql.demand.newInstance { def datasource->
return mockSql
}
mockSql.demand.call { def sql ->
return 0
}
mockSql.use {
MySample targetObject = new MySample()
targetObject.myMethod()
}
}
}
Where this is the target code:
package com.sample
import groovy.sql.Sql
class MySample {
def dataSource
def myMethod() {
def conn = Sql.newInstance(dataSource)
conn.call("test")
}
}
It errs out with:
groovy.lang.MissingMethodException: No signature of method: groovy.mock.interceptor.MockFor.call() is applicable for argument types: (java.lang.String) values: [test]
The error makes it seem like the call() method is not being mocked. Is that the case? What's a fix for it?
Replacing mockSql.demand.call with mockSql.metaClass.call provides a mocked method, but requires manual verification that the method is called, and of the parameter values:
package com.sample
import grails.test.GrailsUnitTestCase
import groovy.mock.interceptor.MockFor
import groovy.sql.Sql
class MySampleTests extends GrailsUnitTestCase {
void testThat_SqlCall_IsInvokedWithexpectedQuery() {
def mockSql = new MockFor(Sql.class)
def callInvoked = 0
mockSql.demand.newInstance { def datasource->
return mockSql
}
mockSql.metaClass.call { def sql ->
assert sql == "test"
++callInvoked
return 0
}
mockSql.use {
MySample targetObject = new MySample()
targetObject.myMethod()
}
assert callInvoked == 1
}
}
I don't know enough groovy yet to understand why this is the case, but it solved the issue for me.
Additional cosmetic changes plus moving the targetObject instantiation out of mySql.use {} result in:
package com.sample
import grails.test.GrailsUnitTestCase
import groovy.mock.interceptor.MockFor
import groovy.sql.Sql
class MySampleTests extends GrailsUnitTestCase {
MySample targetObject = new MySample()
void testThat_SqlCall_IsInvokedWithexpectedQuery() {
def mockSql = new MockFor(Sql)
def callInvoked = 0
mockSql.demand.newInstance { datasource->
mockSql
}
mockSql.metaClass.call { sql ->
assert sql == "test"
++callInvoked
0
}
mockSql.use {
targetObject.myMethod()
}
assert callInvoked == 1
}
}