Skip to content

internal_link: Always include a "/" after hostname #1059

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

Merged
merged 4 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/model/internal_link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) {
fragment.write('/near/$nearMessageId');
}

return store.realmUrl.replace(fragment: fragment.toString());
Uri result = store.realmUrl.replace(fragment: fragment.toString());
if (result.path.isEmpty) {
// Always ensure that there is a '/' right after the hostname.
// A generated URL without '/' looks odd,
// and if used in a Zulip message does not get automatically linkified.
result = result.replace(path: '/');
}
return result;
}

/// A [Narrow] from a given URL, on `store`'s realm.
Expand Down
96 changes: 0 additions & 96 deletions test/model/compose_test.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'package:checks/checks.dart';
import 'package:test/scaffolding.dart';
import 'package:zulip/model/compose.dart';
import 'package:zulip/model/narrow.dart';
import 'package:zulip/model/internal_link.dart';

import '../example_data.dart' as eg;
import 'test_store.dart';
Expand Down Expand Up @@ -222,100 +220,6 @@ hello
});
});

group('narrowLink', () {
test('CombinedFeedNarrow', () {
final store = eg.store();
check(narrowLink(store, const CombinedFeedNarrow()))
.equals(store.realmUrl.resolve('#narrow'));
check(narrowLink(store, const CombinedFeedNarrow(), nearMessageId: 1))
.equals(store.realmUrl.resolve('#narrow/near/1'));
});

test('MentionsNarrow', () {
final store = eg.store();
check(narrowLink(store, const MentionsNarrow()))
.equals(store.realmUrl.resolve('#narrow/is/mentioned'));
check(narrowLink(store, const MentionsNarrow(), nearMessageId: 1))
.equals(store.realmUrl.resolve('#narrow/is/mentioned/near/1'));
});

test('StarredMessagesNarrow', () {
final store = eg.store();
check(narrowLink(store, const StarredMessagesNarrow()))
.equals(store.realmUrl.resolve('#narrow/is/starred'));
check(narrowLink(store, const StarredMessagesNarrow(), nearMessageId: 1))
.equals(store.realmUrl.resolve('#narrow/is/starred/near/1'));
});

test('ChannelNarrow / TopicNarrow', () {
void checkNarrow(String expectedFragment, {
required int streamId,
required String name,
String? topic,
int? nearMessageId,
}) async {
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
final store = eg.store();
await store.addStream(eg.stream(streamId: streamId, name: name));
final narrow = topic == null
? ChannelNarrow(streamId)
: TopicNarrow(streamId, topic);
check(narrowLink(store, narrow, nearMessageId: nearMessageId))
.equals(store.realmUrl.resolve(expectedFragment));
}

checkNarrow(streamId: 1, name: 'announce', '#narrow/stream/1-announce');
checkNarrow(streamId: 378, name: 'api design', '#narrow/stream/378-api-design');
checkNarrow(streamId: 391, name: 'Outreachy', '#narrow/stream/391-Outreachy');
checkNarrow(streamId: 415, name: 'chat.zulip.org', '#narrow/stream/415-chat.2Ezulip.2Eorg');
checkNarrow(streamId: 419, name: 'français', '#narrow/stream/419-fran.C3.A7ais');
checkNarrow(streamId: 403, name: 'Hshs[™~}(.', '#narrow/stream/403-Hshs.5B.E2.84.A2~.7D.28.2E');
checkNarrow(streamId: 60, name: 'twitter', nearMessageId: 1570686, '#narrow/stream/60-twitter/near/1570686');

checkNarrow(streamId: 48, name: 'mobile', topic: 'Welcome screen UI',
'#narrow/stream/48-mobile/topic/Welcome.20screen.20UI');
checkNarrow(streamId: 243, name: 'mobile-team', topic: 'Podfile.lock clash #F92',
'#narrow/stream/243-mobile-team/topic/Podfile.2Elock.20clash.20.23F92');
checkNarrow(streamId: 377, name: 'translation/zh_tw', topic: '翻譯 "stream"',
'#narrow/stream/377-translation.2Fzh_tw/topic/.E7.BF.BB.E8.AD.AF.20.22stream.22');
checkNarrow(streamId: 42, name: 'Outreachy 2016-2017', topic: '2017-18 Stream?', nearMessageId: 302690,
'#narrow/stream/42-Outreachy-2016-2017/topic/2017-18.20Stream.3F/near/302690');
});

test('DmNarrow', () {
void checkNarrow(String expectedFragment, String legacyExpectedFragment, {
required List<int> allRecipientIds,
required int selfUserId,
int? nearMessageId,
}) {
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
final store = eg.store();
final narrow = DmNarrow(allRecipientIds: allRecipientIds, selfUserId: selfUserId);
check(narrowLink(store, narrow, nearMessageId: nearMessageId))
.equals(store.realmUrl.resolve(expectedFragment));
store.connection.zulipFeatureLevel = 176;
check(narrowLink(store, narrow, nearMessageId: nearMessageId))
.equals(store.realmUrl.resolve(legacyExpectedFragment));
}

checkNarrow(allRecipientIds: [1], selfUserId: 1,
'#narrow/dm/1-dm',
'#narrow/pm-with/1-pm');
checkNarrow(allRecipientIds: [1, 2], selfUserId: 1,
'#narrow/dm/1,2-dm',
'#narrow/pm-with/1,2-pm');
checkNarrow(allRecipientIds: [1, 2, 3], selfUserId: 1,
'#narrow/dm/1,2,3-group',
'#narrow/pm-with/1,2,3-group');
checkNarrow(allRecipientIds: [1, 2, 3, 4], selfUserId: 4,
'#narrow/dm/1,2,3,4-group',
'#narrow/pm-with/1,2,3,4-group');
checkNarrow(allRecipientIds: [1, 2], selfUserId: 1, nearMessageId: 12345,
'#narrow/dm/1,2-dm/near/12345',
'#narrow/pm-with/1,2-pm/near/12345');
});
});

group('mention', () {
final user = eg.user(userId: 123, fullName: 'Full Name');
test('not silent', () {
Expand Down
117 changes: 114 additions & 3 deletions test/model/internal_link_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Future<PerAccountStore> setupStore({
List<ZulipStream>? streams,
List<User>? users,
}) async {
final account = eg.selfAccount.copyWith(realmUrl: realmUrl);
final account = eg.account(user: eg.selfUser, realmUrl: realmUrl);
final store = eg.store(account: account);
if (streams != null) {
await store.addStreams(streams);
Expand All @@ -35,6 +35,117 @@ Future<PerAccountStore> setupStore({
}

void main() {
group('narrowLink', () {
test('CombinedFeedNarrow', () {
final store = eg.store();
check(narrowLink(store, const CombinedFeedNarrow()))
.equals(store.realmUrl.resolve('#narrow'));
check(narrowLink(store, const CombinedFeedNarrow(), nearMessageId: 1))
.equals(store.realmUrl.resolve('#narrow/near/1'));
});

test('MentionsNarrow', () {
final store = eg.store();
check(narrowLink(store, const MentionsNarrow()))
.equals(store.realmUrl.resolve('#narrow/is/mentioned'));
check(narrowLink(store, const MentionsNarrow(), nearMessageId: 1))
.equals(store.realmUrl.resolve('#narrow/is/mentioned/near/1'));
});

test('StarredMessagesNarrow', () {
final store = eg.store();
check(narrowLink(store, const StarredMessagesNarrow()))
.equals(store.realmUrl.resolve('#narrow/is/starred'));
check(narrowLink(store, const StarredMessagesNarrow(), nearMessageId: 1))
.equals(store.realmUrl.resolve('#narrow/is/starred/near/1'));
});

test('ChannelNarrow / TopicNarrow', () {
void checkNarrow(String expectedFragment, {
required int streamId,
required String name,
String? topic,
int? nearMessageId,
}) async {
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
final store = eg.store();
await store.addStream(eg.stream(streamId: streamId, name: name));
final narrow = topic == null
? ChannelNarrow(streamId)
: TopicNarrow(streamId, topic);
check(narrowLink(store, narrow, nearMessageId: nearMessageId))
.equals(store.realmUrl.resolve(expectedFragment));
}

checkNarrow(streamId: 1, name: 'announce', '#narrow/stream/1-announce');
checkNarrow(streamId: 378, name: 'api design', '#narrow/stream/378-api-design');
checkNarrow(streamId: 391, name: 'Outreachy', '#narrow/stream/391-Outreachy');
checkNarrow(streamId: 415, name: 'chat.zulip.org', '#narrow/stream/415-chat.2Ezulip.2Eorg');
checkNarrow(streamId: 419, name: 'français', '#narrow/stream/419-fran.C3.A7ais');
checkNarrow(streamId: 403, name: 'Hshs[™~}(.', '#narrow/stream/403-Hshs.5B.E2.84.A2~.7D.28.2E');
checkNarrow(streamId: 60, name: 'twitter', nearMessageId: 1570686, '#narrow/stream/60-twitter/near/1570686');

checkNarrow(streamId: 48, name: 'mobile', topic: 'Welcome screen UI',
'#narrow/stream/48-mobile/topic/Welcome.20screen.20UI');
checkNarrow(streamId: 243, name: 'mobile-team', topic: 'Podfile.lock clash #F92',
'#narrow/stream/243-mobile-team/topic/Podfile.2Elock.20clash.20.23F92');
checkNarrow(streamId: 377, name: 'translation/zh_tw', topic: '翻譯 "stream"',
'#narrow/stream/377-translation.2Fzh_tw/topic/.E7.BF.BB.E8.AD.AF.20.22stream.22');
checkNarrow(streamId: 42, name: 'Outreachy 2016-2017', topic: '2017-18 Stream?', nearMessageId: 302690,
'#narrow/stream/42-Outreachy-2016-2017/topic/2017-18.20Stream.3F/near/302690');
});

test('DmNarrow', () {
void checkNarrow(String expectedFragment, String legacyExpectedFragment, {
required List<int> allRecipientIds,
required int selfUserId,
int? nearMessageId,
}) {
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
final store = eg.store();
final narrow = DmNarrow(allRecipientIds: allRecipientIds, selfUserId: selfUserId);
check(narrowLink(store, narrow, nearMessageId: nearMessageId))
.equals(store.realmUrl.resolve(expectedFragment));
store.connection.zulipFeatureLevel = 176;
check(narrowLink(store, narrow, nearMessageId: nearMessageId))
.equals(store.realmUrl.resolve(legacyExpectedFragment));
}

checkNarrow(allRecipientIds: [1], selfUserId: 1,
'#narrow/dm/1-dm',
'#narrow/pm-with/1-pm');
checkNarrow(allRecipientIds: [1, 2], selfUserId: 1,
'#narrow/dm/1,2-dm',
'#narrow/pm-with/1,2-pm');
checkNarrow(allRecipientIds: [1, 2, 3], selfUserId: 1,
'#narrow/dm/1,2,3-group',
'#narrow/pm-with/1,2,3-group');
checkNarrow(allRecipientIds: [1, 2, 3, 4], selfUserId: 4,
'#narrow/dm/1,2,3,4-group',
'#narrow/pm-with/1,2,3,4-group');
checkNarrow(allRecipientIds: [1, 2], selfUserId: 1, nearMessageId: 12345,
'#narrow/dm/1,2-dm/near/12345',
'#narrow/pm-with/1,2-pm/near/12345');
});

test('normalize links to always include a "/" after hostname', () {
String narrowLinkFor({required String realmUrl}) {
final store = eg.store(
account: eg.account(user: eg.selfUser, realmUrl: Uri.parse(realmUrl)));
return narrowLink(store, const CombinedFeedNarrow()).toString();
}

check(narrowLinkFor(realmUrl: 'http://chat.example.com'))
.equals( 'http://chat.example.com/#narrow');
check(narrowLinkFor(realmUrl: 'http://chat.example.com/'))
.equals( 'http://chat.example.com/#narrow');
check(narrowLinkFor(realmUrl: 'http://chat.example.com/path'))
.equals( 'http://chat.example.com/path#narrow');
check(narrowLinkFor(realmUrl: 'http://chat.example.com/path/'))
.equals( 'http://chat.example.com/path/#narrow');
});
});

final realmUrl = Uri.parse('https://example.com/');

void testExpectedNarrows(List<(String, Narrow?)> testCases, {
Expand All @@ -55,7 +166,7 @@ void main() {
}
}

group('parseInternalLink', () {
group('parseInternalLink is-internal', () {
final streams = [
eg.stream(streamId: 1, name: 'check'),
];
Expand Down Expand Up @@ -398,7 +509,7 @@ void main() {
});
});

group('parseInternalLink', () {
group('parseInternalLink again', () { // TODO perhaps unify with tests above
group('topic link parsing', () {
final stream = eg.stream(name: "general");

Expand Down