#!/usr/bin/env python3 # # Unit test cases for buku # import logging import math import os import re import shutil import sqlite3 import sys import unittest import urllib import zipfile # fmt: off from tempfile import NamedTemporaryFile, TemporaryDirectory from unittest import mock from genericpath import exists # fmt: on import pytest import vcr import yaml from hypothesis import example, given, settings from hypothesis import strategies as st import buku from buku import BukuDb, parse_tags, prompt logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy vcr_log = logging.getLogger("vcr") vcr_log.setLevel(logging.INFO) TEST_TEMP_DIR_OBJ = TemporaryDirectory(prefix="bukutest_") TEST_TEMP_DIR_PATH = TEST_TEMP_DIR_OBJ.name TEST_TEMP_DBDIR_PATH = os.path.join(TEST_TEMP_DIR_PATH, "buku") TEST_TEMP_DBFILE_PATH = os.path.join(TEST_TEMP_DBDIR_PATH, "bookmarks.db") MAX_SQLITE_INT = int(math.pow(2, 63) - 1) TEST_PRINT_REC = ("https://example.com", "", parse_tags(["cat,ant,bee,1"]), "") TEST_BOOKMARKS = [ [ "http://slashdot.org", "SLASHDOT", parse_tags(["old,news"]), "News for old nerds, stuff that doesn't matter", ], [ "http://www.zażółćgęśląjaźń.pl/", "ZAŻÓŁĆ", parse_tags(["zażółć,gęślą,jaźń"]), "Testing UTF-8, zażółć gęślą jaźń.", ], [ "http://example.com/", "test", parse_tags(["test,tes,est,es"]), "a case for replace_tag test", ], ] only_python_3_5 = pytest.mark.skipif( sys.version_info < (3, 5), reason="requires Python 3.5 or later" ) @pytest.fixture(scope="module") def vcr_cassette_dir(request): # Put all cassettes in vhs/{module}/{test}.yaml return os.path.join("tests", "vcr_cassettes", request.module.__name__) @pytest.fixture() def setup(): os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH # start every test from a clean state if exists(TEST_TEMP_DBFILE_PATH): os.remove(TEST_TEMP_DBFILE_PATH) class PrettySafeLoader( yaml.SafeLoader ): # pylint: disable=too-many-ancestors,too-few-public-methods def construct_python_tuple(self, node): return tuple(self.construct_sequence(node)) PrettySafeLoader.add_constructor( u"tag:yaml.org,2002:python/tuple", PrettySafeLoader.construct_python_tuple ) class TestBukuDb(unittest.TestCase): def setUp(self): os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH # start every test from a clean state if exists(TEST_TEMP_DBFILE_PATH): os.remove(TEST_TEMP_DBFILE_PATH) self.bookmarks = TEST_BOOKMARKS self.bdb = BukuDb() def tearDown(self): os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH @pytest.mark.non_tox def test_get_default_dbdir(self): dbdir_expected = TEST_TEMP_DBDIR_PATH dbdir_local_expected = os.path.join( os.path.expanduser("~"), ".local", "share", "buku" ) dbdir_relative_expected = os.path.abspath(".") # desktop linux self.assertEqual(dbdir_expected, BukuDb.get_default_dbdir()) # desktop generic os.environ.pop("XDG_DATA_HOME") self.assertEqual(dbdir_local_expected, BukuDb.get_default_dbdir()) # no desktop # -- home is defined differently on various platforms. # -- keep a copy and set it back once done originals = {} for env_var in ["HOME", "HOMEPATH", "HOMEDIR"]: try: originals[env_var] = os.environ.pop(env_var) except KeyError: pass self.assertEqual(dbdir_relative_expected, BukuDb.get_default_dbdir()) for key, value in list(originals.items()): os.environ[key] = value # # not sure how to test this in nondestructive manner # def test_move_legacy_dbfile(self): # self.fail() def test_initdb(self): if exists(TEST_TEMP_DBFILE_PATH): os.remove(TEST_TEMP_DBFILE_PATH) self.assertIs(False, exists(TEST_TEMP_DBFILE_PATH)) conn, curr = BukuDb.initdb() self.assertIsInstance(conn, sqlite3.Connection) self.assertIsInstance(curr, sqlite3.Cursor) self.assertIs(True, exists(TEST_TEMP_DBFILE_PATH)) curr.close() conn.close() def test_get_rec_by_id(self): for bookmark in self.bookmarks: # adding bookmark from self.bookmarks self.bdb.add_rec(*bookmark) # the expected bookmark expected = ( 1, "http://slashdot.org", "SLASHDOT", ",news,old,", "News for old nerds, stuff that doesn't matter", 0, ) bookmark_from_db = self.bdb.get_rec_by_id(1) # asserting bookmark matches expected self.assertEqual(expected, bookmark_from_db) # asserting None returned if index out of range self.assertIsNone(self.bdb.get_rec_by_id(len(self.bookmarks[0]) + 1)) def test_get_rec_id(self): for idx, bookmark in enumerate(self.bookmarks): # adding bookmark from self.bookmarks to database self.bdb.add_rec(*bookmark) # asserting index is in order idx_from_db = self.bdb.get_rec_id(bookmark[0]) self.assertEqual(idx + 1, idx_from_db) # asserting -1 is returned for nonexistent url idx_from_db = self.bdb.get_rec_id("http://nonexistent.url") self.assertEqual(-1, idx_from_db) def test_add_rec(self): for bookmark in self.bookmarks: # adding bookmark from self.bookmarks to database self.bdb.add_rec(*bookmark) # retrieving bookmark from database index = self.bdb.get_rec_id(bookmark[0]) from_db = self.bdb.get_rec_by_id(index) self.assertIsNotNone(from_db) # comparing data for pair in zip(from_db[1:], bookmark): self.assertEqual(*pair) # TODO: tags should be passed to the api as a sequence... def test_suggest_tags(self): for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) tagstr = ",test,old," with mock.patch("builtins.input", return_value="1 2 3"): expected_results = ",es,est,news,old,test," suggested_results = self.bdb.suggest_similar_tag(tagstr) self.assertEqual(expected_results, suggested_results) # returns user supplied tags if none are in the DB tagstr = ",uniquetag1,uniquetag2," expected_results = tagstr suggested_results = self.bdb.suggest_similar_tag(tagstr) self.assertEqual(expected_results, suggested_results) def test_update_rec(self): old_values = self.bookmarks[0] new_values = self.bookmarks[1] # adding bookmark and getting index self.bdb.add_rec(*old_values) index = self.bdb.get_rec_id(old_values[0]) # updating with new values self.bdb.update_rec(index, *new_values) # retrieving bookmark from database from_db = self.bdb.get_rec_by_id(index) self.assertIsNotNone(from_db) # checking if values are updated for pair in zip(from_db[1:], new_values): self.assertEqual(*pair) def test_append_tag_at_index(self): for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) # tags to add old_tags = self.bdb.get_rec_by_id(1)[3] new_tags = ",foo,bar,baz" self.bdb.append_tag_at_index(1, new_tags) # updated list of tags from_db = self.bdb.get_rec_by_id(1)[3] # checking if new tags were added to the bookmark self.assertTrue(split_and_test_membership(new_tags, from_db)) # checking if old tags still exist self.assertTrue(split_and_test_membership(old_tags, from_db)) def test_append_tag_at_all_indices(self): for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) # tags to add new_tags = ",foo,bar,baz" # record of original tags for each bookmark old_tagsets = { i: self.bdb.get_rec_by_id(i)[3] for i in inclusive_range(1, len(self.bookmarks)) } with mock.patch("builtins.input", return_value="y"): self.bdb.append_tag_at_index(0, new_tags) # updated tags for each bookmark from_db = [ (i, self.bdb.get_rec_by_id(i)[3]) for i in inclusive_range(1, len(self.bookmarks)) ] for index, tagset in from_db: # checking if new tags added to bookmark self.assertTrue(split_and_test_membership(new_tags, tagset)) # checking if old tags still exist for boomark self.assertTrue(split_and_test_membership(old_tagsets[index], tagset)) def test_delete_tag_at_index(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) get_tags_at_idx = lambda i: self.bdb.get_rec_by_id(i)[3] # list of two-tuples, each containg bookmark index and corresponding tags tags_by_index = [ (i, get_tags_at_idx(i)) for i in inclusive_range(1, len(self.bookmarks)) ] for i, tags in tags_by_index: # get the first tag from the bookmark to_delete = re.match(",.*?,", tags).group(0) self.bdb.delete_tag_at_index(i, to_delete) # get updated tags from db from_db = get_tags_at_idx(i) self.assertNotIn(to_delete, from_db) def test_search_keywords_and_filter_by_tags(self): # adding bookmark for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) with mock.patch("buku.prompt"): expected = [ ( 3, "http://example.com/", "test", ",es,est,tes,test,", "a case for replace_tag test", 0, ) ] results = self.bdb.search_keywords_and_filter_by_tags( ["News", "case"], False, False, False, ["est"], ) self.assertIn(expected[0], results) expected = [ ( 3, "http://example.com/", "test", ",es,est,tes,test,", "a case for replace_tag test", 0, ), ( 2, "http://www.zażółćgęśląjaźń.pl/", "ZAŻÓŁĆ", ",gęślą,jaźń,zażółć,", "Testing UTF-8, zażółć gęślą jaźń.", 0, ), ] results = self.bdb.search_keywords_and_filter_by_tags( ["UTF-8", "case"], False, False, False, "jaźń, test", ) self.assertIn(expected[0], results) self.assertIn(expected[1], results) def test_searchdb(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) get_first_tag = lambda x: "".join(x[2].split(",")[:2]) for i, bookmark in enumerate(self.bookmarks): tag_search = get_first_tag(bookmark) # search by the domain name for url url_search = re.match(r"https?://(.*)?\..*", bookmark[0]).group(1) title_search = bookmark[1] # Expect a five-tuple containing all bookmark data # db index, URL, title, tags, description expected = [(i + 1,) + tuple(bookmark)] expected[0] += tuple([0]) # search db by tag, url (domain name), and title for keyword in (tag_search, url_search, title_search): with mock.patch("buku.prompt"): # search by keyword results = self.bdb.searchdb([keyword]) self.assertEqual(results, expected) def test_search_by_tag(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) with mock.patch("buku.prompt"): get_first_tag = lambda x: "".join(x[2].split(",")[:2]) for i in range(len(self.bookmarks)): # search for bookmark with a tag that is known to exist results = self.bdb.search_by_tag(get_first_tag(self.bookmarks[i])) # Expect a five-tuple containing all bookmark data # db index, URL, title, tags, description expected = [(i + 1,) + tuple(self.bookmarks[i])] expected[0] += tuple([0]) self.assertEqual(results, expected) @vcr.use_cassette( "tests/vcr_cassettes/test_search_by_multiple_tags_search_any.yaml" ) def test_search_by_multiple_tags_search_any(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) new_bookmark = [ "https://newbookmark.com", "New Bookmark", parse_tags(["test,old,new"]), "additional bookmark to test multiple tag search", 0, ] self.bdb.add_rec(*new_bookmark) with mock.patch("buku.prompt"): # search for bookmarks matching ANY of the supplied tags results = self.bdb.search_by_tag("test, old") # Expect a list of five-element tuples containing all bookmark data # db index, URL, title, tags, description, ordered by records with # the most number of matches. expected = [ ( 4, "https://newbookmark.com", "New Bookmark", parse_tags([",test,old,new,"]), "additional bookmark to test multiple tag search", 0, ), ( 1, "http://slashdot.org", "SLASHDOT", parse_tags([",news,old,"]), "News for old nerds, stuff that doesn't matter", 0, ), ( 3, "http://example.com/", "test", ",es,est,tes,test,", "a case for replace_tag test", 0, ), ] self.assertEqual(results, expected) @vcr.use_cassette( "tests/vcr_cassettes/test_search_by_multiple_tags_search_all.yaml" ) def test_search_by_multiple_tags_search_all(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) new_bookmark = [ "https://newbookmark.com", "New Bookmark", parse_tags(["test,old,new"]), "additional bookmark to test multiple tag search", ] self.bdb.add_rec(*new_bookmark) with mock.patch("buku.prompt"): # search for bookmarks matching ALL of the supplied tags results = self.bdb.search_by_tag("test + old") # Expect a list of five-element tuples containing all bookmark data # db index, URL, title, tags, description expected = [ ( 4, "https://newbookmark.com", "New Bookmark", parse_tags([",test,old,new,"]), "additional bookmark to test multiple tag search", 0, ) ] self.assertEqual(results, expected) def test_search_by_tags_enforces_space_seprations_search_all(self): bookmark1 = [ "https://bookmark1.com", "Bookmark One", parse_tags(["tag, two,tag+two"]), "test case for bookmark with '+' in tag", ] bookmark2 = [ "https://bookmark2.com", "Bookmark Two", parse_tags(["tag,two, tag-two"]), "test case for bookmark with hyphenated tag", ] self.bdb.add_rec(*bookmark1) self.bdb.add_rec(*bookmark2) with mock.patch("buku.prompt"): # check that space separation for ' + ' operator is enforced results = self.bdb.search_by_tag("tag+two") # Expect a list of five-element tuples containing all bookmark data # db index, URL, title, tags, description expected = [ ( 1, "https://bookmark1.com", "Bookmark One", parse_tags([",tag,two,tag+two,"]), "test case for bookmark with '+' in tag", 0, ) ] self.assertEqual(results, expected) results = self.bdb.search_by_tag("tag + two") # Expect a list of five-element tuples containing all bookmark data # db index, URL, title, tags, description expected = [ ( 1, "https://bookmark1.com", "Bookmark One", parse_tags([",tag,two,tag+two,"]), "test case for bookmark with '+' in tag", 0, ), ( 2, "https://bookmark2.com", "Bookmark Two", parse_tags([",tag,two,tag-two,"]), "test case for bookmark with hyphenated tag", 0, ), ] self.assertEqual(results, expected) def test_search_by_tags_exclusion(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) new_bookmark = [ "https://newbookmark.com", "New Bookmark", parse_tags(["test,old,new"]), "additional bookmark to test multiple tag search", ] self.bdb.add_rec(*new_bookmark) with mock.patch("buku.prompt"): # search for bookmarks matching ANY of the supplied tags # while excluding bookmarks from results that match a given tag results = self.bdb.search_by_tag("test, old - est") # Expect a list of five-element tuples containing all bookmark data # db index, URL, title, tags, description expected = [ ( 4, "https://newbookmark.com", "New Bookmark", parse_tags([",test,old,new,"]), "additional bookmark to test multiple tag search", 0, ), ( 1, "http://slashdot.org", "SLASHDOT", parse_tags([",news,old,"]), "News for old nerds, stuff that doesn't matter", 0, ), ] self.assertEqual(results, expected) @vcr.use_cassette( "tests/vcr_cassettes/test_search_by_tags_enforces_space_seprations_exclusion.yaml" ) def test_search_by_tags_enforces_space_seprations_exclusion(self): bookmark1 = [ "https://bookmark1.com", "Bookmark One", parse_tags(["tag, two,tag+two"]), "test case for bookmark with '+' in tag", ] bookmark2 = [ "https://bookmark2.com", "Bookmark Two", parse_tags(["tag,two, tag-two"]), "test case for bookmark with hyphenated tag", ] bookmark3 = [ "https://bookmark3.com", "Bookmark Three", parse_tags(["tag, tag three"]), "second test case for bookmark with hyphenated tag", ] self.bdb.add_rec(*bookmark1) self.bdb.add_rec(*bookmark2) self.bdb.add_rec(*bookmark3) with mock.patch("buku.prompt"): # check that space separation for ' - ' operator is enforced results = self.bdb.search_by_tag("tag-two") # Expect a list of five-element tuples containing all bookmark data # db index, URL, title, tags, description expected = [ ( 2, "https://bookmark2.com", "Bookmark Two", parse_tags([",tag,two,tag-two,"]), "test case for bookmark with hyphenated tag", 0, ), ] self.assertEqual(results, expected) results = self.bdb.search_by_tag("tag - two") # Expect a list of five-element tuples containing all bookmark data # db index, URL, title, tags, description expected = [ ( 3, "https://bookmark3.com", "Bookmark Three", parse_tags([",tag,tag three,"]), "second test case for bookmark with hyphenated tag", 0, ), ] self.assertEqual(results, expected) def test_search_and_open_in_broswer_by_range(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) # simulate user input, select range of indices 1-3 index_range = "1-%s" % len(self.bookmarks) with mock.patch("builtins.input", side_effect=[index_range]): with mock.patch("buku.browse") as mock_browse: try: # search the db with keywords from each bookmark # searching using the first tag from bookmarks get_first_tag = lambda x: x[2].split(",")[1] results = self.bdb.searchdb( [get_first_tag(bm) for bm in self.bookmarks] ) prompt(self.bdb, results) except StopIteration: # catch exception thrown by reaching the end of the side effect iterable pass # collect arguments passed to browse arg_list = [args[0] for args, _ in mock_browse.call_args_list] # expect a list of one-tuples that are bookmark URLs expected = [x[0] for x in self.bookmarks] # checking if browse called with expected arguments self.assertEqual(arg_list, expected) @vcr.use_cassette("tests/vcr_cassettes/test_search_and_open_all_in_browser.yaml") def test_search_and_open_all_in_browser(self): # adding bookmarks for bookmark in self.bookmarks: self.bdb.add_rec(*bookmark) # simulate user input, select 'a' to open all bookmarks in results with mock.patch("builtins.input", side_effect=["a"]): with mock.patch("buku.browse") as mock_browse: try: # search the db with keywords from each bookmark # searching using the first tag from bookmarks get_first_tag = lambda x: x[2].split(",")[1] results = self.bdb.searchdb( [get_first_tag(bm) for bm in self.bookmarks[:2]] ) prompt(self.bdb, results) except StopIteration: # catch exception thrown by reaching the end of the side effect iterable pass # collect arguments passed to browse arg_list = [args[0] for args, _ in mock_browse.call_args_list] # expect a list of one-tuples that are bookmark URLs expected = [x[0] for x in self.bookmarks][:2] # checking if browse called with expected arguments self.assertEqual(arg_list, expected) def test_delete_rec(self): # adding bookmark and getting index self.bdb.add_rec(*self.bookmarks[0]) index = self.bdb.get_rec_id(self.bookmarks[0][0]) # deleting bookmark self.bdb.delete_rec(index) # asserting it doesn't exist from_db = self.bdb.get_rec_by_id(index) self.assertIsNone(from_db) def test_delete_rec_yes(self): # checking that "y" response causes delete_rec to return True with mock.patch("builtins.input", return_value="y"): self.assertTrue(self.bdb.delete_rec(0)) def test_delete_rec_no(self): # checking that non-"y" response causes delete_rec to return None with mock.patch("builtins.input", return_value="n"): self.assertFalse(self.bdb.delete_rec(0)) def test_cleardb(self): # adding bookmarks self.bdb.add_rec(*self.bookmarks[0]) # deleting all bookmarks with mock.patch("builtins.input", return_value="y"): self.bdb.cleardb() # assert table has been dropped assert self.bdb.get_rec_by_id(0) is None def test_replace_tag(self): indices = [] for bookmark in self.bookmarks: # adding bookmark, getting index self.bdb.add_rec(*bookmark) index = self.bdb.get_rec_id(bookmark[0]) indices += [index] # replacing tags with mock.patch("builtins.input", return_value="y"): self.bdb.replace_tag("news", ["__01"]) with mock.patch("builtins.input", return_value="y"): self.bdb.replace_tag("zażółć", ["__02,__03"]) # replacing tag which is also a substring of other tag with mock.patch("builtins.input", return_value="y"): self.bdb.replace_tag("es", ["__04"]) # removing tags with mock.patch("builtins.input", return_value="y"): self.bdb.replace_tag("gęślą") with mock.patch("builtins.input", return_value="y"): self.bdb.replace_tag("old") # removing non-existent tag with mock.patch("builtins.input", return_value="y"): self.bdb.replace_tag("_") # removing nonexistent tag which is also a substring of other tag with mock.patch("builtins.input", return_value="y"): self.bdb.replace_tag("e") for url, title, _, _ in self.bookmarks: # retrieving from db index = self.bdb.get_rec_id(url) from_db = self.bdb.get_rec_by_id(index) # asserting tags were replaced if title == "SLASHDOT": self.assertEqual(from_db[3], parse_tags(["__01"])) elif title == "ZAŻÓŁĆ": self.assertEqual(from_db[3], parse_tags(["__02,__03,jaźń"])) elif title == "test": self.assertEqual(from_db[3], parse_tags(["test,tes,est,__04"])) def test_tnyfy_url(self): # shorten a well-known url shorturl = self.bdb.tnyfy_url(url="https://www.google.com", shorten=True) self.assertEqual(shorturl, "http://tny.im/yt") # expand a well-known short url url = self.bdb.tnyfy_url(url="http://tny.im/yt", shorten=False) self.assertEqual(url, "https://www.google.com") # def test_browse_by_index(self): # self.fail() def test_close_quit(self): # quitting with no args try: self.bdb.close_quit() except SystemExit as err: self.assertEqual(err.args[0], 0) # quitting with custom arg try: self.bdb.close_quit(1) except SystemExit as err: self.assertEqual(err.args[0], 1) # def test_import_bookmark(self): # self.fail() @pytest.fixture(scope="function") def refreshdb_fixture(): # Setup os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH # start every test from a clean state if exists(TEST_TEMP_DBFILE_PATH): os.remove(TEST_TEMP_DBFILE_PATH) bdb = BukuDb() yield bdb # Teardown os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH @pytest.mark.parametrize( "title_in, exp_res", [ ["?", "Example Domain"], [None, "Example Domain"], ["", "Example Domain"], ["random title", "Example Domain"], ], ) def test_refreshdb(refreshdb_fixture, title_in, exp_res): bdb = refreshdb_fixture args = ["http://example.com"] if title_in: args.append(title_in) bdb.add_rec(*args) bdb.refreshdb(1, 1) from_db = bdb.get_rec_by_id(1) assert from_db[2] == exp_res, "from_db: {}".format(from_db) @pytest.fixture def test_print_caplog(caplog): caplog.handler.records.clear() caplog.records.clear() yield caplog @pytest.mark.parametrize( "kwargs, rec, exp_res", [ [{}, TEST_PRINT_REC, (True, [])], [{"is_range": True}, TEST_PRINT_REC, (True, [])], [{"index": 0}, TEST_PRINT_REC, (True, [])], [{"index": -1}, TEST_PRINT_REC, (True, [])], [{"index": -2}, TEST_PRINT_REC, (True, [])], [{"index": 2}, TEST_PRINT_REC, (False, [("root", 40, "No matching index 2")])], [{"low": -1, "high": -1}, TEST_PRINT_REC, (True, [])], [ {"low": -1, "high": -1, "is_range": True}, TEST_PRINT_REC, (False, [("root", 40, "Negative range boundary")]), ], [{"low": 0, "high": 0, "is_range": True}, TEST_PRINT_REC, (True, [])], [{"low": 0, "high": 1, "is_range": True}, TEST_PRINT_REC, (True, [])], [{"low": 0, "high": 2, "is_range": True}, TEST_PRINT_REC, (True, [])], [{"low": 2, "high": 2, "is_range": True}, TEST_PRINT_REC, (True, [])], [{"low": 2, "high": 3, "is_range": True}, TEST_PRINT_REC, (True, [])], # empty database [{"is_range": True}, None, (True, [])], [{"index": 0}, None, (True, [("root", 40, "0 records")])], [{"index": -1}, None, (False, [("root", 40, "Empty database")])], [{"index": 1}, None, (False, [("root", 40, "No matching index 1")])], [{"low": -1, "high": -1}, TEST_PRINT_REC, (True, [])], [ {"low": -1, "high": -1, "is_range": True}, None, (False, [("root", 40, "Negative range boundary")]), ], [{"low": 0, "high": 0, "is_range": True}, None, (True, [])], [{"low": 0, "high": 1, "is_range": True}, None, (True, [])], [{"low": 0, "high": 2, "is_range": True}, None, (True, [])], [{"low": 2, "high": 2, "is_range": True}, None, (True, [])], [{"low": 2, "high": 3, "is_range": True}, None, (True, [])], ], ) def test_print_rec(setup, kwargs, rec, exp_res, tmp_path, caplog): bdb = BukuDb(dbfile=tmp_path / "tmp.db") if rec: bdb.add_rec(*rec) # run the function assert (bdb.print_rec(**kwargs), caplog.record_tuples) == exp_res def test_list_tags(capsys, setup): bdb = BukuDb() # adding bookmarks bdb.add_rec("http://one.com", "", parse_tags(["cat,ant,bee,1"]), "") bdb.add_rec("http://two.com", "", parse_tags(["Cat,Ant,bee,1"]), "") bdb.add_rec("http://three.com", "", parse_tags(["Cat,Ant,3,Bee,2"]), "") # listing tags, asserting output out, err = capsys.readouterr() prompt(bdb, None, True, listtags=True) out, err = capsys.readouterr() exp_out = " 1. 1 (2)\n 2. 2 (1)\n 3. 3 (1)\n 4. ant (3)\n 5. bee (3)\n 6. cat (3)\n\n" assert out == exp_out assert err == "" def test_compactdb(setup): bdb = BukuDb() # adding bookmarks for bookmark in TEST_BOOKMARKS: bdb.add_rec(*bookmark) # manually deleting 2nd index from db, calling compactdb bdb.cur.execute("DELETE FROM bookmarks WHERE id = ?", (2,)) bdb.compactdb(2) # asserting bookmarks have correct indices assert bdb.get_rec_by_id(1) == ( 1, "http://slashdot.org", "SLASHDOT", ",news,old,", "News for old nerds, stuff that doesn't matter", 0, ) assert bdb.get_rec_by_id(2) == ( 2, "http://example.com/", "test", ",es,est,tes,test,", "a case for replace_tag test", 0, ) assert bdb.get_rec_by_id(3) is None @pytest.mark.vcr() @pytest.mark.parametrize( "low, high, delay_commit, input_retval, exp_res", [ # delay_commit, y input_retval [0, 0, True, "y", (True, [])], # delay_commit, non-y input_retval [ 0, 0, True, "x", ( False, [tuple([x] + y + [0]) for x, y in zip(range(1, 4), TEST_BOOKMARKS)], ), ], # non delay_commit, y input_retval [0, 0, False, "y", (True, [])], # non delay_commit, non-y input_retval [ 0, 0, False, "x", ( False, [tuple([x] + y + [0]) for x, y in zip(range(1, 4), TEST_BOOKMARKS)], ), ], ], ) def test_delete_rec_range_and_delay_commit( setup, tmp_path, low, high, delay_commit, input_retval, exp_res ): """test delete rec, range and delay commit.""" bdb = BukuDb(dbfile=tmp_path / "tmp.db") kwargs = {"is_range": True, "low": low, "high": high, "delay_commit": delay_commit} kwargs["index"] = 0 # Fill bookmark for bookmark in TEST_BOOKMARKS: bdb.add_rec(*bookmark) with mock.patch("builtins.input", return_value=input_retval): res = bdb.delete_rec(**kwargs) assert (res, bdb.get_rec_all()) == exp_res # teardown os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH @pytest.mark.parametrize( "index, delay_commit, input_retval", [ [-1, False, False], [0, False, False], [1, False, True], [1, False, False], [1, True, True], [1, True, False], [100, False, True], ], ) def test_delete_rec_index_and_delay_commit(index, delay_commit, input_retval): """test delete rec, index and delay commit.""" bdb = BukuDb() bdb_dc = BukuDb() # instance for delay_commit check. # Fill bookmark for bookmark in TEST_BOOKMARKS: bdb.add_rec(*bookmark) db_len = len(TEST_BOOKMARKS) n_index = index with mock.patch("builtins.input", return_value=input_retval): res = bdb.delete_rec(index=index, delay_commit=delay_commit) if n_index < 0: assert not res elif n_index > db_len: assert not res assert len(bdb.get_rec_all()) == db_len elif index == 0 and input_retval != "y": assert not res assert len(bdb.get_rec_all()) == db_len else: assert res assert len(bdb.get_rec_all()) == db_len - 1 if delay_commit: assert len(bdb_dc.get_rec_all()) == db_len else: assert len(bdb_dc.get_rec_all()) == db_len - 1 # teardown os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH @pytest.mark.parametrize( "index, is_range, low, high", [ # range on non zero index (0, True, 1, 1), # range on zero index (0, True, 0, 0), # zero index only (0, False, 0, 0), ], ) def test_delete_rec_on_empty_database(setup, index, is_range, low, high): """test delete rec, on empty database.""" bdb = BukuDb() with mock.patch("builtins.input", return_value="y"): res = bdb.delete_rec(index, is_range, low, high) if (is_range and any([low == 0, high == 0])) or (not is_range and index == 0): assert res # teardown os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH return if is_range and low > 1 and high > 1: assert not res # teardown os.environ["XDG_DATA_HOME"] = TEST_TEMP_DIR_PATH @pytest.mark.parametrize( "kwargs, exp_res, raise_error", [ [dict(index="a", low="a", high=1, is_range=True), None, True], [dict(index="a", low="a", high=1, is_range=False), None, True], [dict(index="a", low=1, high="a", is_range=True), None, True], [dict(index="a", is_range=False), None, True], [dict(index="a", is_range=True), None, True], ], ) def test_delete_rec_on_non_integer( setup, tmp_path, monkeypatch, kwargs, exp_res, raise_error ): """test delete rec on non integer arg.""" bdb = BukuDb(dbfile=tmp_path / "tmp.db") for bookmark in TEST_BOOKMARKS: bdb.add_rec(*bookmark) def mockreturn(): return "y" exp_res = None res = None monkeypatch.setattr(buku, "read_in", mockreturn) if raise_error: with pytest.raises(TypeError): res = bdb.delete_rec(**kwargs) else: res = bdb.delete_rec(**kwargs) assert res == exp_res @pytest.mark.parametrize("url", ["", False, None, 0]) def test_add_rec_add_invalid_url(caplog, url): """test method.""" bdb = BukuDb() res = bdb.add_rec(url=url) assert res == -1 caplog.records[0].levelname == "ERROR" caplog.records[0].getMessage() == "Invalid URL" @pytest.mark.parametrize( "kwargs, exp_arg", [ [{"url": "example.com"}, ("example.com", "Example Domain", ",", "", 0)], [ {"url": "http://example.com"}, ("http://example.com", "Example Domain", ",", "", 0), ], [ {"url": "http://example.com", "immutable": 1}, ("http://example.com", "Example Domain", ",", "", 1), ], [ {"url": "http://example.com", "desc": "randomdesc"}, ("http://example.com", "Example Domain", ",", "randomdesc", 0), ], [ {"url": "http://example.com", "title_in": "randomtitle"}, ("http://example.com", "randomtitle", ",", "", 0), ], [ {"url": "http://example.com", "tags_in": "tag1"}, ("http://example.com", "Example Domain", ",tag1,", "", 0), ], [ {"url": "http://example.com", "tags_in": ",tag1"}, ("http://example.com", "Example Domain", ",tag1,", "", 0), ], [ {"url": "http://example.com", "tags_in": ",tag1,"}, ("http://example.com", "Example Domain", ",tag1,", "", 0), ], ], ) def test_add_rec_exec_arg(kwargs, exp_arg): """test func.""" bdb = BukuDb() bdb.cur = mock.Mock() bdb.get_rec_id = mock.Mock(return_value=-1) bdb.add_rec(**kwargs) assert bdb.cur.execute.call_args[0][1] == exp_arg def test_update_rec_index_0(caplog): """test method.""" bdb = BukuDb() res = bdb.update_rec(index=0, url="http://example.com") assert not res assert caplog.records[0].getMessage() == "All URLs cannot be same" assert caplog.records[0].levelname == "ERROR" def test_update_rec_only_index(): """test method.""" bdb = BukuDb() res = bdb.update_rec(index=1) assert res @pytest.mark.parametrize("url", [None, ""]) def test_update_rec_invalid_url(url): """test method.""" bdb = BukuDb() res = bdb.update_rec(index=1, url=url) assert res @pytest.mark.parametrize("invalid_tag", ["+,", "-,"]) def test_update_rec_invalid_tag(caplog, invalid_tag): """test method.""" url = "http://example.com" bdb = BukuDb() res = bdb.update_rec(index=1, url=url, tags_in=invalid_tag) assert not res try: assert caplog.records[0].getMessage() == "Please specify a tag" assert caplog.records[0].levelname == "ERROR" except IndexError as e: if (sys.version_info.major, sys.version_info.minor) == (3, 4): print("caplog records: {}".format(caplog.records)) for idx, record in enumerate(caplog.records): print( "idx:{};{};message:{};levelname:{}".format( idx, record, record.getMessage(), record.levelname ) ) else: raise e @pytest.mark.parametrize("read_in_retval", ["y", "n", ""]) def test_update_rec_update_all_bookmark(caplog, read_in_retval): """test method.""" caplog.set_level(logging.DEBUG) with mock.patch("buku.read_in", return_value=read_in_retval): bdb = BukuDb() res = bdb.update_rec(index=0, tags_in="tags1") assert res if read_in_retval == "y" else not res if read_in_retval == "y": assert ( caplog.records[0].getMessage() == "update_rec query: " "\"UPDATE bookmarks SET tags = ?\", args: [',tags1,']" ) else: assert not caplog.records @pytest.mark.parametrize( "get_system_editor_retval, index, exp_res", [ ["none", 0, False], ["nano", -2, False], ], ) def test_edit_update_rec_with_invalid_input(get_system_editor_retval, index, exp_res): """test method.""" with mock.patch("buku.get_system_editor", return_value=get_system_editor_retval): bdb = buku.BukuDb() res = bdb.edit_update_rec(index=index) assert res == exp_res @vcr.use_cassette("tests/vcr_cassettes/test_browse_by_index.yaml") @given( low=st.integers(min_value=-2, max_value=3), high=st.integers(min_value=-2, max_value=3), index=st.integers(min_value=-2, max_value=3), is_range=st.booleans(), empty_database=st.booleans(), ) @example(low=0, high=0, index=0, is_range=False, empty_database=True) @settings(max_examples=2, deadline=None) def test_browse_by_index(low, high, index, is_range, empty_database): """test method.""" n_low, n_high = (high, low) if low > high else (low, high) with mock.patch("buku.browse"): bdb = buku.BukuDb() bdb.delete_rec_all() db_len = 0 if not empty_database: bdb.add_rec("https://www.google.com/ncr", "?") db_len += 1 res = bdb.browse_by_index(index=index, low=low, high=high, is_range=is_range) if is_range and (low < 0 or high < 0): assert not res elif is_range and n_low > 0 and n_high > 0: assert res elif is_range: assert not res elif not is_range and index < 0: assert not res elif not is_range and index > db_len: assert not res elif not is_range and index >= 0 and empty_database: assert not res elif not is_range and 0 <= index <= db_len and not empty_database: assert res else: raise ValueError bdb.delete_rec_all() @pytest.fixture() def chrome_db(): # compatibility dir_path = os.path.dirname(os.path.realpath(__file__)) res_yaml_file = os.path.join(dir_path, "test_bukuDb", "25491522_res.yaml") res_nopt_yaml_file = os.path.join(dir_path, "test_bukuDb", "25491522_res_nopt.yaml") json_file = os.path.join(dir_path, "test_bukuDb", "Bookmarks") return json_file, res_yaml_file, res_nopt_yaml_file @pytest.mark.parametrize("add_pt", [True, False]) def test_load_chrome_database(chrome_db, add_pt): """test method.""" # compatibility json_file = chrome_db[0] res_yaml_file = chrome_db[1] if add_pt else chrome_db[2] dump_data = False # NOTE: change this value to dump data if not dump_data: with open(res_yaml_file, "r") as f: try: res_yaml = yaml.load(f, Loader=yaml.FullLoader) except RuntimeError: res_yaml = yaml.load(f, Loader=PrettySafeLoader) # init bdb = buku.BukuDb() bdb.add_rec = mock.Mock() bdb.load_chrome_database(json_file, None, add_pt) call_args_list_dict = dict(bdb.add_rec.call_args_list) # test if not dump_data: assert call_args_list_dict == res_yaml # dump data for new test if dump_data: with open(res_yaml_file, "w") as f: yaml.dump(call_args_list_dict, f) print("call args list dict dumped to:{}".format(res_yaml_file)) @pytest.fixture() def firefox_db(tmpdir): zip_url = "https://github.com/jarun/buku/files/1319933/bookmarks.zip" dir_path = os.path.dirname(os.path.realpath(__file__)) res_yaml_file = os.path.join(dir_path, "test_bukuDb", "firefox_res.yaml") res_nopt_yaml_file = os.path.join(dir_path, "test_bukuDb", "firefox_res_nopt.yaml") ff_db_path = os.path.join(dir_path, "test_bukuDb", "places.sqlite") if not os.path.isfile(ff_db_path): tmp_zip = tmpdir.join("bookmarks.zip") with urllib.request.urlopen(zip_url) as response, open( tmp_zip.strpath, "wb" ) as out_file: shutil.copyfileobj(response, out_file) zip_obj = zipfile.ZipFile(tmp_zip.strpath) zip_obj.extractall(path=os.path.join(dir_path, "test_bukuDb")) return ff_db_path, res_yaml_file, res_nopt_yaml_file @pytest.mark.parametrize("add_pt", [True, False]) def test_load_firefox_database(firefox_db, add_pt): # compatibility ff_db_path = firefox_db[0] dump_data = False # NOTE: change this value to dump data res_yaml_file = firefox_db[1] if add_pt else firefox_db[2] if not dump_data: with open(res_yaml_file, "r") as f: try: res_yaml = yaml.load(f) except RuntimeError: res_yaml = yaml.load(f, Loader=PrettySafeLoader) # init bdb = buku.BukuDb() bdb.add_rec = mock.Mock() bdb.load_firefox_database(ff_db_path, None, add_pt) call_args_list_dict = dict(bdb.add_rec.call_args_list) # test if not dump_data: assert call_args_list_dict == res_yaml if dump_data: with open(res_yaml_file, "w") as f: yaml.dump(call_args_list_dict, f) print("call args list dict dumped to:{}".format(res_yaml_file)) @pytest.mark.parametrize( "keyword_results, stag_results, exp_res", [ ([], [], []), (["item1"], ["item1", "item2"], ["item1"]), (["item2"], ["item1"], []), ], ) def test_search_keywords_and_filter_by_tags(keyword_results, stag_results, exp_res): """test method.""" # init bdb = buku.BukuDb() bdb.searchdb = mock.Mock(return_value=keyword_results) bdb.search_by_tag = mock.Mock(return_value=stag_results) # test res = bdb.search_keywords_and_filter_by_tags( mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), [] ) assert exp_res == res @pytest.mark.parametrize( "search_results, exclude_results, exp_res", [ ([], [], []), (["item1", "item2"], ["item2"], ["item1"]), (["item2"], ["item1"], ["item2"]), (["item1", "item2"], ["item1", "item2"], []), ], ) def test_exclude_results_from_search(search_results, exclude_results, exp_res): """test method.""" # init bdb = buku.BukuDb() bdb.searchdb = mock.Mock(return_value=exclude_results) # test res = bdb.exclude_results_from_search(search_results, [], True) assert exp_res == res def test_exportdb_empty_db(): with NamedTemporaryFile(delete=False) as f: db = BukuDb(dbfile=f.name) with NamedTemporaryFile(delete=False) as f2: res = db.exportdb(f2.name) assert not res def test_exportdb_single_rec(tmpdir): with NamedTemporaryFile(delete=False) as f: db = BukuDb(dbfile=f.name) db.add_rec("http://example.com") exp_file = tmpdir.join("export") db.exportdb(exp_file.strpath) with open(exp_file.strpath) as f: assert f.read() def test_exportdb_to_db(): with NamedTemporaryFile(delete=False) as f1, NamedTemporaryFile( delete=False, suffix=".db" ) as f2: db = BukuDb(dbfile=f1.name) db.add_rec("http://example.com") db.add_rec("http://google.com") with mock.patch("builtins.input", return_value="y"): db.exportdb(f2.name) db2 = BukuDb(dbfile=f2.name) assert db.get_rec_all() == db2.get_rec_all() @pytest.mark.parametrize( "urls, exp_res", [ [[], -1], [["http://example.com"], 1], [["htttp://example.com", "http://google.com"], 2], ], ) def test_get_max_id(urls, exp_res): with NamedTemporaryFile(delete=False) as f: db = BukuDb(dbfile=f.name) if urls: list(map(lambda x: db.add_rec(x), urls)) assert db.get_max_id() == exp_res # Helper functions for testcases def split_and_test_membership(a, b): # :param a, b: comma separated strings to split # test everything in a in b return all(x in b.split(",") for x in a.split(",")) def inclusive_range(start, end): return list(range(start, end + 1)) def normalize_range(db_len, low, high): """normalize index and range. Args: db_len (int): database length. low (int): low limit. high (int): high limit. Returns: Tuple contain following normalized variables (low, high) """ require_comparison = True # don't deal with non instance of the variable. if not isinstance(low, int): n_low = low require_comparison = False if not isinstance(high, int): n_high = high require_comparison = False max_value = db_len if low == "max" and high == "max": n_low = db_len n_high = max_value elif low == "max" and high != "max": n_low = high n_high = max_value elif low != "max" and high == "max": n_low = low n_high = max_value else: n_low = low n_high = high if require_comparison: if n_high < n_low: n_high, n_low = n_low, n_high return (n_low, n_high) if __name__ == "__main__": unittest.main()