cojson 0.8.34 → 0.8.35
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/CHANGELOG.md +9 -0
- package/dist/native/coValueCore.js +73 -37
- package/dist/native/coValueCore.js.map +1 -1
- package/dist/native/coValues/coMap.js +2 -2
- package/dist/native/coValues/coMap.js.map +1 -1
- package/dist/native/coValues/group.js +132 -5
- package/dist/native/coValues/group.js.map +1 -1
- package/dist/native/exports.js +5 -2
- package/dist/native/exports.js.map +1 -1
- package/dist/native/ids.js +33 -0
- package/dist/native/ids.js.map +1 -1
- package/dist/native/permissions.js +206 -145
- package/dist/native/permissions.js.map +1 -1
- package/dist/native/storage/index.js +8 -4
- package/dist/native/storage/index.js.map +1 -1
- package/dist/native/sync.js +41 -25
- package/dist/native/sync.js.map +1 -1
- package/dist/web/coValueCore.js +73 -37
- package/dist/web/coValueCore.js.map +1 -1
- package/dist/web/coValues/coMap.js +2 -2
- package/dist/web/coValues/coMap.js.map +1 -1
- package/dist/web/coValues/group.js +132 -5
- package/dist/web/coValues/group.js.map +1 -1
- package/dist/web/exports.js +5 -2
- package/dist/web/exports.js.map +1 -1
- package/dist/web/ids.js +33 -0
- package/dist/web/ids.js.map +1 -1
- package/dist/web/permissions.js +206 -145
- package/dist/web/permissions.js.map +1 -1
- package/dist/web/storage/index.js +8 -4
- package/dist/web/storage/index.js.map +1 -1
- package/dist/web/sync.js +41 -25
- package/dist/web/sync.js.map +1 -1
- package/package.json +1 -1
- package/src/coValueCore.ts +119 -46
- package/src/coValues/coMap.ts +3 -6
- package/src/coValues/group.ts +219 -6
- package/src/exports.ts +18 -3
- package/src/ids.ts +48 -0
- package/src/permissions.ts +297 -204
- package/src/storage/index.ts +12 -4
- package/src/sync.ts +43 -26
- package/src/tests/group.test.ts +152 -1
- package/src/tests/permissions.test.ts +785 -2
- package/src/tests/sync.test.ts +29 -0
- package/src/tests/testUtils.ts +102 -1
|
@@ -4,6 +4,7 @@ import { ControlledAgent } from "../coValues/account.js";
|
|
|
4
4
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
5
5
|
import { expectGroup } from "../typeUtils/expectGroup.js";
|
|
6
6
|
import {
|
|
7
|
+
createTwoConnectedNodes,
|
|
7
8
|
groupWithTwoAdmins,
|
|
8
9
|
groupWithTwoAdminsHighLevel,
|
|
9
10
|
newGroup,
|
|
@@ -1033,7 +1034,7 @@ test("Admins can set group read rey, make a private transaction in an owned obje
|
|
|
1033
1034
|
).toEqual("bar2");
|
|
1034
1035
|
});
|
|
1035
1036
|
|
|
1036
|
-
test("Admins can set group read rey, make a private transaction in an owned object, rotate the read key, add two readers, rotate the read key again to kick out one reader, make another private transaction in the owned object, and only the remaining reader can read both transactions (high level)", () => {
|
|
1037
|
+
test("Admins can set group read rey, make a private transaction in an owned object, rotate the read key, add two readers, rotate the read key again to kick out one reader, make another private transaction in the owned object, and only the remaining reader can read both transactions (high level)", async () => {
|
|
1037
1038
|
const { node, group } = newGroupHighLevel();
|
|
1038
1039
|
|
|
1039
1040
|
const childObject = group.createMap();
|
|
@@ -1057,7 +1058,7 @@ test("Admins can set group read rey, make a private transaction in an owned obje
|
|
|
1057
1058
|
childObject.set("foo2", "bar2", "private");
|
|
1058
1059
|
expect(childObject.get("foo2")).toEqual("bar2");
|
|
1059
1060
|
|
|
1060
|
-
group.removeMember(reader);
|
|
1061
|
+
await group.removeMember(reader);
|
|
1061
1062
|
|
|
1062
1063
|
expect(childObject.core.getCurrentReadKey()).not.toEqual(secondReadKey);
|
|
1063
1064
|
|
|
@@ -1708,3 +1709,785 @@ test("Can give write permissions to 'everyone' (high-level)", async () => {
|
|
|
1708
1709
|
childContent2.set("foo", "bar2", "private");
|
|
1709
1710
|
expect(childContent2.get("foo")).toEqual("bar2");
|
|
1710
1711
|
});
|
|
1712
|
+
|
|
1713
|
+
test("Admins can set parent extensions", () => {
|
|
1714
|
+
const { group, node } = newGroupHighLevel();
|
|
1715
|
+
const parentGroup = node.createGroup();
|
|
1716
|
+
|
|
1717
|
+
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1718
|
+
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
test("Writers, readers and invitees can not set parent extensions", () => {
|
|
1722
|
+
const { group, node } = newGroupHighLevel();
|
|
1723
|
+
const parentGroup = node.createGroup();
|
|
1724
|
+
|
|
1725
|
+
const writer = node.createAccount();
|
|
1726
|
+
const reader = node.createAccount();
|
|
1727
|
+
const adminInvite = node.createAccount();
|
|
1728
|
+
const writerInvite = node.createAccount();
|
|
1729
|
+
const readerInvite = node.createAccount();
|
|
1730
|
+
|
|
1731
|
+
group.addMember(writer, "writer");
|
|
1732
|
+
group.addMember(reader, "reader");
|
|
1733
|
+
group.addMember(adminInvite, "adminInvite");
|
|
1734
|
+
group.addMember(writerInvite, "writerInvite");
|
|
1735
|
+
group.addMember(readerInvite, "readerInvite");
|
|
1736
|
+
|
|
1737
|
+
const groupAsWriter = expectGroup(
|
|
1738
|
+
group.core
|
|
1739
|
+
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
|
1740
|
+
.getCurrentContent(),
|
|
1741
|
+
);
|
|
1742
|
+
|
|
1743
|
+
groupAsWriter.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1744
|
+
expect(groupAsWriter.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
|
1745
|
+
|
|
1746
|
+
const groupAsReader = expectGroup(
|
|
1747
|
+
group.core
|
|
1748
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
1749
|
+
.getCurrentContent(),
|
|
1750
|
+
);
|
|
1751
|
+
|
|
1752
|
+
groupAsReader.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1753
|
+
expect(groupAsReader.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
|
1754
|
+
|
|
1755
|
+
const groupAsAdminInvite = expectGroup(
|
|
1756
|
+
group.core
|
|
1757
|
+
.testWithDifferentAccount(
|
|
1758
|
+
adminInvite,
|
|
1759
|
+
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
|
|
1760
|
+
)
|
|
1761
|
+
.getCurrentContent(),
|
|
1762
|
+
);
|
|
1763
|
+
|
|
1764
|
+
groupAsAdminInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1765
|
+
expect(groupAsAdminInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
|
1766
|
+
|
|
1767
|
+
const groupAsWriterInvite = expectGroup(
|
|
1768
|
+
group.core
|
|
1769
|
+
.testWithDifferentAccount(
|
|
1770
|
+
writerInvite,
|
|
1771
|
+
Crypto.newRandomSessionID(
|
|
1772
|
+
writerInvite.currentAgentID()._unsafeUnwrap(),
|
|
1773
|
+
),
|
|
1774
|
+
)
|
|
1775
|
+
.getCurrentContent(),
|
|
1776
|
+
);
|
|
1777
|
+
|
|
1778
|
+
groupAsWriterInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1779
|
+
expect(groupAsWriterInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
|
1780
|
+
|
|
1781
|
+
const groupAsReaderInvite = expectGroup(
|
|
1782
|
+
group.core
|
|
1783
|
+
.testWithDifferentAccount(
|
|
1784
|
+
readerInvite,
|
|
1785
|
+
Crypto.newRandomSessionID(
|
|
1786
|
+
readerInvite.currentAgentID()._unsafeUnwrap(),
|
|
1787
|
+
),
|
|
1788
|
+
)
|
|
1789
|
+
.getCurrentContent(),
|
|
1790
|
+
);
|
|
1791
|
+
|
|
1792
|
+
groupAsReaderInvite.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1793
|
+
expect(groupAsReaderInvite.get(`parent_${parentGroup.id}`)).toBeUndefined();
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
test("Admins can set child extensions", () => {
|
|
1797
|
+
const { group, node } = newGroupHighLevel();
|
|
1798
|
+
const childGroup = node.createGroup();
|
|
1799
|
+
|
|
1800
|
+
group.set(`child_${childGroup.id}`, "extend", "trusting");
|
|
1801
|
+
expect(group.get(`child_${childGroup.id}`)).toEqual("extend");
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
test("Admins can set child extensions when the admin role is inherited", async () => {
|
|
1805
|
+
const { node1, node2 } = createTwoConnectedNodes("server", "server");
|
|
1806
|
+
|
|
1807
|
+
const node2Account = node2.account;
|
|
1808
|
+
const group = node1.createGroup();
|
|
1809
|
+
|
|
1810
|
+
group.addMember(node2Account, "admin");
|
|
1811
|
+
|
|
1812
|
+
const groupOnNode2 = await node2.load(group.id);
|
|
1813
|
+
|
|
1814
|
+
if (groupOnNode2 === "unavailable") {
|
|
1815
|
+
throw new Error("Group not found on node2");
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
const childGroup = node2.createGroup();
|
|
1819
|
+
childGroup.extend(groupOnNode2);
|
|
1820
|
+
|
|
1821
|
+
const childGroupOnNode1 = await node1.load(childGroup.id);
|
|
1822
|
+
|
|
1823
|
+
if (childGroupOnNode1 === "unavailable") {
|
|
1824
|
+
throw new Error("Child group not found on node1");
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
const grandChildGroup = node2.createGroup();
|
|
1828
|
+
grandChildGroup.extend(childGroupOnNode1);
|
|
1829
|
+
|
|
1830
|
+
expect(childGroupOnNode1.get(`child_${grandChildGroup.id}`)).toEqual(
|
|
1831
|
+
"extend",
|
|
1832
|
+
);
|
|
1833
|
+
expect(grandChildGroup.get(`parent_${childGroupOnNode1.id}`)).toEqual(
|
|
1834
|
+
"extend",
|
|
1835
|
+
);
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
test("Writers, readers and invitees can not set child extensions", () => {
|
|
1839
|
+
const { group, node } = newGroupHighLevel();
|
|
1840
|
+
const childGroup = node.createGroup();
|
|
1841
|
+
|
|
1842
|
+
const writer = node.createAccount();
|
|
1843
|
+
const reader = node.createAccount();
|
|
1844
|
+
const adminInvite = node.createAccount();
|
|
1845
|
+
const writerInvite = node.createAccount();
|
|
1846
|
+
const readerInvite = node.createAccount();
|
|
1847
|
+
|
|
1848
|
+
group.addMember(writer, "writer");
|
|
1849
|
+
group.addMember(reader, "reader");
|
|
1850
|
+
group.addMember(adminInvite, "adminInvite");
|
|
1851
|
+
group.addMember(writerInvite, "writerInvite");
|
|
1852
|
+
group.addMember(readerInvite, "readerInvite");
|
|
1853
|
+
|
|
1854
|
+
const groupAsWriter = expectGroup(
|
|
1855
|
+
group.core
|
|
1856
|
+
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
|
1857
|
+
.getCurrentContent(),
|
|
1858
|
+
);
|
|
1859
|
+
|
|
1860
|
+
groupAsWriter.set(`child_${childGroup.id}`, "extend", "trusting");
|
|
1861
|
+
expect(groupAsWriter.get(`child_${childGroup.id}`)).toBeUndefined();
|
|
1862
|
+
|
|
1863
|
+
const groupAsReader = expectGroup(
|
|
1864
|
+
group.core
|
|
1865
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
1866
|
+
.getCurrentContent(),
|
|
1867
|
+
);
|
|
1868
|
+
|
|
1869
|
+
groupAsReader.set(`child_${childGroup.id}`, "extend", "trusting");
|
|
1870
|
+
expect(groupAsReader.get(`child_${childGroup.id}`)).toBeUndefined();
|
|
1871
|
+
|
|
1872
|
+
const groupAsAdminInvite = expectGroup(
|
|
1873
|
+
group.core
|
|
1874
|
+
.testWithDifferentAccount(
|
|
1875
|
+
adminInvite,
|
|
1876
|
+
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
|
|
1877
|
+
)
|
|
1878
|
+
.getCurrentContent(),
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
groupAsAdminInvite.set(`child_${childGroup.id}`, "extend", "trusting");
|
|
1882
|
+
expect(groupAsAdminInvite.get(`child_${childGroup.id}`)).toBeUndefined();
|
|
1883
|
+
|
|
1884
|
+
const groupAsWriterInvite = expectGroup(
|
|
1885
|
+
group.core
|
|
1886
|
+
.testWithDifferentAccount(
|
|
1887
|
+
writerInvite,
|
|
1888
|
+
Crypto.newRandomSessionID(
|
|
1889
|
+
writerInvite.currentAgentID()._unsafeUnwrap(),
|
|
1890
|
+
),
|
|
1891
|
+
)
|
|
1892
|
+
.getCurrentContent(),
|
|
1893
|
+
);
|
|
1894
|
+
|
|
1895
|
+
groupAsWriterInvite.set(`child_${childGroup.id}`, "extend", "trusting");
|
|
1896
|
+
expect(groupAsWriterInvite.get(`child_${childGroup.id}`)).toBeUndefined();
|
|
1897
|
+
|
|
1898
|
+
const groupAsReaderInvite = expectGroup(
|
|
1899
|
+
group.core
|
|
1900
|
+
.testWithDifferentAccount(
|
|
1901
|
+
readerInvite,
|
|
1902
|
+
Crypto.newRandomSessionID(
|
|
1903
|
+
readerInvite.currentAgentID()._unsafeUnwrap(),
|
|
1904
|
+
),
|
|
1905
|
+
)
|
|
1906
|
+
.getCurrentContent(),
|
|
1907
|
+
);
|
|
1908
|
+
|
|
1909
|
+
groupAsReaderInvite.set(`child_${childGroup.id}`, "extend", "trusting");
|
|
1910
|
+
expect(groupAsReaderInvite.get(`child_${childGroup.id}`)).toBeUndefined();
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
test("Member roles are inherited by child groups (except invites)", () => {
|
|
1914
|
+
const { group, node, admin } = newGroupHighLevel();
|
|
1915
|
+
const parentGroup = node.createGroup();
|
|
1916
|
+
|
|
1917
|
+
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1918
|
+
|
|
1919
|
+
const writer = node.createAccount();
|
|
1920
|
+
const reader = node.createAccount();
|
|
1921
|
+
const adminInvite = node.createAccount();
|
|
1922
|
+
const writerInvite = node.createAccount();
|
|
1923
|
+
const readerInvite = node.createAccount();
|
|
1924
|
+
|
|
1925
|
+
parentGroup.addMember(writer, "writer");
|
|
1926
|
+
parentGroup.addMember(reader, "reader");
|
|
1927
|
+
parentGroup.addMember(adminInvite, "adminInvite");
|
|
1928
|
+
parentGroup.addMember(writerInvite, "writerInvite");
|
|
1929
|
+
parentGroup.addMember(readerInvite, "readerInvite");
|
|
1930
|
+
|
|
1931
|
+
expect(group.roleOfInternal(admin.id)).toEqual({
|
|
1932
|
+
role: "admin",
|
|
1933
|
+
via: undefined,
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
expect(group.roleOfInternal(writer.id)).toEqual({
|
|
1937
|
+
role: "writer",
|
|
1938
|
+
via: parentGroup.id,
|
|
1939
|
+
});
|
|
1940
|
+
expect(group.roleOf(writer.id)).toEqual("writer");
|
|
1941
|
+
|
|
1942
|
+
expect(group.roleOfInternal(reader.id)).toEqual({
|
|
1943
|
+
role: "reader",
|
|
1944
|
+
via: parentGroup.id,
|
|
1945
|
+
});
|
|
1946
|
+
expect(group.roleOf(reader.id)).toEqual("reader");
|
|
1947
|
+
|
|
1948
|
+
expect(group.roleOfInternal(adminInvite.id)).toEqual(undefined);
|
|
1949
|
+
expect(group.roleOf(adminInvite.id)).toEqual(undefined);
|
|
1950
|
+
|
|
1951
|
+
expect(group.roleOfInternal(writerInvite.id)).toEqual(undefined);
|
|
1952
|
+
expect(group.roleOf(writerInvite.id)).toEqual(undefined);
|
|
1953
|
+
|
|
1954
|
+
expect(group.roleOfInternal(readerInvite.id)).toEqual(undefined);
|
|
1955
|
+
expect(group.roleOf(readerInvite.id)).toEqual(undefined);
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
test("Member roles are inherited by grand-children groups (except invites)", () => {
|
|
1959
|
+
const { group, node, admin } = newGroupHighLevel();
|
|
1960
|
+
const parentGroup = node.createGroup();
|
|
1961
|
+
const grandParentGroup = node.createGroup();
|
|
1962
|
+
|
|
1963
|
+
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
1964
|
+
parentGroup.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
|
|
1965
|
+
|
|
1966
|
+
const writer = node.createAccount();
|
|
1967
|
+
const reader = node.createAccount();
|
|
1968
|
+
const adminInvite = node.createAccount();
|
|
1969
|
+
const writerInvite = node.createAccount();
|
|
1970
|
+
const readerInvite = node.createAccount();
|
|
1971
|
+
|
|
1972
|
+
grandParentGroup.addMember(writer, "writer");
|
|
1973
|
+
grandParentGroup.addMember(reader, "reader");
|
|
1974
|
+
grandParentGroup.addMember(adminInvite, "adminInvite");
|
|
1975
|
+
grandParentGroup.addMember(writerInvite, "writerInvite");
|
|
1976
|
+
grandParentGroup.addMember(readerInvite, "readerInvite");
|
|
1977
|
+
|
|
1978
|
+
expect(group.roleOfInternal(admin.id)).toEqual({
|
|
1979
|
+
role: "admin",
|
|
1980
|
+
via: undefined,
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
expect(group.roleOfInternal(writer.id)).toEqual({
|
|
1984
|
+
role: "writer",
|
|
1985
|
+
via: parentGroup.id,
|
|
1986
|
+
});
|
|
1987
|
+
expect(group.roleOf(writer.id)).toEqual("writer");
|
|
1988
|
+
|
|
1989
|
+
expect(group.roleOfInternal(reader.id)).toEqual({
|
|
1990
|
+
role: "reader",
|
|
1991
|
+
via: parentGroup.id,
|
|
1992
|
+
});
|
|
1993
|
+
expect(group.roleOf(reader.id)).toEqual("reader");
|
|
1994
|
+
|
|
1995
|
+
expect(group.roleOfInternal(adminInvite.id)).toEqual(undefined);
|
|
1996
|
+
expect(group.roleOf(adminInvite.id)).toEqual(undefined);
|
|
1997
|
+
|
|
1998
|
+
expect(group.roleOfInternal(writerInvite.id)).toEqual(undefined);
|
|
1999
|
+
expect(group.roleOf(writerInvite.id)).toEqual(undefined);
|
|
2000
|
+
|
|
2001
|
+
expect(group.roleOfInternal(readerInvite.id)).toEqual(undefined);
|
|
2002
|
+
expect(group.roleOf(readerInvite.id)).toEqual(undefined);
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
test("Admins can reveal parent read keys to child groups", () => {
|
|
2006
|
+
const { group, node } = newGroupHighLevel();
|
|
2007
|
+
const parentGroup = node.createGroup();
|
|
2008
|
+
|
|
2009
|
+
const parentReadKeyID = parentGroup.get("readKey");
|
|
2010
|
+
if (!parentReadKeyID) {
|
|
2011
|
+
throw new Error("Can't get parent group read key");
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const readKeyID = group.get("readKey");
|
|
2015
|
+
if (!readKeyID) {
|
|
2016
|
+
throw new Error("Can't get group read key");
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2020
|
+
const encrypted = "fake_encrypted_key_secret" as any;
|
|
2021
|
+
|
|
2022
|
+
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
|
|
2023
|
+
expect(group.get(`${readKeyID}_for_${parentReadKeyID}`)).toEqual(encrypted);
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
test("Writers, readers and invites can't reveal parent read keys to child groups", () => {
|
|
2027
|
+
const { group, node } = newGroupHighLevel();
|
|
2028
|
+
const parentGroup = node.createGroup();
|
|
2029
|
+
|
|
2030
|
+
const parentReadKeyID = parentGroup.get("readKey");
|
|
2031
|
+
if (!parentReadKeyID) {
|
|
2032
|
+
throw new Error("Can't get parent group read key");
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
const readKeyID = group.get("readKey");
|
|
2036
|
+
if (!readKeyID) {
|
|
2037
|
+
throw new Error("Can't get group read key");
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2041
|
+
const encrypted = "fake_encrypted_key_secret" as any;
|
|
2042
|
+
|
|
2043
|
+
const writer = node.createAccount();
|
|
2044
|
+
const reader = node.createAccount();
|
|
2045
|
+
const adminInvite = node.createAccount();
|
|
2046
|
+
const writerInvite = node.createAccount();
|
|
2047
|
+
const readerInvite = node.createAccount();
|
|
2048
|
+
|
|
2049
|
+
group.addMember(writer, "writer");
|
|
2050
|
+
group.addMember(reader, "reader");
|
|
2051
|
+
group.addMember(adminInvite, "adminInvite");
|
|
2052
|
+
group.addMember(writerInvite, "writerInvite");
|
|
2053
|
+
group.addMember(readerInvite, "readerInvite");
|
|
2054
|
+
|
|
2055
|
+
const groupAsWriter = expectGroup(
|
|
2056
|
+
group.core
|
|
2057
|
+
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
|
2058
|
+
.getCurrentContent(),
|
|
2059
|
+
);
|
|
2060
|
+
|
|
2061
|
+
groupAsWriter.set(
|
|
2062
|
+
`${readKeyID}_for_${parentReadKeyID}`,
|
|
2063
|
+
encrypted,
|
|
2064
|
+
"trusting",
|
|
2065
|
+
);
|
|
2066
|
+
expect(
|
|
2067
|
+
groupAsWriter.get(`${readKeyID}_for_${parentReadKeyID}`),
|
|
2068
|
+
).toBeUndefined();
|
|
2069
|
+
|
|
2070
|
+
const groupAsReader = expectGroup(
|
|
2071
|
+
group.core
|
|
2072
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
2073
|
+
.getCurrentContent(),
|
|
2074
|
+
);
|
|
2075
|
+
|
|
2076
|
+
groupAsReader.set(
|
|
2077
|
+
`${readKeyID}_for_${parentReadKeyID}`,
|
|
2078
|
+
encrypted,
|
|
2079
|
+
"trusting",
|
|
2080
|
+
);
|
|
2081
|
+
expect(
|
|
2082
|
+
groupAsReader.get(`${readKeyID}_for_${parentReadKeyID}`),
|
|
2083
|
+
).toBeUndefined();
|
|
2084
|
+
|
|
2085
|
+
const groupAsAdminInvite = expectGroup(
|
|
2086
|
+
group.core
|
|
2087
|
+
.testWithDifferentAccount(
|
|
2088
|
+
adminInvite,
|
|
2089
|
+
Crypto.newRandomSessionID(adminInvite.currentAgentID()._unsafeUnwrap()),
|
|
2090
|
+
)
|
|
2091
|
+
.getCurrentContent(),
|
|
2092
|
+
);
|
|
2093
|
+
|
|
2094
|
+
groupAsAdminInvite.set(
|
|
2095
|
+
`${readKeyID}_for_${parentReadKeyID}`,
|
|
2096
|
+
encrypted,
|
|
2097
|
+
"trusting",
|
|
2098
|
+
);
|
|
2099
|
+
expect(
|
|
2100
|
+
groupAsAdminInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
|
|
2101
|
+
).toBeUndefined();
|
|
2102
|
+
|
|
2103
|
+
const groupAsWriterInvite = expectGroup(
|
|
2104
|
+
group.core
|
|
2105
|
+
.testWithDifferentAccount(
|
|
2106
|
+
writerInvite,
|
|
2107
|
+
Crypto.newRandomSessionID(
|
|
2108
|
+
writerInvite.currentAgentID()._unsafeUnwrap(),
|
|
2109
|
+
),
|
|
2110
|
+
)
|
|
2111
|
+
.getCurrentContent(),
|
|
2112
|
+
);
|
|
2113
|
+
|
|
2114
|
+
groupAsWriterInvite.set(
|
|
2115
|
+
`${readKeyID}_for_${parentReadKeyID}`,
|
|
2116
|
+
encrypted,
|
|
2117
|
+
"trusting",
|
|
2118
|
+
);
|
|
2119
|
+
expect(
|
|
2120
|
+
groupAsWriterInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
|
|
2121
|
+
).toBeUndefined();
|
|
2122
|
+
|
|
2123
|
+
const groupAsReaderInvite = expectGroup(
|
|
2124
|
+
group.core
|
|
2125
|
+
.testWithDifferentAccount(
|
|
2126
|
+
readerInvite,
|
|
2127
|
+
Crypto.newRandomSessionID(
|
|
2128
|
+
readerInvite.currentAgentID()._unsafeUnwrap(),
|
|
2129
|
+
),
|
|
2130
|
+
)
|
|
2131
|
+
.getCurrentContent(),
|
|
2132
|
+
);
|
|
2133
|
+
|
|
2134
|
+
groupAsReaderInvite.set(
|
|
2135
|
+
`${readKeyID}_for_${parentReadKeyID}`,
|
|
2136
|
+
encrypted,
|
|
2137
|
+
"trusting",
|
|
2138
|
+
);
|
|
2139
|
+
expect(
|
|
2140
|
+
groupAsReaderInvite.get(`${readKeyID}_for_${parentReadKeyID}`),
|
|
2141
|
+
).toBeUndefined();
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
test("Writers and readers in a parent group can read from an object owned by a child group", () => {
|
|
2145
|
+
const { group, node } = newGroupHighLevel();
|
|
2146
|
+
const parentGroup = node.createGroup();
|
|
2147
|
+
|
|
2148
|
+
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
2149
|
+
|
|
2150
|
+
const parentReadKeyID = parentGroup.get("readKey");
|
|
2151
|
+
const parentKey =
|
|
2152
|
+
parentReadKeyID && parentGroup.core.getReadKey(parentReadKeyID);
|
|
2153
|
+
if (!parentReadKeyID || !parentKey) {
|
|
2154
|
+
throw new Error("Can't get parent group read key");
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const readKeyID = group.get("readKey");
|
|
2158
|
+
const readKey = readKeyID && group.core.getReadKey(readKeyID);
|
|
2159
|
+
if (!readKeyID || !readKey) {
|
|
2160
|
+
throw new Error("Can't get group read key");
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
const encrypted = node.crypto.encryptKeySecret({
|
|
2164
|
+
toEncrypt: {
|
|
2165
|
+
id: readKeyID,
|
|
2166
|
+
secret: readKey,
|
|
2167
|
+
},
|
|
2168
|
+
encrypting: {
|
|
2169
|
+
id: parentReadKeyID,
|
|
2170
|
+
secret: parentKey,
|
|
2171
|
+
},
|
|
2172
|
+
}).encrypted;
|
|
2173
|
+
|
|
2174
|
+
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
|
|
2175
|
+
|
|
2176
|
+
const writer = node.createAccount();
|
|
2177
|
+
const reader = node.createAccount();
|
|
2178
|
+
parentGroup.addMember(writer, "writer");
|
|
2179
|
+
parentGroup.addMember(reader, "reader");
|
|
2180
|
+
|
|
2181
|
+
const childObject = node.createCoValue({
|
|
2182
|
+
type: "comap",
|
|
2183
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
2184
|
+
meta: null,
|
|
2185
|
+
...Crypto.createdNowUnique(),
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
const childContent = expectMap(childObject.getCurrentContent());
|
|
2189
|
+
|
|
2190
|
+
childContent.set("foo", "bar", "private");
|
|
2191
|
+
expect(childContent.get("foo")).toEqual("bar");
|
|
2192
|
+
|
|
2193
|
+
const childContentAsWriter = expectMap(
|
|
2194
|
+
childObject
|
|
2195
|
+
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
|
2196
|
+
.getCurrentContent(),
|
|
2197
|
+
);
|
|
2198
|
+
|
|
2199
|
+
expect(childContentAsWriter.get("foo")).toEqual("bar");
|
|
2200
|
+
|
|
2201
|
+
const childContentAsReader = expectMap(
|
|
2202
|
+
childObject
|
|
2203
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
2204
|
+
.getCurrentContent(),
|
|
2205
|
+
);
|
|
2206
|
+
|
|
2207
|
+
expect(childContentAsReader.get("foo")).toEqual("bar");
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
test("Writers in a parent group can write to an object owned by a child group", () => {
|
|
2211
|
+
const { group, node } = newGroupHighLevel();
|
|
2212
|
+
const parentGroup = node.createGroup();
|
|
2213
|
+
|
|
2214
|
+
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
2215
|
+
|
|
2216
|
+
const parentReadKeyID = parentGroup.get("readKey");
|
|
2217
|
+
const parentKey =
|
|
2218
|
+
parentReadKeyID && parentGroup.core.getReadKey(parentReadKeyID);
|
|
2219
|
+
if (!parentReadKeyID || !parentKey) {
|
|
2220
|
+
throw new Error("Can't get parent group read key");
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const readKeyID = group.get("readKey");
|
|
2224
|
+
const readKey = readKeyID && group.core.getReadKey(readKeyID);
|
|
2225
|
+
if (!readKeyID || !readKey) {
|
|
2226
|
+
throw new Error("Can't get group read key");
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
const encrypted = node.crypto.encryptKeySecret({
|
|
2230
|
+
toEncrypt: {
|
|
2231
|
+
id: readKeyID,
|
|
2232
|
+
secret: readKey,
|
|
2233
|
+
},
|
|
2234
|
+
encrypting: {
|
|
2235
|
+
id: parentReadKeyID,
|
|
2236
|
+
secret: parentKey,
|
|
2237
|
+
},
|
|
2238
|
+
}).encrypted;
|
|
2239
|
+
|
|
2240
|
+
group.set(`${readKeyID}_for_${parentReadKeyID}`, encrypted, "trusting");
|
|
2241
|
+
|
|
2242
|
+
const writer = node.createAccount();
|
|
2243
|
+
parentGroup.addMember(writer, "writer");
|
|
2244
|
+
|
|
2245
|
+
const childObject = node.createCoValue({
|
|
2246
|
+
type: "comap",
|
|
2247
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
2248
|
+
meta: null,
|
|
2249
|
+
...Crypto.createdNowUnique(),
|
|
2250
|
+
});
|
|
2251
|
+
|
|
2252
|
+
const childContentAsWriter = expectMap(
|
|
2253
|
+
childObject
|
|
2254
|
+
.testWithDifferentAccount(writer, Crypto.newRandomSessionID(writer.id))
|
|
2255
|
+
.getCurrentContent(),
|
|
2256
|
+
);
|
|
2257
|
+
|
|
2258
|
+
childContentAsWriter.set("foo", "bar", "private");
|
|
2259
|
+
expect(childContentAsWriter.get("foo")).toEqual("bar");
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
test("When rotating the key of a child group, the new child key is exposed to the parent group", () => {
|
|
2263
|
+
const { group, node } = newGroupHighLevel();
|
|
2264
|
+
const parentGroup = node.createGroup();
|
|
2265
|
+
|
|
2266
|
+
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
2267
|
+
|
|
2268
|
+
const currentReadKeyID = group.get("readKey");
|
|
2269
|
+
if (!currentReadKeyID) {
|
|
2270
|
+
throw new Error("Can't get group read key");
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
group.rotateReadKey();
|
|
2274
|
+
|
|
2275
|
+
const newReadKeyID = group.get("readKey");
|
|
2276
|
+
if (!newReadKeyID) {
|
|
2277
|
+
throw new Error("Can't get new group read key");
|
|
2278
|
+
}
|
|
2279
|
+
expect(newReadKeyID).not.toEqual(currentReadKeyID);
|
|
2280
|
+
|
|
2281
|
+
const parentReadKeyID = parentGroup.get("readKey");
|
|
2282
|
+
if (!parentReadKeyID) {
|
|
2283
|
+
throw new Error("Can't get parent group read key");
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
console.log("Checking", `${newReadKeyID}_for_${parentReadKeyID}`);
|
|
2287
|
+
|
|
2288
|
+
expect(group.get(`${newReadKeyID}_for_${parentReadKeyID}`)).toBeDefined();
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
test("When rotating the key of a parent group, the keys of all child groups are also rotated", () => {
|
|
2292
|
+
const { group, node } = newGroupHighLevel();
|
|
2293
|
+
const parentGroup = node.createGroup();
|
|
2294
|
+
|
|
2295
|
+
parentGroup.set(`child_${group.id}`, "extend", "trusting");
|
|
2296
|
+
group.set(`parent_${parentGroup.id}`, "extend", "trusting");
|
|
2297
|
+
|
|
2298
|
+
group.rotateReadKey();
|
|
2299
|
+
|
|
2300
|
+
const currentChildReadKeyID = group.get("readKey");
|
|
2301
|
+
if (!currentChildReadKeyID) {
|
|
2302
|
+
throw new Error("Can't get group read key");
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
console.log("child id", group.id);
|
|
2306
|
+
parentGroup.rotateReadKey();
|
|
2307
|
+
|
|
2308
|
+
const newChildReadKeyID = expectGroup(group.core.getCurrentContent()).get(
|
|
2309
|
+
"readKey",
|
|
2310
|
+
);
|
|
2311
|
+
if (!newChildReadKeyID) {
|
|
2312
|
+
throw new Error("Can't get new group read key");
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
expect(newChildReadKeyID).not.toEqual(currentChildReadKeyID);
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
test("When rotating the key of a grand-parent group, the keys of all child and grand-child groups are also rotated", () => {
|
|
2319
|
+
const { group, node } = newGroupHighLevel();
|
|
2320
|
+
const grandParentGroup = node.createGroup();
|
|
2321
|
+
const parentGroup = node.createGroup();
|
|
2322
|
+
|
|
2323
|
+
grandParentGroup.set(`child_${parentGroup.id}`, "extend", "trusting");
|
|
2324
|
+
parentGroup.set(`child_${group.id}`, "extend", "trusting");
|
|
2325
|
+
parentGroup.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
|
|
2326
|
+
group.set(`parent_${grandParentGroup.id}`, "extend", "trusting");
|
|
2327
|
+
|
|
2328
|
+
const currentGrandParentReadKeyID = grandParentGroup.get("readKey");
|
|
2329
|
+
if (!currentGrandParentReadKeyID) {
|
|
2330
|
+
throw new Error("Can't get grand-parent group read key");
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
const currentParentReadKeyID = parentGroup.get("readKey");
|
|
2334
|
+
if (!currentParentReadKeyID) {
|
|
2335
|
+
throw new Error("Can't get parent group read key");
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
const currentChildReadKeyID = group.get("readKey");
|
|
2339
|
+
if (!currentChildReadKeyID) {
|
|
2340
|
+
throw new Error("Can't get group read key");
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
grandParentGroup.rotateReadKey();
|
|
2344
|
+
|
|
2345
|
+
const newGrandParentReadKeyID = grandParentGroup.get("readKey");
|
|
2346
|
+
if (!newGrandParentReadKeyID) {
|
|
2347
|
+
throw new Error("Can't get new grand-parent group read key");
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
expect(newGrandParentReadKeyID).not.toEqual(currentGrandParentReadKeyID);
|
|
2351
|
+
|
|
2352
|
+
const newParentReadKeyID = expectGroup(
|
|
2353
|
+
parentGroup.core.getCurrentContent(),
|
|
2354
|
+
).get("readKey");
|
|
2355
|
+
if (!newParentReadKeyID) {
|
|
2356
|
+
throw new Error("Can't get new parent group read key");
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
expect(newParentReadKeyID).not.toEqual(currentParentReadKeyID);
|
|
2360
|
+
|
|
2361
|
+
const newChildReadKeyID = expectGroup(group.core.getCurrentContent()).get(
|
|
2362
|
+
"readKey",
|
|
2363
|
+
);
|
|
2364
|
+
if (!newChildReadKeyID) {
|
|
2365
|
+
throw new Error("Can't get new group read key");
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
expect(newChildReadKeyID).not.toEqual(currentChildReadKeyID);
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
test("Calling extend on group sets up parent and child references and reveals child key to parent", () => {
|
|
2372
|
+
const { group, node } = newGroupHighLevel();
|
|
2373
|
+
const parentGroup = node.createGroup();
|
|
2374
|
+
|
|
2375
|
+
group.extend(parentGroup);
|
|
2376
|
+
|
|
2377
|
+
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
|
|
2378
|
+
expect(parentGroup.get(`child_${group.id}`)).toEqual("extend");
|
|
2379
|
+
|
|
2380
|
+
const parentReadKeyID = parentGroup.get("readKey");
|
|
2381
|
+
if (!parentReadKeyID) {
|
|
2382
|
+
throw new Error("Can't get parent group read key");
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
const childReadKeyID = group.get("readKey");
|
|
2386
|
+
if (!childReadKeyID) {
|
|
2387
|
+
throw new Error("Can't get group read key");
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
expect(group.get(`${childReadKeyID}_for_${parentReadKeyID}`)).toBeDefined();
|
|
2391
|
+
|
|
2392
|
+
const reader = node.createAccount();
|
|
2393
|
+
parentGroup.addMember(reader, "reader");
|
|
2394
|
+
|
|
2395
|
+
const childObject = node.createCoValue({
|
|
2396
|
+
type: "comap",
|
|
2397
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
2398
|
+
meta: null,
|
|
2399
|
+
...Crypto.createdNowUnique(),
|
|
2400
|
+
});
|
|
2401
|
+
const childMap = expectMap(childObject.getCurrentContent());
|
|
2402
|
+
|
|
2403
|
+
childMap.set("foo", "bar", "private");
|
|
2404
|
+
|
|
2405
|
+
const childContentAsReader = expectMap(
|
|
2406
|
+
childObject
|
|
2407
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
2408
|
+
.getCurrentContent(),
|
|
2409
|
+
);
|
|
2410
|
+
|
|
2411
|
+
expect(childContentAsReader.get("foo")).toEqual("bar");
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
test("Calling extend to create grand-child groups parent and child references and reveals child key to parent(s)", () => {
|
|
2415
|
+
const { group, node } = newGroupHighLevel();
|
|
2416
|
+
const parentGroup = node.createGroup();
|
|
2417
|
+
const grandParentGroup = node.createGroup();
|
|
2418
|
+
|
|
2419
|
+
group.extend(parentGroup);
|
|
2420
|
+
parentGroup.extend(grandParentGroup);
|
|
2421
|
+
|
|
2422
|
+
expect(group.get(`parent_${parentGroup.id}`)).toEqual("extend");
|
|
2423
|
+
expect(parentGroup.get(`parent_${grandParentGroup.id}`)).toEqual("extend");
|
|
2424
|
+
expect(parentGroup.get(`child_${group.id}`)).toEqual("extend");
|
|
2425
|
+
expect(grandParentGroup.get(`child_${parentGroup.id}`)).toEqual("extend");
|
|
2426
|
+
|
|
2427
|
+
const reader = node.createAccount();
|
|
2428
|
+
grandParentGroup.addMember(reader, "reader");
|
|
2429
|
+
|
|
2430
|
+
const childObject = node.createCoValue({
|
|
2431
|
+
type: "comap",
|
|
2432
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
2433
|
+
meta: null,
|
|
2434
|
+
...Crypto.createdNowUnique(),
|
|
2435
|
+
});
|
|
2436
|
+
const childMap = expectMap(childObject.getCurrentContent());
|
|
2437
|
+
|
|
2438
|
+
childMap.set("foo", "bar", "private");
|
|
2439
|
+
|
|
2440
|
+
const childContentAsReader = expectMap(
|
|
2441
|
+
childObject
|
|
2442
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
2443
|
+
.getCurrentContent(),
|
|
2444
|
+
);
|
|
2445
|
+
|
|
2446
|
+
expect(childContentAsReader.get("foo")).toEqual("bar");
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
test("High-level permissions work correctly when a group is extended", async () => {
|
|
2450
|
+
const { group, node } = newGroupHighLevel();
|
|
2451
|
+
const parentGroup = node.createGroup();
|
|
2452
|
+
|
|
2453
|
+
group.extend(parentGroup);
|
|
2454
|
+
|
|
2455
|
+
const reader = node.createAccount();
|
|
2456
|
+
parentGroup.addMember(reader, "reader");
|
|
2457
|
+
|
|
2458
|
+
const mapCore = node.createCoValue({
|
|
2459
|
+
type: "comap",
|
|
2460
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
2461
|
+
meta: null,
|
|
2462
|
+
...Crypto.createdNowUnique(),
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
const map = expectMap(mapCore.getCurrentContent());
|
|
2466
|
+
|
|
2467
|
+
map.set("foo", "bar", "private");
|
|
2468
|
+
|
|
2469
|
+
const mapAsReader = expectMap(
|
|
2470
|
+
mapCore
|
|
2471
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
2472
|
+
.getCurrentContent(),
|
|
2473
|
+
);
|
|
2474
|
+
|
|
2475
|
+
expect(mapAsReader.get("foo")).toEqual("bar");
|
|
2476
|
+
|
|
2477
|
+
const groupKeyBeforeRemove = group.core.getCurrentReadKey().id;
|
|
2478
|
+
|
|
2479
|
+
await parentGroup.removeMember(reader);
|
|
2480
|
+
|
|
2481
|
+
const groupKeyAfterRemove = group.core.getCurrentReadKey().id;
|
|
2482
|
+
expect(groupKeyAfterRemove).not.toEqual(groupKeyBeforeRemove);
|
|
2483
|
+
|
|
2484
|
+
map.set("foo", "baz", "private");
|
|
2485
|
+
|
|
2486
|
+
const mapAsReaderAfterRemove = expectMap(
|
|
2487
|
+
mapCore
|
|
2488
|
+
.testWithDifferentAccount(reader, Crypto.newRandomSessionID(reader.id))
|
|
2489
|
+
.getCurrentContent(),
|
|
2490
|
+
);
|
|
2491
|
+
|
|
2492
|
+
expect(mapAsReaderAfterRemove.get("foo")).not.toEqual("baz");
|
|
2493
|
+
});
|