Skip to content

Commit 958675a

Browse files
Add downloading attachment test
1 parent 029df9d commit 958675a

File tree

5 files changed

+218
-106
lines changed

5 files changed

+218
-106
lines changed

packages/attachments/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
},
5050
"devDependencies": {
5151
"@powersync/common": "workspace:*",
52+
"@powersync/web": "workspace:*",
5253
"@types/node": "^20.17.6",
5354
"vite": "^6.1.0",
54-
"vite-plugin-top-level-await": "^1.4.4"
55+
"vite-plugin-top-level-await": "^1.4.4",
56+
"vite-plugin-wasm": "^3.3.0"
5557
}
5658
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { PowerSyncDatabase, Schema, Table, column } from '@powersync/web';
3+
import { AbstractPowerSyncDatabase } from '@powersync/common';
4+
import { AttachmentQueue } from '../src/AttachmentQueue.js';
5+
import { AttachmentState, AttachmentTable } from '../src/Schema.js';
6+
import { RemoteStorageAdapter } from '../src/RemoteStorageAdapter.js';
7+
import { WatchedAttachmentItem } from '../src/WatchedAttachmentItem.js';
8+
import { IndexDBFileSystemStorageAdapter } from '../src/storageAdapters/IndexDBFileSystemAdapter.js';
9+
10+
const mockRemoteStorage: RemoteStorageAdapter = {
11+
downloadFile: (attachment) => {
12+
return Promise.resolve(new Blob(['data:image/jpeg;base64,FAKE_BASE64_DATA'], { type: 'image/jpeg' }));
13+
},
14+
uploadFile: vi.fn(),
15+
deleteFile: vi.fn()
16+
};
17+
18+
const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => void) => {
19+
db.watch(
20+
/* sql */
21+
`
22+
SELECT
23+
photo_id
24+
FROM
25+
users
26+
WHERE
27+
photo_id IS NOT NULL
28+
`,
29+
[],
30+
{
31+
onResult: (result: any) =>
32+
onUpdate(
33+
result.rows?._array.map((r: any) => ({
34+
id: r.photo_id,
35+
fileExtension: 'jpg'
36+
})) ?? []
37+
)
38+
}
39+
);
40+
};
41+
42+
let db: AbstractPowerSyncDatabase;
43+
44+
beforeAll(async () => {
45+
db = new PowerSyncDatabase({
46+
schema: new Schema({
47+
users: new Table({
48+
name: column.text,
49+
email: column.text,
50+
photo_id: column.text
51+
}),
52+
attachments: new AttachmentTable()
53+
}),
54+
database: {
55+
dbFilename: 'example.db'
56+
}
57+
});
58+
59+
await db.disconnectAndClear();
60+
});
61+
62+
afterAll(async () => {
63+
await db.disconnectAndClear();
64+
});
65+
66+
describe('attachment queue', () => {
67+
it('should download attachments when a new record with an attachment is added', async () => {
68+
const queue = new AttachmentQueue({
69+
db: db,
70+
watchAttachments,
71+
remoteStorage: mockRemoteStorage,
72+
localStorage: new IndexDBFileSystemStorageAdapter(),
73+
});
74+
75+
await queue.startSync();
76+
77+
await db.execute(
78+
/* sql */
79+
`
80+
INSERT INTO
81+
users (id, name, email, photo_id)
82+
VALUES
83+
(
84+
uuid (),
85+
'example',
86+
87+
uuid ()
88+
)
89+
`,
90+
[]
91+
);
92+
93+
const attachmentRecords = await waitForMatch(
94+
() =>
95+
db.watch(
96+
/* sql */
97+
`
98+
SELECT
99+
*
100+
FROM
101+
attachments
102+
`,
103+
[]
104+
),
105+
(results) => {
106+
return results?.rows?._array.some((r: any) => r.state === AttachmentState.SYNCED);
107+
},
108+
5
109+
);
110+
111+
const attachmentRecord = attachmentRecords.rows._array.at(0);
112+
113+
const localData = await queue.localStorage.readFile(attachmentRecord.local_uri!);
114+
const localDataString = new TextDecoder().decode(localData);
115+
expect(localDataString).toBe('data:image/jpeg;base64,FAKE_BASE64_DATA');
116+
117+
await queue.stopSync();
118+
});
119+
});
120+
121+
async function waitForMatch(
122+
iteratorGenerator: () => AsyncIterable<any>,
123+
predicate: (value: any) => boolean,
124+
timeout: number
125+
) {
126+
const timeoutMs = timeout * 1000;
127+
const abortController = new AbortController();
128+
129+
const matchPromise = (async () => {
130+
const asyncIterable = iteratorGenerator();
131+
try {
132+
for await (const value of asyncIterable) {
133+
if (abortController.signal.aborted) {
134+
throw new Error('Timeout');
135+
}
136+
if (predicate(value)) {
137+
return value;
138+
}
139+
}
140+
throw new Error('Stream ended without match');
141+
} finally {
142+
const iterator = asyncIterable[Symbol.asyncIterator]();
143+
if (iterator.return) {
144+
await iterator.return();
145+
}
146+
}
147+
})();
148+
149+
const timeoutPromise = new Promise((_, reject) =>
150+
setTimeout(() => {
151+
abortController.abort();
152+
reject(new Error('Timeout'));
153+
}, timeoutMs)
154+
);
155+
156+
return Promise.race([matchPromise, timeoutPromise]);
157+
}
158+
159+
// describe('attachments', () => {
160+
// beforeEach(() => {
161+
// vi.clearAllMocks();
162+
// });
163+
164+
// it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => {
165+
// const queue = new AttachmentQueue({
166+
// db: mockPowerSync as any,
167+
// watchAttachments: watchAttachments,
168+
// remoteStorage: mockRemoteStorage,
169+
// localStorage: mockLocalStorage
170+
// });
171+
172+
// await queue.saveFile;
173+
174+
// expect(mockLocalStorage.downloadFile).not.toHaveBeenCalled();
175+
// });
176+
177+
// it('should download attachments when downloadRecord is called with downloadAttachments true', async () => {
178+
// const queue = new TestAttachmentQueue({
179+
// powersync: mockPowerSync as any,
180+
// storage: mockLocalStorage,
181+
// downloadAttachments: true
182+
// });
183+
184+
// await queue.downloadRecord(record);
185+
186+
// expect(mockLocalStorage.downloadFile).toHaveBeenCalled();
187+
// });
188+
189+
// // Testing the inverse of this test, i.e. when downloadAttachments is false, is not required as you can't wait for something that does not happen
190+
// it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => {
191+
// const queue = new TestAttachmentQueue({
192+
// powersync: mockPowerSync as any,
193+
// storage: mockLocalStorage,
194+
// downloadAttachments: true
195+
// });
196+
197+
// queue.watchDownloads();
198+
// await vi.waitFor(() => {
199+
// expect(mockLocalStorage.downloadFile).toBeCalledTimes(2);
200+
// });
201+
// });
202+
// });

packages/attachments/tests/attachments/AttachmentQueue.test.ts

Lines changed: 0 additions & 103 deletions
This file was deleted.

packages/attachments/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
"path": "../common"
1414
}
1515
],
16-
"include": ["src/**/*"]
16+
"include": ["src/**/*", "tests/**/*", "package.json"]
1717
}

packages/attachments/vitest.config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import topLevelAwait from 'vite-plugin-top-level-await';
2+
import wasm from 'vite-plugin-wasm';
23
import { defineConfig, UserConfigExport } from 'vitest/config';
34

45
const config: UserConfigExport = {
5-
plugins: [topLevelAwait()],
6+
worker: {
7+
format: 'es',
8+
plugins: () => [wasm(), topLevelAwait()]
9+
},
10+
optimizeDeps: {
11+
// Don't optimise these packages as they contain web workers and WASM files.
12+
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
13+
exclude: ['@journeyapps/wa-sqlite', '@powersync/web'],
14+
include: ['async-mutex', 'comlink', 'bson']
15+
},
16+
plugins: [wasm(), topLevelAwait()],
617
test: {
718
isolate: false,
819
globals: true,

0 commit comments

Comments
 (0)