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

Implement endorsements API #286

Merged
merged 10 commits into from
Oct 21, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package social.bigbone.rx

import io.reactivex.rxjava3.core.Single
import social.bigbone.MastodonClient
import social.bigbone.api.Pageable
import social.bigbone.api.Range
import social.bigbone.api.entity.Account
import social.bigbone.api.method.EndorsementMethods

/**
* Reactive implementation of [EndorsementMethods].
* Feature other profiles on your own profile. See also accounts/:id/{pin,unpin}.
* @see <a href="https://docs.joinmastodon.org/methods/endorsements/">Mastodon endorsement API methods</a>
*/
class RxEndorsementMethods(client: MastodonClient) {

private val endorsementMethods = EndorsementMethods(client)

/**
* Accounts that the user is currently featuring on their profile.
* @param range optional Range for the pageable return value
* @see <a href="https://docs.joinmastodon.org/methods/endorsements/#get">Mastodon API documentation: methods/endorsements/#get</a>
* @return [Pageable] of [Account]s the user is currently featuring on their profile
*/
@JvmOverloads
fun getEndorsements(
range: Range = Range()
): Single<Pageable<Account>> {
return Single.create { emitter ->
try {
emitter.onSuccess(endorsementMethods.getEndorsements(range).execute())
} catch (error: Throwable) {
emitter.onError(error)
}
}
}

}
51 changes: 51 additions & 0 deletions bigbone-rx/src/test/assets/endorsements_view_success.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[
{
"id": "952529",
"username": "alayna",
"acct": "[email protected]",
"display_name": "Alayna Desirae",
"locked": true,
"bot": false,
"created_at": "2019-10-26T23:12:06.570Z",
"note": "experiencing ________ difficulties<br>22y/o INFP in Oklahoma",
"url": "https://desvox.es/users/alayna",
"avatar": "https://files.mastodon.social/accounts/avatars/000/952/529/original/6534122046d050d5.png",
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/952/529/original/6534122046d050d5.png",
"header": "https://files.mastodon.social/accounts/headers/000/952/529/original/496f1f817e042ade.png",
"header_static": "https://files.mastodon.social/accounts/headers/000/952/529/original/496f1f817e042ade.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 955,
"last_status_at": "2019-11-23T07:05:50.682Z",
"emojis": [],
"fields": []
},
{
"id": "832844",
"username": "a9",
"acct": "[email protected]",
"display_name": "vivienne :collar: ",
"locked": true,
"bot": false,
"created_at": "2019-06-12T18:55:12.053Z",
"note": "borderline nsfw, considered a schedule I drug by nixon<br>waiting for the year of the illumos desktop",
"url": "https://broadcast.wolfgirl.engineering/users/a9",
"avatar": "https://files.mastodon.social/accounts/avatars/000/832/844/original/ae1de0b8fb63d1c6.png",
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/832/844/original/ae1de0b8fb63d1c6.png",
"header": "https://files.mastodon.social/accounts/headers/000/832/844/original/5088e4a16e6d8736.png",
"header_static": "https://files.mastodon.social/accounts/headers/000/832/844/original/5088e4a16e6d8736.png",
"followers_count": 43,
"following_count": 67,
"statuses_count": 5906,
"last_status_at": "2019-11-23T05:23:47.911Z",
"emojis": [
{
"shortcode": "collar",
"url": "https://files.mastodon.social/custom_emojis/images/000/106/920/original/80953b9cd96ec4dc.png",
"static_url": "https://files.mastodon.social/custom_emojis/images/000/106/920/static/80953b9cd96ec4dc.png",
"visible_in_picker": true
}
],
"fields": []
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package social.bigbone.rx

import org.junit.jupiter.api.Test
import social.bigbone.api.Pageable
import social.bigbone.api.Range
import social.bigbone.api.entity.Account
import social.bigbone.rx.testtool.MockClient

class RxEndorsementMethodsTest {

@Test
fun `Given a client returning success, when getting endorsements, then emit list of endorsements`() {
val client = MockClient.mock(
jsonName = "endorsements_view_success.json"
)
val endorsements = RxEndorsementMethods(client)

with(endorsements.getEndorsements().test()) {
assertNoErrors()
assertComplete()

assertValueCount(1)
assertValue { endorsements: Pageable<Account> -> endorsements.part.size == 2 }

dispose()
}
}

@Test
fun `Given a client returning success, when getting endorsements with limit of 90, then emit error`() {
val client = MockClient.mock(
jsonName = "endorsements_view_success.json"
)
val endorsements = RxEndorsementMethods(client)

with(endorsements.getEndorsements(range = Range(limit = 90)).test()) {
assertNoValues()
assertNotComplete()

assertError { error: Throwable ->
error is IllegalArgumentException &&
error.message == "limit defined in Range must not be higher than 80 but was 90"
}

dispose()
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,38 @@ import social.bigbone.MastodonClient

object MockClient {

fun mock(jsonName: String, maxId: String? = null, sinceId: String? = null): MastodonClient {
fun mock(
jsonName: String,
maxId: String? = null,
sinceId: String? = null,
requestUrl: String = "https://example.com",
responseBaseUrl: String = "https://mstdn.jp/api/v1/timelines/public"
): MastodonClient {
val client: MastodonClient = mockk()
val response: Response = Response.Builder()
.code(200)
.message("OK")
.request(Request.Builder().url("https://test.com/").build())
.request(Request.Builder().url(requestUrl).build())
.protocol(Protocol.HTTP_1_1)
.body(
AssetsUtil.readFromAssets(jsonName)
AssetsUtil
.readFromAssets(jsonName)
.toResponseBody("application/json; charset=utf-8".toMediaTypeOrNull())
)
.apply {
val linkHeader = arrayListOf<String>().apply {
val linkHeader = buildList {
maxId?.let {
add("""<https://mstdn.jp/api/v1/timelines/public?limit=20&local=true&max_id=$it>; rel="next"""")
add("""<${responseBaseUrl}?limit=20&local=true&max_id=$it>; rel="next"""")
}
sinceId?.let {
add("""<https://mstdn.jp/api/v1/timelines/public?limit=20&local=true&since_id=$it>; rel="prev"""")
add("""<${responseBaseUrl}?limit=20&local=true&since_id=$it>; rel="prev"""")
}
}.joinToString(separator = ",")
}
if (linkHeader.isNotEmpty()) {
header("link", linkHeader)
header(
name = "link",
value = linkHeader.joinToString(separator = ",")
)
}
}
.build()
Expand Down
8 changes: 8 additions & 0 deletions bigbone/src/main/kotlin/social/bigbone/MastodonClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import social.bigbone.api.method.BlockMethods
import social.bigbone.api.method.BookmarkMethods
import social.bigbone.api.method.ConversationMethods
import social.bigbone.api.method.DirectoryMethods
import social.bigbone.api.method.EndorsementMethods
import social.bigbone.api.method.FavouriteMethods
import social.bigbone.api.method.FeaturedTagsMethods
import social.bigbone.api.method.FilterMethods
Expand Down Expand Up @@ -102,6 +103,13 @@ private constructor(
@get:JvmName("directories")
val directories: DirectoryMethods by lazy { DirectoryMethods(this) }

/**
* Access API methods under "api/vX/endorsements" endpoint.
*/
@Suppress("unused") // public API
@get:JvmName("endorsements")
val endorsements: EndorsementMethods by lazy { EndorsementMethods(this) }

/**
* Access API methods under "api/vX/favourites" endpoint.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package social.bigbone.api.method

import social.bigbone.MastodonClient
import social.bigbone.MastodonRequest
import social.bigbone.api.Pageable
import social.bigbone.api.Range
import social.bigbone.api.entity.Account

private const val QUERY_RESULT_LIMIT: Int = 80

/**
* Feature other profiles on your own profile. See also accounts/:id/{pin,unpin}.
* @see <a href="https://docs.joinmastodon.org/methods/endorsements/">Mastodon endorsement API methods</a>
*/
class EndorsementMethods(private val client: MastodonClient) {

private val endorsementsEndpoint = "/api/v1/endorsements"

/**
* Accounts that the user is currently featuring on their profile.
* @param range optional Range for the pageable return value
* @see <a href="https://docs.joinmastodon.org/methods/endorsements/#get">Mastodon API documentation: methods/endorsements/#get</a>
* @return [Pageable] of [Account]s the user is currently featuring on their profile
* @throws [IllegalArgumentException] if [range]'s [Range.limit] is larger than [QUERY_RESULT_LIMIT]
*/
@JvmOverloads
@Throws(IllegalArgumentException::class)
fun getEndorsements(
range: Range = Range()
): MastodonRequest<Pageable<Account>> {
if (range.limit != null && range.limit > QUERY_RESULT_LIMIT) {
throw IllegalArgumentException(
"limit defined in Range must not be higher than $QUERY_RESULT_LIMIT but was ${range.limit}"
)
}
andregasser marked this conversation as resolved.
Show resolved Hide resolved

return client.getPageableMastodonRequest<Account>(
endpoint = endorsementsEndpoint,
method = MastodonClient.Method.GET,
parameters = range.toParameters()
)
}

}
51 changes: 51 additions & 0 deletions bigbone/src/test/assets/endorsements_view_success.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[
{
"id": "952529",
"username": "alayna",
"acct": "[email protected]",
"display_name": "Alayna Desirae",
"locked": true,
"bot": false,
"created_at": "2019-10-26T23:12:06.570Z",
"note": "experiencing ________ difficulties<br>22y/o INFP in Oklahoma",
"url": "https://desvox.es/users/alayna",
"avatar": "https://files.mastodon.social/accounts/avatars/000/952/529/original/6534122046d050d5.png",
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/952/529/original/6534122046d050d5.png",
"header": "https://files.mastodon.social/accounts/headers/000/952/529/original/496f1f817e042ade.png",
"header_static": "https://files.mastodon.social/accounts/headers/000/952/529/original/496f1f817e042ade.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 955,
"last_status_at": "2019-11-23T07:05:50.682Z",
"emojis": [],
"fields": []
},
{
"id": "832844",
"username": "a9",
"acct": "[email protected]",
"display_name": "vivienne :collar: ",
"locked": true,
"bot": false,
"created_at": "2019-06-12T18:55:12.053Z",
"note": "borderline nsfw, considered a schedule I drug by nixon<br>waiting for the year of the illumos desktop",
"url": "https://broadcast.wolfgirl.engineering/users/a9",
"avatar": "https://files.mastodon.social/accounts/avatars/000/832/844/original/ae1de0b8fb63d1c6.png",
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/832/844/original/ae1de0b8fb63d1c6.png",
"header": "https://files.mastodon.social/accounts/headers/000/832/844/original/5088e4a16e6d8736.png",
"header_static": "https://files.mastodon.social/accounts/headers/000/832/844/original/5088e4a16e6d8736.png",
"followers_count": 43,
"following_count": 67,
"statuses_count": 5906,
"last_status_at": "2019-11-23T05:23:47.911Z",
"emojis": [
{
"shortcode": "collar",
"url": "https://files.mastodon.social/custom_emojis/images/000/106/920/original/80953b9cd96ec4dc.png",
"static_url": "https://files.mastodon.social/custom_emojis/images/000/106/920/static/80953b9cd96ec4dc.png",
"visible_in_picker": true
}
],
"fields": []
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package social.bigbone.api.method

import org.amshove.kluent.invoking
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldThrow
import org.amshove.kluent.withMessage
import org.junit.jupiter.api.Test
import social.bigbone.api.Pageable
import social.bigbone.api.Range
import social.bigbone.api.entity.Account
import social.bigbone.api.exception.BigBoneRequestException
import social.bigbone.testtool.MockClient

class EndorsementMethodsTest {

@Test
fun `Given a client returning success, when getting endorsements, then expect values of response`() {
val client = MockClient.mock("endorsements_view_success.json")

val endorsementMethods = EndorsementMethods(client)
val endorsements: Pageable<Account> = endorsementMethods.getEndorsements().execute()

endorsements.part.size shouldBeEqualTo 2
val firstEndorsement = endorsements.part[0]
firstEndorsement.id shouldBeEqualTo "952529"
firstEndorsement.isLocked shouldBeEqualTo true
firstEndorsement.isBot shouldBeEqualTo false
firstEndorsement.statusesCount shouldBeEqualTo 955
firstEndorsement.lastStatusAt shouldBeEqualTo "2019-11-23T07:05:50.682Z"
firstEndorsement.emojis.isEmpty() shouldBeEqualTo true

val secondEndorsement = endorsements.part[1]
secondEndorsement.id shouldBeEqualTo "832844"
secondEndorsement.isLocked shouldBeEqualTo true
secondEndorsement.isBot shouldBeEqualTo false
secondEndorsement.statusesCount shouldBeEqualTo 5906
secondEndorsement.lastStatusAt shouldBeEqualTo "2019-11-23T05:23:47.911Z"
secondEndorsement.emojis.isEmpty() shouldBeEqualTo false
}

@Test
fun `Given a client returning success, when getting endorsements with a limit of 90, then throw IllegalArgumentException`() {
val client = MockClient.mock("endorsements_view_success.json")

invoking {
EndorsementMethods(client).getEndorsements(
range = Range(limit = 90)
).execute()
} shouldThrow IllegalArgumentException::class withMessage "limit defined in Range must not be higher than 80 but was 90"
}

@Test
fun `Given a client returning unauthorized, when getting featured_tags, then propagate error`() {
val client = MockClient.failWithResponse(
responseJsonAssetPath = "error_401_unauthorized.json",
responseCode = 401,
message = "Unauthorized"
)

invoking {
EndorsementMethods(client).getEndorsements().execute()
} shouldThrow BigBoneRequestException::class withMessage "Unauthorized"
}

}
Loading