the-algorithm/unified_user_actions/adapter/src/test/scala/com/twitter/unified_user_actions/adapter/SocialGraphAdapterSpec.scala
twitter-team 617c8c787d Open-sourcing Unified User Actions
Unified User Action (UUA) is a centralized, real-time stream of user actions on Twitter, consumed by various product, ML, and marketing teams. UUA makes sure all internal teams consume the uniformed user actions data in an accurate and fast way.
2023-04-14 16:45:37 -05:00

360 lines
12 KiB
Scala

package com.twitter.unified_user_actions.adapter
import com.twitter.inject.Test
import com.twitter.socialgraph.thriftscala.Action
import com.twitter.socialgraph.thriftscala.BlockGraphEvent
import com.twitter.socialgraph.thriftscala.FollowGraphEvent
import com.twitter.socialgraph.thriftscala.FollowRequestGraphEvent
import com.twitter.socialgraph.thriftscala.FollowRetweetsGraphEvent
import com.twitter.socialgraph.thriftscala.LogEventContext
import com.twitter.socialgraph.thriftscala.MuteGraphEvent
import com.twitter.socialgraph.thriftscala.ReportAsAbuseGraphEvent
import com.twitter.socialgraph.thriftscala.ReportAsSpamGraphEvent
import com.twitter.socialgraph.thriftscala.SrcTargetRequest
import com.twitter.socialgraph.thriftscala.WriteEvent
import com.twitter.socialgraph.thriftscala.WriteRequestResult
import com.twitter.unified_user_actions.adapter.social_graph_event.SocialGraphAdapter
import com.twitter.unified_user_actions.thriftscala._
import com.twitter.util.Time
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.prop.TableFor1
import org.scalatest.prop.TableFor3
class SocialGraphAdapterSpec extends Test with TableDrivenPropertyChecks {
trait Fixture {
val frozenTime: Time = Time.fromMilliseconds(1658949273000L)
val testLogEventContext: LogEventContext = LogEventContext(
timestamp = 1001L,
hostname = "",
transactionId = "",
socialGraphClientId = "",
loggedInUserId = Some(1111L),
)
val testWriteRequestResult: WriteRequestResult = WriteRequestResult(
request = SrcTargetRequest(
source = 1111L,
target = 2222L
)
)
val testWriteRequestResultWithValidationError: WriteRequestResult = WriteRequestResult(
request = SrcTargetRequest(
source = 1111L,
target = 2222L
),
validationError = Some("action unsuccessful")
)
val baseEvent: WriteEvent = WriteEvent(
context = testLogEventContext,
action = Action.AcceptFollowRequest
)
val sgFollowEvent: WriteEvent = baseEvent.copy(
action = Action.Follow,
follow = Some(List(FollowGraphEvent(testWriteRequestResult))))
val sgUnfollowEvent: WriteEvent = baseEvent.copy(
action = Action.Unfollow,
follow = Some(List(FollowGraphEvent(testWriteRequestResult))))
val sgFollowRedundantEvent: WriteEvent = baseEvent.copy(
action = Action.Follow,
follow = Some(
List(
FollowGraphEvent(
result = testWriteRequestResult,
redundantOperation = Some(true)
))))
val sgFollowRedundantIsFalseEvent: WriteEvent = baseEvent.copy(
action = Action.Follow,
follow = Some(
List(
FollowGraphEvent(
result = testWriteRequestResult,
redundantOperation = Some(false)
))))
val sgUnfollowRedundantEvent: WriteEvent = baseEvent.copy(
action = Action.Unfollow,
follow = Some(
List(
FollowGraphEvent(
result = testWriteRequestResult,
redundantOperation = Some(true)
))))
val sgUnfollowRedundantIsFalseEvent: WriteEvent = baseEvent.copy(
action = Action.Unfollow,
follow = Some(
List(
FollowGraphEvent(
result = testWriteRequestResult,
redundantOperation = Some(false)
))))
val sgUnsuccessfulFollowEvent: WriteEvent = baseEvent.copy(
action = Action.Follow,
follow = Some(List(FollowGraphEvent(testWriteRequestResultWithValidationError))))
val sgUnsuccessfulUnfollowEvent: WriteEvent = baseEvent.copy(
action = Action.Unfollow,
follow = Some(List(FollowGraphEvent(testWriteRequestResultWithValidationError))))
val sgBlockEvent: WriteEvent = baseEvent.copy(
action = Action.Block,
block = Some(List(BlockGraphEvent(testWriteRequestResult))))
val sgUnsuccessfulBlockEvent: WriteEvent = baseEvent.copy(
action = Action.Block,
block = Some(List(BlockGraphEvent(testWriteRequestResultWithValidationError))))
val sgUnblockEvent: WriteEvent = baseEvent.copy(
action = Action.Unblock,
block = Some(List(BlockGraphEvent(testWriteRequestResult))))
val sgUnsuccessfulUnblockEvent: WriteEvent = baseEvent.copy(
action = Action.Unblock,
block = Some(List(BlockGraphEvent(testWriteRequestResultWithValidationError))))
val sgMuteEvent: WriteEvent = baseEvent.copy(
action = Action.Mute,
mute = Some(List(MuteGraphEvent(testWriteRequestResult))))
val sgUnsuccessfulMuteEvent: WriteEvent = baseEvent.copy(
action = Action.Mute,
mute = Some(List(MuteGraphEvent(testWriteRequestResultWithValidationError))))
val sgUnmuteEvent: WriteEvent = baseEvent.copy(
action = Action.Unmute,
mute = Some(List(MuteGraphEvent(testWriteRequestResult))))
val sgUnsuccessfulUnmuteEvent: WriteEvent = baseEvent.copy(
action = Action.Unmute,
mute = Some(List(MuteGraphEvent(testWriteRequestResultWithValidationError))))
val sgCreateFollowRequestEvent: WriteEvent = baseEvent.copy(
action = Action.CreateFollowRequest,
followRequest = Some(List(FollowRequestGraphEvent(testWriteRequestResult)))
)
val sgCancelFollowRequestEvent: WriteEvent = baseEvent.copy(
action = Action.CancelFollowRequest,
followRequest = Some(List(FollowRequestGraphEvent(testWriteRequestResult)))
)
val sgAcceptFollowRequestEvent: WriteEvent = baseEvent.copy(
action = Action.AcceptFollowRequest,
followRequest = Some(List(FollowRequestGraphEvent(testWriteRequestResult)))
)
val sgAcceptFollowRetweetEvent: WriteEvent = baseEvent.copy(
action = Action.FollowRetweets,
followRetweets = Some(List(FollowRetweetsGraphEvent(testWriteRequestResult)))
)
val sgAcceptUnfollowRetweetEvent: WriteEvent = baseEvent.copy(
action = Action.UnfollowRetweets,
followRetweets = Some(List(FollowRetweetsGraphEvent(testWriteRequestResult)))
)
val sgReportAsSpamEvent: WriteEvent = baseEvent.copy(
action = Action.ReportAsSpam,
reportAsSpam = Some(
List(
ReportAsSpamGraphEvent(
result = testWriteRequestResult
))))
val sgReportAsAbuseEvent: WriteEvent = baseEvent.copy(
action = Action.ReportAsAbuse,
reportAsAbuse = Some(
List(
ReportAsAbuseGraphEvent(
result = testWriteRequestResult
))))
def getExpectedUUA(
userId: Long,
actionProfileId: Long,
sourceTimestampMs: Long,
actionType: ActionType,
socialGraphAction: Option[Action] = None
): UnifiedUserAction = {
val actionItem = socialGraphAction match {
case Some(sgAction) =>
Item.ProfileInfo(
ProfileInfo(
actionProfileId = actionProfileId,
profileActionInfo = Some(
ProfileActionInfo.ServerProfileReport(
ServerProfileReport(reportType = sgAction)
))
)
)
case _ =>
Item.ProfileInfo(
ProfileInfo(
actionProfileId = actionProfileId
)
)
}
UnifiedUserAction(
userIdentifier = UserIdentifier(userId = Some(userId)),
item = actionItem,
actionType = actionType,
eventMetadata = EventMetadata(
sourceTimestampMs = sourceTimestampMs,
receivedTimestampMs = frozenTime.inMilliseconds,
sourceLineage = SourceLineage.ServerSocialGraphEvents
)
)
}
val expectedUuaFollow: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileFollow
)
val expectedUuaUnfollow: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileUnfollow
)
val expectedUuaMute: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileMute
)
val expectedUuaUnmute: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileUnmute
)
val expectedUuaBlock: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileBlock
)
val expectedUuaUnblock: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileUnblock
)
val expectedUuaReportAsSpam: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileReport,
socialGraphAction = Some(Action.ReportAsSpam)
)
val expectedUuaReportAsAbuse: UnifiedUserAction = getExpectedUUA(
userId = 1111L,
actionProfileId = 2222L,
sourceTimestampMs = 1001L,
actionType = ActionType.ServerProfileReport,
socialGraphAction = Some(Action.ReportAsAbuse)
)
}
test("SocialGraphAdapter ignore events not in the list") {
new Fixture {
Time.withTimeAt(frozenTime) { _ =>
val ignoredSocialGraphEvents: TableFor1[WriteEvent] = Table(
"ignoredSocialGraphEvents",
sgAcceptUnfollowRetweetEvent,
sgAcceptFollowRequestEvent,
sgAcceptFollowRetweetEvent,
sgCreateFollowRequestEvent,
sgCancelFollowRequestEvent,
)
forEvery(ignoredSocialGraphEvents) { writeEvent: WriteEvent =>
val actual = SocialGraphAdapter.adaptEvent(writeEvent)
assert(actual.isEmpty)
}
}
}
}
test("Test SocialGraphAdapter consuming Write events") {
new Fixture {
Time.withTimeAt(frozenTime) { _ =>
val socialProfileActions: TableFor3[String, WriteEvent, UnifiedUserAction] = Table(
("actionType", "event", "expectedUnifiedUserAction"),
("ProfileFollow", sgFollowEvent, expectedUuaFollow),
("ProfileUnfollow", sgUnfollowEvent, expectedUuaUnfollow),
("ProfileBlock", sgBlockEvent, expectedUuaBlock),
("ProfileUnBlock", sgUnblockEvent, expectedUuaUnblock),
("ProfileMute", sgMuteEvent, expectedUuaMute),
("ProfileUnmute", sgUnmuteEvent, expectedUuaUnmute),
("ProfileReportAsSpam", sgReportAsSpamEvent, expectedUuaReportAsSpam),
("ProfileReportAsAbuse", sgReportAsAbuseEvent, expectedUuaReportAsAbuse),
)
forEvery(socialProfileActions) {
(_: String, event: WriteEvent, expected: UnifiedUserAction) =>
val actual = SocialGraphAdapter.adaptEvent(event)
assert(Seq(expected) === actual)
}
}
}
}
test("SocialGraphAdapter ignore redundant follow/unfollow events") {
new Fixture {
Time.withTimeAt(frozenTime) { _ =>
val socialGraphActions: TableFor3[String, WriteEvent, Seq[UnifiedUserAction]] = Table(
("actionType", "ignoredRedundantFollowUnfollowEvents", "expectedUnifiedUserAction"),
("ProfileFollow", sgFollowRedundantEvent, Nil),
("ProfileFollow", sgFollowRedundantIsFalseEvent, Seq(expectedUuaFollow)),
("ProfileUnfollow", sgUnfollowRedundantEvent, Nil),
("ProfileUnfollow", sgUnfollowRedundantIsFalseEvent, Seq(expectedUuaUnfollow))
)
forEvery(socialGraphActions) {
(_: String, event: WriteEvent, expected: Seq[UnifiedUserAction]) =>
val actual = SocialGraphAdapter.adaptEvent(event)
assert(expected === actual)
}
}
}
}
test("SocialGraphAdapter ignore Unsuccessful SocialGraph events") {
new Fixture {
Time.withTimeAt(frozenTime) { _ =>
val unsuccessfulSocialGraphEvents: TableFor1[WriteEvent] = Table(
"ignoredSocialGraphEvents",
sgUnsuccessfulFollowEvent,
sgUnsuccessfulUnfollowEvent,
sgUnsuccessfulBlockEvent,
sgUnsuccessfulUnblockEvent,
sgUnsuccessfulMuteEvent,
sgUnsuccessfulUnmuteEvent
)
forEvery(unsuccessfulSocialGraphEvents) { writeEvent: WriteEvent =>
val actual = SocialGraphAdapter.adaptEvent(writeEvent)
assert(actual.isEmpty)
}
}
}
}
}