diff --git a/pdm.lock b/pdm.lock index 584947f..88c6268 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,18 @@ groups = ["default", "dev"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:e3242526a92487809d1d89d3adc581ebb83c7afc795afac1c3a2c2e57d39f9ac" +content_hash = "sha256:9374c429f1bd5c105306ce55a4d481335bf6ee4aa65613058a47c49799bb0444" + +[[package]] +name = "attrs" +version = "23.2.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] [[package]] name = "black" @@ -66,6 +77,41 @@ files = [ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +marker = "os_name == \"nt\" and implementation_name != \"pypy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -262,6 +308,17 @@ files = [ {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"}, ] +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "httplib2" version = "0.22.0" @@ -411,6 +468,20 @@ files = [ {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, ] +[[package]] +name = "outcome" +version = "1.3.0.post0" +requires_python = ">=3.7" +summary = "Capture the outcome of Python function calls." +groups = ["default"] +dependencies = [ + "attrs>=19.2.0", +] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + [[package]] name = "packaging" version = "23.2" @@ -555,6 +626,18 @@ files = [ {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default"] +marker = "os_name == \"nt\" and implementation_name != \"pypy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pyflakes" version = "3.2.0" @@ -578,6 +661,17 @@ files = [ {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] +[[package]] +name = "pysocks" +version = "1.7.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +groups = ["default"] +files = [ + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "8.2.0" @@ -667,6 +761,79 @@ files = [ {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, ] +[[package]] +name = "selenium" +version = "4.20.0" +requires_python = ">=3.8" +summary = "" +groups = ["default"] +dependencies = [ + "certifi>=2021.10.8", + "trio-websocket~=0.9", + "trio~=0.17", + "typing-extensions>=4.9.0", + "urllib3[socks]<3,>=1.26", +] +files = [ + {file = "selenium-4.20.0-py3-none-any.whl", hash = "sha256:b1d0c33b38ca27d0499183e48e1dd09ff26973481f5d3ef2983073813ae6588d"}, + {file = "selenium-4.20.0.tar.gz", hash = "sha256:0bd564ee166980d419a8aaf4ac00289bc152afcf2eadca5efe8c8e36711853fd"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +groups = ["default"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "trio" +version = "0.25.0" +requires_python = ">=3.8" +summary = "A friendly Python library for async concurrency and I/O" +groups = ["default"] +dependencies = [ + "attrs>=23.2.0", + "cffi>=1.14; os_name == \"nt\" and implementation_name != \"pypy\"", + "idna", + "outcome", + "sniffio>=1.3.0", + "sortedcontainers", +] +files = [ + {file = "trio-0.25.0-py3-none-any.whl", hash = "sha256:e6458efe29cc543e557a91e614e2b51710eba2961669329ce9c862d50c6e8e81"}, + {file = "trio-0.25.0.tar.gz", hash = "sha256:9b41f5993ad2c0e5f62d0acca320ec657fdb6b2a2c22b8c7aed6caf154475c4e"}, +] + +[[package]] +name = "trio-websocket" +version = "0.11.1" +requires_python = ">=3.7" +summary = "WebSocket library for Trio" +groups = ["default"] +dependencies = [ + "trio>=0.11", + "wsproto>=0.14", +] +files = [ + {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, + {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, +] + [[package]] name = "types-psycopg2" version = "2.9.21.20240417" @@ -683,7 +850,7 @@ name = "typing-extensions" version = "4.9.0" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["dev"] +groups = ["default", "dev"] files = [ {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, @@ -711,6 +878,22 @@ files = [ {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] +[[package]] +name = "urllib3" +version = "2.2.1" +extras = ["socks"] +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default"] +dependencies = [ + "pysocks!=1.5.7,<2.0,>=1.5.6", + "urllib3==2.2.1", +] +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + [[package]] name = "werkzeug" version = "3.0.2" @@ -724,3 +907,17 @@ files = [ {file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"}, {file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"}, ] + +[[package]] +name = "wsproto" +version = "1.2.0" +requires_python = ">=3.7.0" +summary = "WebSockets state-machine based protocol implementation" +groups = ["default"] +dependencies = [ + "h11<1,>=0.9.0", +] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] diff --git a/pyproject.toml b/pyproject.toml index 0a84296..7a09680 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "google-auth>=2.29.0", "google-auth-oauthlib>=1.2.0", "google-api-python-client>=2.127.0", + "selenium>=4.20.0", ] requires-python = ">=3.11" @@ -34,3 +35,8 @@ dev = [ "black>=24.1.1", "types-psycopg2>=2.9.21.20240417", ] + +[tool.pytest.ini_options] +pythonpath = [ + "src" +] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_crawl_tasks.py b/tests/test_crawl_tasks.py new file mode 100644 index 0000000..cda4e76 --- /dev/null +++ b/tests/test_crawl_tasks.py @@ -0,0 +1,97 @@ +import pytest +import os +from src import * +from src.open_source_python_template.crawlTasks import get_tasks +from unittest.mock import patch, mock_open, MagicMock, PropertyMock +from google.oauth2.credentials import Credentials +from google.auth.transport.requests import Request +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +# Sample data for mocking responses +MOCK_TASKS = { + "items": [ + {"title": "Task 1", "id": "1", "tasklist_id": "tl1", "tasklist_title": "TaskList 1"} + ] +} + +MOCK_TASKLISTS = { + "items": [ + {"title": "TaskList 1", "id": "tl1"} + ] +} + +def test_get_tasks_existing_token_valid(): + with patch('os.path.exists', return_value=True), \ + patch('google.oauth2.credentials.Credentials.from_authorized_user_file') as mock_cred, \ + patch('googleapiclient.discovery.build') as mock_build: + + mock_cred.return_value.valid = True + mock_cred.return_value.universe_domain = 'googleapis.com' + + # Mock Google API client setup + mock_service = MagicMock() + mock_build.return_value = mock_service + mock_cred.return_value.valid = True + + # Setup Google API client mock + mock_service = MagicMock() + mock_build.return_value = mock_service + mock_service.tasklists().list().execute.return_value = MOCK_TASKLISTS + mock_service.tasks().list().execute.return_value = MOCK_TASKS + + # Execute function + results = get_tasks() + + # Assert results + assert len(results) == 1 + assert results[0]['title'] == "Task 1" + +def test_get_tasks_existing_token_expired(): + with patch('os.path.exists', return_value=True), \ + patch('google.oauth2.credentials.Credentials.from_authorized_user_file') as mock_cred, \ + patch('google.auth.transport.requests.Request'), \ + patch('googleapiclient.discovery.build') as mock_build, \ + patch('builtins.open', mock_open()): + + # Setup mock credentials + mock_cred.return_value.valid = False + mock_cred.return_value.expired = True + mock_cred.return_value.refresh_token = True + + # Refresh method does nothing but update valid state + def refresh(request): + mock_cred.return_value.valid = True + + mock_cred.return_value.refresh = refresh + + # Setup Google API client mock + mock_service = mock_build.return_value + mock_tasklists = mock_service.tasklists().list().execute.return_value = MOCK_TASKLISTS + mock_tasks = mock_service.tasks().list().execute.return_value = MOCK_TASKS + + # Execute function + results = get_tasks() + + # Assert results + assert len(results) == 1 + assert results[0]['title'] == "Task 1" + +def test_get_tasks_no_tasklists(): + with patch('os.path.exists', return_value=True), \ + patch('google.oauth2.credentials.Credentials.from_authorized_user_file') as mock_cred, \ + patch('googleapiclient.discovery.build') as mock_build: + + # Setup mock credentials + mock_cred.return_value.valid = True + + # Setup Google API client mock + mock_service = mock_build.return_value + mock_service.tasklists().list().execute.return_value = {"items": []} + + # Execute function + results = get_tasks() + + # Assert results + assert results == [] \ No newline at end of file diff --git a/tests/test_index.py b/tests/test_index.py new file mode 100644 index 0000000..a596c74 --- /dev/null +++ b/tests/test_index.py @@ -0,0 +1,64 @@ +import pytest +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +@pytest.fixture(scope="class") +def browser(): + # Setup Selenium + service = Service(executable_path='/opt/homebrew/bin/chromedriver') + options = Options() + options.add_argument('--headless') + driver = webdriver.Chrome(service=service, options=options) + driver.maximize_window() + driver.implicitly_wait(10) + yield driver + driver.quit() + +class TestIndex: + # Setup browser + @pytest.fixture(autouse=True) + def setup(self, browser): + self.browser = browser + self.browser.get("http://127.0.0.1:5000") + + # Check if page title is correctly displayed + def test_page_title(self): + expected_title = "Tasks Dashboard" + assert self.browser.title == expected_title, f"Expected page title to be '{expected_title}' but got '{self.browser.title}'" + + # Check if username is correctly displayed + def test_username_displayed(self): + username_span = self.browser.find_element(By.CSS_SELECTOR, "span.username-placeholder") + expected_username = "username" + assert username_span.text == expected_username + + # Check if search bar is correctly displayed + def test_search_bar_displayed(self): + search_bar = self.browser.find_element(By.ID, "search-tasklists") + assert search_bar.is_displayed() + + # Check if task lists are correctly displayed + def test_task_categories_displayed(self): + task_categories = self.browser.find_element(By.ID, 'task-categories') + assert task_categories.is_displayed() + + # Check if task itms are correctly displayed + def test_task_item_displayed(self): + task_item = self.browser.find_element(By.CSS_SELECTOR, ".task-item") + assert task_item.is_displayed() + + # Check if dropdown is correctly displayed + def test_dropdown_displayed(self): + dropdown_button = self.browser.find_element(By.CSS_SELECTOR, ".dropdown-toggle") + assert dropdown_button.is_displayed(), "Dropdown button is not displayed" + + # Check if checkbox correctly displayed and unselected + def test_task_checkbox_displayed(self): + checkbox = self.browser.find_element(By.ID, "task1") + assert checkbox.is_displayed() + assert not checkbox.is_selected() \ No newline at end of file