Project

General

Profile

Download (8.54 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / test / unit / test_indexeddb.py @ 1e4ce148

1
# SPDX-License-Identifier: CC0-1.0
2

    
3
"""
4
Haketilo unit tests - IndexedDB access
5
"""
6

    
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 2021, Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the CC0 1.0 Universal License as published by
13
# the Creative Commons Corporation.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# CC0 1.0 Universal License for more details.
19

    
20
import pytest
21
from hashlib import sha256
22

    
23
from ..script_loader import load_script
24

    
25
@pytest.fixture(scope="session")
26
def indexeddb_code():
27
    yield load_script('common/indexeddb.js', ['common'])
28

    
29
def sample_file(contents):
30
    return {
31
        'hash_key': f'sha256-{sha256(contents.encode()).digest().hex()}',
32
        'contents': contents
33
    }
34

    
35
sample_files = {
36
    'report.spdx':              sample_file('<!-- dummy report -->'),
37
    'LICENSES/somelicense.txt': sample_file('Permission is granted...'),
38
    'hello.js':                 sample_file('console.log("hello!");\n'),
39
    'bye.js':                   sample_file('console.log("bye!");\n'),
40
    'combined.js':              sample_file('console.log("hello!\\nbye!");\n'),
41
    'README.md':                sample_file('# Python Frobnicator\n...')
42
}
43

    
44
sample_files_by_hash = dict([[file['hash_key'], file['contents']]
45
                             for file in sample_files.values()])
46

    
47
def file_ref(file_name):
48
    return {'file': file_name, 'hash_key': sample_files[file_name]['hash_key']}
49

    
50
def test_save_remove_item(execute_in_page, indexeddb_code):
51
    """
52
    indexeddb.js facilitates operating on Haketilo's internal database.
53
    Verify database operations work properly.
54
    """
55
    execute_in_page(indexeddb_code, page='https://gotmyowndoma.in')
56
    # Don't use Haketilo's default initial data.
57
    execute_in_page(
58
        '''{
59
        const _get_db = haketilodb.get;
60
        get_db = () => _get_db({});
61
        haketilodb.get = get_db;
62
        }'''
63
    )
64

    
65
    # Start with no database.
66
    execute_in_page(
67
        '''{
68
        async function delete_db() {
69
            let resolve;
70
            const result = new Promise(_resolve => resolve = _resolve);
71
            const request = indexedDB.deleteDatabase("haketilo");
72
            [request.onsuccess, request.onerror] = [resolve, resolve];
73
            await result;
74
        }
75

    
76
        returnval(delete_db());
77
        }'''
78
    )
79

    
80
    # Facilitate retrieving all IndexedDB contents.
81
    execute_in_page(
82
        '''
83
        async function get_database_contents()
84
        {
85
            const db = await haketilodb.get();
86

    
87
            const transaction = db.transaction(db.objectStoreNames);
88
            const store_names_reqs = [...db.objectStoreNames]
89
                .map(sn => [sn, transaction.objectStore(sn).getAll()])
90

    
91
            const promises = store_names_reqs
92
                .map(([_, req]) => wait_request(req));
93
            await Promise.all(promises);
94

    
95
            const result = {};
96
            store_names_reqs.forEach(([sn, req]) => result[sn] = req.result);
97
            return result;
98
        }
99
        ''')
100

    
101
    # Sample resource definition. It'd normally contain more fields but here
102
    # we use a simplified version.
103
    sample_item = {
104
        'source_copyright': [
105
            file_ref('report.spdx'),
106
            file_ref('LICENSES/somelicense.txt')
107
        ],
108
        'type': 'resource',
109
        'identifier': 'helloapple',
110
        'scripts': [file_ref('hello.js'), file_ref('bye.js')],
111
    }
112
    next(iter(sample_item['source_copyright']))['ugly_extra_property'] = True
113

    
114
    database_contents = execute_in_page(
115
        '''{
116
        const promise = start_items_transaction(["resources"], arguments[1])
117
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
118
            .then(finalize_items_transaction)
119
            .then(get_database_contents);
120
        returnval(promise);
121
        }''',
122
        sample_item, sample_files_by_hash)
123
    assert len(database_contents['files']) == 4
124
    assert all([sample_files_by_hash[file['hash_key']] == file['contents']
125
                for file in database_contents['files']])
126
    assert all([len(file) == 2 for file in database_contents['files']])
127

    
128
    assert len(database_contents['file_uses']) == 4
129
    assert all([uses['uses'] == 1 for uses in database_contents['file_uses']])
130
    assert set([uses['hash_key'] for uses in database_contents['file_uses']]) \
131
        == set([file['hash_key'] for file in database_contents['files']])
132

    
133
    assert database_contents['mappings'] == []
134
    assert database_contents['resources'] == [sample_item]
135

    
136
    # See if trying to add an item without providing all its files ends in an
137
    # exception and aborts the transaction as it should.
138
    sample_item['scripts'].append(file_ref('combined.js'))
139
    incomplete_files = {**sample_files_by_hash}
140
    incomplete_files.pop(sample_files['combined.js']['hash_key'])
141
    print ('incomplete files:', incomplete_files)
142
    print ('sample item:', sample_item)
143
    result = execute_in_page(
144
        '''{
145
        console.log('sample item', arguments[0]);
146
        const promise = (async () => {
147
            const context =
148
                await start_items_transaction(["resources"], arguments[1]);
149
            try {
150
                await save_item(arguments[0], context);
151
                await finalize_items_transaction(context);
152
                return {};
153
            } catch(e) {
154
                var exception = e;
155
            }
156

    
157
            return {exception, db_contents: await get_database_contents()};
158
        })();
159
        returnval(promise);
160
        }''',
161
        sample_item, incomplete_files)
162

    
163
    assert result
164
    assert 'file not present' in result['exception']
165
    for key, val in database_contents.items():
166
        keyfun = lambda item: item.get('hash_key') or item['identifier']
167
        assert sorted(result['db_contents'][key], key=keyfun) \
168
            == sorted(val,                        key=keyfun)
169

    
170
    # See if adding another item that partially uses first's files works OK.
171
    sample_item = {
172
        'source_copyright': [
173
            file_ref('report.spdx'),
174
            file_ref('README.md')
175
        ],
176
        'type': 'mapping',
177
        'identifier': 'helloapple',
178
    }
179
    database_contents = execute_in_page(
180
        '''{
181
        const promise = start_items_transaction(["mappings"], arguments[1])
182
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
183
            .then(finalize_items_transaction)
184
            .then(get_database_contents);
185
        returnval(promise);
186
        }''',
187
        sample_item, sample_files_by_hash)
188

    
189
    names = ['README.md', 'report.spdx', 'LICENSES/somelicense.txt', 'hello.js',
190
             'bye.js']
191
    sample_files_list = [sample_files[name] for name in names]
192
    uses_list = [1, 2, 1, 1, 1]
193

    
194
    uses = dict([(uses['hash_key'], uses['uses'])
195
                 for uses in database_contents['file_uses']])
196
    assert uses  == dict([(file['hash_key'], nr)
197
                          for file, nr in zip(sample_files_list, uses_list)])
198

    
199
    files = dict([(file['hash_key'], file['contents'])
200
                  for file in database_contents['files']])
201
    assert files == dict([(file['hash_key'], file['contents'])
202
                          for file in sample_files_list])
203

    
204
    assert database_contents['mappings'] == [sample_item]
205

    
206
    # Try removing the items to get an empty database again.
207
    results = [None, None]
208
    for i, item_type in enumerate(['resource', 'mapping']):
209
         results[i] = execute_in_page(
210
            f'''{{
211
            const remover = remove_{item_type};
212
            const promise =
213
                start_items_transaction(["{item_type}s"], {{}})
214
                .then(ctx => remover('helloapple', ctx).then(() => ctx))
215
                .then(finalize_items_transaction)
216
                .then(get_database_contents);
217
            returnval(promise);
218
            }}''')
219

    
220
    names = ['README.md', 'report.spdx']
221
    sample_files_list = [sample_files[name] for name in names]
222
    uses_list = [1, 1]
223

    
224
    uses = dict([(uses['hash_key'], uses['uses'])
225
                 for uses in results[0]['file_uses']])
226
    assert uses  == dict([(file['hash_key'], 1) for file in sample_files_list])
227

    
228
    files = dict([(file['hash_key'], file['contents'])
229
                  for file in results[0]['files']])
230
    assert files == dict([(file['hash_key'], file['contents'])
231
                          for file in sample_files_list])
232

    
233
    assert results[0]['resources'] == []
234
    assert results[0]['mappings'] == [sample_item]
235

    
236
    assert results[1] == dict([(key, []) for key in  results[0].keys()])
(4-4/6)