blob: 7e1e47ce7f4b01d9afa864364e47f3465e6c0299 [file] [log] [blame]
Eduardo Sousa0593aba2019-06-04 12:55:43 +01001# Copyright 2019 Canonical
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14#
15# For those usages not covered by the Apache License, Version 2.0 please
16# contact: eduardo.sousa@canonical.com
17##
18
David Garcia8ab6cc62020-06-26 17:04:37 +020019from io import BytesIO
aticig3dd0db62022-03-04 19:35:45 +030020import logging
21import os
22from pathlib import Path
23import subprocess
24import tarfile
25import tempfile
lloretgallegf296d2a2020-09-02 09:36:24 +000026from unittest.mock import Mock
27
aticig3dd0db62022-03-04 19:35:45 +030028from gridfs import GridFSBucket
Eduardo Sousa0593aba2019-06-04 12:55:43 +010029from osm_common.fsbase import FsException
30from osm_common.fsmongo import FsMongo
aticig3dd0db62022-03-04 19:35:45 +030031from pymongo import MongoClient
32import pytest
Eduardo Sousa0593aba2019-06-04 12:55:43 +010033
34__author__ = "Eduardo Sousa <eduardo.sousa@canonical.com>"
35
36
37def valid_path():
garciadeblas2644b762021-03-24 09:21:01 +010038 return tempfile.gettempdir() + "/"
Eduardo Sousa0593aba2019-06-04 12:55:43 +010039
40
41def invalid_path():
garciadeblas2644b762021-03-24 09:21:01 +010042 return "/#tweeter/"
Eduardo Sousa0593aba2019-06-04 12:55:43 +010043
44
45@pytest.fixture(scope="function", params=[True, False])
46def fs_mongo(request, monkeypatch):
aticig7da97952022-03-31 23:07:21 +030047 def mock_mongoclient_constructor(a, b):
Eduardo Sousa0593aba2019-06-04 12:55:43 +010048 pass
49
50 def mock_mongoclient_getitem(a, b):
51 pass
52
53 def mock_gridfs_constructor(a, b):
54 pass
55
garciadeblas2644b762021-03-24 09:21:01 +010056 monkeypatch.setattr(MongoClient, "__init__", mock_mongoclient_constructor)
57 monkeypatch.setattr(MongoClient, "__getitem__", mock_mongoclient_getitem)
58 monkeypatch.setattr(GridFSBucket, "__init__", mock_gridfs_constructor)
Eduardo Sousa0593aba2019-06-04 12:55:43 +010059 fs = FsMongo(lock=request.param)
aticig7da97952022-03-31 23:07:21 +030060 fs.fs_connect({"path": valid_path(), "uri": "mongo:27017", "collection": "files"})
Eduardo Sousa0593aba2019-06-04 12:55:43 +010061 return fs
62
63
64def generic_fs_exception_message(message):
65 return "storage exception {}".format(message)
66
67
68def fs_connect_exception_message(path):
garciadeblas2644b762021-03-24 09:21:01 +010069 return "storage exception Invalid configuration param at '[storage]': path '{}' does not exist".format(
70 path
71 )
Eduardo Sousa0593aba2019-06-04 12:55:43 +010072
73
74def file_open_file_not_found_exception(storage):
garciadeblas2644b762021-03-24 09:21:01 +010075 f = storage if isinstance(storage, str) else "/".join(storage)
Eduardo Sousa0593aba2019-06-04 12:55:43 +010076 return "storage exception File {} does not exist".format(f)
77
78
79def file_open_io_exception(storage):
garciadeblas2644b762021-03-24 09:21:01 +010080 f = storage if isinstance(storage, str) else "/".join(storage)
Eduardo Sousa0593aba2019-06-04 12:55:43 +010081 return "storage exception File {} cannot be opened".format(f)
82
83
84def dir_ls_not_a_directory_exception(storage):
garciadeblas2644b762021-03-24 09:21:01 +010085 f = storage if isinstance(storage, str) else "/".join(storage)
Eduardo Sousa0593aba2019-06-04 12:55:43 +010086 return "storage exception File {} does not exist".format(f)
87
88
89def dir_ls_io_exception(storage):
garciadeblas2644b762021-03-24 09:21:01 +010090 f = storage if isinstance(storage, str) else "/".join(storage)
Eduardo Sousa0593aba2019-06-04 12:55:43 +010091 return "storage exception File {} cannot be opened".format(f)
92
93
94def file_delete_exception_message(storage):
95 return "storage exception File {} does not exist".format(storage)
96
97
98def test_constructor_without_logger():
99 fs = FsMongo()
garciadeblas2644b762021-03-24 09:21:01 +0100100 assert fs.logger == logging.getLogger("fs")
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100101 assert fs.path is None
102 assert fs.client is None
103 assert fs.fs is None
104
105
106def test_constructor_with_logger():
garciadeblas2644b762021-03-24 09:21:01 +0100107 logger_name = "fs_mongo"
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100108 fs = FsMongo(logger_name=logger_name)
109 assert fs.logger == logging.getLogger(logger_name)
110 assert fs.path is None
111 assert fs.client is None
112 assert fs.fs is None
113
114
115def test_get_params(fs_mongo, monkeypatch):
116 def mock_gridfs_find(self, search_query, **kwargs):
117 return []
118
garciadeblas2644b762021-03-24 09:21:01 +0100119 monkeypatch.setattr(GridFSBucket, "find", mock_gridfs_find)
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100120 params = fs_mongo.get_params()
121 assert len(params) == 2
122 assert "fs" in params
123 assert "path" in params
124 assert params["fs"] == "mongo"
125 assert params["path"] == valid_path()
126
127
garciadeblas2644b762021-03-24 09:21:01 +0100128@pytest.mark.parametrize(
129 "config, exp_logger, exp_path",
130 [
131 (
132 {
133 "logger_name": "fs_mongo",
134 "path": valid_path(),
135 "uri": "mongo:27017",
136 "collection": "files",
137 },
138 "fs_mongo",
139 valid_path(),
140 ),
141 (
142 {
143 "logger_name": "fs_mongo",
garciadeblas2644b762021-03-24 09:21:01 +0100144 "path": valid_path()[:-1],
145 "uri": "mongo:27017",
146 "collection": "files",
147 },
148 "fs_mongo",
149 valid_path(),
150 ),
151 (
garciadeblas2644b762021-03-24 09:21:01 +0100152 {"path": valid_path(), "uri": "mongo:27017", "collection": "files"},
153 "fs",
154 valid_path(),
155 ),
156 (
garciadeblas2644b762021-03-24 09:21:01 +0100157 {"path": valid_path()[:-1], "uri": "mongo:27017", "collection": "files"},
158 "fs",
159 valid_path(),
160 ),
garciadeblas2644b762021-03-24 09:21:01 +0100161 ],
162)
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100163def test_fs_connect_with_valid_config(config, exp_logger, exp_path):
164 fs = FsMongo()
165 fs.fs_connect(config)
166 assert fs.logger == logging.getLogger(exp_logger)
167 assert fs.path == exp_path
168 assert type(fs.client) == MongoClient
169 assert type(fs.fs) == GridFSBucket
170
171
garciadeblas2644b762021-03-24 09:21:01 +0100172@pytest.mark.parametrize(
173 "config, exp_exception_message",
174 [
175 (
176 {
177 "logger_name": "fs_mongo",
178 "path": invalid_path(),
179 "uri": "mongo:27017",
180 "collection": "files",
181 },
182 fs_connect_exception_message(invalid_path()),
183 ),
184 (
185 {
186 "logger_name": "fs_mongo",
garciadeblas2644b762021-03-24 09:21:01 +0100187 "path": invalid_path()[:-1],
188 "uri": "mongo:27017",
189 "collection": "files",
190 },
191 fs_connect_exception_message(invalid_path()[:-1]),
192 ),
193 (
garciadeblas2644b762021-03-24 09:21:01 +0100194 {"path": invalid_path(), "uri": "mongo:27017", "collection": "files"},
195 fs_connect_exception_message(invalid_path()),
196 ),
197 (
garciadeblas2644b762021-03-24 09:21:01 +0100198 {"path": invalid_path()[:-1], "uri": "mongo:27017", "collection": "files"},
199 fs_connect_exception_message(invalid_path()[:-1]),
200 ),
201 (
aticig7da97952022-03-31 23:07:21 +0300202 {"path": "/", "uri": "mongo:27017", "collection": "files"},
garciadeblas2644b762021-03-24 09:21:01 +0100203 generic_fs_exception_message(
204 "Invalid configuration param at '[storage]': path '/' is not writable"
205 ),
206 ),
207 ],
208)
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100209def test_fs_connect_with_invalid_path(config, exp_exception_message):
210 fs = FsMongo()
211 with pytest.raises(FsException) as excinfo:
212 fs.fs_connect(config)
213 assert str(excinfo.value) == exp_exception_message
214
215
garciadeblas2644b762021-03-24 09:21:01 +0100216@pytest.mark.parametrize(
217 "config, exp_exception_message",
218 [
219 (
220 {"logger_name": "fs_mongo", "uri": "mongo:27017", "collection": "files"},
221 'Missing parameter "path"',
222 ),
223 (
garciadeblas2644b762021-03-24 09:21:01 +0100224 {"logger_name": "fs_mongo", "path": valid_path(), "collection": "files"},
aticig7da97952022-03-31 23:07:21 +0300225 'Missing parameters: "uri"',
garciadeblas2644b762021-03-24 09:21:01 +0100226 ),
227 (
228 {"logger_name": "fs_mongo", "path": valid_path(), "uri": "mongo:27017"},
229 'Missing parameter "collection"',
230 ),
garciadeblas2644b762021-03-24 09:21:01 +0100231 ],
232)
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100233def test_fs_connect_with_missing_parameters(config, exp_exception_message):
234 fs = FsMongo()
235 with pytest.raises(FsException) as excinfo:
236 fs.fs_connect(config)
237 assert str(excinfo.value) == generic_fs_exception_message(exp_exception_message)
238
239
garciadeblas2644b762021-03-24 09:21:01 +0100240@pytest.mark.parametrize(
241 "config, exp_exception_message",
242 [
243 (
244 {
245 "logger_name": "fs_mongo",
246 "path": valid_path(),
247 "uri": "mongo:27017",
248 "collection": "files",
249 },
250 "MongoClient crashed",
251 ),
garciadeblas2644b762021-03-24 09:21:01 +0100252 ],
253)
254def test_fs_connect_with_invalid_mongoclient(
255 config, exp_exception_message, monkeypatch
256):
aticig7da97952022-03-31 23:07:21 +0300257 def generate_exception(a, b=None):
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100258 raise Exception(exp_exception_message)
259
garciadeblas2644b762021-03-24 09:21:01 +0100260 monkeypatch.setattr(MongoClient, "__init__", generate_exception)
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100261
262 fs = FsMongo()
263 with pytest.raises(FsException) as excinfo:
264 fs.fs_connect(config)
265 assert str(excinfo.value) == generic_fs_exception_message(exp_exception_message)
266
267
garciadeblas2644b762021-03-24 09:21:01 +0100268@pytest.mark.parametrize(
269 "config, exp_exception_message",
270 [
271 (
272 {
273 "logger_name": "fs_mongo",
274 "path": valid_path(),
275 "uri": "mongo:27017",
276 "collection": "files",
277 },
278 "Collection unavailable",
279 ),
garciadeblas2644b762021-03-24 09:21:01 +0100280 ],
281)
282def test_fs_connect_with_invalid_mongo_collection(
283 config, exp_exception_message, monkeypatch
284):
aticig7da97952022-03-31 23:07:21 +0300285 def mock_mongoclient_constructor(a, b=None):
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100286 pass
287
288 def generate_exception(a, b):
289 raise Exception(exp_exception_message)
290
garciadeblas2644b762021-03-24 09:21:01 +0100291 monkeypatch.setattr(MongoClient, "__init__", mock_mongoclient_constructor)
292 monkeypatch.setattr(MongoClient, "__getitem__", generate_exception)
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100293
294 fs = FsMongo()
295 with pytest.raises(FsException) as excinfo:
296 fs.fs_connect(config)
297 assert str(excinfo.value) == generic_fs_exception_message(exp_exception_message)
298
299
garciadeblas2644b762021-03-24 09:21:01 +0100300@pytest.mark.parametrize(
301 "config, exp_exception_message",
302 [
303 (
304 {
305 "logger_name": "fs_mongo",
306 "path": valid_path(),
307 "uri": "mongo:27017",
308 "collection": "files",
309 },
310 "GridFsBucket crashed",
311 ),
garciadeblas2644b762021-03-24 09:21:01 +0100312 ],
313)
314def test_fs_connect_with_invalid_gridfsbucket(
315 config, exp_exception_message, monkeypatch
316):
aticig7da97952022-03-31 23:07:21 +0300317 def mock_mongoclient_constructor(a, b=None):
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100318 pass
319
320 def mock_mongoclient_getitem(a, b):
321 pass
322
323 def generate_exception(a, b):
324 raise Exception(exp_exception_message)
325
garciadeblas2644b762021-03-24 09:21:01 +0100326 monkeypatch.setattr(MongoClient, "__init__", mock_mongoclient_constructor)
327 monkeypatch.setattr(MongoClient, "__getitem__", mock_mongoclient_getitem)
328 monkeypatch.setattr(GridFSBucket, "__init__", generate_exception)
Eduardo Sousa0593aba2019-06-04 12:55:43 +0100329
330 fs = FsMongo()
331 with pytest.raises(FsException) as excinfo:
332 fs.fs_connect(config)
333 assert str(excinfo.value) == generic_fs_exception_message(exp_exception_message)
334
335
336def test_fs_disconnect(fs_mongo):
337 fs_mongo.fs_disconnect()
David Garcia8ab6cc62020-06-26 17:04:37 +0200338
339
340# Example.tar.gz
341# example_tar/
342# ├── directory
343# │ └── file
344# └── symlinks
345# ├── directory_link -> ../directory/
346# └── file_link -> ../directory/file
347class FakeCursor:
348 def __init__(self, id, filename, metadata):
349 self._id = id
350 self.filename = filename
351 self.metadata = metadata
352
353
354class FakeFS:
garciadeblas2644b762021-03-24 09:21:01 +0100355 directory_metadata = {"type": "dir", "permissions": 509}
356 file_metadata = {"type": "file", "permissions": 436}
357 symlink_metadata = {"type": "sym", "permissions": 511}
David Garcia8ab6cc62020-06-26 17:04:37 +0200358
359 tar_info = {
360 1: {
garciadeblas2644b762021-03-24 09:21:01 +0100361 "cursor": FakeCursor(1, "example_tar", directory_metadata),
David Garcia8ab6cc62020-06-26 17:04:37 +0200362 "metadata": directory_metadata,
garciadeblas2644b762021-03-24 09:21:01 +0100363 "stream_content": b"",
David Garcia8ab6cc62020-06-26 17:04:37 +0200364 "stream_content_bad": b"Something",
garciadeblas2644b762021-03-24 09:21:01 +0100365 "path": "./tmp/example_tar",
David Garcia8ab6cc62020-06-26 17:04:37 +0200366 },
367 2: {
garciadeblas2644b762021-03-24 09:21:01 +0100368 "cursor": FakeCursor(2, "example_tar/directory", directory_metadata),
David Garcia8ab6cc62020-06-26 17:04:37 +0200369 "metadata": directory_metadata,
garciadeblas2644b762021-03-24 09:21:01 +0100370 "stream_content": b"",
David Garcia8ab6cc62020-06-26 17:04:37 +0200371 "stream_content_bad": b"Something",
garciadeblas2644b762021-03-24 09:21:01 +0100372 "path": "./tmp/example_tar/directory",
David Garcia8ab6cc62020-06-26 17:04:37 +0200373 },
374 3: {
garciadeblas2644b762021-03-24 09:21:01 +0100375 "cursor": FakeCursor(3, "example_tar/symlinks", directory_metadata),
David Garcia8ab6cc62020-06-26 17:04:37 +0200376 "metadata": directory_metadata,
garciadeblas2644b762021-03-24 09:21:01 +0100377 "stream_content": b"",
David Garcia8ab6cc62020-06-26 17:04:37 +0200378 "stream_content_bad": b"Something",
garciadeblas2644b762021-03-24 09:21:01 +0100379 "path": "./tmp/example_tar/symlinks",
David Garcia8ab6cc62020-06-26 17:04:37 +0200380 },
381 4: {
garciadeblas2644b762021-03-24 09:21:01 +0100382 "cursor": FakeCursor(4, "example_tar/directory/file", file_metadata),
David Garcia8ab6cc62020-06-26 17:04:37 +0200383 "metadata": file_metadata,
384 "stream_content": b"Example test",
385 "stream_content_bad": b"Example test2",
garciadeblas2644b762021-03-24 09:21:01 +0100386 "path": "./tmp/example_tar/directory/file",
David Garcia8ab6cc62020-06-26 17:04:37 +0200387 },
388 5: {
garciadeblas2644b762021-03-24 09:21:01 +0100389 "cursor": FakeCursor(5, "example_tar/symlinks/file_link", symlink_metadata),
David Garcia8ab6cc62020-06-26 17:04:37 +0200390 "metadata": symlink_metadata,
391 "stream_content": b"../directory/file",
392 "stream_content_bad": b"",
garciadeblas2644b762021-03-24 09:21:01 +0100393 "path": "./tmp/example_tar/symlinks/file_link",
David Garcia8ab6cc62020-06-26 17:04:37 +0200394 },
395 6: {
garciadeblas2644b762021-03-24 09:21:01 +0100396 "cursor": FakeCursor(
397 6, "example_tar/symlinks/directory_link", symlink_metadata
398 ),
David Garcia8ab6cc62020-06-26 17:04:37 +0200399 "metadata": symlink_metadata,
400 "stream_content": b"../directory/",
401 "stream_content_bad": b"",
garciadeblas2644b762021-03-24 09:21:01 +0100402 "path": "./tmp/example_tar/symlinks/directory_link",
403 },
David Garcia8ab6cc62020-06-26 17:04:37 +0200404 }
405
406 def upload_from_stream(self, f, stream, metadata=None):
407 found = False
408 for i, v in self.tar_info.items():
409 if f == v["path"]:
410 assert metadata["type"] == v["metadata"]["type"]
411 assert stream.read() == BytesIO(v["stream_content"]).read()
412 stream.seek(0)
413 assert stream.read() != BytesIO(v["stream_content_bad"]).read()
414 found = True
415 continue
416 assert found
417
lloretgallegf296d2a2020-09-02 09:36:24 +0000418 def find(self, type, no_cursor_timeout=True, sort=None):
David Garcia8ab6cc62020-06-26 17:04:37 +0200419 list = []
420 for i, v in self.tar_info.items():
421 if type["metadata.type"] == "dir":
422 if v["metadata"] == self.directory_metadata:
423 list.append(v["cursor"])
424 else:
425 if v["metadata"] != self.directory_metadata:
426 list.append(v["cursor"])
427 return list
428
429 def download_to_stream(self, id, file_stream):
430 file_stream.write(BytesIO(self.tar_info[id]["stream_content"]).read())
431
432
433def test_file_extract():
434 tar_path = "tmp/Example.tar.gz"
435 folder_path = "tmp/example_tar"
436
437 # Generate package
438 subprocess.call(["rm", "-rf", "./tmp"])
439 subprocess.call(["mkdir", "-p", "{}/directory".format(folder_path)])
440 subprocess.call(["mkdir", "-p", "{}/symlinks".format(folder_path)])
441 p = Path("{}/directory/file".format(folder_path))
442 p.write_text("Example test")
443 os.symlink("../directory/file", "{}/symlinks/file_link".format(folder_path))
444 os.symlink("../directory/", "{}/symlinks/directory_link".format(folder_path))
445 if os.path.exists(tar_path):
446 os.remove(tar_path)
447 subprocess.call(["tar", "-czvf", tar_path, folder_path])
448
449 try:
450 tar = tarfile.open(tar_path, "r")
451 fs = FsMongo()
452 fs.fs = FakeFS()
bravof98fc8f02021-11-04 21:16:00 -0300453 fs.file_extract(compressed_object=tar, path=".")
David Garcia8ab6cc62020-06-26 17:04:37 +0200454 finally:
455 os.remove(tar_path)
456 subprocess.call(["rm", "-rf", "./tmp"])
457
458
459def test_upload_local_fs():
460 path = "./tmp/"
461
462 subprocess.call(["rm", "-rf", path])
463 try:
464 fs = FsMongo()
465 fs.path = path
466 fs.fs = FakeFS()
467 fs.sync()
468 assert os.path.isdir("{}example_tar".format(path))
469 assert os.path.isdir("{}example_tar/directory".format(path))
470 assert os.path.isdir("{}example_tar/symlinks".format(path))
471 assert os.path.isfile("{}example_tar/directory/file".format(path))
472 assert os.path.islink("{}example_tar/symlinks/file_link".format(path))
473 assert os.path.islink("{}example_tar/symlinks/directory_link".format(path))
474 finally:
475 subprocess.call(["rm", "-rf", path])
lloretgallegf296d2a2020-09-02 09:36:24 +0000476
477
478def test_upload_mongo_fs():
479 path = "./tmp/"
480
481 subprocess.call(["rm", "-rf", path])
482 try:
483 fs = FsMongo()
484 fs.path = path
485 fs.fs = Mock()
486 fs.fs.find.return_value = {}
487
488 file_content = "Test file content"
489
490 # Create local dir and upload content to fakefs
491 os.mkdir(path)
492 os.mkdir("{}example_local".format(path))
493 os.mkdir("{}example_local/directory".format(path))
garciadeblas2644b762021-03-24 09:21:01 +0100494 with open(
495 "{}example_local/directory/test_file".format(path), "w+"
496 ) as test_file:
lloretgallegf296d2a2020-09-02 09:36:24 +0000497 test_file.write(file_content)
498 fs.reverse_sync("example_local")
499
500 assert fs.fs.upload_from_stream.call_count == 2
501
502 # first call to upload_from_stream, dir_name
503 dir_name = "example_local/directory"
504 call_args_0 = fs.fs.upload_from_stream.call_args_list[0]
505 assert call_args_0[0][0] == dir_name
506 assert call_args_0[1].get("metadata").get("type") == "dir"
507
508 # second call to upload_from_stream, dir_name
509 file_name = "example_local/directory/test_file"
510 call_args_1 = fs.fs.upload_from_stream.call_args_list[1]
511 assert call_args_1[0][0] == file_name
512 assert call_args_1[1].get("metadata").get("type") == "file"
513
514 finally:
515 subprocess.call(["rm", "-rf", path])
516 pass