@fedify/vocab 2.4.0-pr.936.41 → 2.4.1
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 +32014 -9323
- package/dist/mod.d.cts +4770 -91
- package/dist/mod.d.ts +4770 -91
- package/dist/mod.js +31907 -9223
- package/dist-tests/actor-D_QaCMro.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/{actor-MBpzF3-D.mjs → esm-YWo4aQQe.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/lookup-BevjwZhB.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -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/{vocab-Bjwctrxn.mjs → vocab-DaniQwU7.mjs} +31669 -9267
- package/dist-tests/vocab.test.mjs +901 -11
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +291 -15
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/gateway.test.ts +2637 -0
- 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/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +1 -1
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/vocab.test.ts +1219 -5
- /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,13 +17,16 @@ 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,
|
|
20
24
|
ok,
|
|
21
25
|
rejects,
|
|
26
|
+
strictEqual,
|
|
22
27
|
throws,
|
|
23
28
|
} from "node:assert/strict";
|
|
29
|
+
import { inspect } from "node:util";
|
|
24
30
|
import { assertInstanceOf } from "./utils.ts";
|
|
25
31
|
import * as vocab from "./vocab.ts";
|
|
26
32
|
import {
|
|
@@ -58,6 +64,7 @@ import {
|
|
|
58
64
|
Organization,
|
|
59
65
|
Person,
|
|
60
66
|
Place,
|
|
67
|
+
PropertyValue,
|
|
61
68
|
Proposal,
|
|
62
69
|
Question,
|
|
63
70
|
QuoteAuthorization,
|
|
@@ -397,6 +404,112 @@ test("Object.toJsonLd()", async () => {
|
|
|
397
404
|
});
|
|
398
405
|
});
|
|
399
406
|
|
|
407
|
+
test("Note.fromJsonLd() ignores malformed language tags", async () => {
|
|
408
|
+
const note = await Note.fromJsonLd({
|
|
409
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
410
|
+
type: "Note",
|
|
411
|
+
contentMap: {
|
|
412
|
+
invalid_tag: "Hello",
|
|
413
|
+
en: "Hi",
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
deepStrictEqual(note.contents, [new LanguageString("Hi", "en")]);
|
|
418
|
+
const jsonLd = await note.toJsonLd() as Record<string, unknown>;
|
|
419
|
+
deepStrictEqual(jsonLd.contentMap, { en: "Hi" });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("Note.fromJsonLd() ignores malformed language tags in nested objects", async () => {
|
|
423
|
+
const note = await Note.fromJsonLd({
|
|
424
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Note"],
|
|
425
|
+
"https://www.w3.org/ns/activitystreams#attachment": [{
|
|
426
|
+
"@type": ["http://schema.org#PropertyValue"],
|
|
427
|
+
"https://www.w3.org/ns/activitystreams#name": [
|
|
428
|
+
{ "@value": "Malformed", "@language": "invalid_tag" },
|
|
429
|
+
{ "@value": "Valid", "@language": "en" },
|
|
430
|
+
],
|
|
431
|
+
}],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
deepStrictEqual(await Array.fromAsync(note.getAttachments()), [
|
|
435
|
+
new PropertyValue({ name: new LanguageString("Valid", "en") }),
|
|
436
|
+
]);
|
|
437
|
+
const jsonLd = await note.toJsonLd() as Record<string, unknown>;
|
|
438
|
+
const attachment = jsonLd.attachment as Record<string, unknown>;
|
|
439
|
+
deepStrictEqual(attachment.nameMap, { en: "Valid" });
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("Note.fromJsonLd() normalizes extlang language tags", async () => {
|
|
443
|
+
const json = {
|
|
444
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
445
|
+
type: "Note",
|
|
446
|
+
content: "<p>早晨</p>",
|
|
447
|
+
contentMap: {
|
|
448
|
+
"zh-YUE": "<p>早晨</p>",
|
|
449
|
+
"yue": "<p>早晨呀</p>",
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
const note = await Note.fromJsonLd(json);
|
|
453
|
+
// JSON-LD expansion sorts language map keys, so yue comes before zh-yue:
|
|
454
|
+
deepStrictEqual(note.contents, [
|
|
455
|
+
"<p>早晨</p>",
|
|
456
|
+
new LanguageString("<p>早晨呀</p>", "yue"),
|
|
457
|
+
new LanguageString("<p>早晨</p>", "yue"),
|
|
458
|
+
]);
|
|
459
|
+
deepStrictEqual(
|
|
460
|
+
note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName),
|
|
461
|
+
[null, "yue", "yue"],
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
// The unmodified object is serialized from the cached original document:
|
|
465
|
+
deepStrictEqual(await note.toJsonLd(), json);
|
|
466
|
+
const compact = await note.toJsonLd({ format: "compact" }) as Record<
|
|
467
|
+
string,
|
|
468
|
+
unknown
|
|
469
|
+
>;
|
|
470
|
+
deepStrictEqual(compact.contentMap, {
|
|
471
|
+
yue: ["<p>早晨呀</p>", "<p>早晨</p>"],
|
|
472
|
+
});
|
|
473
|
+
const expanded = await note.toJsonLd({ format: "expand" });
|
|
474
|
+
deepStrictEqual(
|
|
475
|
+
(expanded as Record<string, unknown>[])[0][
|
|
476
|
+
"https://www.w3.org/ns/activitystreams#content"
|
|
477
|
+
],
|
|
478
|
+
[
|
|
479
|
+
{ "@value": "<p>早晨</p>" },
|
|
480
|
+
{ "@value": "<p>早晨呀</p>", "@language": "yue" },
|
|
481
|
+
{ "@value": "<p>早晨</p>", "@language": "yue" },
|
|
482
|
+
],
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const expandedNote = await Note.fromJsonLd({
|
|
486
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Note"],
|
|
487
|
+
"https://www.w3.org/ns/activitystreams#name": [
|
|
488
|
+
{ "@value": "早晨", "@language": "zh-yue-HK" },
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
|
|
492
|
+
deepStrictEqual(
|
|
493
|
+
(expandedNote.names[0] as LanguageString).locale.baseName,
|
|
494
|
+
"yue-HK",
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("Note.fromJsonLd() caches original language tags", async () => {
|
|
499
|
+
const json = {
|
|
500
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
501
|
+
type: "Note",
|
|
502
|
+
contentMap: { "EN-us": "Hello" },
|
|
503
|
+
};
|
|
504
|
+
const note = await Note.fromJsonLd(json);
|
|
505
|
+
deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
|
|
506
|
+
deepStrictEqual(
|
|
507
|
+
(note.contents[0] as LanguageString).locale.baseName,
|
|
508
|
+
"en-US",
|
|
509
|
+
);
|
|
510
|
+
deepStrictEqual(await note.toJsonLd(), json);
|
|
511
|
+
});
|
|
512
|
+
|
|
400
513
|
test("Note.toJsonLd()", async () => {
|
|
401
514
|
const note = new Note({
|
|
402
515
|
tags: [
|
|
@@ -628,6 +741,31 @@ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
|
|
|
628
741
|
);
|
|
629
742
|
});
|
|
630
743
|
|
|
744
|
+
test("fromJsonLd() refuses portable IDs changed by URL parsing", async () => {
|
|
745
|
+
const base = "ap://did:key:z6Mkabc";
|
|
746
|
+
for (
|
|
747
|
+
const id of [
|
|
748
|
+
`${base}/a/../b`,
|
|
749
|
+
`${base}/a/%2e%2E/b`,
|
|
750
|
+
"https://gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
|
|
751
|
+
"https:/gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
|
|
752
|
+
]
|
|
753
|
+
) {
|
|
754
|
+
await rejects(
|
|
755
|
+
() =>
|
|
756
|
+
Note.fromJsonLd({
|
|
757
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
758
|
+
type: "Note",
|
|
759
|
+
id,
|
|
760
|
+
}, {
|
|
761
|
+
documentLoader: mockDocumentLoader,
|
|
762
|
+
contextLoader: mockDocumentLoader,
|
|
763
|
+
}),
|
|
764
|
+
TypeError,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
631
769
|
test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
|
|
632
770
|
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
633
771
|
const gateways = [
|
|
@@ -647,15 +785,123 @@ test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
|
|
|
647
785
|
});
|
|
648
786
|
deepStrictEqual(actor.gateways, gateways);
|
|
649
787
|
|
|
650
|
-
const jsonLd = await actor.toJsonLd(
|
|
788
|
+
const jsonLd = await actor.toJsonLd({
|
|
789
|
+
format: "compact",
|
|
790
|
+
}) as Record<string, unknown>;
|
|
651
791
|
deepStrictEqual(jsonLd.type, ActorClass.name);
|
|
652
|
-
deepStrictEqual(jsonLd.gateways, gateways.map((gateway) => gateway.
|
|
792
|
+
deepStrictEqual(jsonLd.gateways, gateways.map((gateway) => gateway.origin));
|
|
653
793
|
|
|
654
794
|
const restored = await ActorClass.fromJsonLd(jsonLd);
|
|
655
795
|
deepStrictEqual(restored.gateways, gateways);
|
|
656
796
|
}
|
|
657
797
|
});
|
|
658
798
|
|
|
799
|
+
test("FEP-ef61: actor gateways serialize as origins", async () => {
|
|
800
|
+
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
801
|
+
const gateways = [
|
|
802
|
+
new URL("https://first.example:443/"),
|
|
803
|
+
new URL("http://second.example:80/"),
|
|
804
|
+
new URL("https://third.example:8443/"),
|
|
805
|
+
new URL("http://fourth.example:8080/"),
|
|
806
|
+
];
|
|
807
|
+
const origins = [
|
|
808
|
+
"https://first.example",
|
|
809
|
+
"http://second.example",
|
|
810
|
+
"https://third.example:8443",
|
|
811
|
+
"http://fourth.example:8080",
|
|
812
|
+
];
|
|
813
|
+
for (const ActorClass of actorClasses) {
|
|
814
|
+
const actor = new ActorClass({
|
|
815
|
+
id: parseIri("ap://did:key:z6Mkabc/actor"),
|
|
816
|
+
gateways,
|
|
817
|
+
});
|
|
818
|
+
const compact = await actor.toJsonLd() as Record<string, unknown>;
|
|
819
|
+
deepStrictEqual(compact.gateways, origins);
|
|
820
|
+
const expanded = await actor.toJsonLd({
|
|
821
|
+
format: "expand",
|
|
822
|
+
}) as Record<string, unknown>[];
|
|
823
|
+
deepStrictEqual(expanded[0]["https://w3id.org/fep/ef61/gateways"], [
|
|
824
|
+
{ "@list": origins.map((origin) => ({ "@id": origin })) },
|
|
825
|
+
]);
|
|
826
|
+
deepStrictEqual(actor.gateways, gateways);
|
|
827
|
+
deepStrictEqual(
|
|
828
|
+
actor.gateways.map((gateway) => gateway.href),
|
|
829
|
+
origins.map((origin) => `${origin}/`),
|
|
830
|
+
);
|
|
831
|
+
deepStrictEqual((await ActorClass.fromJsonLd(compact)).gateways, gateways);
|
|
832
|
+
deepStrictEqual((await ActorClass.fromJsonLd(expanded)).gateways, gateways);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("FEP-ef61: portable actors parse unmapped gateway lists", async () => {
|
|
837
|
+
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
838
|
+
const gateways = ["https://first.example", "https://second.example"];
|
|
839
|
+
const ids = [
|
|
840
|
+
"ap://did:key:z6Mkabc/actor",
|
|
841
|
+
"https://first.example/.well-known/apgateway/did:key:z6Mkabc/actor",
|
|
842
|
+
];
|
|
843
|
+
for (const ActorClass of actorClasses) {
|
|
844
|
+
for (const id of ids) {
|
|
845
|
+
const actor = await ActorClass.fromJsonLd({
|
|
846
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
847
|
+
type: ActorClass.name,
|
|
848
|
+
id,
|
|
849
|
+
inbox: `${id}/inbox`,
|
|
850
|
+
outbox: `${id}/outbox`,
|
|
851
|
+
gateways,
|
|
852
|
+
});
|
|
853
|
+
deepStrictEqual(actor.gateways, gateways.map((g) => new URL(g)));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("FEP-ef61: unmapped gateways retain their context meaning", async () => {
|
|
859
|
+
const actorClasses = [Application, Group, Organization, Person, Service];
|
|
860
|
+
const id = "ap://did:key:z6Mkabc/actor";
|
|
861
|
+
for (const ActorClass of actorClasses) {
|
|
862
|
+
const document = {
|
|
863
|
+
type: ActorClass.name,
|
|
864
|
+
id,
|
|
865
|
+
inbox: `${id}/inbox`,
|
|
866
|
+
outbox: `${id}/outbox`,
|
|
867
|
+
gateways: ["https://unmapped.example"],
|
|
868
|
+
};
|
|
869
|
+
for (
|
|
870
|
+
const context of [
|
|
871
|
+
{ gateways: "https://example.com/unrelated" },
|
|
872
|
+
{ gateways: null },
|
|
873
|
+
{ "@vocab": "https://example.com/" },
|
|
874
|
+
{
|
|
875
|
+
[ActorClass.name]: {
|
|
876
|
+
"@id": `https://www.w3.org/ns/activitystreams#${ActorClass.name}`,
|
|
877
|
+
"@context": { gateways: "https://example.com/scoped" },
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
]
|
|
881
|
+
) {
|
|
882
|
+
const actor = await ActorClass.fromJsonLd({
|
|
883
|
+
"@context": ["https://www.w3.org/ns/activitystreams", context],
|
|
884
|
+
...document,
|
|
885
|
+
});
|
|
886
|
+
deepStrictEqual(actor.gateways, []);
|
|
887
|
+
}
|
|
888
|
+
const ordinary = await ActorClass.fromJsonLd({
|
|
889
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
890
|
+
...document,
|
|
891
|
+
id: "https://ordinary.example/actor",
|
|
892
|
+
});
|
|
893
|
+
deepStrictEqual(ordinary.gateways, []);
|
|
894
|
+
const canonical = await ActorClass.fromJsonLd({
|
|
895
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
896
|
+
...document,
|
|
897
|
+
"https://w3id.org/fep/ef61/gateways": {
|
|
898
|
+
"@list": [{ "@id": "https://canonical.example" }],
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
deepStrictEqual(canonical.gateways, [new URL("https://canonical.example")]);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
|
|
659
905
|
test("FEP-ef61: actor gateways preserve single, empty, and invalid cases", async () => {
|
|
660
906
|
const singleGateway = new Person({
|
|
661
907
|
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
@@ -663,7 +909,7 @@ test("FEP-ef61: actor gateways preserve single, empty, and invalid cases", async
|
|
|
663
909
|
});
|
|
664
910
|
deepStrictEqual(
|
|
665
911
|
(await singleGateway.toJsonLd() as Record<string, unknown>).gateways,
|
|
666
|
-
["https://server.example
|
|
912
|
+
["https://server.example"],
|
|
667
913
|
);
|
|
668
914
|
|
|
669
915
|
const noGateways = new Person({
|
|
@@ -724,7 +970,9 @@ test("FEP-ef61: actor gateways must be HTTP(S) base URIs", async () => {
|
|
|
724
970
|
"https://user@server.example/",
|
|
725
971
|
"https://server.example/path",
|
|
726
972
|
"https://server.example/?x=1",
|
|
973
|
+
"https://server.example/?",
|
|
727
974
|
"https://server.example/#fragment",
|
|
975
|
+
"https://server.example/#",
|
|
728
976
|
]
|
|
729
977
|
) {
|
|
730
978
|
throws(
|
|
@@ -1768,6 +2016,130 @@ test({
|
|
|
1768
2016
|
});
|
|
1769
2017
|
},
|
|
1770
2018
|
});
|
|
2019
|
+
test({
|
|
2020
|
+
name: "Announce.getObject() logs suppressed failures as warnings",
|
|
2021
|
+
permissions: { env: true, read: true },
|
|
2022
|
+
async fn() {
|
|
2023
|
+
const records: LogRecord[] = [];
|
|
2024
|
+
const notFoundUrl = "https://example.com/suppressed-not-found";
|
|
2025
|
+
const invalidObjectUrl = "https://example.com/invalid-object";
|
|
2026
|
+
|
|
2027
|
+
await reset();
|
|
2028
|
+
fetchMock.spyGlobal();
|
|
2029
|
+
|
|
2030
|
+
try {
|
|
2031
|
+
await configure({
|
|
2032
|
+
sinks: {
|
|
2033
|
+
buffer(record: LogRecord): void {
|
|
2034
|
+
records.push(record);
|
|
2035
|
+
},
|
|
2036
|
+
},
|
|
2037
|
+
filters: {},
|
|
2038
|
+
loggers: [{ category: [], sinks: ["buffer"] }],
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
fetchMock.get(notFoundUrl, { status: 404 });
|
|
2042
|
+
|
|
2043
|
+
const suppressedFetch = new Announce({
|
|
2044
|
+
object: new URL(notFoundUrl),
|
|
2045
|
+
});
|
|
2046
|
+
deepStrictEqual(
|
|
2047
|
+
await suppressedFetch.getObject({ suppressError: true }),
|
|
2048
|
+
null,
|
|
2049
|
+
);
|
|
2050
|
+
ok(
|
|
2051
|
+
records.some((record) =>
|
|
2052
|
+
record.level === "warning" &&
|
|
2053
|
+
record.rawMessage ===
|
|
2054
|
+
"Failed to fetch document: {status} {url} {headers}"
|
|
2055
|
+
),
|
|
2056
|
+
);
|
|
2057
|
+
ok(
|
|
2058
|
+
records.some((record) =>
|
|
2059
|
+
record.level === "warning" &&
|
|
2060
|
+
record.rawMessage === "Failed to fetch {url}: {error}"
|
|
2061
|
+
),
|
|
2062
|
+
);
|
|
2063
|
+
deepStrictEqual(
|
|
2064
|
+
records.some((record) =>
|
|
2065
|
+
record.level === "error" &&
|
|
2066
|
+
(
|
|
2067
|
+
record.rawMessage ===
|
|
2068
|
+
"Failed to fetch document: {status} {url} {headers}" ||
|
|
2069
|
+
record.rawMessage === "Failed to fetch {url}: {error}"
|
|
2070
|
+
)
|
|
2071
|
+
),
|
|
2072
|
+
false,
|
|
2073
|
+
);
|
|
2074
|
+
|
|
2075
|
+
records.length = 0;
|
|
2076
|
+
|
|
2077
|
+
const unsuppressedFetch = new Announce({
|
|
2078
|
+
object: new URL(notFoundUrl),
|
|
2079
|
+
});
|
|
2080
|
+
await rejects(
|
|
2081
|
+
() => unsuppressedFetch.getObject(),
|
|
2082
|
+
FetchError,
|
|
2083
|
+
);
|
|
2084
|
+
ok(
|
|
2085
|
+
records.some((record) =>
|
|
2086
|
+
record.level === "error" &&
|
|
2087
|
+
record.rawMessage ===
|
|
2088
|
+
"Failed to fetch document: {status} {url} {headers}"
|
|
2089
|
+
),
|
|
2090
|
+
);
|
|
2091
|
+
|
|
2092
|
+
records.length = 0;
|
|
2093
|
+
|
|
2094
|
+
// deno-lint-ignore require-await
|
|
2095
|
+
const invalidDocumentLoader: DocumentLoader = async (url) => ({
|
|
2096
|
+
contextUrl: null,
|
|
2097
|
+
documentUrl: url,
|
|
2098
|
+
document: null,
|
|
2099
|
+
});
|
|
2100
|
+
|
|
2101
|
+
const suppressedParsing = new Announce({
|
|
2102
|
+
object: new URL(invalidObjectUrl),
|
|
2103
|
+
});
|
|
2104
|
+
deepStrictEqual(
|
|
2105
|
+
await suppressedParsing.getObject({
|
|
2106
|
+
documentLoader: invalidDocumentLoader,
|
|
2107
|
+
suppressError: true,
|
|
2108
|
+
}),
|
|
2109
|
+
null,
|
|
2110
|
+
);
|
|
2111
|
+
ok(
|
|
2112
|
+
records.some((record) =>
|
|
2113
|
+
record.level === "warning" &&
|
|
2114
|
+
record.rawMessage === "Failed to parse {url}: {error}"
|
|
2115
|
+
),
|
|
2116
|
+
);
|
|
2117
|
+
deepStrictEqual(
|
|
2118
|
+
records.some((record) =>
|
|
2119
|
+
record.level === "error" &&
|
|
2120
|
+
record.rawMessage === "Failed to parse {url}: {error}"
|
|
2121
|
+
),
|
|
2122
|
+
false,
|
|
2123
|
+
);
|
|
2124
|
+
|
|
2125
|
+
records.length = 0;
|
|
2126
|
+
|
|
2127
|
+
const unsuppressedParsing = new Announce({
|
|
2128
|
+
object: new URL(invalidObjectUrl),
|
|
2129
|
+
});
|
|
2130
|
+
await rejects(
|
|
2131
|
+
() =>
|
|
2132
|
+
unsuppressedParsing.getObject({
|
|
2133
|
+
documentLoader: invalidDocumentLoader,
|
|
2134
|
+
}),
|
|
2135
|
+
TypeError,
|
|
2136
|
+
);
|
|
2137
|
+
} finally {
|
|
2138
|
+
fetchMock.hardReset();
|
|
2139
|
+
await reset();
|
|
2140
|
+
}
|
|
2141
|
+
},
|
|
2142
|
+
});
|
|
1771
2143
|
|
|
1772
2144
|
test("Activity.getObject() fetches canonical portable IRIs", async () => {
|
|
1773
2145
|
const fetchedUrls: string[] = [];
|
|
@@ -1794,6 +2166,8 @@ test("Activity.getObject() fetches canonical portable IRIs", async () => {
|
|
|
1794
2166
|
const object = await activity.getObject({
|
|
1795
2167
|
documentLoader,
|
|
1796
2168
|
contextLoader: mockDocumentLoader,
|
|
2169
|
+
// deno-lint-ignore require-await
|
|
2170
|
+
verifyPortableObject: async () => ({ verified: true }),
|
|
1797
2171
|
});
|
|
1798
2172
|
|
|
1799
2173
|
assertInstanceOf(object, Note);
|
|
@@ -1842,6 +2216,104 @@ test({
|
|
|
1842
2216
|
},
|
|
1843
2217
|
});
|
|
1844
2218
|
|
|
2219
|
+
// The alias is inherited during initial expansion, but is unavailable when
|
|
2220
|
+
// an embedded value is reparsed standalone from the cached JSON-LD.
|
|
2221
|
+
function embeddedPropertyDocument(property: "attachment" | "object") {
|
|
2222
|
+
const context = "https://www.w3.org/ns/activitystreams";
|
|
2223
|
+
return {
|
|
2224
|
+
"@context": [context, { CustomImage: `${context}#Image` }],
|
|
2225
|
+
type: property === "attachment" ? "Note" : "Create",
|
|
2226
|
+
[property]: [
|
|
2227
|
+
{
|
|
2228
|
+
"@context": [context, { "@vocab": "https://example.com/" }],
|
|
2229
|
+
type: "CustomImage",
|
|
2230
|
+
name: "Malformed when reparsed",
|
|
2231
|
+
},
|
|
2232
|
+
{ "@context": context, type: "Image", name: "Cached image" },
|
|
2233
|
+
{ type: "Image", name: "Decoded image" },
|
|
2234
|
+
],
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
for (const suppressError of [undefined, false, true]) {
|
|
2239
|
+
test(
|
|
2240
|
+
`Note.getAttachments() cached parse failure (suppressError: ${suppressError})`,
|
|
2241
|
+
async () => {
|
|
2242
|
+
const jsonLd = embeddedPropertyDocument("attachment");
|
|
2243
|
+
const note = await Note.fromJsonLd(jsonLd);
|
|
2244
|
+
if (suppressError) {
|
|
2245
|
+
for (let i = 0; i < 2; i++) {
|
|
2246
|
+
const attachments: (Object | Link | PropertyValue)[] = await Array
|
|
2247
|
+
.fromAsync(
|
|
2248
|
+
note.getAttachments({ suppressError }),
|
|
2249
|
+
);
|
|
2250
|
+
deepStrictEqual(
|
|
2251
|
+
attachments.map((attachment) => attachment.name),
|
|
2252
|
+
["Cached image", "Decoded image"],
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
// Suppression must not remove or replace the cached malformed value.
|
|
2256
|
+
await rejects(() => Array.fromAsync(note.getAttachments()), TypeError);
|
|
2257
|
+
} else {
|
|
2258
|
+
await rejects(
|
|
2259
|
+
() => Array.fromAsync(note.getAttachments({ suppressError })),
|
|
2260
|
+
TypeError,
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
deepStrictEqual(await note.toJsonLd(), jsonLd);
|
|
2264
|
+
},
|
|
2265
|
+
);
|
|
2266
|
+
|
|
2267
|
+
test(
|
|
2268
|
+
`Activity.getObject() cached parse failure (suppressError: ${suppressError})`,
|
|
2269
|
+
async () => {
|
|
2270
|
+
const jsonLd = embeddedPropertyDocument("object");
|
|
2271
|
+
const activity = await Activity.fromJsonLd(jsonLd);
|
|
2272
|
+
if (suppressError) {
|
|
2273
|
+
deepStrictEqual(await activity.getObject({ suppressError }), null);
|
|
2274
|
+
deepStrictEqual(await activity.getObject({ suppressError }), null);
|
|
2275
|
+
await rejects(() => activity.getObject(), TypeError);
|
|
2276
|
+
} else {
|
|
2277
|
+
await rejects(() => activity.getObject({ suppressError }), TypeError);
|
|
2278
|
+
}
|
|
2279
|
+
deepStrictEqual(await activity.toJsonLd(), jsonLd);
|
|
2280
|
+
},
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
test("Note.getAttachments() suppresses cached context loader failures", async () => {
|
|
2285
|
+
const note = await Note.fromJsonLd(embeddedPropertyDocument("attachment"));
|
|
2286
|
+
const contextLoader = () =>
|
|
2287
|
+
Promise.reject(new Error("Context loader failed"));
|
|
2288
|
+
await rejects(
|
|
2289
|
+
() => Array.fromAsync(note.getAttachments({ contextLoader })),
|
|
2290
|
+
(error: unknown) => error instanceof Error && !(error instanceof TypeError),
|
|
2291
|
+
);
|
|
2292
|
+
// Both cached values need the failing loader; the decoded value remains usable.
|
|
2293
|
+
const attachments: (Object | Link | PropertyValue)[] = await Array.fromAsync(
|
|
2294
|
+
note.getAttachments({ contextLoader, suppressError: true }),
|
|
2295
|
+
);
|
|
2296
|
+
deepStrictEqual(attachments.map((attachment) => attachment.name), [
|
|
2297
|
+
"Decoded image",
|
|
2298
|
+
]);
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
test("Activity.getObject() suppresses cached context loader failures", async () => {
|
|
2302
|
+
const activity = await Activity.fromJsonLd(
|
|
2303
|
+
embeddedPropertyDocument("object"),
|
|
2304
|
+
);
|
|
2305
|
+
const contextLoader = () =>
|
|
2306
|
+
Promise.reject(new Error("Context loader failed"));
|
|
2307
|
+
await rejects(
|
|
2308
|
+
() => activity.getObject({ contextLoader }),
|
|
2309
|
+
(error: unknown) => error instanceof Error && !(error instanceof TypeError),
|
|
2310
|
+
);
|
|
2311
|
+
deepStrictEqual(
|
|
2312
|
+
await activity.getObject({ contextLoader, suppressError: true }),
|
|
2313
|
+
null,
|
|
2314
|
+
);
|
|
2315
|
+
});
|
|
2316
|
+
|
|
1845
2317
|
test("Activity.clone()", async () => {
|
|
1846
2318
|
const activity = new Activity({
|
|
1847
2319
|
actor: new Person({
|
|
@@ -1922,6 +2394,26 @@ test({
|
|
|
1922
2394
|
},
|
|
1923
2395
|
});
|
|
1924
2396
|
|
|
2397
|
+
test("inspecting an object prints portable IRIs in their canonical form", () => {
|
|
2398
|
+
const did = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
|
|
2399
|
+
const obj = new Object({
|
|
2400
|
+
id: parseIri(`ap://${did}/note`),
|
|
2401
|
+
attribution: parseIri(`ap://${did}/actor`),
|
|
2402
|
+
});
|
|
2403
|
+
const inspected = [inspect(obj, { colors: false })];
|
|
2404
|
+
if ("Deno" in globalThis) {
|
|
2405
|
+
inspected.push(Deno.inspect(obj, { colors: false }));
|
|
2406
|
+
}
|
|
2407
|
+
for (const text of inspected) {
|
|
2408
|
+
ok(text.includes(`ap+ef61://${did}/note`), text);
|
|
2409
|
+
ok(text.includes(`ap+ef61://${did}/actor`), text);
|
|
2410
|
+
ok(!text.includes("%3A"), text);
|
|
2411
|
+
}
|
|
2412
|
+
// A URL that cannot be formatted as an IRI does not break inspection:
|
|
2413
|
+
const malformed = new Object({ id: new URL("ap://not-a-did/note") });
|
|
2414
|
+
ok(inspect(malformed).includes("ap://not-a-did/note"));
|
|
2415
|
+
});
|
|
2416
|
+
|
|
1925
2417
|
test("Person.fromJsonLd()", async () => {
|
|
1926
2418
|
const person = await Person.fromJsonLd({
|
|
1927
2419
|
"@context": [
|
|
@@ -3189,6 +3681,105 @@ test("Delete.toJsonLd() compacts embedded QuoteRequest", async () => {
|
|
|
3189
3681
|
);
|
|
3190
3682
|
});
|
|
3191
3683
|
|
|
3684
|
+
test("Note.quoteUrl (IRI-typed alias terms)", async () => {
|
|
3685
|
+
const jsonLd: Record<string, unknown> = {
|
|
3686
|
+
"@context": [
|
|
3687
|
+
"https://www.w3.org/ns/activitystreams",
|
|
3688
|
+
{
|
|
3689
|
+
fedibird: "http://fedibird.com/ns#",
|
|
3690
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
3691
|
+
_misskey_quote: {
|
|
3692
|
+
"@id": "misskey:_misskey_quote",
|
|
3693
|
+
"@type": "@id",
|
|
3694
|
+
},
|
|
3695
|
+
quoteUri: {
|
|
3696
|
+
"@id": "fedibird:quoteUri",
|
|
3697
|
+
"@type": "@id",
|
|
3698
|
+
},
|
|
3699
|
+
},
|
|
3700
|
+
],
|
|
3701
|
+
id: "https://example.com/notes/1",
|
|
3702
|
+
type: "Note",
|
|
3703
|
+
_misskey_quote: "https://example.com/notes/quoted",
|
|
3704
|
+
quoteUri: "https://example.com/notes/quoted2",
|
|
3705
|
+
};
|
|
3706
|
+
|
|
3707
|
+
const loaded = await Note.fromJsonLd(jsonLd);
|
|
3708
|
+
deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/notes/quoted"));
|
|
3709
|
+
|
|
3710
|
+
delete jsonLd._misskey_quote;
|
|
3711
|
+
const loaded2 = await Note.fromJsonLd(jsonLd);
|
|
3712
|
+
deepStrictEqual(
|
|
3713
|
+
loaded2.quoteUrl,
|
|
3714
|
+
new URL("https://example.com/notes/quoted2"),
|
|
3715
|
+
);
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
test("Note.quoteUrl (IRI-typed primary term)", async () => {
|
|
3719
|
+
const context = [
|
|
3720
|
+
"https://www.w3.org/ns/activitystreams",
|
|
3721
|
+
{
|
|
3722
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
3723
|
+
quoteUrl: { "@id": "as:quoteUrl", "@type": "@id" },
|
|
3724
|
+
_misskey_quote: "misskey:_misskey_quote",
|
|
3725
|
+
},
|
|
3726
|
+
];
|
|
3727
|
+
|
|
3728
|
+
const loaded = await Note.fromJsonLd({
|
|
3729
|
+
"@context": context,
|
|
3730
|
+
type: "Note",
|
|
3731
|
+
quoteUrl: "https://example.com/object",
|
|
3732
|
+
});
|
|
3733
|
+
deepStrictEqual(loaded.quoteUrl, new URL("https://example.com/object"));
|
|
3734
|
+
|
|
3735
|
+
// An IRI-valued quoteUrl still takes precedence over a plain-string alias:
|
|
3736
|
+
const loaded2 = await Note.fromJsonLd({
|
|
3737
|
+
"@context": context,
|
|
3738
|
+
type: "Note",
|
|
3739
|
+
quoteUrl: "https://example.com/object",
|
|
3740
|
+
_misskey_quote: "https://example.com/object2",
|
|
3741
|
+
});
|
|
3742
|
+
deepStrictEqual(loaded2.quoteUrl, new URL("https://example.com/object"));
|
|
3743
|
+
});
|
|
3744
|
+
|
|
3745
|
+
test("Note.quoteUrl (unparsable IRI-valued terms)", async () => {
|
|
3746
|
+
const context = [
|
|
3747
|
+
"https://www.w3.org/ns/activitystreams",
|
|
3748
|
+
{
|
|
3749
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
3750
|
+
_misskey_quote: { "@id": "misskey:_misskey_quote", "@type": "@id" },
|
|
3751
|
+
},
|
|
3752
|
+
];
|
|
3753
|
+
for (
|
|
3754
|
+
const quote of [
|
|
3755
|
+
{ type: "Note", content: "An inlined quote without an id" },
|
|
3756
|
+
"_:b0",
|
|
3757
|
+
"",
|
|
3758
|
+
"notes/relative",
|
|
3759
|
+
]
|
|
3760
|
+
) {
|
|
3761
|
+
const loaded = await Note.fromJsonLd({
|
|
3762
|
+
"@context": context,
|
|
3763
|
+
id: "https://example.com/notes/1",
|
|
3764
|
+
type: "Note",
|
|
3765
|
+
content: "Hello",
|
|
3766
|
+
_misskey_quote: quote,
|
|
3767
|
+
});
|
|
3768
|
+
deepStrictEqual(loaded.quoteUrl, null, JSON.stringify(quote));
|
|
3769
|
+
deepStrictEqual(loaded.content, "Hello");
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
const atUri = await Note.fromJsonLd({
|
|
3773
|
+
"@context": context,
|
|
3774
|
+
type: "Note",
|
|
3775
|
+
_misskey_quote: "at://did:plc:abc/app.bsky.feed.post/xyz",
|
|
3776
|
+
});
|
|
3777
|
+
deepStrictEqual(
|
|
3778
|
+
atUri.quoteUrl,
|
|
3779
|
+
new URL("at://" + encodeURIComponent("did:plc:abc/app.bsky.feed.post/xyz")),
|
|
3780
|
+
);
|
|
3781
|
+
});
|
|
3782
|
+
|
|
3192
3783
|
test("Key.publicKey", async () => {
|
|
3193
3784
|
const jwk = {
|
|
3194
3785
|
kty: "RSA",
|
|
@@ -3333,6 +3924,29 @@ test("Link.fromJsonLd()", async () => {
|
|
|
3333
3924
|
);
|
|
3334
3925
|
});
|
|
3335
3926
|
|
|
3927
|
+
test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
|
|
3928
|
+
const link = await Link.fromJsonLd({
|
|
3929
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
3930
|
+
type: "Link",
|
|
3931
|
+
href: "https://example.com/yue",
|
|
3932
|
+
hreflang: "zh-YUE",
|
|
3933
|
+
});
|
|
3934
|
+
deepStrictEqual(link.language?.baseName, "yue");
|
|
3935
|
+
const compact = await link.toJsonLd({ format: "compact" }) as Record<
|
|
3936
|
+
string,
|
|
3937
|
+
unknown
|
|
3938
|
+
>;
|
|
3939
|
+
deepStrictEqual(compact.hreflang, "yue");
|
|
3940
|
+
|
|
3941
|
+
await rejects(() =>
|
|
3942
|
+
Link.fromJsonLd({
|
|
3943
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
3944
|
+
type: "Link",
|
|
3945
|
+
href: "https://example.com/en",
|
|
3946
|
+
hreflang: "en-USA",
|
|
3947
|
+
}), RangeError);
|
|
3948
|
+
});
|
|
3949
|
+
|
|
3336
3950
|
test("Person.fromJsonLd() with relative URLs", async () => {
|
|
3337
3951
|
const json = {
|
|
3338
3952
|
"@context": [
|
|
@@ -3918,6 +4532,216 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
|
|
|
3918
4532
|
);
|
|
3919
4533
|
});
|
|
3920
4534
|
|
|
4535
|
+
for (const property of ["object", "attachment"] as const) {
|
|
4536
|
+
for (const fetchOriginal of [false, true]) {
|
|
4537
|
+
test(
|
|
4538
|
+
`clone() isolates ${property} dereferencing (${
|
|
4539
|
+
fetchOriginal ? "original" : "clone"
|
|
4540
|
+
} first)`,
|
|
4541
|
+
async () => {
|
|
4542
|
+
const embedded = {
|
|
4543
|
+
id: "https://b.example/notes/1",
|
|
4544
|
+
type: "Note",
|
|
4545
|
+
content: "embedded",
|
|
4546
|
+
};
|
|
4547
|
+
const json = {
|
|
4548
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
4549
|
+
id: "https://a.example/activities/1",
|
|
4550
|
+
type: "Create",
|
|
4551
|
+
[property]: embedded,
|
|
4552
|
+
};
|
|
4553
|
+
const original = await Create.fromJsonLd(json, {
|
|
4554
|
+
contextLoader: mockDocumentLoader,
|
|
4555
|
+
});
|
|
4556
|
+
const copy = original.clone();
|
|
4557
|
+
const fetching = fetchOriginal ? original : copy;
|
|
4558
|
+
const untouched = fetchOriginal ? copy : original;
|
|
4559
|
+
let fetches = 0;
|
|
4560
|
+
// deno-lint-ignore require-await
|
|
4561
|
+
const documentLoader = async (url: string) => {
|
|
4562
|
+
fetches++;
|
|
4563
|
+
return {
|
|
4564
|
+
contextUrl: null,
|
|
4565
|
+
documentUrl: url,
|
|
4566
|
+
document: {
|
|
4567
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
4568
|
+
...embedded,
|
|
4569
|
+
content: "fetched",
|
|
4570
|
+
},
|
|
4571
|
+
};
|
|
4572
|
+
};
|
|
4573
|
+
const options = { documentLoader, contextLoader: mockDocumentLoader };
|
|
4574
|
+
const fetched = property === "object"
|
|
4575
|
+
? await fetching.getObject(options)
|
|
4576
|
+
: (await Array.fromAsync(fetching.getAttachments(options)))[0];
|
|
4577
|
+
assertInstanceOf(fetched, Note);
|
|
4578
|
+
deepStrictEqual(fetched.content, "fetched");
|
|
4579
|
+
|
|
4580
|
+
// Inspect the other instance before it fetches anything itself.
|
|
4581
|
+
const trusted = { crossOrigin: "trust" as const };
|
|
4582
|
+
const preserved = property === "object"
|
|
4583
|
+
? await untouched.getObject(trusted)
|
|
4584
|
+
: (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
|
|
4585
|
+
assertInstanceOf(preserved, Note);
|
|
4586
|
+
deepStrictEqual(preserved.content, "embedded");
|
|
4587
|
+
const serialized = await original.toJsonLd() as Record<string, unknown>;
|
|
4588
|
+
deepStrictEqual(
|
|
4589
|
+
serialized[property],
|
|
4590
|
+
fetchOriginal ? { ...embedded, content: "fetched" } : embedded,
|
|
4591
|
+
);
|
|
4592
|
+
const accept = new Accept({ object: untouched });
|
|
4593
|
+
const acceptJson = await accept.toJsonLd() as {
|
|
4594
|
+
object: Record<string, unknown>;
|
|
4595
|
+
};
|
|
4596
|
+
const nested = acceptJson.object;
|
|
4597
|
+
deepStrictEqual(nested[property], embedded);
|
|
4598
|
+
|
|
4599
|
+
// Successful lookups and their trust metadata belong to each instance.
|
|
4600
|
+
const cached = property === "object"
|
|
4601
|
+
? await fetching.getObject(options)
|
|
4602
|
+
: (await Array.fromAsync(fetching.getAttachments(options)))[0];
|
|
4603
|
+
strictEqual(cached, fetched);
|
|
4604
|
+
deepStrictEqual(fetches, 1);
|
|
4605
|
+
const independentlyFetched = property === "object"
|
|
4606
|
+
? await untouched.getObject(options)
|
|
4607
|
+
: (await Array.fromAsync(untouched.getAttachments(options)))[0];
|
|
4608
|
+
assertInstanceOf(independentlyFetched, Note);
|
|
4609
|
+
deepStrictEqual(independentlyFetched.content, "fetched");
|
|
4610
|
+
deepStrictEqual(fetches, 2);
|
|
4611
|
+
const warmedClone = fetching.clone();
|
|
4612
|
+
strictEqual(
|
|
4613
|
+
property === "object"
|
|
4614
|
+
? await warmedClone.getObject(options)
|
|
4615
|
+
: (await Array.fromAsync(warmedClone.getAttachments(options)))[0],
|
|
4616
|
+
fetched,
|
|
4617
|
+
);
|
|
4618
|
+
deepStrictEqual(fetches, 2);
|
|
4619
|
+
},
|
|
4620
|
+
);
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
for (const clone of [false, true]) {
|
|
4625
|
+
for (const frozen of [false, true]) {
|
|
4626
|
+
test(
|
|
4627
|
+
`${clone ? "clone()" : "constructor"} copies ${
|
|
4628
|
+
frozen ? "frozen" : "mutable"
|
|
4629
|
+
} plural arrays`,
|
|
4630
|
+
async () => {
|
|
4631
|
+
const items = [
|
|
4632
|
+
new URL("https://example.com/object"),
|
|
4633
|
+
new URL("https://example.com/object"),
|
|
4634
|
+
];
|
|
4635
|
+
const before = items.slice();
|
|
4636
|
+
if (frozen) globalThis.Object.freeze(items);
|
|
4637
|
+
const base = new Collection({});
|
|
4638
|
+
const collection = clone
|
|
4639
|
+
? base.clone({ items })
|
|
4640
|
+
: new Collection({ items });
|
|
4641
|
+
const sibling = clone
|
|
4642
|
+
? base.clone({ items })
|
|
4643
|
+
: new Collection({ items });
|
|
4644
|
+
const fetched = await Array.fromAsync(collection.getItems({
|
|
4645
|
+
documentLoader: mockDocumentLoader,
|
|
4646
|
+
contextLoader: mockDocumentLoader,
|
|
4647
|
+
}));
|
|
4648
|
+
deepStrictEqual(fetched.length, 2);
|
|
4649
|
+
deepStrictEqual(fetched.map((item) => item.name), [
|
|
4650
|
+
"Fetched object",
|
|
4651
|
+
"Fetched object",
|
|
4652
|
+
]);
|
|
4653
|
+
deepStrictEqual(items, before);
|
|
4654
|
+
strictEqual(items[0], before[0]);
|
|
4655
|
+
strictEqual(items[1], before[1]);
|
|
4656
|
+
const siblingJson = await sibling.toJsonLd() as { items: unknown };
|
|
4657
|
+
deepStrictEqual(siblingJson.items, before.map(String));
|
|
4658
|
+
if (!frozen) {
|
|
4659
|
+
items.push(new URL("https://example.com/extra"));
|
|
4660
|
+
deepStrictEqual(collection.itemIds, before);
|
|
4661
|
+
deepStrictEqual(sibling.itemIds, before);
|
|
4662
|
+
}
|
|
4663
|
+
},
|
|
4664
|
+
);
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
test("clone() copies scalar arrays without changing sparse inputs", () => {
|
|
4669
|
+
const names = ["original"];
|
|
4670
|
+
const original = new Object({ names });
|
|
4671
|
+
names.push("caller mutation");
|
|
4672
|
+
deepStrictEqual(original.names, ["original"]);
|
|
4673
|
+
const copy = original.clone();
|
|
4674
|
+
copy.names.push("clone mutation");
|
|
4675
|
+
deepStrictEqual(original.names, ["original"]);
|
|
4676
|
+
deepStrictEqual(copy.names, ["original", "clone mutation"]);
|
|
4677
|
+
const replacements = ["replacement"];
|
|
4678
|
+
const replaced = original.clone({ names: replacements });
|
|
4679
|
+
replacements.push("caller mutation");
|
|
4680
|
+
deepStrictEqual(replaced.names, ["replacement"]);
|
|
4681
|
+
|
|
4682
|
+
const sparseNames = new Array<string>(2);
|
|
4683
|
+
sparseNames[1] = "kept";
|
|
4684
|
+
const sparse = new Object({ names: sparseNames });
|
|
4685
|
+
for (
|
|
4686
|
+
const instance of [
|
|
4687
|
+
sparse,
|
|
4688
|
+
sparse.clone(),
|
|
4689
|
+
sparse.clone({ names: sparse.names }),
|
|
4690
|
+
]
|
|
4691
|
+
) {
|
|
4692
|
+
deepStrictEqual(instance.names.length, 2);
|
|
4693
|
+
deepStrictEqual(0 in instance.names, false);
|
|
4694
|
+
deepStrictEqual(instance.names[1], "kept");
|
|
4695
|
+
}
|
|
4696
|
+
});
|
|
4697
|
+
|
|
4698
|
+
test("clone() preserves embedded object identity and trust", async () => {
|
|
4699
|
+
const note = new Note({ id: new URL("https://b.example/note") });
|
|
4700
|
+
const original = new Create({
|
|
4701
|
+
id: new URL("https://a.example/create"),
|
|
4702
|
+
object: note,
|
|
4703
|
+
});
|
|
4704
|
+
// deno-lint-ignore require-await
|
|
4705
|
+
const documentLoader = async () => {
|
|
4706
|
+
throw new Error("Trusted embedded objects must not be fetched.");
|
|
4707
|
+
};
|
|
4708
|
+
for (
|
|
4709
|
+
const instance of [
|
|
4710
|
+
original,
|
|
4711
|
+
original.clone(),
|
|
4712
|
+
original.clone({ objects: [note] }),
|
|
4713
|
+
]
|
|
4714
|
+
) {
|
|
4715
|
+
strictEqual(await instance.getObject({ documentLoader }), note);
|
|
4716
|
+
strictEqual(instance.objectId, note.id);
|
|
4717
|
+
}
|
|
4718
|
+
});
|
|
4719
|
+
|
|
4720
|
+
test("clone() isolates crossOrigin trust when fetching", async () => {
|
|
4721
|
+
const original = new Create({
|
|
4722
|
+
id: new URL("https://a.example/create"),
|
|
4723
|
+
object: new URL("https://b.example/note"),
|
|
4724
|
+
});
|
|
4725
|
+
const copy = original.clone();
|
|
4726
|
+
// deno-lint-ignore require-await
|
|
4727
|
+
const documentLoader = async (url: string) => ({
|
|
4728
|
+
contextUrl: null,
|
|
4729
|
+
documentUrl: url,
|
|
4730
|
+
document: {
|
|
4731
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
4732
|
+
id: "https://other.example/note",
|
|
4733
|
+
type: "Note",
|
|
4734
|
+
content: "cross-origin",
|
|
4735
|
+
},
|
|
4736
|
+
});
|
|
4737
|
+
const options = { documentLoader, contextLoader: mockDocumentLoader };
|
|
4738
|
+
const trusted = await copy.getObject({ ...options, crossOrigin: "trust" });
|
|
4739
|
+
assertInstanceOf(trusted, Note);
|
|
4740
|
+
deepStrictEqual(trusted.content, "cross-origin");
|
|
4741
|
+
deepStrictEqual(await original.getObject(options), null);
|
|
4742
|
+
deepStrictEqual(original.objectId, new URL("https://b.example/note"));
|
|
4743
|
+
});
|
|
4744
|
+
|
|
3921
4745
|
test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
|
|
3922
4746
|
// Create a mock document loader that returns objects with different origins
|
|
3923
4747
|
// deno-lint-ignore require-await
|
|
@@ -4248,7 +5072,12 @@ test(
|
|
|
4248
5072
|
throw new Error("Document not found");
|
|
4249
5073
|
};
|
|
4250
5074
|
|
|
4251
|
-
const result = await create.getObject({
|
|
5075
|
+
const result = await create.getObject({
|
|
5076
|
+
documentLoader,
|
|
5077
|
+
contextLoader: mockDocumentLoader,
|
|
5078
|
+
// deno-lint-ignore require-await
|
|
5079
|
+
verifyPortableObject: async () => ({ verified: true }),
|
|
5080
|
+
});
|
|
4252
5081
|
assertInstanceOf(result, Note);
|
|
4253
5082
|
deepStrictEqual(result.content, "Fetched portable note");
|
|
4254
5083
|
},
|
|
@@ -5379,7 +6208,25 @@ for (const typeUri in types) {
|
|
|
5379
6208
|
const jsonLd = await instance.toJsonLd({
|
|
5380
6209
|
contextLoader: mockDocumentLoader,
|
|
5381
6210
|
});
|
|
5382
|
-
|
|
6211
|
+
const defaultContexts = Array.isArray(type.defaultContext)
|
|
6212
|
+
? type.defaultContext
|
|
6213
|
+
: [type.defaultContext];
|
|
6214
|
+
const extraContexts = [
|
|
6215
|
+
...new Set(
|
|
6216
|
+
allProperties.flatMap((property) =>
|
|
6217
|
+
property.extraContext == null ||
|
|
6218
|
+
defaultContexts.some((context) => context === property.extraContext)
|
|
6219
|
+
? []
|
|
6220
|
+
: [property.extraContext]
|
|
6221
|
+
),
|
|
6222
|
+
),
|
|
6223
|
+
];
|
|
6224
|
+
deepStrictEqual(
|
|
6225
|
+
jsonLd["@context"],
|
|
6226
|
+
extraContexts.length === 0
|
|
6227
|
+
? type.defaultContext
|
|
6228
|
+
: [...defaultContexts, ...extraContexts],
|
|
6229
|
+
);
|
|
5383
6230
|
if (type.entity) deepStrictEqual(jsonLd.id, "https://example.com/");
|
|
5384
6231
|
const restored = await cls.fromJsonLd(jsonLd, {
|
|
5385
6232
|
documentLoader: mockDocumentLoader,
|
|
@@ -5498,3 +6345,370 @@ for (const typeUri in types) {
|
|
|
5498
6345
|
deepStrictEqual(cls.typeId, new URL(type.uri));
|
|
5499
6346
|
});
|
|
5500
6347
|
}
|
|
6348
|
+
|
|
6349
|
+
test("Announce.getObject() suppresses redirect, context, and private URL logs", async () => {
|
|
6350
|
+
const records: LogRecord[] = [];
|
|
6351
|
+
const objectUrl = "https://example.com/suppression-object";
|
|
6352
|
+
const contextUrl = "https://example.com/suppression-context";
|
|
6353
|
+
const scenarios = [
|
|
6354
|
+
"redirect limit",
|
|
6355
|
+
"redirect loop",
|
|
6356
|
+
"context",
|
|
6357
|
+
"private",
|
|
6358
|
+
"private redirect",
|
|
6359
|
+
];
|
|
6360
|
+
await reset();
|
|
6361
|
+
await configure({
|
|
6362
|
+
sinks: {
|
|
6363
|
+
capture: (record) => {
|
|
6364
|
+
records.push(record);
|
|
6365
|
+
},
|
|
6366
|
+
},
|
|
6367
|
+
loggers: [{
|
|
6368
|
+
category: ["fedify"],
|
|
6369
|
+
lowestLevel: "debug",
|
|
6370
|
+
sinks: ["capture"],
|
|
6371
|
+
}],
|
|
6372
|
+
});
|
|
6373
|
+
try {
|
|
6374
|
+
for (const scenario of scenarios) {
|
|
6375
|
+
fetchMock.spyGlobal();
|
|
6376
|
+
let fetches = 0;
|
|
6377
|
+
fetchMock.get("begin:https://example.com/", (call) => {
|
|
6378
|
+
fetches++;
|
|
6379
|
+
if (scenario === "redirect limit") {
|
|
6380
|
+
return Response.redirect(
|
|
6381
|
+
`https://example.com/redirect-${fetches}`,
|
|
6382
|
+
302,
|
|
6383
|
+
);
|
|
6384
|
+
}
|
|
6385
|
+
if (scenario === "redirect loop") {
|
|
6386
|
+
return Response.redirect(objectUrl, 302);
|
|
6387
|
+
}
|
|
6388
|
+
if (scenario === "private redirect") {
|
|
6389
|
+
return Response.redirect("http://127.0.0.1/private", 302);
|
|
6390
|
+
}
|
|
6391
|
+
if (call.url === contextUrl) return new Response(null, { status: 404 });
|
|
6392
|
+
return new Response(
|
|
6393
|
+
JSON.stringify({
|
|
6394
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
6395
|
+
id: objectUrl,
|
|
6396
|
+
type: "Note",
|
|
6397
|
+
}),
|
|
6398
|
+
{ headers: { "Content-Type": "application/activity+json" } },
|
|
6399
|
+
);
|
|
6400
|
+
});
|
|
6401
|
+
try {
|
|
6402
|
+
for (const suppressError of [true, false, undefined]) {
|
|
6403
|
+
records.length = 0;
|
|
6404
|
+
fetches = 0;
|
|
6405
|
+
const announce = new Announce({
|
|
6406
|
+
object: new URL(
|
|
6407
|
+
scenario === "private" ? "http://127.0.0.1/private" : objectUrl,
|
|
6408
|
+
),
|
|
6409
|
+
});
|
|
6410
|
+
if (suppressError) {
|
|
6411
|
+
deepStrictEqual(await announce.getObject({ suppressError }), null);
|
|
6412
|
+
deepStrictEqual(
|
|
6413
|
+
records.filter((r) => r.level === "error"),
|
|
6414
|
+
[],
|
|
6415
|
+
scenario,
|
|
6416
|
+
);
|
|
6417
|
+
ok(records.some((r) => r.level === "warning"), scenario);
|
|
6418
|
+
} else {
|
|
6419
|
+
await rejects(() => announce.getObject({ suppressError }));
|
|
6420
|
+
ok(records.some((r) => r.level === "error"), scenario);
|
|
6421
|
+
}
|
|
6422
|
+
if (scenario === "private") deepStrictEqual(fetches, 0);
|
|
6423
|
+
if (scenario === "private redirect") deepStrictEqual(fetches, 1);
|
|
6424
|
+
}
|
|
6425
|
+
} finally {
|
|
6426
|
+
fetchMock.hardReset();
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
} finally {
|
|
6430
|
+
await reset();
|
|
6431
|
+
}
|
|
6432
|
+
});
|
|
6433
|
+
|
|
6434
|
+
test("suppressed context loading does not persist on fetched objects", async () => {
|
|
6435
|
+
const states: (boolean | undefined)[] = [];
|
|
6436
|
+
const contextLoader: DocumentLoader = (url, options) => {
|
|
6437
|
+
if (url === "https://www.w3.org/ns/activitystreams") {
|
|
6438
|
+
return mockDocumentLoader(url);
|
|
6439
|
+
}
|
|
6440
|
+
states.push(options?.suppressError);
|
|
6441
|
+
return Promise.resolve({
|
|
6442
|
+
contextUrl: null,
|
|
6443
|
+
documentUrl: url,
|
|
6444
|
+
document: {
|
|
6445
|
+
"@context": {
|
|
6446
|
+
as: "https://www.w3.org/ns/activitystreams#",
|
|
6447
|
+
name: "as:name",
|
|
6448
|
+
},
|
|
6449
|
+
},
|
|
6450
|
+
});
|
|
6451
|
+
};
|
|
6452
|
+
const documentLoader: DocumentLoader = (url) =>
|
|
6453
|
+
Promise.resolve({
|
|
6454
|
+
contextUrl: null,
|
|
6455
|
+
documentUrl: url,
|
|
6456
|
+
document: {
|
|
6457
|
+
"@context": [
|
|
6458
|
+
"https://www.w3.org/ns/activitystreams",
|
|
6459
|
+
"https://example.com/scoped-context",
|
|
6460
|
+
],
|
|
6461
|
+
id: url,
|
|
6462
|
+
type: url.endsWith("actor") ? "Person" : "Note",
|
|
6463
|
+
attributedTo: "https://example.com/scoped-actor",
|
|
6464
|
+
},
|
|
6465
|
+
});
|
|
6466
|
+
const note = await new Announce({
|
|
6467
|
+
object: new URL("https://example.com/scoped-note"),
|
|
6468
|
+
}).getObject({
|
|
6469
|
+
documentLoader,
|
|
6470
|
+
contextLoader,
|
|
6471
|
+
suppressError: true,
|
|
6472
|
+
});
|
|
6473
|
+
assertInstanceOf(note, Note);
|
|
6474
|
+
ok(states.includes(true));
|
|
6475
|
+
states.length = 0;
|
|
6476
|
+
const actor = await note.getAttribution();
|
|
6477
|
+
assertInstanceOf(actor, Person);
|
|
6478
|
+
ok(states.length > 0);
|
|
6479
|
+
deepStrictEqual(states.includes(true), false);
|
|
6480
|
+
});
|
|
6481
|
+
|
|
6482
|
+
test("attribution chains alternate ordinary and portable suppression", async () => {
|
|
6483
|
+
const states: (boolean | undefined)[] = [];
|
|
6484
|
+
const depths: number[][] = [];
|
|
6485
|
+
let hop = 0;
|
|
6486
|
+
const contextUrl = "https://example.com/attribution-context";
|
|
6487
|
+
const contextLoader: DocumentLoader = (url, options) => {
|
|
6488
|
+
if (url !== contextUrl) return mockDocumentLoader(url, options);
|
|
6489
|
+
states.push(options?.suppressError);
|
|
6490
|
+
const stack = new Error().stack;
|
|
6491
|
+
ok(stack);
|
|
6492
|
+
depths[hop]?.push(stack.split("\n").length);
|
|
6493
|
+
return Promise.resolve({
|
|
6494
|
+
contextUrl: null,
|
|
6495
|
+
documentUrl: url,
|
|
6496
|
+
document: {
|
|
6497
|
+
"@context": { name: "https://www.w3.org/ns/activitystreams#name" },
|
|
6498
|
+
},
|
|
6499
|
+
});
|
|
6500
|
+
};
|
|
6501
|
+
const documentLoader: DocumentLoader = (url) => {
|
|
6502
|
+
const hop = Number(new URL(url).pathname.split("/").at(-1));
|
|
6503
|
+
return Promise.resolve({
|
|
6504
|
+
contextUrl: null,
|
|
6505
|
+
documentUrl: url,
|
|
6506
|
+
document: {
|
|
6507
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
6508
|
+
id: url,
|
|
6509
|
+
type: "Person",
|
|
6510
|
+
name: `Actor ${hop}`,
|
|
6511
|
+
attributedTo: `https://example.com/actors/${hop + 1}`,
|
|
6512
|
+
},
|
|
6513
|
+
});
|
|
6514
|
+
};
|
|
6515
|
+
let person = new Person({
|
|
6516
|
+
id: new URL("https://example.com/actors/root"),
|
|
6517
|
+
attribution: new URL("https://example.com/actors/0"),
|
|
6518
|
+
}, { documentLoader, contextLoader });
|
|
6519
|
+
const stackTraceLimit = globalThis.Object.getOwnPropertyDescriptor(
|
|
6520
|
+
Error,
|
|
6521
|
+
"stackTraceLimit",
|
|
6522
|
+
);
|
|
6523
|
+
try {
|
|
6524
|
+
ok(Reflect.set(Error, "stackTraceLimit", Infinity));
|
|
6525
|
+
for (; hop < 100; hop++) {
|
|
6526
|
+
depths[hop] = [];
|
|
6527
|
+
const next = await person.getAttribution({
|
|
6528
|
+
suppressError: true,
|
|
6529
|
+
...(hop % 2 === 0 ? {} : {
|
|
6530
|
+
verifyPortableObject: () =>
|
|
6531
|
+
Promise.resolve({ verified: true as const }),
|
|
6532
|
+
}),
|
|
6533
|
+
});
|
|
6534
|
+
assertInstanceOf(next, Person);
|
|
6535
|
+
deepStrictEqual(next.id, new URL(`https://example.com/actors/${hop}`));
|
|
6536
|
+
person = next;
|
|
6537
|
+
}
|
|
6538
|
+
// Compare the same mode after warmup; a retained wrapper per hop would
|
|
6539
|
+
// increase the synchronous call depth even before the stack overflows.
|
|
6540
|
+
ok(depths.every((values) => values.length > 0));
|
|
6541
|
+
deepStrictEqual(depths[98], depths[2]);
|
|
6542
|
+
deepStrictEqual(depths[99], depths[3]);
|
|
6543
|
+
ok(states.length >= 100);
|
|
6544
|
+
ok(states.every((state) => state === true));
|
|
6545
|
+
states.length = 0;
|
|
6546
|
+
assertInstanceOf(await person.getAttribution(), Person);
|
|
6547
|
+
ok(states.length > 0);
|
|
6548
|
+
ok(states.every((state) => state !== true));
|
|
6549
|
+
} finally {
|
|
6550
|
+
if (stackTraceLimit == null) {
|
|
6551
|
+
Reflect.deleteProperty(Error, "stackTraceLimit");
|
|
6552
|
+
} else {
|
|
6553
|
+
globalThis.Object.defineProperty(
|
|
6554
|
+
Error,
|
|
6555
|
+
"stackTraceLimit",
|
|
6556
|
+
stackTraceLimit,
|
|
6557
|
+
);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
});
|
|
6561
|
+
|
|
6562
|
+
test("portable accessors suppress document and context failure logs", async () => {
|
|
6563
|
+
const records: LogRecord[] = [];
|
|
6564
|
+
const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
6565
|
+
const contextUrl = "https://example.com/portable-suppression-context";
|
|
6566
|
+
await reset();
|
|
6567
|
+
await configure({
|
|
6568
|
+
sinks: {
|
|
6569
|
+
capture: (record) => {
|
|
6570
|
+
records.push(record);
|
|
6571
|
+
},
|
|
6572
|
+
},
|
|
6573
|
+
loggers: [{
|
|
6574
|
+
category: ["fedify"],
|
|
6575
|
+
lowestLevel: "debug",
|
|
6576
|
+
sinks: ["capture"],
|
|
6577
|
+
}],
|
|
6578
|
+
});
|
|
6579
|
+
try {
|
|
6580
|
+
for (const failContext of [false, true]) {
|
|
6581
|
+
fetchMock.spyGlobal();
|
|
6582
|
+
fetchMock.get("begin:https://example.com/", (call) => {
|
|
6583
|
+
if (!failContext || call.url === contextUrl) {
|
|
6584
|
+
return new Response(null, { status: 404 });
|
|
6585
|
+
}
|
|
6586
|
+
return new Response(
|
|
6587
|
+
JSON.stringify({
|
|
6588
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
6589
|
+
id: "ap://did:key:z6Mkabc/objects/1",
|
|
6590
|
+
type: "Note",
|
|
6591
|
+
}),
|
|
6592
|
+
{ headers: { "Content-Type": "application/activity+json" } },
|
|
6593
|
+
);
|
|
6594
|
+
});
|
|
6595
|
+
try {
|
|
6596
|
+
for (const suppressError of [true, false, undefined]) {
|
|
6597
|
+
records.length = 0;
|
|
6598
|
+
const announce = new Announce({ object: id });
|
|
6599
|
+
const options = {
|
|
6600
|
+
suppressError,
|
|
6601
|
+
gateways: ["https://example.com/"],
|
|
6602
|
+
verifyPortableObject: () =>
|
|
6603
|
+
Promise.resolve({ verified: true as const }),
|
|
6604
|
+
};
|
|
6605
|
+
if (suppressError) {
|
|
6606
|
+
deepStrictEqual(await announce.getObject(options), null);
|
|
6607
|
+
deepStrictEqual(records.filter((r) => r.level === "error"), []);
|
|
6608
|
+
ok(records.some((r) => r.level === "warning"));
|
|
6609
|
+
} else {
|
|
6610
|
+
await rejects(() => announce.getObject(options));
|
|
6611
|
+
ok(records.some((r) => r.level === "error"));
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
} finally {
|
|
6615
|
+
fetchMock.hardReset();
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
} finally {
|
|
6619
|
+
await reset();
|
|
6620
|
+
}
|
|
6621
|
+
});
|
|
6622
|
+
|
|
6623
|
+
test("portable verifiers suppress document logs only during dereferencing", async () => {
|
|
6624
|
+
const records: LogRecord[] = [];
|
|
6625
|
+
const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
6626
|
+
const verificationUrl = "https://example.com/verification-document";
|
|
6627
|
+
const missingUrl = "https://example.com/missing-verification-document";
|
|
6628
|
+
const signal = new AbortController().signal;
|
|
6629
|
+
await reset();
|
|
6630
|
+
await configure({
|
|
6631
|
+
sinks: { capture: (record) => records.push(record) },
|
|
6632
|
+
loggers: [{
|
|
6633
|
+
category: ["fedify"],
|
|
6634
|
+
lowestLevel: "debug",
|
|
6635
|
+
sinks: ["capture"],
|
|
6636
|
+
}],
|
|
6637
|
+
});
|
|
6638
|
+
try {
|
|
6639
|
+
for (const failVerification of [false, true]) {
|
|
6640
|
+
fetchMock.spyGlobal();
|
|
6641
|
+
fetchMock.get("begin:https://example.com/.well-known/", {
|
|
6642
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
6643
|
+
body: {
|
|
6644
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
6645
|
+
id: "ap://did:key:z6Mkabc/objects/1",
|
|
6646
|
+
type: "Note",
|
|
6647
|
+
},
|
|
6648
|
+
});
|
|
6649
|
+
fetchMock.get(verificationUrl, {
|
|
6650
|
+
status: failVerification ? 404 : 200,
|
|
6651
|
+
headers: { "Content-Type": "application/ld+json" },
|
|
6652
|
+
body: {},
|
|
6653
|
+
});
|
|
6654
|
+
fetchMock.get(missingUrl, { status: 404 });
|
|
6655
|
+
try {
|
|
6656
|
+
for (const suppressError of [true, false, undefined]) {
|
|
6657
|
+
records.length = 0;
|
|
6658
|
+
const baseLoader = getDocumentLoader();
|
|
6659
|
+
const calls: Parameters<DocumentLoader>[1][] = [];
|
|
6660
|
+
const documentLoader: DocumentLoader = (url, options) => {
|
|
6661
|
+
calls.push(options);
|
|
6662
|
+
return baseLoader(url, options);
|
|
6663
|
+
};
|
|
6664
|
+
let retainedLoader: DocumentLoader | undefined;
|
|
6665
|
+
const loaderOptions = { signal };
|
|
6666
|
+
const announce = new Announce({ object: id });
|
|
6667
|
+
const object = await announce.getObject({
|
|
6668
|
+
suppressError,
|
|
6669
|
+
documentLoader,
|
|
6670
|
+
gateways: ["https://example.com/"],
|
|
6671
|
+
verifyPortableObject: async (_document, options) => {
|
|
6672
|
+
retainedLoader = options.documentLoader;
|
|
6673
|
+
ok(options.documentLoader);
|
|
6674
|
+
await options.documentLoader(verificationUrl, loaderOptions);
|
|
6675
|
+
return { verified: true };
|
|
6676
|
+
},
|
|
6677
|
+
});
|
|
6678
|
+
if (failVerification) {
|
|
6679
|
+
deepStrictEqual(object, null);
|
|
6680
|
+
deepStrictEqual(
|
|
6681
|
+
records.some((record) => record.level === "error"),
|
|
6682
|
+
!suppressError,
|
|
6683
|
+
);
|
|
6684
|
+
ok(records.some((record) => record.level === "warning"));
|
|
6685
|
+
} else {
|
|
6686
|
+
assertInstanceOf(object, Note);
|
|
6687
|
+
}
|
|
6688
|
+
deepStrictEqual(calls.at(-1)?.signal, signal);
|
|
6689
|
+
deepStrictEqual(
|
|
6690
|
+
calls.at(-1)?.suppressError,
|
|
6691
|
+
suppressError ? true : undefined,
|
|
6692
|
+
);
|
|
6693
|
+
deepStrictEqual(loaderOptions, { signal });
|
|
6694
|
+
|
|
6695
|
+
// A verifier may retain the loader after success or rejection.
|
|
6696
|
+
// Suppression must end when the accessor returns in either case.
|
|
6697
|
+
records.length = 0;
|
|
6698
|
+
const releasedLoader = retainedLoader;
|
|
6699
|
+
ok(releasedLoader);
|
|
6700
|
+
await rejects(
|
|
6701
|
+
() => releasedLoader(missingUrl, loaderOptions),
|
|
6702
|
+
FetchError,
|
|
6703
|
+
);
|
|
6704
|
+
deepStrictEqual(calls.at(-1), loaderOptions);
|
|
6705
|
+
ok(records.some((record) => record.level === "error"));
|
|
6706
|
+
}
|
|
6707
|
+
} finally {
|
|
6708
|
+
fetchMock.hardReset();
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
} finally {
|
|
6712
|
+
await reset();
|
|
6713
|
+
}
|
|
6714
|
+
});
|