Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sending Sorted Attendees List and Validation for CloseRollCall #1906

Merged
merged 11 commits into from
Jun 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class QuestionResult(ballotOption: String?, count: Int) {
val count: Int

init {
verify().stringNotEmpty(ballotOption, "ballot option").greaterOrEqualThan(count, 0, "count")
verify()
.stringNotEmpty(ballotOption, "ballot option")
.greaterOrEqualThan(count.toLong(), 0, "count")

ballot = ballotOption!!
this.count = count
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import com.github.dedis.popstellar.model.Immutable
import com.github.dedis.popstellar.model.network.method.message.data.Action
import com.github.dedis.popstellar.model.network.method.message.data.Data
import com.github.dedis.popstellar.model.network.method.message.data.Objects
import com.github.dedis.popstellar.model.objects.RollCall
import com.github.dedis.popstellar.model.objects.RollCall.Companion.generateCloseRollCallId
import com.github.dedis.popstellar.model.objects.security.PublicKey
import com.github.dedis.popstellar.utility.MessageValidator.verify
import com.google.gson.annotations.SerializedName

/** Data sent to close a Roll-Call */
Expand All @@ -25,12 +26,23 @@ class CloseRollCall(
@field:SerializedName("closed_at") val closedAt: Long,
attendees: List<PublicKey>
) : Data {
@SerializedName("update_id")
val updateId: String = RollCall.generateCloseRollCallId(laoId, closes, closedAt)
@SerializedName("update_id") val updateId: String

val attendees: List<PublicKey> = ArrayList(attendees)
var attendees: List<PublicKey> = ArrayList()
get() = ArrayList(field)

init {
verify()
.stringListIsSorted(attendees, "attendees")
.validPastTimes(closedAt)
.greaterOrEqualThan(closedAt, 0, "closedAt")
.isNotEmptyBase64(laoId, "laoId")
.isNotEmptyBase64(closes, "closes")

this.updateId = generateCloseRollCallId(laoId, closes, closedAt)
this.attendees = ArrayList(attendees)
}

override val `object`: String
get() = Objects.ROLL_CALL.`object`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RollCallBuilder {
start = rollCall.startTimestamp
end = rollCall.end
state = rollCall.state
attendees = HashSet(rollCall.attendees)
attendees = LinkedHashSet(rollCall.attendees)
location = rollCall.location
description = rollCall.description
}
Expand Down Expand Up @@ -66,12 +66,12 @@ class RollCallBuilder {
}

fun setAttendees(attendees: Set<PublicKey>): RollCallBuilder {
this.attendees = HashSet(attendees)
this.attendees = LinkedHashSet(attendees)
return this
}

fun setEmptyAttendees(): RollCallBuilder {
attendees = HashSet()
attendees = LinkedHashSet()
return this
}

Expand Down Expand Up @@ -102,7 +102,7 @@ class RollCallBuilder {
start,
end,
state!!,
attendees!!,
LinkedHashSet(attendees),
location!!,
description!!)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ class RollCallArrayAdapter(
private val layout: Int,
private val attendeesList: List<String>,
private val myToken: PoPToken?,
private val fragment: RollCallFragment
) : ArrayAdapter<String>(context, layout, attendeesList) {

init {
fragment.isAttendeeListSorted(attendeesList, context)
}

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.dedis.popstellar.ui.lao.event.rollcall

import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Color
import android.os.Bundle
Expand All @@ -9,6 +10,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.MutableLiveData
import com.github.dedis.popstellar.R
import com.github.dedis.popstellar.databinding.RollCallFragmentBinding
import com.github.dedis.popstellar.model.objects.RollCall
Expand Down Expand Up @@ -51,6 +53,8 @@ class RollCallFragment : AbstractEventFragment {
private val managementTextMap = buildManagementTextMap()
private val managementIconMap = buildManagementIconMap()

private val deAnonymizationWarned = MutableLiveData(false)

constructor()

override fun onCreateView(
Expand Down Expand Up @@ -253,15 +257,16 @@ class RollCallFragment : AbstractEventFragment {
.getAttendees()
.stream()
.map(PublicKey::encoded)
.sorted(compareBy(String::toString))
.collect(Collectors.toList())

binding.rollCallAttendeesText.text =
String.format(
resources.getString(R.string.roll_call_scanned),
rollCallViewModel.getAttendees().size)
} else if (rollCall.isClosed) {
attendeesList =
rollCall.attendees.stream().map(PublicKey::encoded).collect(Collectors.toList())
val orderedAttendees: MutableSet<PublicKey> = LinkedHashSet(rollCall.attendees)
attendeesList = orderedAttendees.stream().map(PublicKey::encoded).collect(Collectors.toList())

// Show the list of attendees if the roll call has ended
binding.rollCallAttendeesText.text =
Expand All @@ -271,11 +276,7 @@ class RollCallFragment : AbstractEventFragment {
if (attendeesList != null) {
binding.listViewAttendees.adapter =
RollCallArrayAdapter(
requireContext(),
android.R.layout.simple_list_item_1,
attendeesList,
popToken,
)
requireContext(), android.R.layout.simple_list_item_1, attendeesList, popToken, this)
}
}

Expand Down Expand Up @@ -335,6 +336,17 @@ class RollCallFragment : AbstractEventFragment {
return map
}

fun isAttendeeListSorted(attendeesList: List<String>, context: Context): Boolean {
if (attendeesList.isNotEmpty() &&
attendeesList != attendeesList.sorted() &&
deAnonymizationWarned.value == false) {
deAnonymizationWarned.value = true
logAndShow(context, TAG, R.string.roll_call_attendees_list_not_sorted)
return false
}
return true
}

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
constructor(rollCall: RollCall) {
this.rollCall = rollCall
Expand All @@ -349,7 +361,6 @@ class RollCallFragment : AbstractEventFragment {
val bundle = Bundle(1)
bundle.putString(ROLL_CALL_ID, persistentId)
fragment.arguments = bundle

return fragment
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single
import java.time.Instant
import java.util.ArrayList
import java.util.TreeSet
import java.util.stream.Collectors
import javax.inject.Inject
import timber.log.Timber
Expand All @@ -52,7 +54,7 @@ constructor(
) : AndroidViewModel(application), QRCodeScanningViewModel {
private lateinit var laoId: String

private val attendees: MutableSet<PublicKey> = HashSet()
private val attendees: TreeSet<PublicKey> = TreeSet(compareBy { it.toString() })
override val nbScanned = MutableLiveData<Int>()
lateinit var attendedRollCalls: Observable<List<RollCall>>
private set
Expand Down Expand Up @@ -261,6 +263,7 @@ constructor(
nbScanned.postValue(attendees.size)
}

@Inject
fun getAttendees(): Set<PublicKey> {
return attendees
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ object MessageValidator {
* @param value the value to compare to
* @throws IllegalArgumentException if the value is not greater or equal than the given value
*/
fun greaterOrEqualThan(input: Int, value: Int, field: String): MessageValidatorBuilder {
fun greaterOrEqualThan(input: Long, value: Long, field: String): MessageValidatorBuilder {
require(input >= value) { "$field must be greater or equal than $value" }
return this
}
Expand Down Expand Up @@ -305,6 +305,12 @@ object MessageValidator {
return this
}

/** Helper method to check that a string list is sorted */
fun stringListIsSorted(list: List<*>, field: String): MessageValidatorBuilder {
require(list == list.sortedBy { it.toString() }) { "$field must be sorted" }
return this
}

private fun validBallotOptions(ballotOptions: List<String>?): MessageValidatorBuilder {
requireNotNull(ballotOptions) { "Ballot options cannot be null" }
listNotEmpty(ballotOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,18 @@ constructor(
val updateId = closeRollCall.updateId
val closes = closeRollCall.closes
val existingRollCall = rollCallRepo.getRollCallWithId(laoView.id, closes)
val currentAttendees = existingRollCall.attendees
currentAttendees.addAll(closeRollCall.attendees)

val currentAttendees: Set<PublicKey>
if (closeRollCall.attendees.containsAll(existingRollCall.attendees)) {
// closeRollCall.attendees is sorted, so we prefer to use it if we can
currentAttendees = closeRollCall.attendees.toMutableSet()
} else {
// if both lists have different attendees, we merge them even though we lose the order
// We are not ordering it because it is important to keep the order that we received to know
// if we face de-anonymization
currentAttendees = existingRollCall.attendees
currentAttendees.addAll(closeRollCall.attendees)
}

val builder = RollCallBuilder()
builder
Expand All @@ -142,7 +152,6 @@ constructor(
.setEnd(closeRollCall.closedAt)
val laoId = laoView.id
val rollCall = builder.build()

witnessingRepo.addWitnessMessage(laoId, closeRollCallWitnessMessage(messageId, rollCall))
if (witnessingRepo.areWitnessesEmpty(laoId)) {
addRollCallRoutine(rollCallRepo, digitalCashRepo, laoId, rollCall)
Expand Down
2 changes: 2 additions & 0 deletions fe2-android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@
<string name="roll_call_scanned">Scanned tokens : %1$d</string>
<string name="roll_call_description_title">Description</string>
<string name="roll_call_location_title">Location</string>
<string name="roll_call_attendees_list_not_sorted">Attendees list is not sorted, risk of de-anonymization"</string>


<!-- Meeting -->
<string name="meeting_title">Meeting</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ private RollCallFragmentPageObject() {
public static ViewInteraction rollCallTitle() {
return onView(withId(R.id.roll_call_fragment_title));
}

public static ViewInteraction rollCallStatusText() {
return onView(withId(R.id.roll_call_status));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ object UITestUtils {
Assert.assertEquals(expected, ShadowToast.getTextOfLatestToast())
}

/**
* Assert that the latest toast was shown with the expected text
*
* @param resId expected
* @param args arguments to the resource
*/
@JvmStatic
fun assertToastDisplayedHasNotText(@StringRes resId: Int, vararg args: Any?) {
MatcherAssert.assertThat(
"No toast was displayed",
ShadowToast.getLatestToast(),
Matchers.notNullValue()
)

val expected = ApplicationProvider.getApplicationContext<Context>().getString(resId, *args)
Assert.assertNotEquals(expected, ShadowToast.getTextOfLatestToast())
}

@JvmStatic
fun assertToastIsDisplayedContainsText(@StringRes resId: Int, vararg args: Any?) {
MatcherAssert.assertThat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package com.github.dedis.popstellar.ui.lao

import android.content.ClipboardManager
import android.content.Context
import android.widget.Button
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.espresso.Espresso
import androidx.test.espresso.EspressoException
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.assertion.ViewAssertions
Expand All @@ -20,13 +17,11 @@ import com.github.dedis.popstellar.testutils.BundleBuilder
import com.github.dedis.popstellar.testutils.fragment.ActivityFragmentScenarioRule
import com.github.dedis.popstellar.testutils.pages.lao.InviteFragmentPageObject
import com.github.dedis.popstellar.testutils.pages.lao.LaoActivityPageObject
import com.github.dedis.popstellar.ui.lao.event.election.CastVoteOpenBallotFragmentTest
import com.github.dedis.popstellar.utility.security.KeyManager
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import junit.framework.TestCase
import javax.inject.Inject
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExternalResource
Expand All @@ -35,6 +30,7 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoTestRule
import javax.inject.Inject

@SmallTest
@HiltAndroidTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class CastVoteOpenBallotFragmentTest {
private const val PLURALITY = "Plurality"
private val laoSubject = BehaviorSubject.createDefault(LaoView(LAO))
private val ROLL_CALL =
RollCall("id", "id", "rc", 0L, 1L, 2L, EventState.CLOSED, HashSet(), "nowhere", "none")
RollCall("id", "id", "rc", 0L, 1L, 2L, EventState.CLOSED, LinkedHashSet(), "nowhere", "none")
private val ELECTION_ID = generateElectionSetupId(LAO_ID, CREATION, TITLE)
private val ELECTION_QUESTION_1 =
ElectionQuestion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class CastVoteSecretBallotFragmentTest {
private const val PLURALITY = "Plurality"
private val laoSubject = BehaviorSubject.createDefault(LaoView(LAO))
private val ROLL_CALL =
RollCall("id", "id", "rc", 0L, 1L, 2L, EventState.CLOSED, HashSet(), "nowhere", "none")
RollCall("id", "id", "rc", 0L, 1L, 2L, EventState.CLOSED, LinkedHashSet(), "nowhere", "none")
private val ELECTION_ID = generateElectionSetupId(LAO_ID, CREATION, TITLE)
private val ELECTION_QUESTION_1 =
ElectionQuestion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class ElectionFragmentTest {
START,
END,
EventState.CLOSED,
HashSet(),
LinkedHashSet(),
LOCATION,
ROLL_CALL_DESC
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ class EventListAdapterTest {
1L,
2L,
EventState.CREATED,
HashSet(),
LinkedHashSet(),
"not lausanne",
"no"
)
private val ROLL_CALL2 =
RollCall("12345", "12345", "Name", 2L, 3L, 4L, EventState.CREATED, HashSet(), "nowhere", "foo")
RollCall("12345", "12345", "Name", 2L, 3L, 4L, EventState.CREATED, LinkedHashSet(), "nowhere", "foo")

private lateinit var events: Subject<Set<Event>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,21 @@ class RollCallArrayAdapterTest {
val myToken = PoPToken(MY_PRIVATE_KEY, MY_PUBLIC_KEY)
val otherToken = PoPToken(OTHER_PRIVATE_KEY, OTHER_PUBLIC_KEY)
attendeesList = listOf(myToken.publicKey.encoded, otherToken.publicKey.encoded)
adapter = RollCallArrayAdapter(context, R.id.valid_token_layout_text, attendeesList, myToken)
adapter = RollCallArrayAdapter(context, R.id.valid_token_layout_text, attendeesList, myToken, mock(RollCallFragment::class.java))
mockView = TextView(context)
val colorAccent = ContextCompat.getColor(context, R.color.textOnBackground)
(mockView as TextView).setTextColor(colorAccent)
}

@Test
fun verify_our_token_is_highlighted() {
fun verifyOurTokenIsHighlighted() {
val view = adapter.getView(0, mockView, mock(ViewGroup::class.java)) as TextView
val color = ContextCompat.getColor(context, R.color.colorAccent)
Assert.assertEquals(color, view.currentTextColor)
}

@Test
fun verify_other_token_is_not_highlighted() {
fun verifyOtherTokenIsNotHighlighted() {
val view = adapter.getView(1, mockView, mock(ViewGroup::class.java)) as TextView
val color = ContextCompat.getColor(context, R.color.textOnBackground)
Assert.assertEquals(color, view.currentTextColor)
Expand Down
Loading
Loading