package social.grain import android.net.Uri import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import social.grain.navigation.DeepLink /** * The manifest claims `https://grain.social/profile/...`, so every link the * system hands over has to resolve to something — a claimed link that lands on * the feed is worse than one that opened the browser. * * Instrumented rather than a plain unit test because `Uri` is stubbed out on * the JVM and every parse would return null for the wrong reason. */ @RunWith(AndroidJUnit4::class) class DeepLinkTest { private fun parse(url: String) = DeepLink.from(Uri.parse(url)) @Test fun profileLinkResolvesToTheProfile() { assertEquals( DeepLink.Profile("did:plc:abc"), parse("https://grain.social/profile/did:plc:abc"), ) } /** Shared links usually name the handle, not the DID. */ @Test fun profileLinkAcceptsAHandle() { assertEquals( DeepLink.Profile("alice.grain.social"), parse("https://grain.social/profile/alice.grain.social"), ) } @Test fun galleryLinkCarriesTheAtUri() { val link = parse("https://grain.social/profile/did:plc:abc/gallery/3kabc") assertEquals(DeepLink.Gallery("did:plc:abc", "3kabc"), link) assertEquals( "at://did:plc:abc/social.grain.gallery/3kabc", (link as DeepLink.Gallery).uri, ) } @Test fun storyLinkResolvesToTheAuthorsReel() { assertEquals( DeepLink.Story("did:plc:abc", "3kxyz"), parse("https://grain.social/profile/did:plc:abc/story/3kxyz"), ) } /** A trailing slash is what a share sheet hands over half the time. */ @Test fun trailingSlashesAreIgnored() { assertEquals( DeepLink.Profile("did:plc:abc"), parse("https://grain.social/profile/did:plc:abc/"), ) } @Test fun grainSchemeUsesTheHostAsTheFirstSegment() { assertEquals( DeepLink.Gallery("did:plc:abc", "3kabc"), parse("grain://profile/did:plc:abc/gallery/3kabc"), ) } /** MainActivity handles this one before the parser sees it; it must not be * mistaken for a profile either way. */ @Test fun oauthCallbackIsNotADeepLink() { assertNull(parse("grain://oauth/callback?code=abc")) } @Test fun unknownPathsAreRejectedRatherThanFallingBackToTheFeed() { assertNull(parse("https://grain.social/about")) assertNull(parse("https://grain.social/")) assertNull(parse("https://grain.social/profile")) assertNull(parse("https://example.com/profile/did:plc:abc")) } /** A sub-path we don't recognise still names an account, so the profile is * a better answer than dropping the link. */ @Test fun unknownSubPathFallsBackToTheProfile() { assertEquals( DeepLink.Profile("did:plc:abc"), parse("https://grain.social/profile/did:plc:abc/photos/3kabc"), ) } // MARK: - Push payloads @Test fun followPushOpensTheFollowersProfile() { assertEquals( DeepLink.Profile("did:plc:abc"), DeepLink.fromPush(type = "follow", uri = null, did = "did:plc:abc"), ) } @Test fun galleryFavoritePushOpensTheGallery() { assertEquals( DeepLink.Gallery("did:plc:abc", "3kabc"), DeepLink.fromPush( type = "gallery-favorite", uri = "at://did:plc:abc/social.grain.gallery/3kabc", did = null, ), ) } /** A comment names itself, not the gallery it hangs off, so the author's * profile is the honest destination. */ @Test fun commentReplyPushFallsBackToTheAuthor() { assertEquals( DeepLink.Profile("did:plc:abc"), DeepLink.fromPush( type = "comment-reply", uri = "at://did:plc:abc/social.grain.comment/3kabc", did = null, ), ) } @Test fun payloadWithNoTypeIsIgnored() { assertNull(DeepLink.fromPush(type = null, uri = null, did = null)) assertNull(DeepLink.fromPush(type = "follow", uri = null, did = null)) } }