@aerogel/plugin-local-first 0.0.0-next.4c77ee2164e8a4f72bdd920cb56ec059299ccc5f
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/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +9 -0
- package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +1 -0
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +5 -0
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +1 -0
- package/dist/aerogel-plugin-local-first.d.ts +269 -0
- package/dist/aerogel-plugin-local-first.js +1686 -0
- package/dist/aerogel-plugin-local-first.js.map +1 -0
- package/package.json +50 -0
- package/src/components/AGCloudStatus.vue +31 -0
- package/src/components/index.ts +3 -0
- package/src/index.ts +29 -0
- package/src/jobs/Backup.ts +98 -0
- package/src/jobs/Migrate.test.ts +217 -0
- package/src/jobs/Migrate.ts +179 -0
- package/src/jobs/Sync.test.ts +982 -0
- package/src/jobs/Sync.ts +590 -0
- package/src/jobs/mixins/LoadsChildren.ts +210 -0
- package/src/jobs/mixins/LoadsTypeIndex.ts +23 -0
- package/src/jobs/mixins/TracksLocalModels.ts +32 -0
- package/src/lib/SyncQueue.ts +94 -0
- package/src/lib/inference.ts +124 -0
- package/src/lib/mirroring.ts +327 -0
- package/src/lib/utils.ts +21 -0
- package/src/services/Cloud.state.ts +111 -0
- package/src/services/Cloud.ts +450 -0
- package/src/services/DocumentsCache.ts +35 -0
- package/src/testing/cloud.ts +193 -0
- package/src/testing/mocks/Solid.mock.ts +34 -0
- package/src/testing/setup.ts +32 -0
- package/src/testing/stubs/LegacyTask.schema.ts +15 -0
- package/src/testing/stubs/Movie.schema.ts +15 -0
- package/src/testing/stubs/Movie.ts +3 -0
- package/src/testing/stubs/MoviesContainer.ts +20 -0
- package/src/testing/stubs/Task.schema.ts +12 -0
- package/src/testing/stubs/Task.ts +3 -0
- package/src/testing/stubs/TasksList.ts +20 -0
- package/src/testing/stubs/Workspace.ts +15 -0
- package/src/types/shims.d.ts +7 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { Events } from '@aerogel/core';
|
|
3
|
+
import { FakeResponse, FakeServer, fakeContainerUrl, fakeDocumentUrl } from '@noeldemartin/testing';
|
|
4
|
+
import { arrayFind, required } from '@noeldemartin/utils';
|
|
5
|
+
import { Solid } from '@aerogel/plugin-solid';
|
|
6
|
+
import { SolidTypeRegistration } from 'soukai-solid';
|
|
7
|
+
|
|
8
|
+
import Cloud from '@aerogel/plugin-local-first/services/Cloud';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
containerResponse,
|
|
12
|
+
movieResponse,
|
|
13
|
+
setupCloudTests,
|
|
14
|
+
taskResponse,
|
|
15
|
+
testRegisterVariants,
|
|
16
|
+
tombstoneResponse,
|
|
17
|
+
typeIndexResponse,
|
|
18
|
+
} from '@aerogel/plugin-local-first/testing/cloud';
|
|
19
|
+
import Movie from '@aerogel/plugin-local-first/testing/stubs/Movie';
|
|
20
|
+
import MoviesContainer from '@aerogel/plugin-local-first/testing/stubs/MoviesContainer';
|
|
21
|
+
import SolidMock from '@aerogel/plugin-local-first/testing/mocks/Solid.mock';
|
|
22
|
+
import Workspace from '@aerogel/plugin-local-first/testing/stubs/Workspace';
|
|
23
|
+
|
|
24
|
+
describe('Sync', () => {
|
|
25
|
+
|
|
26
|
+
beforeEach(setupCloudTests);
|
|
27
|
+
|
|
28
|
+
it('Syncs containers', async () => {
|
|
29
|
+
// Arrange - Mint urls
|
|
30
|
+
const parentContainerUrl = SolidMock.requireUser().storageUrls[0];
|
|
31
|
+
// fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
32
|
+
const remoteContainerUrl = parentContainerUrl + 'remote/';
|
|
33
|
+
// fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
34
|
+
const localContainerUrl = parentContainerUrl + 'local/';
|
|
35
|
+
|
|
36
|
+
// Arrange - Prepare models
|
|
37
|
+
await MoviesContainer.at(parentContainerUrl).create({
|
|
38
|
+
url: localContainerUrl,
|
|
39
|
+
name: 'Local Movies',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Arrange - Prepare responses
|
|
43
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
44
|
+
|
|
45
|
+
FakeServer.respond(typeIndexUrl, typeIndexResponse({ [remoteContainerUrl]: '<https://schema.org/Movie>' }));
|
|
46
|
+
FakeServer.respond(remoteContainerUrl, containerResponse({ name: 'Remote Movies' }));
|
|
47
|
+
FakeServer.respondOnce(localContainerUrl, FakeResponse.notFound()); // Tombstone check
|
|
48
|
+
FakeServer.respondOnce(localContainerUrl, FakeResponse.notFound()); // Check before create
|
|
49
|
+
FakeServer.respondOnce(localContainerUrl, FakeResponse.created()); // Create
|
|
50
|
+
FakeServer.respondOnce(
|
|
51
|
+
// Read described-by header
|
|
52
|
+
localContainerUrl,
|
|
53
|
+
FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
|
|
54
|
+
);
|
|
55
|
+
FakeServer.respondOnce(`${localContainerUrl}.meta`, FakeResponse.success()); // Update meta
|
|
56
|
+
|
|
57
|
+
// Arrange - Prepare service
|
|
58
|
+
await Cloud.launch();
|
|
59
|
+
await Cloud.register(MoviesContainer);
|
|
60
|
+
await Events.emit('application-ready');
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
const updates: number[] = [];
|
|
64
|
+
|
|
65
|
+
await Cloud.sync({ onUpdated: (progress) => updates.push(progress) });
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(updates).toEqual([0, 0.9, 1]);
|
|
69
|
+
|
|
70
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
|
|
71
|
+
expect(FakeServer.getRequests(remoteContainerUrl)).toHaveLength(1);
|
|
72
|
+
expect(FakeServer.getRequests(localContainerUrl)).toHaveLength(4);
|
|
73
|
+
expect(FakeServer.getRequests(localContainerUrl + '.meta')).toHaveLength(1);
|
|
74
|
+
expect(FakeServer.getRequests()).toHaveLength(9);
|
|
75
|
+
expect(FakeServer.getRequest('PATCH', typeIndexUrl)?.body).toEqualSparql(`
|
|
76
|
+
INSERT DATA {
|
|
77
|
+
<#[[.*]]>
|
|
78
|
+
a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
|
|
79
|
+
<http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Movie> ;
|
|
80
|
+
<http://www.w3.org/ns/solid/terms#instanceContainer> <${localContainerUrl}> .
|
|
81
|
+
}
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
const typeIndex = await Solid.findPrivateTypeIndex();
|
|
85
|
+
const findRegistration = (url: string) => arrayFind(typeIndex?.registrations ?? [], 'instanceContainer', url);
|
|
86
|
+
expect(typeIndex).not.toBeNull();
|
|
87
|
+
expect(typeIndex?.registrations).toHaveLength(2);
|
|
88
|
+
expect(findRegistration(remoteContainerUrl)).toBeInstanceOf(SolidTypeRegistration);
|
|
89
|
+
expect(findRegistration(localContainerUrl)).toBeInstanceOf(SolidTypeRegistration);
|
|
90
|
+
|
|
91
|
+
const containers = await MoviesContainer.all();
|
|
92
|
+
expect(containers).toHaveLength(2);
|
|
93
|
+
expect(arrayFind(containers, 'name', 'Local Movies')).toBeInstanceOf(MoviesContainer);
|
|
94
|
+
expect(arrayFind(containers, 'name', 'Remote Movies')).toBeInstanceOf(MoviesContainer);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('Syncs container documents', async () => {
|
|
98
|
+
// Arrange - Mint urls
|
|
99
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
100
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
101
|
+
const localDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
102
|
+
const remoteDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
103
|
+
|
|
104
|
+
// Arrange - Prepare models
|
|
105
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({
|
|
106
|
+
url: containerUrl,
|
|
107
|
+
name: 'Remote Movies',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await container.relatedMovies.create({ url: `${localDocumentUrl}#it`, name: 'The Tale of Princess Kaguya' });
|
|
111
|
+
|
|
112
|
+
// Arrange - Prepare responses
|
|
113
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
114
|
+
|
|
115
|
+
FakeServer.respond(typeIndexUrl, typeIndexResponse({ [containerUrl]: '<https://schema.org/Movie>' }));
|
|
116
|
+
FakeServer.respond(
|
|
117
|
+
containerUrl,
|
|
118
|
+
containerResponse({
|
|
119
|
+
name: 'Remote Movies',
|
|
120
|
+
documentUrls: [remoteDocumentUrl],
|
|
121
|
+
createdAt: container.createdAt,
|
|
122
|
+
updatedAt: container.updatedAt,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
FakeServer.respond(remoteDocumentUrl, movieResponse('Spirited Away'));
|
|
126
|
+
FakeServer.respondOnce(localDocumentUrl, FakeResponse.notFound()); // Check before create
|
|
127
|
+
FakeServer.respondOnce(localDocumentUrl, FakeResponse.created()); // Create
|
|
128
|
+
|
|
129
|
+
// Arrange - Prepare service
|
|
130
|
+
await Cloud.launch();
|
|
131
|
+
await Cloud.register(MoviesContainer);
|
|
132
|
+
await Events.emit('application-ready');
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
const updates: number[] = [];
|
|
136
|
+
|
|
137
|
+
await Cloud.sync({ onUpdated: (progress) => updates.push(progress) });
|
|
138
|
+
|
|
139
|
+
// Assert
|
|
140
|
+
expect(updates).toEqual([0, 0.9, 0.95, 1]);
|
|
141
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
|
|
142
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(1);
|
|
143
|
+
expect(FakeServer.getRequests(localDocumentUrl)).toHaveLength(2);
|
|
144
|
+
expect(FakeServer.getRequests(remoteDocumentUrl)).toHaveLength(1);
|
|
145
|
+
expect(FakeServer.getRequests()).toHaveLength(5);
|
|
146
|
+
|
|
147
|
+
const movies = await Movie.from(containerUrl).all();
|
|
148
|
+
expect(movies).toHaveLength(2);
|
|
149
|
+
expect(arrayFind(movies, 'name', 'The Tale of Princess Kaguya')).toBeInstanceOf(Movie);
|
|
150
|
+
expect(arrayFind(movies, 'name', 'Spirited Away')).toBeInstanceOf(Movie);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('Syncs documents', async () => {
|
|
154
|
+
// Arrange - Mint urls
|
|
155
|
+
const containerUrl = Solid.requireUser().storageUrls[0];
|
|
156
|
+
const remoteDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
157
|
+
const localDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
158
|
+
|
|
159
|
+
// Arrange - Prepare models
|
|
160
|
+
await Movie.at(containerUrl).create({
|
|
161
|
+
url: `${localDocumentUrl}#it`,
|
|
162
|
+
name: 'The Tale of Princess Kaguya',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Arrange - Prepare responses
|
|
166
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
167
|
+
|
|
168
|
+
FakeServer.respond(typeIndexUrl, typeIndexResponse({ [containerUrl]: '<https://schema.org/Movie>' }));
|
|
169
|
+
FakeServer.respond(containerUrl, containerResponse({ name: 'Movies', documentUrls: [remoteDocumentUrl] }));
|
|
170
|
+
FakeServer.respond(remoteDocumentUrl, movieResponse('Spirited Away'));
|
|
171
|
+
FakeServer.respondOnce(localDocumentUrl, FakeResponse.notFound()); // Tombstone check
|
|
172
|
+
FakeServer.respondOnce(localDocumentUrl, FakeResponse.notFound()); // Check before create
|
|
173
|
+
FakeServer.respondOnce(localDocumentUrl, FakeResponse.created()); // Create
|
|
174
|
+
|
|
175
|
+
// Arrange - Prepare service
|
|
176
|
+
await Cloud.launch();
|
|
177
|
+
await Cloud.register(Movie);
|
|
178
|
+
await Events.emit('application-ready');
|
|
179
|
+
|
|
180
|
+
// Act
|
|
181
|
+
const updates: number[] = [];
|
|
182
|
+
|
|
183
|
+
await Cloud.sync({ onUpdated: (progress) => updates.push(progress) });
|
|
184
|
+
|
|
185
|
+
// Assert
|
|
186
|
+
expect(updates).toEqual([0, 0.9, 1]);
|
|
187
|
+
|
|
188
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
|
|
189
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(1);
|
|
190
|
+
expect(FakeServer.getRequests(localDocumentUrl)).toHaveLength(3);
|
|
191
|
+
expect(FakeServer.getRequests(remoteDocumentUrl)).toHaveLength(1);
|
|
192
|
+
expect(FakeServer.getRequests()).toHaveLength(6);
|
|
193
|
+
|
|
194
|
+
const movies = await Movie.all();
|
|
195
|
+
expect(movies).toHaveLength(2);
|
|
196
|
+
expect(arrayFind(movies, 'name', 'The Tale of Princess Kaguya')).toBeInstanceOf(Movie);
|
|
197
|
+
expect(arrayFind(movies, 'name', 'Spirited Away')).toBeInstanceOf(Movie);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('Syncs individual container updates', async () => {
|
|
201
|
+
// Arrange - Mint urls
|
|
202
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
203
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
204
|
+
const movieDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
205
|
+
|
|
206
|
+
// Arrange - Prepare models
|
|
207
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({
|
|
208
|
+
url: containerUrl,
|
|
209
|
+
name: 'Movies',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const movie = await container.relatedMovies.create({
|
|
213
|
+
url: `${movieDocumentUrl}#it`,
|
|
214
|
+
name: 'The Tale of Princess Kaguya',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Arrange - Prepare responses
|
|
218
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
219
|
+
const typeIndexTurtle = `<${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .`;
|
|
220
|
+
|
|
221
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
|
|
222
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Check before update
|
|
223
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
|
|
224
|
+
|
|
225
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
|
|
226
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
|
|
227
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
|
|
228
|
+
FakeServer.respondOnce(
|
|
229
|
+
// Read described-by header
|
|
230
|
+
containerUrl,
|
|
231
|
+
FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
|
|
232
|
+
);
|
|
233
|
+
FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
|
|
234
|
+
|
|
235
|
+
FakeServer.respondOnce(movieDocumentUrl, FakeResponse.notFound()); // Check before create
|
|
236
|
+
FakeServer.respondOnce(movieDocumentUrl, FakeResponse.created()); // Create
|
|
237
|
+
|
|
238
|
+
FakeServer.respondOnce(
|
|
239
|
+
containerUrl,
|
|
240
|
+
containerResponse({
|
|
241
|
+
name: 'Movies',
|
|
242
|
+
documentUrls: [movieDocumentUrl],
|
|
243
|
+
createdAt: container.createdAt,
|
|
244
|
+
updatedAt: container.updatedAt,
|
|
245
|
+
}),
|
|
246
|
+
); // Sync
|
|
247
|
+
FakeServer.respondOnce(movieDocumentUrl, FakeResponse.success(await movie.toTurtle())); // Sync children
|
|
248
|
+
FakeServer.respondOnce(
|
|
249
|
+
`${containerUrl}.meta`,
|
|
250
|
+
containerResponse({
|
|
251
|
+
name: 'Movies',
|
|
252
|
+
documentUrls: [movieDocumentUrl],
|
|
253
|
+
createdAt: container.createdAt,
|
|
254
|
+
updatedAt: container.updatedAt,
|
|
255
|
+
}),
|
|
256
|
+
); // Before update
|
|
257
|
+
FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update
|
|
258
|
+
|
|
259
|
+
// Arrange - Prepare service
|
|
260
|
+
await Cloud.launch();
|
|
261
|
+
await Cloud.register(MoviesContainer);
|
|
262
|
+
await Events.emit('application-ready');
|
|
263
|
+
|
|
264
|
+
// Arrange - Initial sync
|
|
265
|
+
await Cloud.sync();
|
|
266
|
+
|
|
267
|
+
// Act
|
|
268
|
+
const updates: number[] = [];
|
|
269
|
+
|
|
270
|
+
await container.update({ name: 'Great Movies' });
|
|
271
|
+
await Cloud.sync({ model: container, onUpdated: (progress) => updates.push(progress) });
|
|
272
|
+
|
|
273
|
+
// Assert
|
|
274
|
+
expect(updates).toEqual([0, 0.9, 1]);
|
|
275
|
+
|
|
276
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
|
|
277
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(5);
|
|
278
|
+
expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(3);
|
|
279
|
+
expect(FakeServer.getRequests(movieDocumentUrl)).toHaveLength(3);
|
|
280
|
+
expect(FakeServer.getRequests()).toHaveLength(14);
|
|
281
|
+
|
|
282
|
+
expect(FakeServer.getRequests('PATCH', `${containerUrl}.meta`)[1]?.body).toEqualSparql(`
|
|
283
|
+
DELETE DATA {
|
|
284
|
+
@prefix crdt: <https://vocab.noeldemartin.com/crdt/> .
|
|
285
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
286
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
|
|
287
|
+
|
|
288
|
+
<${containerUrl}> rdfs:label "Movies" .
|
|
289
|
+
<${containerUrl}#metadata> crdt:updatedAt "[[createdAt][.*]]"^^xsd:dateTime .
|
|
290
|
+
} ;
|
|
291
|
+
INSERT DATA {
|
|
292
|
+
@prefix crdt: <https://vocab.noeldemartin.com/crdt/> .
|
|
293
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
294
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
|
|
295
|
+
|
|
296
|
+
<${containerUrl}> rdfs:label "Great Movies" .
|
|
297
|
+
<${containerUrl}#metadata> crdt:updatedAt "[[updatedAt][.*]]"^^xsd:dateTime .
|
|
298
|
+
|
|
299
|
+
<${containerUrl}#operation-[[.*]]>
|
|
300
|
+
a crdt:SetPropertyOperation ;
|
|
301
|
+
crdt:resource <${containerUrl}> ;
|
|
302
|
+
crdt:date "[[createdAt][.*]]"^^xsd:dateTime ;
|
|
303
|
+
crdt:property rdfs:label ;
|
|
304
|
+
crdt:value "Movies" .
|
|
305
|
+
|
|
306
|
+
<${containerUrl}#operation-[[.*]]>
|
|
307
|
+
a crdt:SetPropertyOperation ;
|
|
308
|
+
crdt:resource <${containerUrl}> ;
|
|
309
|
+
crdt:date "[[updatedAt][.*]]"^^xsd:dateTime ;
|
|
310
|
+
crdt:property rdfs:label ;
|
|
311
|
+
crdt:value "Great Movies" .
|
|
312
|
+
} .
|
|
313
|
+
`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('Syncs individual container documents', async () => {
|
|
317
|
+
// Arrange - Mint urls
|
|
318
|
+
const containerUrl = Solid.requireUser().storageUrls[0];
|
|
319
|
+
const documentUrl = fakeDocumentUrl({ containerUrl });
|
|
320
|
+
|
|
321
|
+
// Arrange - Prepare models
|
|
322
|
+
const container = await MoviesContainer.create({ url: containerUrl });
|
|
323
|
+
|
|
324
|
+
// Arrange - Prepare responses
|
|
325
|
+
|
|
326
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Pull
|
|
327
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Tombstone check
|
|
328
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Check before create
|
|
329
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.created()); // Create
|
|
330
|
+
|
|
331
|
+
// Arrange - Prepare service
|
|
332
|
+
await Cloud.launch();
|
|
333
|
+
await Cloud.register(MoviesContainer);
|
|
334
|
+
await Events.emit('application-ready');
|
|
335
|
+
|
|
336
|
+
// Act
|
|
337
|
+
const updates: number[] = [];
|
|
338
|
+
const movie = await container.relatedMovies.create({
|
|
339
|
+
url: `${documentUrl}#it`,
|
|
340
|
+
name: 'The Tale of Princess Kaguya',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await Cloud.sync({ model: movie, onUpdated: (progress) => updates.push(progress) });
|
|
344
|
+
|
|
345
|
+
// Assert
|
|
346
|
+
expect(updates).toEqual([0, 0.9, 1]);
|
|
347
|
+
|
|
348
|
+
expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
|
|
349
|
+
expect(FakeServer.getRequests()).toHaveLength(4);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('Creates containers with documents', async () => {
|
|
353
|
+
// Arrange - Mint urls
|
|
354
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
355
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
356
|
+
const documentUrl = fakeDocumentUrl({ containerUrl });
|
|
357
|
+
|
|
358
|
+
// Arrange - Prepare models
|
|
359
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({
|
|
360
|
+
url: containerUrl,
|
|
361
|
+
name: 'Movies',
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await container.relatedMovies.create({
|
|
365
|
+
url: `${documentUrl}#it`,
|
|
366
|
+
name: 'The Tale of Princess Kaguya',
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Arrange - Prepare responses
|
|
370
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
371
|
+
const typeIndexTurtle = `<${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .`;
|
|
372
|
+
|
|
373
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
|
|
374
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Check before update
|
|
375
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
|
|
376
|
+
|
|
377
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
|
|
378
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
|
|
379
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
|
|
380
|
+
FakeServer.respondOnce(
|
|
381
|
+
// Read described-by header
|
|
382
|
+
containerUrl,
|
|
383
|
+
FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
|
|
384
|
+
);
|
|
385
|
+
FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
|
|
386
|
+
|
|
387
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Check before create
|
|
388
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.created()); // Create
|
|
389
|
+
|
|
390
|
+
// Arrange - Prepare service
|
|
391
|
+
await Cloud.launch();
|
|
392
|
+
await Cloud.register(MoviesContainer);
|
|
393
|
+
await Events.emit('application-ready');
|
|
394
|
+
|
|
395
|
+
// Act
|
|
396
|
+
await Cloud.sync();
|
|
397
|
+
|
|
398
|
+
// Assert
|
|
399
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
|
|
400
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(4);
|
|
401
|
+
expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(1);
|
|
402
|
+
expect(FakeServer.getRequests(documentUrl)).toHaveLength(2);
|
|
403
|
+
expect(FakeServer.getRequests()).toHaveLength(10);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('Skips pulling fresh documents', async () => {
|
|
407
|
+
// Arrange - Mint urls
|
|
408
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
409
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
410
|
+
const childContainerUrl = fakeContainerUrl({ baseUrl: containerUrl });
|
|
411
|
+
const freshDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
412
|
+
const staleDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
413
|
+
const freshChildDocumentUrl = fakeDocumentUrl({ containerUrl: childContainerUrl });
|
|
414
|
+
const staleChildDocumentUrl = fakeDocumentUrl({ containerUrl: childContainerUrl });
|
|
415
|
+
|
|
416
|
+
// Arrange - Prepare models
|
|
417
|
+
const workspace = await Workspace.at(parentContainerUrl).create({
|
|
418
|
+
url: containerUrl,
|
|
419
|
+
name: 'Tasks',
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const tasksList = await workspace.relatedLists.create({
|
|
423
|
+
url: childContainerUrl,
|
|
424
|
+
name: 'Child Tasks',
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await workspace.relatedTasks.create({
|
|
428
|
+
url: `${freshDocumentUrl}#it`,
|
|
429
|
+
name: 'One',
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await workspace.relatedTasks.create({
|
|
433
|
+
url: `${staleDocumentUrl}#it`,
|
|
434
|
+
name: 'Two',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await tasksList.relatedTasks.create({
|
|
438
|
+
url: `${freshChildDocumentUrl}#it`,
|
|
439
|
+
name: 'Three',
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await tasksList.relatedTasks.create({
|
|
443
|
+
url: `${staleChildDocumentUrl}#it`,
|
|
444
|
+
name: 'Four',
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Arrange - Prepare responses
|
|
448
|
+
const now = Date.now();
|
|
449
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
450
|
+
const typeIndexTurtle = `
|
|
451
|
+
<${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .
|
|
452
|
+
|
|
453
|
+
<#tasks>
|
|
454
|
+
a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
|
|
455
|
+
<http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Action> ;
|
|
456
|
+
<http://www.w3.org/ns/solid/terms#instanceContainer> <${containerUrl}> .
|
|
457
|
+
`;
|
|
458
|
+
|
|
459
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
|
|
460
|
+
FakeServer.respondOnce(
|
|
461
|
+
containerUrl,
|
|
462
|
+
containerResponse({
|
|
463
|
+
name: 'Tasks',
|
|
464
|
+
documentUrls: [freshDocumentUrl, staleDocumentUrl, childContainerUrl],
|
|
465
|
+
createdAt: workspace.createdAt,
|
|
466
|
+
updatedAt: workspace.updatedAt,
|
|
467
|
+
append: `
|
|
468
|
+
<${freshDocumentUrl}>
|
|
469
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
470
|
+
<http://purl.org/dc/terms/modified>
|
|
471
|
+
"${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
472
|
+
|
|
473
|
+
<${staleDocumentUrl}>
|
|
474
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
475
|
+
<http://purl.org/dc/terms/modified>
|
|
476
|
+
"${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
477
|
+
`,
|
|
478
|
+
}),
|
|
479
|
+
); // Container
|
|
480
|
+
FakeServer.respondOnce(
|
|
481
|
+
childContainerUrl,
|
|
482
|
+
containerResponse({
|
|
483
|
+
name: 'Child Tasks',
|
|
484
|
+
documentUrls: [freshChildDocumentUrl, staleChildDocumentUrl],
|
|
485
|
+
createdAt: tasksList.createdAt,
|
|
486
|
+
updatedAt: tasksList.updatedAt,
|
|
487
|
+
append: `
|
|
488
|
+
<${freshChildDocumentUrl}>
|
|
489
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
490
|
+
<http://purl.org/dc/terms/modified>
|
|
491
|
+
"${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
492
|
+
|
|
493
|
+
<${staleChildDocumentUrl}>
|
|
494
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
495
|
+
<http://purl.org/dc/terms/modified>
|
|
496
|
+
"${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
497
|
+
`,
|
|
498
|
+
}),
|
|
499
|
+
); // Child container
|
|
500
|
+
FakeServer.respondOnce(freshDocumentUrl, taskResponse('One'));
|
|
501
|
+
FakeServer.respondOnce(staleDocumentUrl, taskResponse('Two'));
|
|
502
|
+
FakeServer.respondOnce(freshChildDocumentUrl, taskResponse('Three'));
|
|
503
|
+
FakeServer.respondOnce(staleChildDocumentUrl, taskResponse('Four'));
|
|
504
|
+
|
|
505
|
+
FakeServer.respondOnce(
|
|
506
|
+
containerUrl,
|
|
507
|
+
containerResponse({
|
|
508
|
+
name: 'Tasks',
|
|
509
|
+
documentUrls: [freshDocumentUrl, staleDocumentUrl, childContainerUrl],
|
|
510
|
+
createdAt: workspace.createdAt,
|
|
511
|
+
updatedAt: workspace.updatedAt,
|
|
512
|
+
append: `
|
|
513
|
+
<${freshDocumentUrl}>
|
|
514
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
515
|
+
<http://purl.org/dc/terms/modified>
|
|
516
|
+
"${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
517
|
+
|
|
518
|
+
<${staleDocumentUrl}>
|
|
519
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
520
|
+
<http://purl.org/dc/terms/modified>
|
|
521
|
+
"${new Date(now + 1000).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
522
|
+
`,
|
|
523
|
+
}),
|
|
524
|
+
); // Container
|
|
525
|
+
FakeServer.respondOnce(
|
|
526
|
+
childContainerUrl,
|
|
527
|
+
containerResponse({
|
|
528
|
+
name: 'Child Tasks',
|
|
529
|
+
documentUrls: [freshChildDocumentUrl, staleChildDocumentUrl],
|
|
530
|
+
createdAt: tasksList.createdAt,
|
|
531
|
+
updatedAt: tasksList.updatedAt,
|
|
532
|
+
append: `
|
|
533
|
+
<${freshChildDocumentUrl}>
|
|
534
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
535
|
+
<http://purl.org/dc/terms/modified>
|
|
536
|
+
"${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
537
|
+
|
|
538
|
+
<${staleChildDocumentUrl}>
|
|
539
|
+
a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
|
|
540
|
+
<http://purl.org/dc/terms/modified>
|
|
541
|
+
"${new Date(now + 1000).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
|
|
542
|
+
`,
|
|
543
|
+
}),
|
|
544
|
+
); // Child container
|
|
545
|
+
FakeServer.respondOnce(staleDocumentUrl, taskResponse('Two'));
|
|
546
|
+
FakeServer.respondOnce(staleChildDocumentUrl, taskResponse('Four'));
|
|
547
|
+
|
|
548
|
+
// Arrange - Prepare service
|
|
549
|
+
Cloud.ready = true;
|
|
550
|
+
|
|
551
|
+
await Cloud.launch();
|
|
552
|
+
await Cloud.register(Workspace);
|
|
553
|
+
await Events.emit('application-ready');
|
|
554
|
+
|
|
555
|
+
// Arrange - Initial sync
|
|
556
|
+
await Cloud.sync();
|
|
557
|
+
|
|
558
|
+
// Act
|
|
559
|
+
await Cloud.sync();
|
|
560
|
+
|
|
561
|
+
// Assert
|
|
562
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
|
|
563
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(2);
|
|
564
|
+
expect(FakeServer.getRequests(childContainerUrl)).toHaveLength(2);
|
|
565
|
+
expect(FakeServer.getRequests(freshDocumentUrl)).toHaveLength(1);
|
|
566
|
+
expect(FakeServer.getRequests(staleDocumentUrl)).toHaveLength(2);
|
|
567
|
+
expect(FakeServer.getRequests(freshChildDocumentUrl)).toHaveLength(1);
|
|
568
|
+
expect(FakeServer.getRequests(staleChildDocumentUrl)).toHaveLength(2);
|
|
569
|
+
expect(FakeServer.getRequests()).toHaveLength(11);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('Ignores missing children', async () => {
|
|
573
|
+
// Arrange - Mint urls
|
|
574
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
575
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
576
|
+
const existingChildDocumentUrl = fakeContainerUrl({ baseUrl: containerUrl });
|
|
577
|
+
const notFoundChildDocumentUrl = fakeDocumentUrl({ containerUrl });
|
|
578
|
+
const existingChildContainerUrl = fakeContainerUrl({ baseUrl: containerUrl });
|
|
579
|
+
const notFoundChildContainerUrl = fakeContainerUrl({ baseUrl: containerUrl });
|
|
580
|
+
|
|
581
|
+
// Arrange - Prepare responses
|
|
582
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
583
|
+
const typeIndexTurtle = `
|
|
584
|
+
<${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .
|
|
585
|
+
|
|
586
|
+
<#tasks>
|
|
587
|
+
a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
|
|
588
|
+
<http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Action> ;
|
|
589
|
+
<http://www.w3.org/ns/solid/terms#instanceContainer> <${containerUrl}> .
|
|
590
|
+
`;
|
|
591
|
+
|
|
592
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
|
|
593
|
+
FakeServer.respondOnce(
|
|
594
|
+
containerUrl,
|
|
595
|
+
containerResponse({
|
|
596
|
+
name: 'Tasks',
|
|
597
|
+
documentUrls: [
|
|
598
|
+
existingChildDocumentUrl,
|
|
599
|
+
notFoundChildDocumentUrl,
|
|
600
|
+
existingChildContainerUrl,
|
|
601
|
+
notFoundChildContainerUrl,
|
|
602
|
+
],
|
|
603
|
+
}),
|
|
604
|
+
); // Container
|
|
605
|
+
FakeServer.respondOnce(existingChildDocumentUrl, taskResponse());
|
|
606
|
+
FakeServer.respondOnce(notFoundChildDocumentUrl, FakeResponse.notFound());
|
|
607
|
+
FakeServer.respondOnce(existingChildContainerUrl, containerResponse());
|
|
608
|
+
FakeServer.respondOnce(notFoundChildContainerUrl, FakeResponse.notFound());
|
|
609
|
+
|
|
610
|
+
// Arrange - Prepare service
|
|
611
|
+
Cloud.ready = true;
|
|
612
|
+
|
|
613
|
+
await Cloud.launch();
|
|
614
|
+
await Cloud.register(Workspace);
|
|
615
|
+
await Events.emit('application-ready');
|
|
616
|
+
|
|
617
|
+
// Act
|
|
618
|
+
await Cloud.sync();
|
|
619
|
+
|
|
620
|
+
// Assert
|
|
621
|
+
const workspace = required(await Workspace.find(containerUrl));
|
|
622
|
+
|
|
623
|
+
await workspace.loadRelationIfUnloaded('tasks');
|
|
624
|
+
await workspace.loadRelationIfUnloaded('lists');
|
|
625
|
+
|
|
626
|
+
expect(workspace.tasks).toHaveLength(1);
|
|
627
|
+
expect(workspace.lists).toHaveLength(1);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('Ignores malformed documents', async () => {
|
|
631
|
+
// Arrange - Mint urls
|
|
632
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
633
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
634
|
+
const firstDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
|
|
635
|
+
const secondDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
|
|
636
|
+
const thirdDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
|
|
637
|
+
const fourthDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
|
|
638
|
+
|
|
639
|
+
// Arrange - Prepare models
|
|
640
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({
|
|
641
|
+
url: containerUrl,
|
|
642
|
+
name: 'Movies',
|
|
643
|
+
});
|
|
644
|
+
const movie = await container.relatedMovies.create({
|
|
645
|
+
url: `${thirdDocumentUrl}#it`,
|
|
646
|
+
name: 'The Tale of Princess Kaguya',
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await container.relatedMovies.create({ url: `${secondDocumentUrl}#it`, name: 'Spirited Away' });
|
|
650
|
+
await movie.update({ name: 'かぐや姫の物語' });
|
|
651
|
+
|
|
652
|
+
// Arrange - Prepare responses
|
|
653
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
654
|
+
const typeIndexTurtle = `
|
|
655
|
+
<${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .
|
|
656
|
+
|
|
657
|
+
<#tasks>
|
|
658
|
+
a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
|
|
659
|
+
<http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Movie> ;
|
|
660
|
+
<http://www.w3.org/ns/solid/terms#instanceContainer> <${containerUrl}> .
|
|
661
|
+
`;
|
|
662
|
+
|
|
663
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
|
|
664
|
+
FakeServer.respondOnce(
|
|
665
|
+
containerUrl,
|
|
666
|
+
containerResponse({
|
|
667
|
+
documentUrls: [firstDocumentUrl, secondDocumentUrl, thirdDocumentUrl, fourthDocumentUrl],
|
|
668
|
+
createdAt: container.createdAt,
|
|
669
|
+
updatedAt: container.updatedAt,
|
|
670
|
+
}),
|
|
671
|
+
); // Container
|
|
672
|
+
FakeServer.respondOnce(firstDocumentUrl, FakeResponse.success('invalid turtle'));
|
|
673
|
+
FakeServer.respondOnce(secondDocumentUrl, FakeResponse.success('invalid turtle'));
|
|
674
|
+
FakeServer.respondOnce(thirdDocumentUrl, FakeResponse.success('invalid turtle'));
|
|
675
|
+
FakeServer.respondOnce(fourthDocumentUrl, FakeResponse.success('invalid turtle'));
|
|
676
|
+
|
|
677
|
+
// Arrange - Prepare service
|
|
678
|
+
Cloud.ready = true;
|
|
679
|
+
Cloud.localModelUpdates = { [movie.url]: 1 };
|
|
680
|
+
|
|
681
|
+
await Cloud.launch();
|
|
682
|
+
await Cloud.register(MoviesContainer);
|
|
683
|
+
await Events.emit('application-ready');
|
|
684
|
+
|
|
685
|
+
// Act
|
|
686
|
+
await Cloud.sync();
|
|
687
|
+
|
|
688
|
+
// Assert
|
|
689
|
+
expect(FakeServer.getRequests()).toHaveLength(6);
|
|
690
|
+
expect(Cloud.localModelUpdates).toEqual({ [movie.url]: 1 });
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it('Ignores malformed documents in individual syncs', async () => {
|
|
694
|
+
// Arrange - Mint urls
|
|
695
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
696
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
697
|
+
const documentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
|
|
698
|
+
|
|
699
|
+
// Arrange - Prepare models
|
|
700
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({ url: containerUrl, name: 'Movies' });
|
|
701
|
+
const movie = await container.relatedMovies.create({
|
|
702
|
+
url: `${documentUrl}#it`,
|
|
703
|
+
name: 'The Tale of Princess Kaguya',
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await movie.update({ name: 'かぐや姫の物語' });
|
|
707
|
+
|
|
708
|
+
// Arrange - Prepare responses
|
|
709
|
+
|
|
710
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.success('invalid turtle'));
|
|
711
|
+
|
|
712
|
+
// Arrange - Prepare service
|
|
713
|
+
Cloud.ready = true;
|
|
714
|
+
Cloud.localModelUpdates = { [movie.url]: 1 };
|
|
715
|
+
|
|
716
|
+
await Cloud.launch();
|
|
717
|
+
await Cloud.register(MoviesContainer);
|
|
718
|
+
await Events.emit('application-ready');
|
|
719
|
+
|
|
720
|
+
// Act
|
|
721
|
+
await Cloud.sync(movie);
|
|
722
|
+
|
|
723
|
+
// Assert
|
|
724
|
+
expect(FakeServer.getRequests()).toHaveLength(1);
|
|
725
|
+
expect(Cloud.localModelUpdates).toEqual({ [movie.url]: 1 });
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
testRegisterVariants('Leaves tombstones behind', async (registerModels) => {
|
|
729
|
+
// Arrange - Mint urls
|
|
730
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
731
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
732
|
+
const documentUrl = fakeDocumentUrl({ containerUrl });
|
|
733
|
+
|
|
734
|
+
// Arrange - Prepare models
|
|
735
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({
|
|
736
|
+
url: containerUrl,
|
|
737
|
+
name: 'Movies',
|
|
738
|
+
});
|
|
739
|
+
const movie = await container.relatedMovies.create({ url: `${documentUrl}#it`, name: 'Spirited Away' });
|
|
740
|
+
const movieTurtle = (await movie.toTurtle()).replaceAll(documentUrl, '');
|
|
741
|
+
|
|
742
|
+
// Arrange - Prepare responses
|
|
743
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
744
|
+
|
|
745
|
+
FakeServer.respond(typeIndexUrl, typeIndexResponse({ [containerUrl]: '<https://schema.org/Movie>' }));
|
|
746
|
+
FakeServer.respond(
|
|
747
|
+
containerUrl,
|
|
748
|
+
containerResponse({
|
|
749
|
+
documentUrls: [documentUrl],
|
|
750
|
+
createdAt: container.createdAt,
|
|
751
|
+
updatedAt: container.updatedAt,
|
|
752
|
+
}),
|
|
753
|
+
);
|
|
754
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.success(movieTurtle)); // Pull
|
|
755
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.success(movieTurtle)); // Model GET before delete
|
|
756
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.success(movieTurtle)); // Client GET before update
|
|
757
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.success()); // Client Tombstone PATCH
|
|
758
|
+
|
|
759
|
+
// Arrange - Prepare service
|
|
760
|
+
await Cloud.launch();
|
|
761
|
+
await registerModels();
|
|
762
|
+
await Events.emit('application-ready');
|
|
763
|
+
|
|
764
|
+
// Act
|
|
765
|
+
await movie.softDelete();
|
|
766
|
+
await Cloud.sync();
|
|
767
|
+
|
|
768
|
+
// Assert
|
|
769
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
|
|
770
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(1);
|
|
771
|
+
expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
|
|
772
|
+
expect(FakeServer.getRequests()).toHaveLength(6);
|
|
773
|
+
|
|
774
|
+
expect(FakeServer.getRequest('PATCH', documentUrl)?.body).toEqualSparql(`
|
|
775
|
+
DELETE DATA { ${movieTurtle} } ;
|
|
776
|
+
INSERT DATA {
|
|
777
|
+
@prefix crdt: <https://vocab.noeldemartin.com/crdt/> .
|
|
778
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
779
|
+
|
|
780
|
+
<#it-metadata>
|
|
781
|
+
a crdt:Tombstone ;
|
|
782
|
+
crdt:resource <#it> ;
|
|
783
|
+
crdt:deletedAt "[[.*]]"^^xsd:dateTime .
|
|
784
|
+
} .
|
|
785
|
+
`);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
testRegisterVariants('Deletes tombstone models', async (registerModels, variant) => {
|
|
789
|
+
const usingContainers = variant === 'container registration';
|
|
790
|
+
|
|
791
|
+
// Arrange - Mint urls
|
|
792
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
793
|
+
const containerUrl = parentContainerUrl + 'delete/'; //fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
794
|
+
const documentUrl = fakeDocumentUrl({ containerUrl });
|
|
795
|
+
|
|
796
|
+
// Arrange - Prepare models
|
|
797
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({
|
|
798
|
+
url: containerUrl,
|
|
799
|
+
name: 'Movies',
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const movie = await container.relatedMovies.create({ url: `${documentUrl}#it`, name: 'Spirited Away' });
|
|
803
|
+
|
|
804
|
+
// Arrange - Prepare initial sync responses
|
|
805
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
806
|
+
|
|
807
|
+
FakeServer.respondOnce(typeIndexUrl, typeIndexResponse());
|
|
808
|
+
|
|
809
|
+
if (usingContainers) {
|
|
810
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
|
|
811
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
|
|
812
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
|
|
813
|
+
FakeServer.respondOnce(
|
|
814
|
+
// Read described-by header
|
|
815
|
+
containerUrl,
|
|
816
|
+
FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
|
|
817
|
+
);
|
|
818
|
+
FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
|
|
819
|
+
FakeServer.respondOnce(typeIndexUrl, typeIndexResponse()); // Check before update
|
|
820
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
|
|
821
|
+
} else {
|
|
822
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Tombstone check
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Existence check
|
|
826
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.success()); // Create
|
|
827
|
+
|
|
828
|
+
// Arrange - Prepare service
|
|
829
|
+
if (!usingContainers) {
|
|
830
|
+
Cloud.modelCollections[Movie.modelName] = [containerUrl];
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
await Cloud.launch();
|
|
834
|
+
await registerModels();
|
|
835
|
+
await Events.emit('application-ready');
|
|
836
|
+
|
|
837
|
+
// Arrange - Initial sync
|
|
838
|
+
await Cloud.sync();
|
|
839
|
+
|
|
840
|
+
// Arrange - Sync responses
|
|
841
|
+
FakeServer.respond(
|
|
842
|
+
containerUrl,
|
|
843
|
+
containerResponse({
|
|
844
|
+
documentUrls: [documentUrl],
|
|
845
|
+
createdAt: container.createdAt,
|
|
846
|
+
updatedAt: container.updatedAt,
|
|
847
|
+
}),
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
FakeServer.respond(documentUrl, tombstoneResponse());
|
|
851
|
+
|
|
852
|
+
// Act
|
|
853
|
+
await Cloud.sync();
|
|
854
|
+
|
|
855
|
+
// Assert
|
|
856
|
+
const freshMovie = await Movie.at(containerUrl).find(movie.url);
|
|
857
|
+
const cloud = Cloud;
|
|
858
|
+
|
|
859
|
+
cloud;
|
|
860
|
+
|
|
861
|
+
expect(freshMovie).toBeNull();
|
|
862
|
+
|
|
863
|
+
if (usingContainers) {
|
|
864
|
+
const freshContainer = await container.fresh();
|
|
865
|
+
|
|
866
|
+
await freshContainer.loadRelation('movies');
|
|
867
|
+
|
|
868
|
+
expect(freshContainer.movies).toHaveLength(0);
|
|
869
|
+
expect(freshContainer.resourceUrls).toHaveLength(1);
|
|
870
|
+
|
|
871
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
|
|
872
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(5);
|
|
873
|
+
expect(FakeServer.getRequests(documentUrl)).toHaveLength(3);
|
|
874
|
+
expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(1);
|
|
875
|
+
expect(FakeServer.getRequests()).toHaveLength(12);
|
|
876
|
+
} else {
|
|
877
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
|
|
878
|
+
expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
|
|
879
|
+
expect(FakeServer.getRequests()).toHaveLength(5);
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
testRegisterVariants('Deletes updated tombstone models', async (registerModels, variant) => {
|
|
884
|
+
const usingContainers = variant === 'container registration';
|
|
885
|
+
|
|
886
|
+
// Arrange - Mint urls
|
|
887
|
+
const parentContainerUrl = Solid.requireUser().storageUrls[0];
|
|
888
|
+
const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
|
|
889
|
+
const documentUrl = fakeDocumentUrl({ containerUrl });
|
|
890
|
+
|
|
891
|
+
// Arrange - Prepare models
|
|
892
|
+
const container = await MoviesContainer.at(parentContainerUrl).create({
|
|
893
|
+
url: containerUrl,
|
|
894
|
+
name: 'Movies',
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const movie = await container.relatedMovies.create({ url: `${documentUrl}#it`, name: 'Spirited Away' });
|
|
898
|
+
|
|
899
|
+
// Arrange - Prepare initial sync responses
|
|
900
|
+
const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
|
|
901
|
+
|
|
902
|
+
FakeServer.respondOnce(typeIndexUrl, typeIndexResponse());
|
|
903
|
+
|
|
904
|
+
if (usingContainers) {
|
|
905
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
|
|
906
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
|
|
907
|
+
FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
|
|
908
|
+
FakeServer.respondOnce(
|
|
909
|
+
// Read described-by header
|
|
910
|
+
containerUrl,
|
|
911
|
+
FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
|
|
912
|
+
);
|
|
913
|
+
FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
|
|
914
|
+
FakeServer.respondOnce(typeIndexUrl, typeIndexResponse()); // Check before update
|
|
915
|
+
FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
|
|
916
|
+
} else {
|
|
917
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Tombstone check
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Existence check
|
|
921
|
+
FakeServer.respondOnce(documentUrl, FakeResponse.success()); // Create
|
|
922
|
+
|
|
923
|
+
// Arrange - Prepare service
|
|
924
|
+
if (!usingContainers) {
|
|
925
|
+
Cloud.modelCollections[Movie.modelName] = [containerUrl];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
Cloud.autoPush = false;
|
|
929
|
+
|
|
930
|
+
await Cloud.launch();
|
|
931
|
+
await registerModels();
|
|
932
|
+
await Events.emit('application-ready');
|
|
933
|
+
|
|
934
|
+
// Arrange - Initial sync
|
|
935
|
+
await Cloud.sync();
|
|
936
|
+
|
|
937
|
+
// Arrange - Sync responses
|
|
938
|
+
FakeServer.respond(documentUrl, tombstoneResponse());
|
|
939
|
+
|
|
940
|
+
// Act
|
|
941
|
+
await movie.update({ name: 'Updated' });
|
|
942
|
+
await Cloud.sync([movie]); // Simulate auto push
|
|
943
|
+
|
|
944
|
+
// Assert
|
|
945
|
+
expect(await Movie.at(containerUrl).find(movie.url)).toBeNull();
|
|
946
|
+
|
|
947
|
+
if (usingContainers) {
|
|
948
|
+
const freshContainer = await container.fresh();
|
|
949
|
+
|
|
950
|
+
await freshContainer.loadRelation('movies');
|
|
951
|
+
|
|
952
|
+
expect(freshContainer.movies).toHaveLength(0);
|
|
953
|
+
expect(freshContainer.resourceUrls).toHaveLength(1);
|
|
954
|
+
|
|
955
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
|
|
956
|
+
expect(FakeServer.getRequests(containerUrl)).toHaveLength(4);
|
|
957
|
+
expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
|
|
958
|
+
expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(1);
|
|
959
|
+
expect(FakeServer.getRequests()).toHaveLength(12);
|
|
960
|
+
} else {
|
|
961
|
+
expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
|
|
962
|
+
expect(FakeServer.getRequests(documentUrl)).toHaveLength(5);
|
|
963
|
+
expect(FakeServer.getRequests()).toHaveLength(6);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
testRegisterVariants('Tracks model updates', async (registerModels) => {
|
|
968
|
+
// Arrange
|
|
969
|
+
const container = await MoviesContainer.create({ name: 'Movies' });
|
|
970
|
+
const movie = await container.relatedMovies.create({ name: 'The Boy and The Heron' });
|
|
971
|
+
|
|
972
|
+
await registerModels();
|
|
973
|
+
|
|
974
|
+
// Act
|
|
975
|
+
await movie.update({ releaseDate: new Date('2023-07-14') });
|
|
976
|
+
await container.relatedMovies.create({ name: 'The Tale of Princess Kaguya' });
|
|
977
|
+
|
|
978
|
+
// Assert
|
|
979
|
+
expect(Cloud.localChanges).toEqual(2);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
});
|