Move construction of UserNewsResource into separate function

Change-Id: I7c1f6427cd7d95c2016349fec301b88455b33cf2
pull/521/head
Don Turner 2 years ago
parent 99227b06b5
commit 355b0540aa

@ -18,7 +18,6 @@ package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
@ -48,30 +47,15 @@ class GetUserNewsResourcesUseCase @Inject constructor(
newsRepository.getNewsResources()
} else {
newsRepository.getNewsResources(filterTopicIds = filterTopicIds)
}.mapToSaveableNewsResources(userDataRepository.userData)
}.mapToUserNewsResources(userDataRepository.userData)
}
private fun Flow<List<NewsResource>>.mapToSaveableNewsResources(
userData: Flow<UserData>
private fun Flow<List<NewsResource>>.mapToUserNewsResources(
userDataStream: Flow<UserData>
): Flow<List<UserNewsResource>> =
filterNot { it.isEmpty() }
.combine(userData) { newsResources, userData ->
.combine(userDataStream) { newsResources, userData ->
newsResources.map { newsResource ->
UserNewsResource(
id = newsResource.id,
title = newsResource.title,
content = newsResource.content,
url = newsResource.url,
headerImageUrl = newsResource.headerImageUrl,
publishDate = newsResource.publishDate,
type = newsResource.type,
topics = newsResource.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
},
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id)
)
UserNewsResource.from(newsResource, userData)
}
}

@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Codelab
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Unknown
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
@ -39,9 +40,30 @@ data class UserNewsResource(
val headerImageUrl: String?,
val publishDate: Instant,
val type: NewsResourceType,
val topics: List<FollowableTopic>,
val followableTopics: List<FollowableTopic>,
val isSaved: Boolean
)
) {
companion object {
fun from(newsResource: NewsResource, userData: UserData): UserNewsResource {
return UserNewsResource(
id = newsResource.id,
title = newsResource.title,
content = newsResource.content,
url = newsResource.url,
headerImageUrl = newsResource.headerImageUrl,
publishDate = newsResource.publishDate,
type = newsResource.type,
followableTopics = newsResource.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
},
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id)
)
}
}
}
val previewUserNewsResources = listOf(
UserNewsResource(
@ -60,7 +82,7 @@ val previewUserNewsResources = listOf(
nanosecond = 0
).toInstant(TimeZone.UTC),
type = Codelab,
topics = listOf(previewFollowableTopics[1]),
followableTopics = listOf(previewFollowableTopics[1]),
isSaved = true
),
UserNewsResource(
@ -74,7 +96,7 @@ val previewUserNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
topics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]),
followableTopics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]),
isSaved = false
),
UserNewsResource(
@ -88,7 +110,7 @@ val previewUserNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = Video,
topics = listOf(previewFollowableTopics[2]),
followableTopics = listOf(previewFollowableTopics[2]),
isSaved = false
),
UserNewsResource(
@ -100,7 +122,7 @@ val previewUserNewsResources = listOf(
headerImageUrl = "",
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = Unknown,
topics = listOf(previewFollowableTopics[2]),
followableTopics = listOf(previewFollowableTopics[2]),
isSaved = true
)
)

@ -16,11 +16,13 @@
package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
@ -44,107 +46,59 @@ class GetUserNewsResourcesUseCaseTest {
@Test
fun whenNoFilters_allNewsResourcesAreReturned() = runTest {
// Obtain the saveable news resources stream.
val saveableNewsResources = useCase()
// Obtain the user news resources stream.
val userNewsResources = useCase()
// Send some news resources and bookmarks.
// Send some news resources and user data into the data repositories.
newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setNewsResourceBookmarks(
setOf(sampleNewsResources[0].id, sampleNewsResources[2].id)
// Construct the test user data with bookmarks and followed topics.
val userData = UserData(
bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id),
followedTopics = setOf(sampleTopic1.id),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
shouldHideOnboarding = false
)
// Set a followed topic for the user.
userDataRepository.setFollowedTopicIds(setOf(sampleTopic1.id))
userDataRepository.setUserData(userData)
// Check that the correct news resources are returned with their bookmarked state.
assertEquals(
listOf(
UserNewsResource(
sampleNewsResources[0].id,
sampleNewsResources[0].title,
sampleNewsResources[0].content,
sampleNewsResources[0].url,
sampleNewsResources[0].headerImageUrl,
sampleNewsResources[0].publishDate,
sampleNewsResources[0].type,
sampleNewsResources[0].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == sampleTopic1.id
)
},
true
),
UserNewsResource(
sampleNewsResources[1].id,
sampleNewsResources[1].title,
sampleNewsResources[1].content,
sampleNewsResources[1].url,
sampleNewsResources[1].headerImageUrl,
sampleNewsResources[1].publishDate,
sampleNewsResources[1].type,
sampleNewsResources[1].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == sampleTopic1.id
)
},
false
),
UserNewsResource(
sampleNewsResources[2].id,
sampleNewsResources[2].title,
sampleNewsResources[2].content,
sampleNewsResources[2].url,
sampleNewsResources[2].headerImageUrl,
sampleNewsResources[2].publishDate,
sampleNewsResources[2].type,
sampleNewsResources[2].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == sampleTopic1.id
)
},
true
),
UserNewsResource.from(sampleNewsResources[0], userData),
UserNewsResource.from(sampleNewsResources[1], userData),
UserNewsResource.from(sampleNewsResources[2], userData),
),
saveableNewsResources.first()
userNewsResources.first()
)
}
@Test
fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest {
// Obtain a stream of saveable news resources for the given topic id.
val saveableNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id))
// Obtain a stream of user news resources for the given topic id.
val userNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id))
// Send some news resources and bookmarks.
// Send test data into the repositories.
newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setNewsResourceBookmarks(setOf())
val userData = UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = emptySet(),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
shouldHideOnboarding = false
)
userDataRepository.setUserData(userData)
// Check that only news resources with the given topic id are returned.
assertEquals(
sampleNewsResources
.filter { it.topics.contains(sampleTopic1) }
.map {
UserNewsResource(
id = it.id,
title = it.title,
content = it.content,
url = it.url,
headerImageUrl = it.headerImageUrl,
publishDate = it.publishDate,
type = it.type,
topics = it.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = false
)
},
isSaved = false
)
UserNewsResource.from(it, userData)
},
saveableNewsResources.first()
userNewsResources.first()
)
}
}

@ -0,0 +1,106 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.domain
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.datetime.Clock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class UserNewsResourceTest {
/**
* Given: Some user data and news resources
* When: They are combined using `UserNewsResource.from`
* Then: The correct UserNewsResources are constructed
*/
@Test
fun userNewsResourcesAreConstructedFromNewsResourcesAndUserData() {
val newsResource1 = NewsResource(
id = "N1",
title = "Test news title",
content = "Test news content",
url = "Test URL",
headerImageUrl = "Test image URL",
publishDate = Clock.System.now(),
type = Article,
topics = listOf(
Topic(
id = "T1",
name = "Topic 1",
shortDescription = "Topic 1 short description",
longDescription = "Topic 1 long description",
url = "Topic 1 URL",
imageUrl = "Topic 1 image URL"
),
Topic(
id = "T2",
name = "Topic 2",
shortDescription = "Topic 2 short description",
longDescription = "Topic 2 long description",
url = "Topic 2 URL",
imageUrl = "Topic 2 image URL"
),
)
)
val userData = UserData(
bookmarkedNewsResources = setOf("N1"),
followedTopics = setOf("T1"),
themeBrand = DEFAULT,
darkThemeConfig = FOLLOW_SYSTEM,
shouldHideOnboarding = true
)
val userNewsResource = UserNewsResource.from(newsResource1, userData)
// Check that the simple field mappings have been done correctly.
assertEquals(newsResource1.id, userNewsResource.id)
assertEquals(newsResource1.title, userNewsResource.title)
assertEquals(newsResource1.content, userNewsResource.content)
assertEquals(newsResource1.url, userNewsResource.url)
assertEquals(newsResource1.headerImageUrl, userNewsResource.headerImageUrl)
assertEquals(newsResource1.publishDate, userNewsResource.publishDate)
// Check that each Topic has been converted to a FollowedTopic correctly.
assertEquals(newsResource1.topics.size, userNewsResource.followableTopics.size)
for (topic in newsResource1.topics) {
// Construct the expected FollowableTopic.
val followableTopic = FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id)
)
assertTrue(userNewsResource.followableTopics.contains(followableTopic))
}
// Check that the saved flag is set correctly.
assertEquals(
userData.bookmarkedNewsResources.contains(newsResource1.id),
userNewsResource.isSaved
)
}
}

@ -98,4 +98,11 @@ class TestUserDataRepository : UserDataRepository {
*/
fun getCurrentFollowedTopics(): Set<String>? =
_userData.replayCache.firstOrNull()?.followedTopics
/**
* A test-only API to allow setting of user data directly.
*/
fun setUserData(userData: UserData) {
_userData.tryEmit(userData)
}
}

@ -79,7 +79,7 @@ class NewsResourceCardTest {
@Test
fun testTopicsChipColorBackground_matchesFollowedState() {
val followableTopics = previewUserNewsResources[1].topics
val followableTopics = previewUserNewsResources[1].followableTopics
composeTestRule.setContent {
NewsResourceTopics(topics = followableTopics)

@ -116,7 +116,7 @@ fun NewsResourceCardExpanded(
Spacer(modifier = Modifier.height(12.dp))
NewsResourceShortDescription(userNewsResource.content)
Spacer(modifier = Modifier.height(12.dp))
NewsResourceTopics(userNewsResource.topics)
NewsResourceTopics(userNewsResource.followableTopics)
}
}
}

@ -292,7 +292,7 @@ class ForYouViewModelTest {
headerImageUrl = it.headerImageUrl,
publishDate = it.publishDate,
type = it.type,
topics = it.topics.map { topic ->
followableTopics = it.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = followedTopicIds.contains(topic.id)
@ -356,7 +356,7 @@ class ForYouViewModelTest {
headerImageUrl = sampleNewsResources[1].headerImageUrl,
publishDate = sampleNewsResources[1].publishDate,
type = sampleNewsResources[1].type,
topics = sampleNewsResources[1].topics.map { topic ->
followableTopics = sampleNewsResources[1].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == followedTopicId
@ -372,7 +372,7 @@ class ForYouViewModelTest {
headerImageUrl = sampleNewsResources[2].headerImageUrl,
publishDate = sampleNewsResources[2].publishDate,
type = sampleNewsResources[2].type,
topics = sampleNewsResources[2].topics.map { topic ->
followableTopics = sampleNewsResources[2].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id == followedTopicId
@ -482,7 +482,7 @@ class ForYouViewModelTest {
headerImageUrl = sampleNewsResources[1].headerImageUrl,
publishDate = sampleNewsResources[1].publishDate,
type = sampleNewsResources[1].type,
topics = sampleNewsResources[1].topics.map { topic ->
followableTopics = sampleNewsResources[1].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = followedTopicIds.contains(topic.id)
@ -498,7 +498,7 @@ class ForYouViewModelTest {
headerImageUrl = sampleNewsResources[2].headerImageUrl,
publishDate = sampleNewsResources[2].publishDate,
type = sampleNewsResources[2].type,
topics = sampleNewsResources[2].topics.map { topic ->
followableTopics = sampleNewsResources[2].topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = followedTopicIds.contains(topic.id)

@ -187,7 +187,7 @@ private val sampleUserNewsResources = listOf(
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = Video,
topics = listOf(
followableTopics = listOf(
FollowableTopic(
topic = Topic(
id = "0",

Loading…
Cancel
Save