Skip to content

Commit c161f23

Browse files
Merge pull request #226 from SwEnt-Group13/feat/permissions-&-access-rights-v3
Feat/permissions & access rights v3
2 parents 7648c7a + 163cc79 commit c161f23

File tree

21 files changed

+518
-91
lines changed

21 files changed

+518
-91
lines changed

app/src/androidTest/java/com/android/unio/components/home/HomeTest.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.android.unio.model.image.ImageRepositoryFirebaseStorage
2424
import com.android.unio.model.search.SearchRepository
2525
import com.android.unio.model.search.SearchViewModel
2626
import com.android.unio.model.strings.test_tags.HomeTestTags
27+
import com.android.unio.model.user.User
2728
import com.android.unio.model.user.UserRepositoryFirestore
2829
import com.android.unio.model.user.UserViewModel
2930
import com.android.unio.ui.home.HomeScreen
@@ -44,8 +45,6 @@ import io.mockk.impl.annotations.MockK
4445
import io.mockk.mockk
4546
import io.mockk.spyk
4647
import io.mockk.verify
47-
import kotlinx.coroutines.ExperimentalCoroutinesApi
48-
import kotlinx.coroutines.test.runBlockingTest
4948
import org.junit.Before
5049
import org.junit.Rule
5150
import org.junit.Test
@@ -94,6 +93,12 @@ class HomeTest : TearDown() {
9493
onSuccess()
9594
}
9695

96+
every { userRepository.getUserWithId(any(), any(), any()) } answers
97+
{
98+
val onSuccess = args[1] as (User) -> Unit
99+
onSuccess(MockUser.createMockUser())
100+
}
101+
97102
userViewModel = spyk(UserViewModel(userRepository))
98103
val asso = MockAssociation.createMockAssociation()
99104
val user =
@@ -207,9 +212,8 @@ class HomeTest : TearDown() {
207212
* Tests the sequence of clicking on the 'Following' tab and then on the 'Map' button to ensure
208213
* that both actions trigger their respective animations and behaviors.
209214
*/
210-
@OptIn(ExperimentalCoroutinesApi::class)
211215
@Test
212-
fun testClickFollowingAndAdd() = runBlockingTest {
216+
fun testClickFollowingAndAdd() {
213217
composeTestRule.setContent {
214218
val eventViewModel =
215219
EventViewModel(eventRepository, imageRepository, associationRepositoryFirestore)

app/src/androidTest/java/com/android/unio/end2end/AssociationProfileE2ETest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ class AssociationProfileE2ETest : EndToEndTest() {
3939
composeTestRule.waitUntil(10000) {
4040
composeTestRule.onNodeWithTag(AssociationProfileTestTags.SCREEN).isDisplayed()
4141
}
42-
42+
Thread.sleep(1000)
4343
composeTestRule.onNodeWithText(ASSOCIATION_MEMBERS).assertDisplayComponentInScroll()
4444
composeTestRule.onNodeWithText(ASSOCIATION_MEMBERS).performClick()
4545

4646
composeTestRule.waitUntil(10000) {
4747
composeTestRule.onNodeWithTag(SomeoneElseUserProfileTestTags.SCREEN).isDisplayed()
4848
}
49+
4950
composeTestRule.onNodeWithTag(SomeoneElseUserProfileTestTags.NAME).assertIsDisplayed()
5051

5152
composeTestRule.waitUntil(10000) {

app/src/androidTest/java/com/android/unio/end2end/EndToEndTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ open class EndToEndTest : FirebaseEmulatorFunctions {
107107
} finally {
108108
val currentHost = Firebase.firestore.firestoreSettings.host
109109
if (!currentHost.contains(HOST)) {
110-
throw Exception("Failed to connect to Firebase Emulators.")
110+
throw Exception("Failed to connect to Firebase Emulators. Host is $currentHost")
111111
}
112112
}
113113
}

app/src/main/java/com/android/unio/mocks/association/MockAssociation.kt

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package com.android.unio.mocks.association
22

33
import com.android.unio.mocks.event.MockEvent
4+
import com.android.unio.mocks.firestore.MockReferenceElement
45
import com.android.unio.mocks.firestore.MockReferenceList
56
import com.android.unio.mocks.user.MockUser
67
import com.android.unio.model.association.Association
78
import com.android.unio.model.association.AssociationCategory
9+
import com.android.unio.model.association.Member
10+
import com.android.unio.model.association.Role
811
import com.android.unio.model.event.Event
912
import com.android.unio.model.firestore.ReferenceList
1013
import com.android.unio.model.firestore.emptyFirestoreReferenceList
11-
import com.android.unio.model.user.User
1214

1315
/**
1416
* MockAssociation class provides edge-case instances of the Association data class for testing
@@ -80,23 +82,32 @@ class MockAssociation {
8082
category: AssociationCategory = AssociationCategory.ENTERTAINMENT,
8183
description: String = "This is the best description",
8284
image: String = "image1.png",
83-
members: List<User> = emptyList(),
85+
members: List<Member> = emptyList(),
86+
roles: List<Role> = listOf(Role.GUEST, Role.ADMIN),
8487
events: ReferenceList<Event> =
85-
MockReferenceList(
86-
listOf(
87-
MockEvent.createMockEvent(
88-
associationDependency = true, userDependency = userDependency))),
89-
principalEmailAddress: String = "principal@email_adress.com",
90-
parentAssociations: ReferenceList<Association> = Association.emptyFirestoreReferenceList(),
91-
childAssociations: ReferenceList<Association> = Association.emptyFirestoreReferenceList(),
92-
adminUid: String = "1"
88+
if (eventDependency) {
89+
Event.emptyFirestoreReferenceList()
90+
} else {
91+
MockReferenceList(
92+
listOf(
93+
MockEvent.createMockEvent(
94+
associationDependency = true, userDependency = userDependency)))
95+
},
96+
principalEmailAddress: String = "principal@email_adress.com"
9397
): Association {
9498
val membersHelper =
9599
if (userDependency) {
96100
members
97101
} else {
98-
MockUser.createAllMockUsers(
99-
associationDependency = true, eventDependency = eventDependency)
102+
listOf(
103+
Member(
104+
MockReferenceElement(
105+
MockUser.createMockUser(uid = "1", associationDependency = true)),
106+
Role.GUEST),
107+
Member(
108+
MockReferenceElement(
109+
MockUser.createMockUser(uid = "2", associationDependency = true)),
110+
Role.GUEST))
100111
}
101112
return Association(
102113
uid = uid,
@@ -105,12 +116,12 @@ class MockAssociation {
105116
fullName = fullName,
106117
category = category,
107118
description = description,
108-
members = MockReferenceList(membersHelper),
119+
members = membersHelper,
120+
roles = roles,
109121
image = image,
110122
followersCount = 2,
111123
events = events,
112-
principalEmailAddress = principalEmailAddress,
113-
adminUid = adminUid)
124+
principalEmailAddress = principalEmailAddress)
114125
}
115126

116127
fun createAllMockAssociations(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.android.unio.mocks.firestore
2+
3+
import com.android.unio.model.firestore.ReferenceElement
4+
import com.android.unio.model.firestore.UniquelyIdentifiable
5+
import kotlinx.coroutines.flow.MutableStateFlow
6+
import kotlinx.coroutines.flow.StateFlow
7+
8+
/** A mock implementation of [ReferenceElement] for testing purposes. */
9+
class MockReferenceElement<T : UniquelyIdentifiable>(initialElement: T? = null) :
10+
ReferenceElement<T> {
11+
12+
private val _element = MutableStateFlow(initialElement)
13+
override val element: StateFlow<T?> = _element
14+
15+
private var _uid: String = initialElement?.uid ?: ""
16+
override val uid: String
17+
get() = _uid
18+
19+
override fun set(uid: String) {
20+
_uid = uid
21+
}
22+
23+
override fun fetch(onSuccess: () -> Unit, lazy: Boolean) {
24+
onSuccess()
25+
}
26+
}

app/src/main/java/com/android/unio/model/association/Association.kt

Lines changed: 152 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@ import androidx.appsearch.annotation.Document.Namespace
66
import androidx.appsearch.annotation.Document.StringProperty
77
import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig
88
import com.android.unio.model.event.Event
9+
import com.android.unio.model.firestore.ReferenceElement
910
import com.android.unio.model.firestore.ReferenceList
1011
import com.android.unio.model.firestore.UniquelyIdentifiable
1112
import com.android.unio.model.strings.AssociationStrings
1213
import com.android.unio.model.user.User
1314

1415
/**
15-
* Association data class
16+
* Represents an association within the system. This class holds various details about the
17+
* association and its related entities.
1618
*
17-
* Make sure to update the hydration and serialization methods when changing the data class
19+
* **Note:** When modifying this class, ensure the serialization and hydration methods are updated
20+
* accordingly to reflect the changes.
1821
*
19-
* @property uid association id
20-
* @property url association url
21-
* @property name association acronym
22-
* @property fullName association full name
23-
* @property category association category
24-
* @property description association description
25-
* @property followersCount number of association followers
26-
* @property members list of association members
27-
* @property image association image
28-
* @property events list of association events
22+
* @property uid Unique identifier for the association.
23+
* @property url URL of the association's homepage.
24+
* @property name Acronym or short name of the association.
25+
* @property fullName Full name of the association.
26+
* @property category The category under which the association falls.
27+
* @property description Brief description of the association's purpose and activities.
28+
* @property followersCount Number of users following this association.
29+
* @property members List of members associated with the association.
30+
* @property roles List of roles defined within the association.
31+
* @property image URL or path to the association's image/logo.
32+
* @property events A reference list containing events associated with the association.
33+
* @property principalEmailAddress Primary email address for contacting the association.
2934
*/
3035
data class Association(
3136
override val uid: String,
@@ -35,15 +40,20 @@ data class Association(
3540
val category: AssociationCategory,
3641
val description: String,
3742
val followersCount: Int,
38-
val members: ReferenceList<User>,
43+
val members: List<Member>,
44+
val roles: List<Role>,
3945
var image: String,
4046
val events: ReferenceList<Event>,
41-
val principalEmailAddress: String,
42-
val adminUid: String
47+
val principalEmailAddress: String
4348
) : UniquelyIdentifiable {
4449
companion object
4550
}
4651

52+
/**
53+
* Enum representing different categories of associations.
54+
*
55+
* @property displayName A human-readable name for the category.
56+
*/
4757
enum class AssociationCategory(val displayName: String) {
4858
EPFL_BODIES(AssociationStrings.EPFL_BODIES),
4959
REPRESENTATION(AssociationStrings.REPRESENTATION),
@@ -60,6 +70,133 @@ enum class AssociationCategory(val displayName: String) {
6070
UNKNOWN(AssociationStrings.UNKNOWN)
6171
}
6272

73+
/**
74+
* Represents a member of an association.
75+
*
76+
* @property user Reference to the user who is a member.
77+
* @property role The role assigned to the member within the association.
78+
*/
79+
data class Member(val user: ReferenceElement<User>, val role: Role) : UniquelyIdentifiable {
80+
class Companion {}
81+
82+
override val uid: String
83+
get() = user.uid
84+
}
85+
86+
/**
87+
* Represents a role within an association.
88+
*
89+
* @property displayName Name of the role.
90+
* @property permissions Set of permissions assigned to this role.
91+
* @property uid Unique identifier for the role.
92+
*/
93+
class Role(val displayName: String, val permissions: Permissions, override val uid: String) :
94+
UniquelyIdentifiable {
95+
96+
companion object {
97+
// Predefined roles
98+
val ADMIN = Role("Administrator", Permissions.FULL_RIGHTS, "Administrator")
99+
val COMITE =
100+
Role(
101+
"Committee",
102+
Permissions.PermissionsBuilder()
103+
.addPermission(PermissionType.VIEW_MEMBERS)
104+
.addPermission(PermissionType.EDIT_MEMBERS)
105+
.addPermission(PermissionType.VIEW_EVENTS)
106+
.build(),
107+
"Committee")
108+
val MEMBER = Role("Member", Permissions.NONE, "Member")
109+
val GUEST = Role("Guest", Permissions.NONE, "Guest")
110+
111+
// Factory method to create new roles
112+
fun createRole(displayName: String, permissions: Permissions, uid: String): Role {
113+
return Role(displayName, permissions, uid)
114+
}
115+
}
116+
}
117+
118+
/**
119+
* Represents a set of permissions for a role.
120+
*
121+
* @property grantedPermissions The permissions granted to this set.
122+
*/
123+
class Permissions private constructor(private val grantedPermissions: MutableSet<PermissionType>) {
124+
125+
fun hasPermission(permission: PermissionType): Boolean {
126+
return grantedPermissions.contains(permission) ||
127+
grantedPermissions.contains(PermissionType.FULL_RIGHTS)
128+
}
129+
130+
fun getGrantedPermissions(): Set<PermissionType> = grantedPermissions.toSet()
131+
132+
fun addPermission(permission: PermissionType): Permissions {
133+
grantedPermissions.add(permission)
134+
return this.copy()
135+
}
136+
137+
fun addPermissions(permissionList: List<PermissionType>): Permissions {
138+
grantedPermissions.addAll(permissionList)
139+
return this.copy()
140+
}
141+
142+
fun deletePermission(permission: PermissionType): Permissions {
143+
grantedPermissions.remove(permission)
144+
return this.copy()
145+
}
146+
147+
fun deleteAllPermissions(permissionList: List<PermissionType>): Permissions {
148+
grantedPermissions.removeAll(permissionList)
149+
return this.copy()
150+
}
151+
152+
// create and return a copy of the current Permissions object with updated permissions
153+
private fun copy(): Permissions {
154+
return Permissions(grantedPermissions.toMutableSet())
155+
}
156+
157+
companion object {
158+
// predefined permission sets
159+
val FULL_RIGHTS = Permissions(mutableSetOf(PermissionType.FULL_RIGHTS))
160+
val NONE = Permissions(mutableSetOf())
161+
}
162+
163+
// PermissionsBuilder class to create Permissions with a list of roles/permissions
164+
class PermissionsBuilder {
165+
private val permissions = mutableSetOf<PermissionType>()
166+
167+
// Add a specific permission to the permissions set
168+
fun addPermission(permission: PermissionType): PermissionsBuilder {
169+
permissions.add(permission)
170+
return this
171+
}
172+
173+
// Add a list of permissions to the permissions set
174+
fun addPermissions(permissionList: List<PermissionType>): PermissionsBuilder {
175+
permissions.addAll(permissionList)
176+
return this
177+
}
178+
179+
// Build and return the Permissions object
180+
fun build(): Permissions {
181+
// Ensure that FULL_RIGHTS is not included explicitly
182+
if (permissions.contains(PermissionType.FULL_RIGHTS)) {
183+
throw IllegalArgumentException("Cannot grant FULL_RIGHTS explicitly.")
184+
}
185+
return Permissions(permissions)
186+
}
187+
}
188+
}
189+
190+
enum class PermissionType(val stringName: String) {
191+
FULL_RIGHTS("Full rights"), // Special permission granting all rights
192+
VIEW_MEMBERS("View members"),
193+
EDIT_MEMBERS("Edit members"),
194+
DELETE_MEMBERS("Delete members"),
195+
VIEW_EVENTS("View events"),
196+
EDIT_EVENTS("Edit events"),
197+
DELETE_EVENTS("Delete Events")
198+
}
199+
63200
@Document
64201
data class AssociationDocument(
65202
@Namespace val namespace: String = "unio",

0 commit comments

Comments
 (0)