Something went wrong. Try again.
Main coves client
Something went wrong. Try again.
27 kB · 883 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884import 'package:coves_flutter/config/environment_config.dart';import 'package:coves_flutter/utils/web_link_builder.dart';import 'package:flutter_test/flutter_test.dart';
void main() { const production = WebLinkBuilder(webUrl: 'https://coves.social'); const communityDid = 'did:plc:community456';
group('WebLinkBuilder.community', () { test('local community uses the bare name segment', () { expect( production.community( did: communityDid, name: 'Gaming', origin: 'coves.social', ), 'https://coves.social/c/gaming', ); });
test('trims and lowercases the name and the origin', () { expect( production.community( did: communityDid, name: ' Gaming ', origin: ' COVES.SOCIAL ', ), 'https://coves.social/c/gaming', ); });
test('remote community keeps a literal @ between name and origin', () { expect( production.community( did: communityDid, name: 'ComicStrips', origin: 'Lemmy.World', ), 'https://coves.social/c/comicstrips@lemmy.world', ); });
test('local match ignores the port in the web URL', () { const local = WebLinkBuilder(webUrl: 'http://127.0.0.1:8080'); expect( local.community(did: communityDid, name: 'Gaming', origin: '127.0.0.1'), 'http://127.0.0.1:8080/c/gaming', ); });
test('a trailing slash on the web URL does not double up', () { const trailingSlash = WebLinkBuilder(webUrl: 'https://coves.social/'); expect( trailingSlash.community( did: communityDid, name: 'Gaming', origin: 'coves.social', ), 'https://coves.social/c/gaming', ); });
test('a 63-character name is still a valid DNS label', () { const longestValidName = 'gaming-community-with-a-very-long-but-still-valid-name-abcdefgh'; expect(longestValidName.length, 63); expect( production.community( did: communityDid, name: longestValidName, origin: 'coves.social', ), 'https://coves.social/c/gaming-community-with-a-very-long-but-' 'still-valid-name-abcdefgh', ); }); });
group('WebLinkBuilder.community fallbacks', () { // The web matcher accepts a bare DID for /c/ and redirects it to the // canonical form, so anything canonicalCommunityParam rejects falls // back to the DID segment. const didLink = 'https://coves.social/c/did%3Aplc%3Acommunity456';
test('null origin falls back to the DID', () { expect(production.community(did: communityDid, name: 'Gaming'), didLink); });
test('blank origin falls back to the DID', () { expect( production.community(did: communityDid, name: 'Gaming', origin: ' '), didLink, ); });
test('blank name falls back to the DID', () { expect( production.community( did: communityDid, name: ' ', origin: 'coves.social', ), didLink, ); });
test('the DID segment is percent-encoded exactly once', () { final link = production.community(did: communityDid, name: 'Gaming'); expect(link, didLink); expect(link, isNot(contains('%253A'))); });
test('a name with an underscore falls back to the DID', () { expect( production.community( did: communityDid, name: 'cool_stuff', origin: 'coves.social', ), didLink, ); });
test('a dotted name falls back to the DID', () { expect( production.community( did: communityDid, name: 'a.b', origin: 'coves.social', ), didLink, ); });
test('a 64-character name falls back to the DID', () { const overlongName = 'gaming-community-with-a-very-long-but-still-valid-name-abcdefghi'; expect(overlongName.length, 64); expect( production.community( did: communityDid, name: overlongName, origin: 'coves.social', ), didLink, ); });
test('a remote origin with a space falls back to the DID', () { expect( production.community( did: communityDid, name: 'Gaming', origin: 'not a domain', ), didLink, ); });
test('a remote origin with no dot falls back to the DID', () { expect( production.community( did: communityDid, name: 'Gaming', origin: 'nodots', ), didLink, ); });
test('a remote origin with an underscore falls back to the DID', () { expect( production.community( did: communityDid, name: 'Gaming', origin: 'exa_mple.com', ), didLink, ); }); });
group('WebLinkBuilder.community with an unusable DID', () { // The DID segment is only a link because the web matcher accepts a bare // DID and redirects it. A value that is not a DID matches nothing there, // so there is no link to hand out at all. test('an unusable name and a relative-path DID has no link', () { expect(production.community(did: '..', name: ''), isNull); });
test('an unusable name and a DID that is not a DID has no link', () { expect(production.community(did: 'x@y', name: 'Gaming'), isNull); });
test('a canonical name form never looks at the DID', () { expect( production.community( did: 'x@y', name: 'Gaming', origin: 'coves.social', ), 'https://coves.social/c/gaming', ); }); });
group('WebLinkBuilder.post', () { // The owner segment names the repo the record lives in, which is the // at-URI authority: the author for a postv2 record, the community for a // legacy community-owned one. const authorDid = 'did:plc:author123'; const authorHandle = 'alice.coves.social'; const authorOwnedUri = 'at://did:plc:author123/social.coves.community.postv2/3kabcxyz'; const communityOwnedUri = 'at://did:plc:community456/social.coves.community.post/3kabcxyz';
test('an author-owned post uses the author handle as the owner', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, authorHandle: authorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/alice.coves.social/3kabcxyz', ); });
test('a legacy community-owned post uses the community DID as the ' 'owner, not the author handle', () { expect( production.post( postUri: communityOwnedUri, authorDid: authorDid, authorHandle: authorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/did%3Aplc%3Acommunity456/3kabcxyz', ); });
test('a null author handle falls back to the authority DID', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/did%3Aplc%3Aauthor123/3kabcxyz', ); });
test('an unresolved handle.invalid falls back to the authority DID', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, authorHandle: 'handle.invalid', communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/did%3Aplc%3Aauthor123/3kabcxyz', ); });
test('a remote community keeps the name@origin segment', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, authorHandle: authorHandle, communityDid: communityDid, communityName: 'ComicStrips', communityOrigin: 'lemmy.world', ), 'https://coves.social/c/comicstrips@lemmy.world/post/' 'alice.coves.social/3kabcxyz', ); });
test('a community with no origin falls back to the community DID ' 'segment', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, authorHandle: authorHandle, communityDid: communityDid, communityName: 'Gaming', ), 'https://coves.social/c/did%3Aplc%3Acommunity456/post/' 'alice.coves.social/3kabcxyz', ); });
test('the owner DID is percent-encoded exactly once', () { final link = production.post( postUri: authorOwnedUri, authorDid: authorDid, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ); expect( link, 'https://coves.social/c/gaming/post/did%3Aplc%3Aauthor123/3kabcxyz', ); expect(link, isNot(contains('%253A'))); }); });
group('WebLinkBuilder.post with unusable identities', () { const authorDid = 'did:plc:author123'; const authorOwnedUri = 'at://did:plc:author123/social.coves.community.postv2/3kabcxyz'; const authorDidLink = 'https://coves.social/c/gaming/post/did%3Aplc%3Aauthor123/3kabcxyz';
test('a relative-path author handle falls back to the authority DID', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, authorHandle: '..', communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), authorDidLink, ); });
test('a community DID that is not a DID still builds under the name', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, authorHandle: 'alice.coves.social', communityDid: 'x@y', communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/alice.coves.social/3kabcxyz', ); });
test('a community DID that is not a DID and no origin has no link', () { expect( production.post( postUri: authorOwnedUri, authorDid: authorDid, authorHandle: 'alice.coves.social', communityDid: 'x@y', communityName: 'Gaming', ), isNull, ); }); });
group('WebLinkBuilder.post with a did:web authority', () { const didWebUri = 'at://did:web:alice.example.com/social.coves.community.postv2' '/3kabcxyz';
test('a matching did:web handle is the owner segment', () { expect( production.post( postUri: didWebUri, authorDid: 'did:web:alice.example.com', authorHandle: 'alice.example.com', communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/alice.example.com/3kabcxyz', ); });
test('a did:web authority with no handle is encoded exactly once', () { expect( production.post( postUri: didWebUri, authorDid: 'did:web:alice.example.com', communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/' 'did%3Aweb%3Aalice.example.com/3kabcxyz', ); });
test('a port-carrying did:web keeps its own escape intact', () { // did:web spells the port colon as %3A, and the path-segment encoding // escapes that percent in turn, so decoding the segment once hands the // DID back exactly as the record carries it. expect( production.post( postUri: 'at://did:web:example.com%3A8080' '/social.coves.community.postv2/3kabcxyz', authorDid: 'did:web:example.com%3A8080', communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/' 'did%3Aweb%3Aexample.com%253A8080/3kabcxyz', ); }); });
group('WebLinkBuilder.post malformed URIs', () { // Stricter than the web's parser on purpose: only an exact // at://<authority>/<collection>/<rkey> yields a link, so a malformed // record URI is never turned into a URL that points somewhere wrong. String? postLinkFor(String postUri) => production.post( postUri: postUri, authorDid: 'did:plc:author123', authorHandle: 'alice.coves.social', communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', );
test('an https URL is not an at-URI', () { expect( postLinkFor( 'https://did:plc:author123/social.coves.community.postv2/3kabcxyz', ), isNull, ); });
test('the at:// scheme must start the URI', () { expect( postLinkFor( 'xat://did:plc:author123/social.coves.community.postv2/3kabcxyz', ), isNull, ); });
test('an empty URI has no link', () { expect(postLinkFor(''), isNull); });
test('a bare scheme has no link', () { expect(postLinkFor('at://'), isNull); });
test('a missing record key has no link', () { expect( postLinkFor('at://did:plc:author123/social.coves.community.postv2'), isNull, ); });
test('a trailing slash is not a record key', () { expect( postLinkFor('at://did:plc:author123/social.coves.community.postv2/'), isNull, ); });
test('a missing collection has no link', () { expect(postLinkFor('at://did:plc:author123'), isNull); });
test('an empty authority has no link', () { expect( postLinkFor('at:///social.coves.community.postv2/3kabcxyz'), isNull, ); });
test('an extra path segment has no link', () { expect( postLinkFor( 'at://did:plc:author123/social.coves.community.postv2/3kabcxyz' '/extra', ), isNull, ); });
test('a query string has no link', () { expect( postLinkFor( 'at://did:plc:author123/social.coves.community.postv2/3kabcxyz' '?x=1', ), isNull, ); });
test('a fragment has no link', () { expect( postLinkFor( 'at://did:plc:author123/social.coves.community.postv2/3kabcxyz' '#frag', ), isNull, ); });
test('a handle authority has no link', () { // The AppView always emits DID authorities; echoing a handle as the // owner segment would address a repo the record may not live in. expect( postLinkFor( 'at://alice.coves.social/social.coves.community.postv2/3kabcxyz', ), isNull, ); });
test('whitespace inside the record key has no link', () { expect( postLinkFor( 'at://did:plc:author123/social.coves.community.postv2/3k abc', ), isNull, ); }); });
group('WebLinkBuilder.comment', () { // The commenter segment follows the same repo rule as the post owner, // but against the comment URI's authority: the handle only when the // commenter ref proves the comment lives in that repo. const postUri = 'at://did:plc:author123/social.coves.community.postv2/3kabcxyz'; const postAuthorDid = 'did:plc:author123'; const postAuthorHandle = 'alice.coves.social'; const commentUri = 'at://did:plc:commenter789/social.coves.community.comment/3kcmt001'; const commenterDid = 'did:plc:commenter789'; const didPermalink = 'https://coves.social/c/gaming/post/' 'alice.coves.social/3kabcxyz/comment/' 'did%3Aplc%3Acommenter789/3kcmt001';
test('a resolved commenter handle is the commenter segment', () { expect( production.comment( commentUri: commentUri, commenterDid: commenterDid, commenterHandle: 'bob.coves.social', postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/alice.coves.social/3kabcxyz' '/comment/bob.coves.social/3kcmt001', ); });
test('a null commenter handle falls back to the authority DID', () { expect( production.comment( commentUri: commentUri, commenterDid: commenterDid, postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), didPermalink, ); });
test('an unresolved handle.invalid falls back to the authority DID', () { expect( production.comment( commentUri: commentUri, commenterDid: commenterDid, commenterHandle: 'handle.invalid', postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), didPermalink, ); });
test('an absent commenter still yields the DID permalink', () { // A deleted or unhydrated comment author leaves the URI authority as // the only identity, which is a working permalink on its own. expect( production.comment( commentUri: commentUri, postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), didPermalink, ); });
test('a commenter from another repo does not lend its handle', () { expect( production.comment( commentUri: commentUri, commenterDid: postAuthorDid, commenterHandle: postAuthorHandle, postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), didPermalink, ); });
test('a legacy community-owned post in a remote community composes', () { expect( production.comment( commentUri: commentUri, commenterDid: commenterDid, commenterHandle: 'bob.coves.social', postUri: 'at://did:plc:community456/social.coves.community.post/3kabcxyz', postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'ComicStrips', communityOrigin: 'lemmy.world', ), 'https://coves.social/c/comicstrips@lemmy.world/post/' 'did%3Aplc%3Acommunity456/3kabcxyz' '/comment/bob.coves.social/3kcmt001', ); });
test('the commenter DID is percent-encoded exactly once', () { final link = production.comment( commentUri: commentUri, commenterDid: commenterDid, postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ); expect(link, didPermalink); expect(link, isNot(contains('%253A'))); });
test('a comment URI with a query string has no link', () { expect( production.comment( commentUri: '$commentUri?x=1', commenterDid: commenterDid, commenterHandle: 'bob.coves.social', postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), isNull, ); });
test('a comment URI with a handle authority has no link', () { expect( production.comment( commentUri: 'at://bob.coves.social/social.coves.community.comment/3kcmt001', commenterDid: commenterDid, commenterHandle: 'bob.coves.social', postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), isNull, ); });
test('a malformed post URI has no link even with a valid comment URI', () { expect( production.comment( commentUri: commentUri, commenterDid: commenterDid, commenterHandle: 'bob.coves.social', postUri: 'https://did:plc:author123/social.coves.community.postv2/3kabcxyz', postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), isNull, ); }); });
group('WebLinkBuilder.comment with unusable identities', () { const postUri = 'at://did:plc:author123/social.coves.community.postv2/3kabcxyz'; const postAuthorDid = 'did:plc:author123'; const postAuthorHandle = 'alice.coves.social'; const commentUri = 'at://did:plc:commenter789/social.coves.community.comment/3kcmt001'; const commenterDid = 'did:plc:commenter789';
test( 'a relative-path commenter handle falls back to the authority DID', () { expect( production.comment( commentUri: commentUri, commenterDid: commenterDid, commenterHandle: '..', postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: communityDid, communityName: 'Gaming', communityOrigin: 'coves.social', ), 'https://coves.social/c/gaming/post/alice.coves.social/3kabcxyz' '/comment/did%3Aplc%3Acommenter789/3kcmt001', ); }, );
test('a community DID that is not a DID and no origin has no link', () { expect( production.comment( commentUri: commentUri, commenterDid: commenterDid, commenterHandle: 'bob.coves.social', postUri: postUri, postAuthorDid: postAuthorDid, postAuthorHandle: postAuthorHandle, communityDid: 'x@y', communityName: 'Gaming', ), isNull, ); }); });
group('WebLinkBuilder.profile', () { // The web's actor matcher accepts a DID or a DNS handle, so an // unresolved handle is addressed by DID instead. const authorDid = 'did:plc:author123'; const didLink = 'https://coves.social/profile/did%3Aplc%3Aauthor123';
test('a resolved handle is the profile segment', () { expect( production.profile(did: authorDid, handle: 'alice.coves.social'), 'https://coves.social/profile/alice.coves.social', ); });
test('a null handle falls back to the DID', () { expect(production.profile(did: authorDid), didLink); });
test('an empty handle falls back to the DID', () { expect(production.profile(did: authorDid, handle: ''), didLink); });
test('a blank handle falls back to the DID', () { expect(production.profile(did: authorDid, handle: ' '), didLink); });
test('an unresolved handle.invalid falls back to the DID', () { expect( production.profile(did: authorDid, handle: 'handle.invalid'), didLink, ); });
test('surrounding whitespace is trimmed off the handle', () { expect( production.profile(did: authorDid, handle: ' alice.coves.social '), 'https://coves.social/profile/alice.coves.social', ); });
test('the DID segment is percent-encoded exactly once', () { final link = production.profile(did: authorDid); expect(link, didLink); expect(link, isNot(contains('%253A'))); }); });
group('WebLinkBuilder.profile handle grammar', () { // A handle segment has to satisfy the same hostname grammar as a remote // community origin: the web's handle matcher is the one rule, and a // segment outside it either 404s or addresses another route entirely. const authorDid = 'did:plc:author123'; const didLink = 'https://coves.social/profile/did%3Aplc%3Aauthor123';
test('a parent-directory handle falls back to the DID', () { expect(production.profile(did: authorDid, handle: '..'), didLink); });
test('a current-directory handle falls back to the DID', () { expect(production.profile(did: authorDid, handle: '.'), didLink); });
test('a handle with a space falls back to the DID', () { expect( production.profile(did: authorDid, handle: 'not a handle'), didLink, ); });
test('an undotted handle falls back to the DID', () { expect(production.profile(did: authorDid, handle: 'nodots'), didLink); });
test('a handle with an underscore falls back to the DID', () { expect( production.profile(did: authorDid, handle: 'exa_mple.com'), didLink, ); }); });
group('WebLinkBuilder.profile with no usable segment', () { // With no usable handle the DID is the segment, so a DID the web's actor // matcher would reject leaves nothing to link to. test('a parent-directory DID and no handle has no link', () { expect(production.profile(did: '..'), isNull); });
test('a DID that is not a DID has no link', () { expect(production.profile(did: 'not-a-did'), isNull); }); });
group('WebLinkBuilder.current', () { test('builds against the current environment web origin', () { expect(WebLinkBuilder.current().webUrl, EnvironmentConfig.current.webUrl); // Under `flutter test` the current environment is the test // environment, which keeps the production web origin for links. expect(WebLinkBuilder.current().webUrl, 'https://coves.social'); });
test('links built from it carry the production origin', () { expect( WebLinkBuilder.current().profile( did: 'did:plc:author123', handle: 'alice.coves.social', ), 'https://coves.social/profile/alice.coves.social', ); }); });}