1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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))
}
}