@fedify/vocab 2.4.0-pr.934.40 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/mod.cjs +31713 -8698
- package/dist/mod.d.cts +4708 -7
- package/dist/mod.d.ts +4708 -7
- package/dist/mod.js +31606 -8598
- package/dist-tests/actor-D7_iX4I4.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/application.yaml +15 -0
- package/dist-tests/audio.yaml +2 -1
- package/dist-tests/document.yaml +14 -1
- package/dist-tests/{actor-CnnyTI7i.mjs → esm-DP2Spt0u.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/group.yaml +15 -0
- package/dist-tests/image.yaml +2 -1
- package/dist-tests/link.yaml +14 -1
- package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/organization.yaml +15 -0
- package/dist-tests/page.yaml +2 -1
- package/dist-tests/person.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/service.yaml +15 -0
- package/dist-tests/signed-representation.test.mjs +189 -0
- package/dist-tests/translation.test.mjs +404 -0
- package/dist-tests/translation.yaml +91 -0
- package/dist-tests/type.test.mjs +2 -2
- package/dist-tests/video.yaml +2 -1
- package/dist-tests/{vocab-OU5GYbNg.mjs → vocab-COZcxf00.mjs} +31227 -8501
- package/dist-tests/vocab.test.mjs +824 -6
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +345 -30
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/application.yaml +15 -0
- package/src/audio.yaml +2 -1
- package/src/document.yaml +14 -1
- package/src/gateway.test.ts +2637 -0
- package/src/group.yaml +15 -0
- package/src/image.yaml +2 -1
- package/src/link.yaml +14 -1
- package/src/lookup.test.ts +6 -5
- package/src/lookup.ts +460 -33
- package/src/mod.ts +9 -0
- package/src/object.yaml +15 -0
- package/src/organization.yaml +15 -0
- package/src/page.yaml +2 -1
- package/src/person.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +4 -2
- package/src/service.yaml +15 -0
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/video.yaml +2 -1
- package/src/vocab.test.ts +1155 -2
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
package/src/vocab.test.ts
CHANGED
|
@@ -2,8 +2,11 @@ import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
|
2
2
|
import {
|
|
3
3
|
decodeMultibase,
|
|
4
4
|
type DocumentLoader,
|
|
5
|
+
FetchError,
|
|
6
|
+
getDocumentLoader,
|
|
5
7
|
LanguageString,
|
|
6
8
|
parseDecimal,
|
|
9
|
+
parseIri,
|
|
7
10
|
type RemoteDocument,
|
|
8
11
|
} from "@fedify/vocab-runtime";
|
|
9
12
|
import {
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
} from "@fedify/vocab-tools";
|
|
15
18
|
import { configure, type LogRecord, reset } from "@logtape/logtape";
|
|
16
19
|
import { pascalCase } from "es-toolkit";
|
|
20
|
+
import fetchMock from "fetch-mock";
|
|
17
21
|
import {
|
|
18
22
|
deepStrictEqual,
|
|
19
23
|
notDeepStrictEqual,
|
|
@@ -21,6 +25,7 @@ import {
|
|
|
21
25
|
rejects,
|
|
22
26
|
throws,
|
|
23
27
|
} from "node:assert/strict";
|
|
28
|
+
import { inspect } from "node:util";
|
|
24
29
|
import { assertInstanceOf } from "./utils.ts";
|
|
25
30
|
import * as vocab from "./vocab.ts";
|
|
26
31
|
import {
|
|
@@ -28,6 +33,7 @@ import {
|
|
|
28
33
|
Activity,
|
|
29
34
|
Agreement,
|
|
30
35
|
Announce,
|
|
36
|
+
Application,
|
|
31
37
|
Collection,
|
|
32
38
|
Commitment,
|
|
33
39
|
Create,
|
|
@@ -41,7 +47,9 @@ import {
|
|
|
41
47
|
FeaturedItem,
|
|
42
48
|
FeatureRequest,
|
|
43
49
|
Follow,
|
|
50
|
+
Group,
|
|
44
51
|
Hashtag,
|
|
52
|
+
Image,
|
|
45
53
|
Intent,
|
|
46
54
|
InteractionPolicy,
|
|
47
55
|
InteractionRule,
|
|
@@ -52,13 +60,16 @@ import {
|
|
|
52
60
|
Object,
|
|
53
61
|
Offer,
|
|
54
62
|
OrderedCollectionPage,
|
|
63
|
+
Organization,
|
|
55
64
|
Person,
|
|
56
65
|
Place,
|
|
66
|
+
PropertyValue,
|
|
57
67
|
Proposal,
|
|
58
68
|
Question,
|
|
59
69
|
QuoteAuthorization,
|
|
60
70
|
QuoteRequest,
|
|
61
71
|
Reject,
|
|
72
|
+
Service,
|
|
62
73
|
Source,
|
|
63
74
|
Tombstone,
|
|
64
75
|
} from "./vocab.ts";
|
|
@@ -392,6 +403,41 @@ test("Object.toJsonLd()", async () => {
|
|
|
392
403
|
});
|
|
393
404
|
});
|
|
394
405
|
|
|
406
|
+
test("Note.fromJsonLd() ignores malformed language tags", async () => {
|
|
407
|
+
const note = await Note.fromJsonLd({
|
|
408
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
409
|
+
type: "Note",
|
|
410
|
+
contentMap: {
|
|
411
|
+
invalid_tag: "Hello",
|
|
412
|
+
en: "Hi",
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
deepStrictEqual(note.contents, [new LanguageString("Hi", "en")]);
|
|
417
|
+
const jsonLd = await note.toJsonLd() as Record<string, unknown>;
|
|
418
|
+
deepStrictEqual(jsonLd.contentMap, { en: "Hi" });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("Note.fromJsonLd() ignores malformed language tags in nested objects", async () => {
|
|
422
|
+
const note = await Note.fromJsonLd({
|
|
423
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Note"],
|
|
424
|
+
"https://www.w3.org/ns/activitystreams#attachment": [{
|
|
425
|
+
"@type": ["http://schema.org#PropertyValue"],
|
|
426
|
+
"https://www.w3.org/ns/activitystreams#name": [
|
|
427
|
+
{ "@value": "Malformed", "@language": "invalid_tag" },
|
|
428
|
+
{ "@value": "Valid", "@language": "en" },
|
|
429
|
+
],
|
|
430
|
+
}],
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
deepStrictEqual(await Array.fromAsync(note.getAttachments()), [
|
|
434
|
+
new PropertyValue({ name: new LanguageString("Valid", "en") }),
|
|
435
|
+
]);
|
|
436
|
+
const jsonLd = await note.toJsonLd() as Record<string, unknown>;
|
|
437
|
+
const attachment = jsonLd.attachment as Record<string, unknown>;
|
|
438
|
+
deepStrictEqual(attachment.nameMap, { en: "Valid" });
|
|
439
|
+
});
|
|
440
|
+
|
|
395
441
|
test("Note.toJsonLd()", async () => {
|
|
396
442
|
const note = new Note({
|
|
397
443
|
tags: [
|
|
@@ -623,6 +669,377 @@ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
|
|
|
623
669
|
);
|
|
624
670
|
});
|
|
625
671
|
|
|
672
|
+
test("fromJsonLd() refuses portable IDs changed by URL parsing", async () => {
|
|
673
|
+
const base = "ap://did:key:z6Mkabc";
|
|
674
|
+
for (
|
|
675
|
+
const id of [
|
|
676
|
+
`${base}/a/../b`,
|
|
677
|
+
`${base}/a/%2e%2E/b`,
|
|
678
|
+
"https://gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
|
|
679
|
+
"https:/gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
|
|
680
|
+
]
|
|
681
|
+
) {
|
|
682
|
+
await rejects(
|
|
683
|
+
() =>
|
|
684
|
+
Note.fromJsonLd({
|
|
685
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
686
|
+
type: "Note",
|
|
687
|
+
id,
|
|
688
|
+
}, {
|
|
689
|
+
documentLoader: mockDocumentLoader,
|
|
690
|
+
contextLoader: mockDocumentLoader,
|
|
691
|
+
}),
|
|
692
|
+
TypeError,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
|
|
698
|
+
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
699
|
+
const gateways = [
|
|
700
|
+
new URL("https://server1.example/"),
|
|
701
|
+
new URL("https://server2.example/"),
|
|
702
|
+
];
|
|
703
|
+
|
|
704
|
+
for (const ActorClass of actorClasses) {
|
|
705
|
+
const actor = await ActorClass.fromJsonLd({
|
|
706
|
+
"@context": [
|
|
707
|
+
"https://www.w3.org/ns/activitystreams",
|
|
708
|
+
"https://w3id.org/fep/ef61",
|
|
709
|
+
],
|
|
710
|
+
type: ActorClass.name,
|
|
711
|
+
id: "ap+ef61://did:key:z6Mkabc/actor",
|
|
712
|
+
gateways: gateways.map((gateway) => gateway.href),
|
|
713
|
+
});
|
|
714
|
+
deepStrictEqual(actor.gateways, gateways);
|
|
715
|
+
|
|
716
|
+
const jsonLd = await actor.toJsonLd({
|
|
717
|
+
format: "compact",
|
|
718
|
+
}) as Record<string, unknown>;
|
|
719
|
+
deepStrictEqual(jsonLd.type, ActorClass.name);
|
|
720
|
+
deepStrictEqual(jsonLd.gateways, gateways.map((gateway) => gateway.origin));
|
|
721
|
+
|
|
722
|
+
const restored = await ActorClass.fromJsonLd(jsonLd);
|
|
723
|
+
deepStrictEqual(restored.gateways, gateways);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test("FEP-ef61: actor gateways serialize as origins", async () => {
|
|
728
|
+
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
729
|
+
const gateways = [
|
|
730
|
+
new URL("https://first.example:443/"),
|
|
731
|
+
new URL("http://second.example:80/"),
|
|
732
|
+
new URL("https://third.example:8443/"),
|
|
733
|
+
new URL("http://fourth.example:8080/"),
|
|
734
|
+
];
|
|
735
|
+
const origins = [
|
|
736
|
+
"https://first.example",
|
|
737
|
+
"http://second.example",
|
|
738
|
+
"https://third.example:8443",
|
|
739
|
+
"http://fourth.example:8080",
|
|
740
|
+
];
|
|
741
|
+
for (const ActorClass of actorClasses) {
|
|
742
|
+
const actor = new ActorClass({
|
|
743
|
+
id: parseIri("ap://did:key:z6Mkabc/actor"),
|
|
744
|
+
gateways,
|
|
745
|
+
});
|
|
746
|
+
const compact = await actor.toJsonLd() as Record<string, unknown>;
|
|
747
|
+
deepStrictEqual(compact.gateways, origins);
|
|
748
|
+
const expanded = await actor.toJsonLd({
|
|
749
|
+
format: "expand",
|
|
750
|
+
}) as Record<string, unknown>[];
|
|
751
|
+
deepStrictEqual(expanded[0]["https://w3id.org/fep/ef61/gateways"], [
|
|
752
|
+
{ "@list": origins.map((origin) => ({ "@id": origin })) },
|
|
753
|
+
]);
|
|
754
|
+
deepStrictEqual(actor.gateways, gateways);
|
|
755
|
+
deepStrictEqual(
|
|
756
|
+
actor.gateways.map((gateway) => gateway.href),
|
|
757
|
+
origins.map((origin) => `${origin}/`),
|
|
758
|
+
);
|
|
759
|
+
deepStrictEqual((await ActorClass.fromJsonLd(compact)).gateways, gateways);
|
|
760
|
+
deepStrictEqual((await ActorClass.fromJsonLd(expanded)).gateways, gateways);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
test("FEP-ef61: portable actors parse unmapped gateway lists", async () => {
|
|
765
|
+
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
766
|
+
const gateways = ["https://first.example", "https://second.example"];
|
|
767
|
+
const ids = [
|
|
768
|
+
"ap://did:key:z6Mkabc/actor",
|
|
769
|
+
"https://first.example/.well-known/apgateway/did:key:z6Mkabc/actor",
|
|
770
|
+
];
|
|
771
|
+
for (const ActorClass of actorClasses) {
|
|
772
|
+
for (const id of ids) {
|
|
773
|
+
const actor = await ActorClass.fromJsonLd({
|
|
774
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
775
|
+
type: ActorClass.name,
|
|
776
|
+
id,
|
|
777
|
+
inbox: `${id}/inbox`,
|
|
778
|
+
outbox: `${id}/outbox`,
|
|
779
|
+
gateways,
|
|
780
|
+
});
|
|
781
|
+
deepStrictEqual(actor.gateways, gateways.map((g) => new URL(g)));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("FEP-ef61: unmapped gateways retain their context meaning", async () => {
|
|
787
|
+
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
788
|
+
const id = "ap://did:key:z6Mkabc/actor";
|
|
789
|
+
for (const ActorClass of actorClasses) {
|
|
790
|
+
const document = {
|
|
791
|
+
type: ActorClass.name,
|
|
792
|
+
id,
|
|
793
|
+
inbox: `${id}/inbox`,
|
|
794
|
+
outbox: `${id}/outbox`,
|
|
795
|
+
gateways: ["https://unmapped.example"],
|
|
796
|
+
};
|
|
797
|
+
for (
|
|
798
|
+
const context of [
|
|
799
|
+
{ gateways: "https://example.com/unrelated" },
|
|
800
|
+
{ gateways: null },
|
|
801
|
+
{ "@vocab": "https://example.com/" },
|
|
802
|
+
{
|
|
803
|
+
[ActorClass.name]: {
|
|
804
|
+
"@id": `https://www.w3.org/ns/activitystreams#${ActorClass.name}`,
|
|
805
|
+
"@context": { gateways: "https://example.com/scoped" },
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
]
|
|
809
|
+
) {
|
|
810
|
+
const actor = await ActorClass.fromJsonLd({
|
|
811
|
+
"@context": ["https://www.w3.org/ns/activitystreams", context],
|
|
812
|
+
...document,
|
|
813
|
+
});
|
|
814
|
+
deepStrictEqual(actor.gateways, []);
|
|
815
|
+
}
|
|
816
|
+
const ordinary = await ActorClass.fromJsonLd({
|
|
817
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
818
|
+
...document,
|
|
819
|
+
id: "https://ordinary.example/actor",
|
|
820
|
+
});
|
|
821
|
+
deepStrictEqual(ordinary.gateways, []);
|
|
822
|
+
const canonical = await ActorClass.fromJsonLd({
|
|
823
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
824
|
+
...document,
|
|
825
|
+
"https://w3id.org/fep/ef61/gateways": {
|
|
826
|
+
"@list": [{ "@id": "https://canonical.example" }],
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
deepStrictEqual(canonical.gateways, [new URL("https://canonical.example")]);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test("FEP-ef61: actor gateways preserve single, empty, and invalid cases", async () => {
|
|
834
|
+
const singleGateway = new Person({
|
|
835
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
836
|
+
gateways: [new URL("https://server.example/")],
|
|
837
|
+
});
|
|
838
|
+
deepStrictEqual(
|
|
839
|
+
(await singleGateway.toJsonLd() as Record<string, unknown>).gateways,
|
|
840
|
+
["https://server.example"],
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
const noGateways = new Person({
|
|
844
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
845
|
+
gateways: [],
|
|
846
|
+
});
|
|
847
|
+
ok(!("gateways" in (await noGateways.toJsonLd() as Record<string, unknown>)));
|
|
848
|
+
|
|
849
|
+
await rejects(
|
|
850
|
+
() =>
|
|
851
|
+
Person.fromJsonLd({
|
|
852
|
+
"@context": [
|
|
853
|
+
"https://www.w3.org/ns/activitystreams",
|
|
854
|
+
"https://w3id.org/fep/ef61",
|
|
855
|
+
],
|
|
856
|
+
type: "Person",
|
|
857
|
+
gateways: ["not a uri"],
|
|
858
|
+
}),
|
|
859
|
+
TypeError,
|
|
860
|
+
);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("FEP-ef61: actor gateways accept @id typed JSON-LD references", async () => {
|
|
864
|
+
const actor = await Person.fromJsonLd({
|
|
865
|
+
"@context": [
|
|
866
|
+
"https://www.w3.org/ns/activitystreams",
|
|
867
|
+
{
|
|
868
|
+
gateways: {
|
|
869
|
+
"@id": "https://w3id.org/fep/ef61/gateways",
|
|
870
|
+
"@type": "@id",
|
|
871
|
+
"@container": "@list",
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
type: "Person",
|
|
876
|
+
id: "ap+ef61://did:key:z6Mkabc/actor",
|
|
877
|
+
gateways: ["https://gateway.example/"],
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
deepStrictEqual(actor.gateways, [new URL("https://gateway.example/")]);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test("FEP-ef61: actor gateways must be HTTP(S) base URIs", async () => {
|
|
884
|
+
const validGateways = [
|
|
885
|
+
new URL("https://server.example/"),
|
|
886
|
+
new URL("http://server.example/"),
|
|
887
|
+
];
|
|
888
|
+
const actor = new Person({
|
|
889
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
890
|
+
gateways: validGateways,
|
|
891
|
+
});
|
|
892
|
+
deepStrictEqual(actor.gateways, validGateways);
|
|
893
|
+
|
|
894
|
+
for (
|
|
895
|
+
const gateway of [
|
|
896
|
+
"ftp://server.example/",
|
|
897
|
+
"https://user:pass@server.example/",
|
|
898
|
+
"https://user@server.example/",
|
|
899
|
+
"https://server.example/path",
|
|
900
|
+
"https://server.example/?x=1",
|
|
901
|
+
"https://server.example/?",
|
|
902
|
+
"https://server.example/#fragment",
|
|
903
|
+
"https://server.example/#",
|
|
904
|
+
]
|
|
905
|
+
) {
|
|
906
|
+
throws(
|
|
907
|
+
() =>
|
|
908
|
+
new Person({
|
|
909
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
910
|
+
gateways: [new URL(gateway)],
|
|
911
|
+
}),
|
|
912
|
+
TypeError,
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
await rejects(
|
|
916
|
+
() =>
|
|
917
|
+
Person.fromJsonLd({
|
|
918
|
+
"@context": [
|
|
919
|
+
"https://www.w3.org/ns/activitystreams",
|
|
920
|
+
"https://w3id.org/fep/ef61",
|
|
921
|
+
],
|
|
922
|
+
type: "Person",
|
|
923
|
+
gateways: [gateway],
|
|
924
|
+
}),
|
|
925
|
+
TypeError,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("FEP-ef61: digestMultibase round-trips on links and media objects", async () => {
|
|
931
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
932
|
+
|
|
933
|
+
const link = await Link.fromJsonLd({
|
|
934
|
+
"@context": [
|
|
935
|
+
"https://www.w3.org/ns/activitystreams",
|
|
936
|
+
"https://w3id.org/fep/ef61",
|
|
937
|
+
],
|
|
938
|
+
type: "Link",
|
|
939
|
+
href: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
940
|
+
digestMultibase,
|
|
941
|
+
});
|
|
942
|
+
deepStrictEqual(link.digestMultibase, digestMultibase);
|
|
943
|
+
deepStrictEqual(
|
|
944
|
+
(await link.toJsonLd() as Record<string, unknown>).digestMultibase,
|
|
945
|
+
digestMultibase,
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
for (const cls of [Document, Image]) {
|
|
949
|
+
const media = await cls.fromJsonLd({
|
|
950
|
+
"@context": [
|
|
951
|
+
"https://www.w3.org/ns/activitystreams",
|
|
952
|
+
"https://w3id.org/fep/ef61",
|
|
953
|
+
],
|
|
954
|
+
type: cls.name,
|
|
955
|
+
url: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
956
|
+
mediaType: "image/png",
|
|
957
|
+
digestMultibase,
|
|
958
|
+
});
|
|
959
|
+
deepStrictEqual(media.digestMultibase, digestMultibase);
|
|
960
|
+
const jsonLd = await media.toJsonLd() as Record<string, unknown>;
|
|
961
|
+
deepStrictEqual(jsonLd.type, cls.name);
|
|
962
|
+
deepStrictEqual(jsonLd.digestMultibase, digestMultibase);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("FEP-ef61: digestMultibase avoids Data Integrity context conflicts", async () => {
|
|
967
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
968
|
+
const image = new Image({
|
|
969
|
+
url: new URL("hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"),
|
|
970
|
+
mediaType: "image/png",
|
|
971
|
+
digestMultibase,
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
const expanded = await image.toJsonLd({ format: "expand" });
|
|
975
|
+
deepStrictEqual(
|
|
976
|
+
(expanded as Record<string, unknown>[])[0][
|
|
977
|
+
"https://www.w3.org/ns/credentials/v2#digestMultibase"
|
|
978
|
+
],
|
|
979
|
+
[{ "@value": digestMultibase }],
|
|
980
|
+
);
|
|
981
|
+
ok(
|
|
982
|
+
!(
|
|
983
|
+
"https://w3id.org/security#digestMultibase" in
|
|
984
|
+
(expanded as Record<string, unknown>[])[0]
|
|
985
|
+
),
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
const compact = await image.toJsonLd() as Record<string, unknown>;
|
|
989
|
+
deepStrictEqual(compact.digestMultibase, digestMultibase);
|
|
990
|
+
ok(
|
|
991
|
+
!(compact["@context"] as unknown[]).includes("https://w3id.org/fep/ef61"),
|
|
992
|
+
);
|
|
993
|
+
ok(
|
|
994
|
+
(compact["@context"] as unknown[]).some((context) =>
|
|
995
|
+
context != null && typeof context === "object" &&
|
|
996
|
+
(context as Record<string, unknown>).digestMultibase ===
|
|
997
|
+
"https://www.w3.org/ns/credentials/v2#digestMultibase"
|
|
998
|
+
),
|
|
999
|
+
);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
test("FEP-ef61: digestMultibase parses after Data Integrity v1 contexts", async () => {
|
|
1003
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
1004
|
+
const image = await Image.fromJsonLd({
|
|
1005
|
+
"@context": [
|
|
1006
|
+
"https://www.w3.org/ns/activitystreams",
|
|
1007
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
1008
|
+
"https://w3id.org/fep/ef61",
|
|
1009
|
+
],
|
|
1010
|
+
type: "Image",
|
|
1011
|
+
url: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
1012
|
+
mediaType: "image/png",
|
|
1013
|
+
digestMultibase,
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
deepStrictEqual(image.digestMultibase, digestMultibase);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
test("FEP-ef61: Link image normalization preserves digestMultibase", async () => {
|
|
1020
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
1021
|
+
const obj = await Object.fromJsonLd({
|
|
1022
|
+
"@context": [
|
|
1023
|
+
"https://www.w3.org/ns/activitystreams",
|
|
1024
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
1025
|
+
"https://w3id.org/fep/ef61",
|
|
1026
|
+
],
|
|
1027
|
+
type: "Note",
|
|
1028
|
+
image: {
|
|
1029
|
+
type: "Link",
|
|
1030
|
+
href: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
1031
|
+
mediaType: "image/png",
|
|
1032
|
+
digestMultibase,
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
const images = [];
|
|
1036
|
+
for await (const img of obj.getImages()) {
|
|
1037
|
+
images.push(img);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
deepStrictEqual(images[0]?.digestMultibase, digestMultibase);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
626
1043
|
test("fromJsonLd() caches text that mentions portable ActivityPub IRIs", async () => {
|
|
627
1044
|
const noteJson = {
|
|
628
1045
|
"@context": [
|
|
@@ -1527,6 +1944,130 @@ test({
|
|
|
1527
1944
|
});
|
|
1528
1945
|
},
|
|
1529
1946
|
});
|
|
1947
|
+
test({
|
|
1948
|
+
name: "Announce.getObject() logs suppressed failures as warnings",
|
|
1949
|
+
permissions: { env: true, read: true },
|
|
1950
|
+
async fn() {
|
|
1951
|
+
const records: LogRecord[] = [];
|
|
1952
|
+
const notFoundUrl = "https://example.com/suppressed-not-found";
|
|
1953
|
+
const invalidObjectUrl = "https://example.com/invalid-object";
|
|
1954
|
+
|
|
1955
|
+
await reset();
|
|
1956
|
+
fetchMock.spyGlobal();
|
|
1957
|
+
|
|
1958
|
+
try {
|
|
1959
|
+
await configure({
|
|
1960
|
+
sinks: {
|
|
1961
|
+
buffer(record: LogRecord): void {
|
|
1962
|
+
records.push(record);
|
|
1963
|
+
},
|
|
1964
|
+
},
|
|
1965
|
+
filters: {},
|
|
1966
|
+
loggers: [{ category: [], sinks: ["buffer"] }],
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
fetchMock.get(notFoundUrl, { status: 404 });
|
|
1970
|
+
|
|
1971
|
+
const suppressedFetch = new Announce({
|
|
1972
|
+
object: new URL(notFoundUrl),
|
|
1973
|
+
});
|
|
1974
|
+
deepStrictEqual(
|
|
1975
|
+
await suppressedFetch.getObject({ suppressError: true }),
|
|
1976
|
+
null,
|
|
1977
|
+
);
|
|
1978
|
+
ok(
|
|
1979
|
+
records.some((record) =>
|
|
1980
|
+
record.level === "warning" &&
|
|
1981
|
+
record.rawMessage ===
|
|
1982
|
+
"Failed to fetch document: {status} {url} {headers}"
|
|
1983
|
+
),
|
|
1984
|
+
);
|
|
1985
|
+
ok(
|
|
1986
|
+
records.some((record) =>
|
|
1987
|
+
record.level === "warning" &&
|
|
1988
|
+
record.rawMessage === "Failed to fetch {url}: {error}"
|
|
1989
|
+
),
|
|
1990
|
+
);
|
|
1991
|
+
deepStrictEqual(
|
|
1992
|
+
records.some((record) =>
|
|
1993
|
+
record.level === "error" &&
|
|
1994
|
+
(
|
|
1995
|
+
record.rawMessage ===
|
|
1996
|
+
"Failed to fetch document: {status} {url} {headers}" ||
|
|
1997
|
+
record.rawMessage === "Failed to fetch {url}: {error}"
|
|
1998
|
+
)
|
|
1999
|
+
),
|
|
2000
|
+
false,
|
|
2001
|
+
);
|
|
2002
|
+
|
|
2003
|
+
records.length = 0;
|
|
2004
|
+
|
|
2005
|
+
const unsuppressedFetch = new Announce({
|
|
2006
|
+
object: new URL(notFoundUrl),
|
|
2007
|
+
});
|
|
2008
|
+
await rejects(
|
|
2009
|
+
() => unsuppressedFetch.getObject(),
|
|
2010
|
+
FetchError,
|
|
2011
|
+
);
|
|
2012
|
+
ok(
|
|
2013
|
+
records.some((record) =>
|
|
2014
|
+
record.level === "error" &&
|
|
2015
|
+
record.rawMessage ===
|
|
2016
|
+
"Failed to fetch document: {status} {url} {headers}"
|
|
2017
|
+
),
|
|
2018
|
+
);
|
|
2019
|
+
|
|
2020
|
+
records.length = 0;
|
|
2021
|
+
|
|
2022
|
+
// deno-lint-ignore require-await
|
|
2023
|
+
const invalidDocumentLoader: DocumentLoader = async (url) => ({
|
|
2024
|
+
contextUrl: null,
|
|
2025
|
+
documentUrl: url,
|
|
2026
|
+
document: null,
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
const suppressedParsing = new Announce({
|
|
2030
|
+
object: new URL(invalidObjectUrl),
|
|
2031
|
+
});
|
|
2032
|
+
deepStrictEqual(
|
|
2033
|
+
await suppressedParsing.getObject({
|
|
2034
|
+
documentLoader: invalidDocumentLoader,
|
|
2035
|
+
suppressError: true,
|
|
2036
|
+
}),
|
|
2037
|
+
null,
|
|
2038
|
+
);
|
|
2039
|
+
ok(
|
|
2040
|
+
records.some((record) =>
|
|
2041
|
+
record.level === "warning" &&
|
|
2042
|
+
record.rawMessage === "Failed to parse {url}: {error}"
|
|
2043
|
+
),
|
|
2044
|
+
);
|
|
2045
|
+
deepStrictEqual(
|
|
2046
|
+
records.some((record) =>
|
|
2047
|
+
record.level === "error" &&
|
|
2048
|
+
record.rawMessage === "Failed to parse {url}: {error}"
|
|
2049
|
+
),
|
|
2050
|
+
false,
|
|
2051
|
+
);
|
|
2052
|
+
|
|
2053
|
+
records.length = 0;
|
|
2054
|
+
|
|
2055
|
+
const unsuppressedParsing = new Announce({
|
|
2056
|
+
object: new URL(invalidObjectUrl),
|
|
2057
|
+
});
|
|
2058
|
+
await rejects(
|
|
2059
|
+
() =>
|
|
2060
|
+
unsuppressedParsing.getObject({
|
|
2061
|
+
documentLoader: invalidDocumentLoader,
|
|
2062
|
+
}),
|
|
2063
|
+
TypeError,
|
|
2064
|
+
);
|
|
2065
|
+
} finally {
|
|
2066
|
+
fetchMock.hardReset();
|
|
2067
|
+
await reset();
|
|
2068
|
+
}
|
|
2069
|
+
},
|
|
2070
|
+
});
|
|
1530
2071
|
|
|
1531
2072
|
test("Activity.getObject() fetches canonical portable IRIs", async () => {
|
|
1532
2073
|
const fetchedUrls: string[] = [];
|
|
@@ -1553,6 +2094,8 @@ test("Activity.getObject() fetches canonical portable IRIs", async () => {
|
|
|
1553
2094
|
const object = await activity.getObject({
|
|
1554
2095
|
documentLoader,
|
|
1555
2096
|
contextLoader: mockDocumentLoader,
|
|
2097
|
+
// deno-lint-ignore require-await
|
|
2098
|
+
verifyPortableObject: async () => ({ verified: true }),
|
|
1556
2099
|
});
|
|
1557
2100
|
|
|
1558
2101
|
assertInstanceOf(object, Note);
|
|
@@ -1601,6 +2144,104 @@ test({
|
|
|
1601
2144
|
},
|
|
1602
2145
|
});
|
|
1603
2146
|
|
|
2147
|
+
// The alias is inherited during initial expansion, but is unavailable when
|
|
2148
|
+
// an embedded value is reparsed standalone from the cached JSON-LD.
|
|
2149
|
+
function embeddedPropertyDocument(property: "attachment" | "object") {
|
|
2150
|
+
const context = "https://www.w3.org/ns/activitystreams";
|
|
2151
|
+
return {
|
|
2152
|
+
"@context": [context, { CustomImage: `${context}#Image` }],
|
|
2153
|
+
type: property === "attachment" ? "Note" : "Create",
|
|
2154
|
+
[property]: [
|
|
2155
|
+
{
|
|
2156
|
+
"@context": [context, { "@vocab": "https://example.com/" }],
|
|
2157
|
+
type: "CustomImage",
|
|
2158
|
+
name: "Malformed when reparsed",
|
|
2159
|
+
},
|
|
2160
|
+
{ "@context": context, type: "Image", name: "Cached image" },
|
|
2161
|
+
{ type: "Image", name: "Decoded image" },
|
|
2162
|
+
],
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
for (const suppressError of [undefined, false, true]) {
|
|
2167
|
+
test(
|
|
2168
|
+
`Note.getAttachments() cached parse failure (suppressError: ${suppressError})`,
|
|
2169
|
+
async () => {
|
|
2170
|
+
const jsonLd = embeddedPropertyDocument("attachment");
|
|
2171
|
+
const note = await Note.fromJsonLd(jsonLd);
|
|
2172
|
+
if (suppressError) {
|
|
2173
|
+
for (let i = 0; i < 2; i++) {
|
|
2174
|
+
const attachments: (Object | Link | PropertyValue)[] = await Array
|
|
2175
|
+
.fromAsync(
|
|
2176
|
+
note.getAttachments({ suppressError }),
|
|
2177
|
+
);
|
|
2178
|
+
deepStrictEqual(
|
|
2179
|
+
attachments.map((attachment) => attachment.name),
|
|
2180
|
+
["Cached image", "Decoded image"],
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
// Suppression must not remove or replace the cached malformed value.
|
|
2184
|
+
await rejects(() => Array.fromAsync(note.getAttachments()), TypeError);
|
|
2185
|
+
} else {
|
|
2186
|
+
await rejects(
|
|
2187
|
+
() => Array.fromAsync(note.getAttachments({ suppressError })),
|
|
2188
|
+
TypeError,
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
deepStrictEqual(await note.toJsonLd(), jsonLd);
|
|
2192
|
+
},
|
|
2193
|
+
);
|
|
2194
|
+
|
|
2195
|
+
test(
|
|
2196
|
+
`Activity.getObject() cached parse failure (suppressError: ${suppressError})`,
|
|
2197
|
+
async () => {
|
|
2198
|
+
const jsonLd = embeddedPropertyDocument("object");
|
|
2199
|
+
const activity = await Activity.fromJsonLd(jsonLd);
|
|
2200
|
+
if (suppressError) {
|
|
2201
|
+
deepStrictEqual(await activity.getObject({ suppressError }), null);
|
|
2202
|
+
deepStrictEqual(await activity.getObject({ suppressError }), null);
|
|
2203
|
+
await rejects(() => activity.getObject(), TypeError);
|
|
2204
|
+
} else {
|
|
2205
|
+
await rejects(() => activity.getObject({ suppressError }), TypeError);
|
|
2206
|
+
}
|
|
2207
|
+
deepStrictEqual(await activity.toJsonLd(), jsonLd);
|
|
2208
|
+
},
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
test("Note.getAttachments() suppresses cached context loader failures", async () => {
|
|
2213
|
+
const note = await Note.fromJsonLd(embeddedPropertyDocument("attachment"));
|
|
2214
|
+
const contextLoader = () =>
|
|
2215
|
+
Promise.reject(new Error("Context loader failed"));
|
|
2216
|
+
await rejects(
|
|
2217
|
+
() => Array.fromAsync(note.getAttachments({ contextLoader })),
|
|
2218
|
+
(error: unknown) => error instanceof Error && !(error instanceof TypeError),
|
|
2219
|
+
);
|
|
2220
|
+
// Both cached values need the failing loader; the decoded value remains usable.
|
|
2221
|
+
const attachments: (Object | Link | PropertyValue)[] = await Array.fromAsync(
|
|
2222
|
+
note.getAttachments({ contextLoader, suppressError: true }),
|
|
2223
|
+
);
|
|
2224
|
+
deepStrictEqual(attachments.map((attachment) => attachment.name), [
|
|
2225
|
+
"Decoded image",
|
|
2226
|
+
]);
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
test("Activity.getObject() suppresses cached context loader failures", async () => {
|
|
2230
|
+
const activity = await Activity.fromJsonLd(
|
|
2231
|
+
embeddedPropertyDocument("object"),
|
|
2232
|
+
);
|
|
2233
|
+
const contextLoader = () =>
|
|
2234
|
+
Promise.reject(new Error("Context loader failed"));
|
|
2235
|
+
await rejects(
|
|
2236
|
+
() => activity.getObject({ contextLoader }),
|
|
2237
|
+
(error: unknown) => error instanceof Error && !(error instanceof TypeError),
|
|
2238
|
+
);
|
|
2239
|
+
deepStrictEqual(
|
|
2240
|
+
await activity.getObject({ contextLoader, suppressError: true }),
|
|
2241
|
+
null,
|
|
2242
|
+
);
|
|
2243
|
+
});
|
|
2244
|
+
|
|
1604
2245
|
test("Activity.clone()", async () => {
|
|
1605
2246
|
const activity = new Activity({
|
|
1606
2247
|
actor: new Person({
|
|
@@ -1681,6 +2322,26 @@ test({
|
|
|
1681
2322
|
},
|
|
1682
2323
|
});
|
|
1683
2324
|
|
|
2325
|
+
test("inspecting an object prints portable IRIs in their canonical form", () => {
|
|
2326
|
+
const did = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
|
|
2327
|
+
const obj = new Object({
|
|
2328
|
+
id: parseIri(`ap://${did}/note`),
|
|
2329
|
+
attribution: parseIri(`ap://${did}/actor`),
|
|
2330
|
+
});
|
|
2331
|
+
const inspected = [inspect(obj, { colors: false })];
|
|
2332
|
+
if ("Deno" in globalThis) {
|
|
2333
|
+
inspected.push(Deno.inspect(obj, { colors: false }));
|
|
2334
|
+
}
|
|
2335
|
+
for (const text of inspected) {
|
|
2336
|
+
ok(text.includes(`ap+ef61://${did}/note`), text);
|
|
2337
|
+
ok(text.includes(`ap+ef61://${did}/actor`), text);
|
|
2338
|
+
ok(!text.includes("%3A"), text);
|
|
2339
|
+
}
|
|
2340
|
+
// A URL that cannot be formatted as an IRI does not break inspection:
|
|
2341
|
+
const malformed = new Object({ id: new URL("ap://not-a-did/note") });
|
|
2342
|
+
ok(inspect(malformed).includes("ap://not-a-did/note"));
|
|
2343
|
+
});
|
|
2344
|
+
|
|
1684
2345
|
test("Person.fromJsonLd()", async () => {
|
|
1685
2346
|
const person = await Person.fromJsonLd({
|
|
1686
2347
|
"@context": [
|
|
@@ -1751,6 +2412,7 @@ test("Person.toJsonLd()", async () => {
|
|
|
1751
2412
|
deepStrictEqual(await person.toJsonLd(), {
|
|
1752
2413
|
"@context": [
|
|
1753
2414
|
"https://www.w3.org/ns/activitystreams",
|
|
2415
|
+
"https://w3id.org/fep/ef61",
|
|
1754
2416
|
"https://w3id.org/security/v1",
|
|
1755
2417
|
"https://w3id.org/security/data-integrity/v1",
|
|
1756
2418
|
"https://www.w3.org/ns/did/v1",
|
|
@@ -2531,6 +3193,7 @@ test("InteractionPolicy.canFeature", async () => {
|
|
|
2531
3193
|
const expected = {
|
|
2532
3194
|
"@context": [
|
|
2533
3195
|
"https://www.w3.org/ns/activitystreams",
|
|
3196
|
+
"https://w3id.org/fep/ef61",
|
|
2534
3197
|
"https://w3id.org/security/v1",
|
|
2535
3198
|
"https://w3id.org/security/data-integrity/v1",
|
|
2536
3199
|
"https://www.w3.org/ns/did/v1",
|
|
@@ -2946,6 +3609,105 @@ test("Delete.toJsonLd() compacts embedded QuoteRequest", async () => {
|
|
|
2946
3609
|
);
|
|
2947
3610
|
});
|
|
2948
3611
|
|
|
3612
|
+
test("Note.quoteUrl (IRI-typed alias terms)", async () => {
|
|
3613
|
+
const jsonLd: Record<string, unknown> = {
|
|
3614
|
+
"@context": [
|
|
3615
|
+
"https://www.w3.org/ns/activitystreams",
|
|
3616
|
+
{
|
|
3617
|
+
fedibird: "http://fedibird.com/ns#",
|
|
3618
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
3619
|
+
_misskey_quote: {
|
|
3620
|
+
"@id": "misskey:_misskey_quote",
|
|
3621
|
+
"@type": "@id",
|
|
3622
|
+
},
|
|
3623
|
+
quoteUri: {
|
|
3624
|
+
"@id": "fedibird:quoteUri",
|
|
3625
|
+
"@type": "@id",
|
|
3626
|
+
},
|
|
3627
|
+
},
|
|
3628
|
+
],
|
|
3629
|
+
id: "https://example.com/notes/1",
|
|
3630
|
+
type: "Note",
|
|
3631
|
+
_misskey_quote: "https://example.com/notes/quoted",
|
|
3632
|
+
quoteUri: "https://example.com/notes/quoted2",
|
|
3633
|
+
};
|
|
3634
|
+
|
|
3635
|
+
const loaded = await Note.fromJsonLd(jsonLd);
|
|
3636
|
+
deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/notes/quoted"));
|
|
3637
|
+
|
|
3638
|
+
delete jsonLd._misskey_quote;
|
|
3639
|
+
const loaded2 = await Note.fromJsonLd(jsonLd);
|
|
3640
|
+
deepStrictEqual(
|
|
3641
|
+
loaded2.quoteUrl,
|
|
3642
|
+
new URL("https://example.com/notes/quoted2"),
|
|
3643
|
+
);
|
|
3644
|
+
});
|
|
3645
|
+
|
|
3646
|
+
test("Note.quoteUrl (IRI-typed primary term)", async () => {
|
|
3647
|
+
const context = [
|
|
3648
|
+
"https://www.w3.org/ns/activitystreams",
|
|
3649
|
+
{
|
|
3650
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
3651
|
+
quoteUrl: { "@id": "as:quoteUrl", "@type": "@id" },
|
|
3652
|
+
_misskey_quote: "misskey:_misskey_quote",
|
|
3653
|
+
},
|
|
3654
|
+
];
|
|
3655
|
+
|
|
3656
|
+
const loaded = await Note.fromJsonLd({
|
|
3657
|
+
"@context": context,
|
|
3658
|
+
type: "Note",
|
|
3659
|
+
quoteUrl: "https://example.com/object",
|
|
3660
|
+
});
|
|
3661
|
+
deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/object"));
|
|
3662
|
+
|
|
3663
|
+
// An IRI-valued quoteUrl still takes precedence over a plain-string alias:
|
|
3664
|
+
const loaded2 = await Note.fromJsonLd({
|
|
3665
|
+
"@context": context,
|
|
3666
|
+
type: "Note",
|
|
3667
|
+
quoteUrl: "https://example.com/object",
|
|
3668
|
+
_misskey_quote: "https://example.com/object2",
|
|
3669
|
+
});
|
|
3670
|
+
deepStrictEqual(loaded2.quoteUrl, new URL("https://example.com/object"));
|
|
3671
|
+
});
|
|
3672
|
+
|
|
3673
|
+
test("Note.quoteUrl (unparsable IRI-valued terms)", async () => {
|
|
3674
|
+
const context = [
|
|
3675
|
+
"https://www.w3.org/ns/activitystreams",
|
|
3676
|
+
{
|
|
3677
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
3678
|
+
_misskey_quote: { "@id": "misskey:_misskey_quote", "@type": "@id" },
|
|
3679
|
+
},
|
|
3680
|
+
];
|
|
3681
|
+
for (
|
|
3682
|
+
const quote of [
|
|
3683
|
+
{ type: "Note", content: "An inlined quote without an id" },
|
|
3684
|
+
"_:b0",
|
|
3685
|
+
"",
|
|
3686
|
+
"notes/relative",
|
|
3687
|
+
]
|
|
3688
|
+
) {
|
|
3689
|
+
const loaded = await Note.fromJsonLd({
|
|
3690
|
+
"@context": context,
|
|
3691
|
+
id: "https://example.com/notes/1",
|
|
3692
|
+
type: "Note",
|
|
3693
|
+
content: "Hello",
|
|
3694
|
+
_misskey_quote: quote,
|
|
3695
|
+
});
|
|
3696
|
+
deepStrictEqual(loaded.quoteUrl, null, JSON.stringify(quote));
|
|
3697
|
+
deepStrictEqual(loaded.content, "Hello");
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
const atUri = await Note.fromJsonLd({
|
|
3701
|
+
"@context": context,
|
|
3702
|
+
type: "Note",
|
|
3703
|
+
_misskey_quote: "at://did:plc:abc/app.bsky.feed.post/xyz",
|
|
3704
|
+
});
|
|
3705
|
+
deepStrictEqual(
|
|
3706
|
+
atUri.quoteUrl,
|
|
3707
|
+
new URL("at://" + encodeURIComponent("did:plc:abc/app.bsky.feed.post/xyz")),
|
|
3708
|
+
);
|
|
3709
|
+
});
|
|
3710
|
+
|
|
2949
3711
|
test("Key.publicKey", async () => {
|
|
2950
3712
|
const jwk = {
|
|
2951
3713
|
kty: "RSA",
|
|
@@ -4005,7 +4767,12 @@ test(
|
|
|
4005
4767
|
throw new Error("Document not found");
|
|
4006
4768
|
};
|
|
4007
4769
|
|
|
4008
|
-
const result = await create.getObject({
|
|
4770
|
+
const result = await create.getObject({
|
|
4771
|
+
documentLoader,
|
|
4772
|
+
contextLoader: mockDocumentLoader,
|
|
4773
|
+
// deno-lint-ignore require-await
|
|
4774
|
+
verifyPortableObject: async () => ({ verified: true }),
|
|
4775
|
+
});
|
|
4009
4776
|
assertInstanceOf(result, Note);
|
|
4010
4777
|
deepStrictEqual(result.content, "Fetched portable note");
|
|
4011
4778
|
},
|
|
@@ -4780,6 +5547,7 @@ const sampleValues: Record<string, any> = {
|
|
|
4780
5547
|
]),
|
|
4781
5548
|
"fedify:langTag": new Intl.Locale("en-Latn-US"),
|
|
4782
5549
|
"fedify:url": new URL("https://fedify.dev/"),
|
|
5550
|
+
"fedify:gatewayUrl": new URL("https://gateway.example/"),
|
|
4783
5551
|
"fedify:publicKey": rsaPublicKey.publicKey,
|
|
4784
5552
|
"fedify:multibaseKey": ed25519PublicKey.publicKey,
|
|
4785
5553
|
"fedify:proofPurpose": "assertionMethod",
|
|
@@ -5135,7 +5903,25 @@ for (const typeUri in types) {
|
|
|
5135
5903
|
const jsonLd = await instance.toJsonLd({
|
|
5136
5904
|
contextLoader: mockDocumentLoader,
|
|
5137
5905
|
});
|
|
5138
|
-
|
|
5906
|
+
const defaultContexts = Array.isArray(type.defaultContext)
|
|
5907
|
+
? type.defaultContext
|
|
5908
|
+
: [type.defaultContext];
|
|
5909
|
+
const extraContexts = [
|
|
5910
|
+
...new Set(
|
|
5911
|
+
allProperties.flatMap((property) =>
|
|
5912
|
+
property.extraContext == null ||
|
|
5913
|
+
defaultContexts.some((context) => context === property.extraContext)
|
|
5914
|
+
? []
|
|
5915
|
+
: [property.extraContext]
|
|
5916
|
+
),
|
|
5917
|
+
),
|
|
5918
|
+
];
|
|
5919
|
+
deepStrictEqual(
|
|
5920
|
+
jsonLd["@context"],
|
|
5921
|
+
extraContexts.length === 0
|
|
5922
|
+
? type.defaultContext
|
|
5923
|
+
: [...defaultContexts, ...extraContexts],
|
|
5924
|
+
);
|
|
5139
5925
|
if (type.entity) deepStrictEqual(jsonLd.id, "https://example.com/");
|
|
5140
5926
|
const restored = await cls.fromJsonLd(jsonLd, {
|
|
5141
5927
|
documentLoader: mockDocumentLoader,
|
|
@@ -5254,3 +6040,370 @@ for (const typeUri in types) {
|
|
|
5254
6040
|
deepStrictEqual(cls.typeId, new URL(type.uri));
|
|
5255
6041
|
});
|
|
5256
6042
|
}
|
|
6043
|
+
|
|
6044
|
+
test("Announce.getObject() suppresses redirect, context, and private URL logs", async () => {
|
|
6045
|
+
const records: LogRecord[] = [];
|
|
6046
|
+
const objectUrl = "https://example.com/suppression-object";
|
|
6047
|
+
const contextUrl = "https://example.com/suppression-context";
|
|
6048
|
+
const scenarios = [
|
|
6049
|
+
"redirect limit",
|
|
6050
|
+
"redirect loop",
|
|
6051
|
+
"context",
|
|
6052
|
+
"private",
|
|
6053
|
+
"private redirect",
|
|
6054
|
+
];
|
|
6055
|
+
await reset();
|
|
6056
|
+
await configure({
|
|
6057
|
+
sinks: {
|
|
6058
|
+
capture: (record) => {
|
|
6059
|
+
records.push(record);
|
|
6060
|
+
},
|
|
6061
|
+
},
|
|
6062
|
+
loggers: [{
|
|
6063
|
+
category: ["fedify"],
|
|
6064
|
+
lowestLevel: "debug",
|
|
6065
|
+
sinks: ["capture"],
|
|
6066
|
+
}],
|
|
6067
|
+
});
|
|
6068
|
+
try {
|
|
6069
|
+
for (const scenario of scenarios) {
|
|
6070
|
+
fetchMock.spyGlobal();
|
|
6071
|
+
let fetches = 0;
|
|
6072
|
+
fetchMock.get("begin:https://example.com/", (call) => {
|
|
6073
|
+
fetches++;
|
|
6074
|
+
if (scenario === "redirect limit") {
|
|
6075
|
+
return Response.redirect(
|
|
6076
|
+
`https://example.com/redirect-${fetches}`,
|
|
6077
|
+
302,
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
if (scenario === "redirect loop") {
|
|
6081
|
+
return Response.redirect(objectUrl, 302);
|
|
6082
|
+
}
|
|
6083
|
+
if (scenario === "private redirect") {
|
|
6084
|
+
return Response.redirect("http://127.0.0.1/private", 302);
|
|
6085
|
+
}
|
|
6086
|
+
if (call.url === contextUrl) return new Response(null, { status: 404 });
|
|
6087
|
+
return new Response(
|
|
6088
|
+
JSON.stringify({
|
|
6089
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
6090
|
+
id: objectUrl,
|
|
6091
|
+
type: "Note",
|
|
6092
|
+
}),
|
|
6093
|
+
{ headers: { "Content-Type": "application/activity+json" } },
|
|
6094
|
+
);
|
|
6095
|
+
});
|
|
6096
|
+
try {
|
|
6097
|
+
for (const suppressError of [true, false, undefined]) {
|
|
6098
|
+
records.length = 0;
|
|
6099
|
+
fetches = 0;
|
|
6100
|
+
const announce = new Announce({
|
|
6101
|
+
object: new URL(
|
|
6102
|
+
scenario === "private" ? "http://127.0.0.1/private" : objectUrl,
|
|
6103
|
+
),
|
|
6104
|
+
});
|
|
6105
|
+
if (suppressError) {
|
|
6106
|
+
deepStrictEqual(await announce.getObject({ suppressError }), null);
|
|
6107
|
+
deepStrictEqual(
|
|
6108
|
+
records.filter((r) => r.level === "error"),
|
|
6109
|
+
[],
|
|
6110
|
+
scenario,
|
|
6111
|
+
);
|
|
6112
|
+
ok(records.some((r) => r.level === "warning"), scenario);
|
|
6113
|
+
} else {
|
|
6114
|
+
await rejects(() => announce.getObject({ suppressError }));
|
|
6115
|
+
ok(records.some((r) => r.level === "error"), scenario);
|
|
6116
|
+
}
|
|
6117
|
+
if (scenario === "private") deepStrictEqual(fetches, 0);
|
|
6118
|
+
if (scenario === "private redirect") deepStrictEqual(fetches, 1);
|
|
6119
|
+
}
|
|
6120
|
+
} finally {
|
|
6121
|
+
fetchMock.hardReset();
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
} finally {
|
|
6125
|
+
await reset();
|
|
6126
|
+
}
|
|
6127
|
+
});
|
|
6128
|
+
|
|
6129
|
+
test("suppressed context loading does not persist on fetched objects", async () => {
|
|
6130
|
+
const states: (boolean | undefined)[] = [];
|
|
6131
|
+
const contextLoader: DocumentLoader = (url, options) => {
|
|
6132
|
+
if (url === "https://www.w3.org/ns/activitystreams") {
|
|
6133
|
+
return mockDocumentLoader(url);
|
|
6134
|
+
}
|
|
6135
|
+
states.push(options?.suppressError);
|
|
6136
|
+
return Promise.resolve({
|
|
6137
|
+
contextUrl: null,
|
|
6138
|
+
documentUrl: url,
|
|
6139
|
+
document: {
|
|
6140
|
+
"@context": {
|
|
6141
|
+
as: "https://www.w3.org/ns/activitystreams#",
|
|
6142
|
+
name: "as:name",
|
|
6143
|
+
},
|
|
6144
|
+
},
|
|
6145
|
+
});
|
|
6146
|
+
};
|
|
6147
|
+
const documentLoader: DocumentLoader = (url) =>
|
|
6148
|
+
Promise.resolve({
|
|
6149
|
+
contextUrl: null,
|
|
6150
|
+
documentUrl: url,
|
|
6151
|
+
document: {
|
|
6152
|
+
"@context": [
|
|
6153
|
+
"https://www.w3.org/ns/activitystreams",
|
|
6154
|
+
"https://example.com/scoped-context",
|
|
6155
|
+
],
|
|
6156
|
+
id: url,
|
|
6157
|
+
type: url.endsWith("actor") ? "Person" : "Note",
|
|
6158
|
+
attributedTo: "https://example.com/scoped-actor",
|
|
6159
|
+
},
|
|
6160
|
+
});
|
|
6161
|
+
const note = await new Announce({
|
|
6162
|
+
object: new URL("https://example.com/scoped-note"),
|
|
6163
|
+
}).getObject({
|
|
6164
|
+
documentLoader,
|
|
6165
|
+
contextLoader,
|
|
6166
|
+
suppressError: true,
|
|
6167
|
+
});
|
|
6168
|
+
assertInstanceOf(note, Note);
|
|
6169
|
+
ok(states.includes(true));
|
|
6170
|
+
states.length = 0;
|
|
6171
|
+
const actor = await note.getAttribution();
|
|
6172
|
+
assertInstanceOf(actor, Person);
|
|
6173
|
+
ok(states.length > 0);
|
|
6174
|
+
deepStrictEqual(states.includes(true), false);
|
|
6175
|
+
});
|
|
6176
|
+
|
|
6177
|
+
test("attribution chains alternate ordinary and portable suppression", async () => {
|
|
6178
|
+
const states: (boolean | undefined)[] = [];
|
|
6179
|
+
const depths: number[][] = [];
|
|
6180
|
+
let hop = 0;
|
|
6181
|
+
const contextUrl = "https://example.com/attribution-context";
|
|
6182
|
+
const contextLoader: DocumentLoader = (url, options) => {
|
|
6183
|
+
if (url !== contextUrl) return mockDocumentLoader(url, options);
|
|
6184
|
+
states.push(options?.suppressError);
|
|
6185
|
+
const stack = new Error().stack;
|
|
6186
|
+
ok(stack);
|
|
6187
|
+
depths[hop]?.push(stack.split("\n").length);
|
|
6188
|
+
return Promise.resolve({
|
|
6189
|
+
contextUrl: null,
|
|
6190
|
+
documentUrl: url,
|
|
6191
|
+
document: {
|
|
6192
|
+
"@context": { name: "https://www.w3.org/ns/activitystreams#name" },
|
|
6193
|
+
},
|
|
6194
|
+
});
|
|
6195
|
+
};
|
|
6196
|
+
const documentLoader: DocumentLoader = (url) => {
|
|
6197
|
+
const hop = Number(new URL(url).pathname.split("/").at(-1));
|
|
6198
|
+
return Promise.resolve({
|
|
6199
|
+
contextUrl: null,
|
|
6200
|
+
documentUrl: url,
|
|
6201
|
+
document: {
|
|
6202
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
6203
|
+
id: url,
|
|
6204
|
+
type: "Person",
|
|
6205
|
+
name: `Actor ${hop}`,
|
|
6206
|
+
attributedTo: `https://example.com/actors/${hop + 1}`,
|
|
6207
|
+
},
|
|
6208
|
+
});
|
|
6209
|
+
};
|
|
6210
|
+
let person = new Person({
|
|
6211
|
+
id: new URL("https://example.com/actors/root"),
|
|
6212
|
+
attribution: new URL("https://example.com/actors/0"),
|
|
6213
|
+
}, { documentLoader, contextLoader });
|
|
6214
|
+
const stackTraceLimit = globalThis.Object.getOwnPropertyDescriptor(
|
|
6215
|
+
Error,
|
|
6216
|
+
"stackTraceLimit",
|
|
6217
|
+
);
|
|
6218
|
+
try {
|
|
6219
|
+
ok(Reflect.set(Error, "stackTraceLimit", Infinity));
|
|
6220
|
+
for (; hop < 100; hop++) {
|
|
6221
|
+
depths[hop] = [];
|
|
6222
|
+
const next = await person.getAttribution({
|
|
6223
|
+
suppressError: true,
|
|
6224
|
+
...(hop % 2 === 0 ? {} : {
|
|
6225
|
+
verifyPortableObject: () =>
|
|
6226
|
+
Promise.resolve({ verified: true as const }),
|
|
6227
|
+
}),
|
|
6228
|
+
});
|
|
6229
|
+
assertInstanceOf(next, Person);
|
|
6230
|
+
deepStrictEqual(next.id, new URL(`https://example.com/actors/${hop}`));
|
|
6231
|
+
person = next;
|
|
6232
|
+
}
|
|
6233
|
+
// Compare the same mode after warmup; a retained wrapper per hop would
|
|
6234
|
+
// increase the synchronous call depth even before the stack overflows.
|
|
6235
|
+
ok(depths.every((values) => values.length > 0));
|
|
6236
|
+
deepStrictEqual(depths[98], depths[2]);
|
|
6237
|
+
deepStrictEqual(depths[99], depths[3]);
|
|
6238
|
+
ok(states.length >= 100);
|
|
6239
|
+
ok(states.every((state) => state === true));
|
|
6240
|
+
states.length = 0;
|
|
6241
|
+
assertInstanceOf(await person.getAttribution(), Person);
|
|
6242
|
+
ok(states.length > 0);
|
|
6243
|
+
ok(states.every((state) => state !== true));
|
|
6244
|
+
} finally {
|
|
6245
|
+
if (stackTraceLimit == null) {
|
|
6246
|
+
Reflect.deleteProperty(Error, "stackTraceLimit");
|
|
6247
|
+
} else {
|
|
6248
|
+
globalThis.Object.defineProperty(
|
|
6249
|
+
Error,
|
|
6250
|
+
"stackTraceLimit",
|
|
6251
|
+
stackTraceLimit,
|
|
6252
|
+
);
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
6255
|
+
});
|
|
6256
|
+
|
|
6257
|
+
test("portable accessors suppress document and context failure logs", async () => {
|
|
6258
|
+
const records: LogRecord[] = [];
|
|
6259
|
+
const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
6260
|
+
const contextUrl = "https://example.com/portable-suppression-context";
|
|
6261
|
+
await reset();
|
|
6262
|
+
await configure({
|
|
6263
|
+
sinks: {
|
|
6264
|
+
capture: (record) => {
|
|
6265
|
+
records.push(record);
|
|
6266
|
+
},
|
|
6267
|
+
},
|
|
6268
|
+
loggers: [{
|
|
6269
|
+
category: ["fedify"],
|
|
6270
|
+
lowestLevel: "debug",
|
|
6271
|
+
sinks: ["capture"],
|
|
6272
|
+
}],
|
|
6273
|
+
});
|
|
6274
|
+
try {
|
|
6275
|
+
for (const failContext of [false, true]) {
|
|
6276
|
+
fetchMock.spyGlobal();
|
|
6277
|
+
fetchMock.get("begin:https://example.com/", (call) => {
|
|
6278
|
+
if (!failContext || call.url === contextUrl) {
|
|
6279
|
+
return new Response(null, { status: 404 });
|
|
6280
|
+
}
|
|
6281
|
+
return new Response(
|
|
6282
|
+
JSON.stringify({
|
|
6283
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
6284
|
+
id: "ap://did:key:z6Mkabc/objects/1",
|
|
6285
|
+
type: "Note",
|
|
6286
|
+
}),
|
|
6287
|
+
{ headers: { "Content-Type": "application/activity+json" } },
|
|
6288
|
+
);
|
|
6289
|
+
});
|
|
6290
|
+
try {
|
|
6291
|
+
for (const suppressError of [true, false, undefined]) {
|
|
6292
|
+
records.length = 0;
|
|
6293
|
+
const announce = new Announce({ object: id });
|
|
6294
|
+
const options = {
|
|
6295
|
+
suppressError,
|
|
6296
|
+
gateways: ["https://example.com/"],
|
|
6297
|
+
verifyPortableObject: () =>
|
|
6298
|
+
Promise.resolve({ verified: true as const }),
|
|
6299
|
+
};
|
|
6300
|
+
if (suppressError) {
|
|
6301
|
+
deepStrictEqual(await announce.getObject(options), null);
|
|
6302
|
+
deepStrictEqual(records.filter((r) => r.level === "error"), []);
|
|
6303
|
+
ok(records.some((r) => r.level === "warning"));
|
|
6304
|
+
} else {
|
|
6305
|
+
await rejects(() => announce.getObject(options));
|
|
6306
|
+
ok(records.some((r) => r.level === "error"));
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
} finally {
|
|
6310
|
+
fetchMock.hardReset();
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
} finally {
|
|
6314
|
+
await reset();
|
|
6315
|
+
}
|
|
6316
|
+
});
|
|
6317
|
+
|
|
6318
|
+
test("portable verifiers suppress document logs only during dereferencing", async () => {
|
|
6319
|
+
const records: LogRecord[] = [];
|
|
6320
|
+
const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
6321
|
+
const verificationUrl = "https://example.com/verification-document";
|
|
6322
|
+
const missingUrl = "https://example.com/missing-verification-document";
|
|
6323
|
+
const signal = new AbortController().signal;
|
|
6324
|
+
await reset();
|
|
6325
|
+
await configure({
|
|
6326
|
+
sinks: { capture: (record) => records.push(record) },
|
|
6327
|
+
loggers: [{
|
|
6328
|
+
category: ["fedify"],
|
|
6329
|
+
lowestLevel: "debug",
|
|
6330
|
+
sinks: ["capture"],
|
|
6331
|
+
}],
|
|
6332
|
+
});
|
|
6333
|
+
try {
|
|
6334
|
+
for (const failVerification of [false, true]) {
|
|
6335
|
+
fetchMock.spyGlobal();
|
|
6336
|
+
fetchMock.get("begin:https://example.com/.well-known/", {
|
|
6337
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
6338
|
+
body: {
|
|
6339
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
6340
|
+
id: "ap://did:key:z6Mkabc/objects/1",
|
|
6341
|
+
type: "Note",
|
|
6342
|
+
},
|
|
6343
|
+
});
|
|
6344
|
+
fetchMock.get(verificationUrl, {
|
|
6345
|
+
status: failVerification ? 404 : 200,
|
|
6346
|
+
headers: { "Content-Type": "application/ld+json" },
|
|
6347
|
+
body: {},
|
|
6348
|
+
});
|
|
6349
|
+
fetchMock.get(missingUrl, { status: 404 });
|
|
6350
|
+
try {
|
|
6351
|
+
for (const suppressError of [true, false, undefined]) {
|
|
6352
|
+
records.length = 0;
|
|
6353
|
+
const baseLoader = getDocumentLoader();
|
|
6354
|
+
const calls: Parameters<DocumentLoader>[1][] = [];
|
|
6355
|
+
const documentLoader: DocumentLoader = (url, options) => {
|
|
6356
|
+
calls.push(options);
|
|
6357
|
+
return baseLoader(url, options);
|
|
6358
|
+
};
|
|
6359
|
+
let retainedLoader: DocumentLoader | undefined;
|
|
6360
|
+
const loaderOptions = { signal };
|
|
6361
|
+
const announce = new Announce({ object: id });
|
|
6362
|
+
const object = await announce.getObject({
|
|
6363
|
+
suppressError,
|
|
6364
|
+
documentLoader,
|
|
6365
|
+
gateways: ["https://example.com/"],
|
|
6366
|
+
verifyPortableObject: async (_document, options) => {
|
|
6367
|
+
retainedLoader = options.documentLoader;
|
|
6368
|
+
ok(options.documentLoader);
|
|
6369
|
+
await options.documentLoader(verificationUrl, loaderOptions);
|
|
6370
|
+
return { verified: true };
|
|
6371
|
+
},
|
|
6372
|
+
});
|
|
6373
|
+
if (failVerification) {
|
|
6374
|
+
deepStrictEqual(object, null);
|
|
6375
|
+
deepStrictEqual(
|
|
6376
|
+
records.some((record) => record.level === "error"),
|
|
6377
|
+
!suppressError,
|
|
6378
|
+
);
|
|
6379
|
+
ok(records.some((record) => record.level === "warning"));
|
|
6380
|
+
} else {
|
|
6381
|
+
assertInstanceOf(object, Note);
|
|
6382
|
+
}
|
|
6383
|
+
deepStrictEqual(calls.at(-1)?.signal, signal);
|
|
6384
|
+
deepStrictEqual(
|
|
6385
|
+
calls.at(-1)?.suppressError,
|
|
6386
|
+
suppressError ? true : undefined,
|
|
6387
|
+
);
|
|
6388
|
+
deepStrictEqual(loaderOptions, { signal });
|
|
6389
|
+
|
|
6390
|
+
// A verifier may retain the loader after success or rejection.
|
|
6391
|
+
// Suppression must end when the accessor returns in either case.
|
|
6392
|
+
records.length = 0;
|
|
6393
|
+
const releasedLoader = retainedLoader;
|
|
6394
|
+
ok(releasedLoader);
|
|
6395
|
+
await rejects(
|
|
6396
|
+
() => releasedLoader(missingUrl, loaderOptions),
|
|
6397
|
+
FetchError,
|
|
6398
|
+
);
|
|
6399
|
+
deepStrictEqual(calls.at(-1), loaderOptions);
|
|
6400
|
+
ok(records.some((record) => record.level === "error"));
|
|
6401
|
+
}
|
|
6402
|
+
} finally {
|
|
6403
|
+
fetchMock.hardReset();
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
} finally {
|
|
6407
|
+
await reset();
|
|
6408
|
+
}
|
|
6409
|
+
});
|