import PouchDB from 'pouchdb'; import memory from 'pouchdb-adapter-memory'; import { PouchDBAttachmentProxy, SENTRY_MIMETYPE, blobToString, stringToBlob } from '../src/index.js'; const notDesignDocs = d => !d.id.startsWith('_design'); const dataUrlToBlob = data => { const [header, b64str, ...nothing] = data.split(';base64,'); const byteCharacters = atob(b64str); const byteNumbers = new Array(byteCharacters.length); for (var i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); return new Blob([byteArray], { type: header.substr(5) }); }; describe('PouchDB Attachment Proxy', () => { let db; let wrap = true; const LocalStorageExampleAdapter = PouchDBAttachmentProxy({ getFn: async function getAttachment(blob) { if (!wrap) { return blob; } const id = await blobToString(blob); return dataUrlToBlob(localStorage[id]); }, remove: async function removeAttachment(blob) { if (!wrap) { return blob; } const id = await blobToString(blob); delete localStorage[id]; }, save: async function saveAttachment(blob) { if (!wrap) { return blob; } let id; while (!id || localStorage[id] !== undefined) { id = '' + Math.ceil(Math.random() * 100000); } localStorage[id] = true; // placeholder to prevent a race condition while the FileReader reads the blob. return new Promise(resolve => { const reader = new FileReader(); reader.onloadend = function() { localStorage[id] = reader.result; resolve(stringToBlob(id)); }; reader.readAsDataURL(blob); }); } }); const PDB = PouchDB.plugin(memory).plugin(LocalStorageExampleAdapter); beforeEach(() => { db = new PDB('pouchdb-test', { adapter: 'memory' }); wrap = true; }); afterEach(async () => { const res = await db.allDocs(); await Promise.all(res.rows.filter(notDesignDocs).map(d => db.remove(d.id, d.value.rev))).catch( () => {} ); await db.compact(); }); async function checkBlob(blob, data) { const blobData = await blobToString(blob); expect(blobData).toEqual(data); } async function checkDocument(id, attName, mime_type, data) { wrap = false; const placeholderBlob = await db.getAttachment(id, attName); wrap = true; const placeholder = await blobToString(placeholderBlob); expect(placeholderBlob.type).toEqual(SENTRY_MIMETYPE); const resultDataStr = localStorage[placeholder]; const [header, b64str, ...nothing] = resultDataStr.split(';base64,'); expect(header.substr(5)).toEqual(mime_type); expect(b64str).toEqual(btoa(data)); return resultDataStr; } describe('getFn()', () => { it('returns attachments when PouchDB.getAttachment() is called', async () => { const id = 'abc123'; localStorage['abc123'] = `data:text/plain;base64,${btoa('Test3 document')}`; wrap = false; await db.bulkDocs([ { _id: 'test3', _attachments: { test3: { content_type: SENTRY_MIMETYPE, data: new Blob(['abc123'], { type: SENTRY_MIMETYPE }) } } } ]); wrap = true; const blob = await db.getAttachment('test3', 'test3'); checkBlob(blob, 'Test3 document'); expect(blob.type).toEqual('text/plain'); }); }); describe('save()', () => { it('writes attachments in documents saved with PouchDB.put()', async () => { await db.put({ _id: 'test1', _attachments: { test: { content_type: 'text/plain', data: new Blob(['Test document'], { type: 'text/plain' }) } } }); await checkDocument('test1', 'test', 'text/plain', 'Test document'); }); it('writes attachments in documents saved with PouchDB.bulkDocs()', async () => { await db.bulkDocs([ { _id: 'test2', _attachments: { test2: { content_type: 'text/plain', data: new Blob(['Test2 document'], { type: 'text/plain' }) } } } ]); await checkDocument('test2', 'test2', 'text/plain', 'Test2 document'); }); it('writes attachments saved with PouchDB.putAttachment() ', async () => { const res = await db.put({ _id: 'test5', hi: 'there' }); const res2 = await db.putAttachment( res.id, 'test5', res.rev, new Blob(['Test5 document'], { type: 'text/plain' }), 'text/plain' ); await checkDocument('test5', 'test5', 'text/plain', 'Test5 document'); }); }); describe('remove()', () => { it('is called after PouchDB.removeAttachment()', async () => { await db.bulkDocs([ { _id: 'test4', _attachments: { test4: { content_type: 'text/plain', data: new Blob(['Test4 document'], { type: 'text/plain' }) } } } ]); const doc = await db.get('test4', { attachments: true }); expect(doc && doc._attachments && doc._attachments.test4).toBeTruthy(); wrap = false; const placeholderBlob = await db.getAttachment('test4', 'test4'); wrap = true; const placeholder = await blobToString(placeholderBlob); expect(placeholderBlob.type).toEqual(SENTRY_MIMETYPE); expect(localStorage[placeholder]).toBeTruthy(); await db.removeAttachment(doc._id, 'test4', doc._rev); const doc2 = await db.get('test4', { attachments: true }); expect(doc2._attachments).toBeFalsy(); expect(localStorage[placeholder]).toBeUndefined(); }); it('is called for attachments of deleted documents', async () => { await db.bulkDocs([ { _id: 'test6', _attachments: { test6: { content_type: 'text/plain', data: new Blob(['Test6 document'], { type: 'text/plain' }) } } } ]); await checkDocument('test6', 'test6', 'text/plain', 'Test6 document'); wrap = false; const placeholderBlob = await db.getAttachment('test6', 'test6'); wrap = true; const placeholder = await blobToString(placeholderBlob); const doc = await db.get('test6'); await db.remove(doc._id, doc._rev); expect(localStorage[placeholder]).toBeUndefined(); }); }); });