"""Test Conditions module."""
-from unittest import TestCase
-from tests.utils import TestCaseWithDB, TestCaseWithServer
+from typing import Any
+from tests.utils import (TestCaseSansDB, TestCaseWithDB, TestCaseWithServer,
+ Expected)
from plomtask.conditions import Condition
-from plomtask.processes import Process
-from plomtask.todos import Todo
-from plomtask.exceptions import NotFoundException, HandledException
-class TestsSansDB(TestCase):
+class TestsSansDB(TestCaseSansDB):
"""Tests requiring no DB setup."""
-
- def test_Condition_id_setting(self) -> None:
- """Test .id_ being set and its legal range being enforced."""
- with self.assertRaises(HandledException):
- Condition(0)
- condition = Condition(5)
- self.assertEqual(condition.id_, 5)
+ checked_class = Condition
class TestsWithDB(TestCaseWithDB):
"""Tests requiring DB, but not server setup."""
+ checked_class = Condition
+ default_init_kwargs = {'is_active': 0}
+
+
+class ExpectedGetConditions(Expected):
+ """Builder of expectations for GET /conditions."""
+ _default_dict = {'sort_by': 'title', 'pattern': ''}
+
+ def recalc(self) -> None:
+ """Update internal dictionary by subclass-specific rules."""
+ super().recalc()
+ self._fields['conditions'] = self.as_ids(self.lib_all('Condition'))
- def check_storage(self, content: list[Condition]) -> None:
- """Test cache and DB equal content."""
- expected_cache = {}
- for item in content:
- expected_cache[item.id_] = item
- self.assertEqual(Condition.get_cache(), expected_cache)
- db_found: list[Condition] = []
- for item in content:
- assert isinstance(item.id_, int)
- for row in self.db_conn.row_where(Condition.table_name, 'id',
- item.id_):
- db_found += [Condition.from_table_row(self.db_conn, row)]
- self.assertEqual(sorted(content), sorted(db_found))
-
- def test_Condition_saving_and_caching(self) -> None:
- """Test .save/.save_core."""
- c = Condition(None, False)
- c.title.set('title1')
- c.title.set('title2')
- c.description.set('desc1')
- c.description.set('desc2')
- # check object init itself doesn't store anything yet
- self.check_storage([])
- # check saving stores in cache and DB
- c.save(self.db_conn)
- self.check_storage([c])
- # check attributes set properly (and not unset by saving)
- self.assertEqual(c.id_, 1)
- self.assertEqual(c.is_active, False)
- self.assertEqual(sorted(c.title.history.values()),
- ['title1', 'title2'])
- self.assertEqual(sorted(c.description.history.values()),
- ['desc1', 'desc2'])
-
- def test_Condition_from_table_row(self) -> None:
- """Test .from_table_row() properly reads in class from DB"""
- c = Condition(1, True)
- c.title.set('title1')
- c.title.set('title2')
- c.description.set('desc1')
- c.description.set('desc2')
- c.save(self.db_conn)
- assert isinstance(c.id_, int)
- for row in self.db_conn.row_where(Condition.table_name, 'id', c.id_):
- retrieved = Condition.from_table_row(self.db_conn, row)
- assert isinstance(retrieved, Condition)
- self.assertEqual(c, retrieved)
- self.assertEqual({c.id_: c}, Condition.get_cache())
- # pylint: disable=no-member
- self.assertEqual(sorted(retrieved.title.history.values()),
- ['title1', 'title2'])
- # pylint: disable=no-member
- self.assertEqual(sorted(retrieved.description.history.values()),
- ['desc1', 'desc2'])
-
- def test_Condition_by_id(self) -> None:
- """Test .by_id(), including creation."""
- # check failure if not yet saved
- c = Condition(3, False)
- with self.assertRaises(NotFoundException):
- Condition.by_id(self.db_conn, 3)
- # check identity of saved and retrieved
- c.save(self.db_conn)
- self.assertEqual(c, Condition.by_id(self.db_conn, 3))
- # check create=True acts like normal instantiation (sans saving)
- by_id_created = Condition.by_id(self.db_conn, 4, create=True)
- self.assertEqual(Condition(4), by_id_created)
- self.check_storage([c])
-
- def test_Condition_all(self) -> None:
- """Test .all()."""
- c_1 = Condition(None, False)
- # check pre-save .all() returns empty list
- self.assertEqual(Condition.all(self.db_conn), [])
- # check .save() fills .all() result
- c_1.save(self.db_conn)
- self.assertEqual(Condition.all(self.db_conn), [c_1])
- c_2 = Condition(None, True)
- c_2.save(self.db_conn)
- self.assertEqual(sorted(Condition.all(self.db_conn)),
- sorted([c_1, c_2]))
-
- def test_Condition_singularity(self) -> None:
- """Test pointers made for single object keep pointing to it."""
- c = Condition(None, False)
- c.save(self.db_conn)
- c.is_active = True
- retrieved = Condition.by_id(self.db_conn, 1)
- self.assertEqual(True, retrieved.is_active)
-
- def test_Condition_remove(self) -> None:
- """Test .remove() effects on DB and cache."""
- # check only saved item can be removed
- c = Condition(None, False)
- with self.assertRaises(HandledException):
- c.remove(self.db_conn)
- c.save(self.db_conn)
- c.remove(self.db_conn)
- self.check_storage([])
- # check guard against deleting dependencies of other classes
- proc = Process(None)
- todo = Todo(None, proc, False, '2024-01-01')
- for depender in (proc, todo):
- assert hasattr(depender, 'save')
- assert hasattr(depender, 'set_conditions')
- c.save(self.db_conn)
- depender.save(self.db_conn)
- depender.set_conditions(self.db_conn, [c.id_], 'conditions')
- depender.save(self.db_conn)
- with self.assertRaises(HandledException):
- c.remove(self.db_conn)
- depender.set_conditions(self.db_conn, [], 'conditions')
- depender.save(self.db_conn)
- c.remove(self.db_conn)
+
+class ExpectedGetCondition(Expected):
+ """Builder of expectations for GET /condition."""
+ _default_dict = {'is_new': False}
+ _on_empty_make_temp = ('Condition', 'cond_as_dict')
+
+ def __init__(self, id_: int | None, *args: Any, **kwargs: Any) -> None:
+ self._fields = {'condition': id_}
+ super().__init__(*args, **kwargs)
+
+ def recalc(self) -> None:
+ """Update internal dictionary by subclass-specific rules."""
+ super().recalc()
+ for p_field, c_field in [('conditions', 'enabled_processes'),
+ ('disables', 'disabling_processes'),
+ ('blockers', 'disabled_processes'),
+ ('enables', 'enabling_processes')]:
+ self._fields[c_field] = self.as_ids([
+ p for p in self.lib_all('Process')
+ if self._fields['condition'] in p[p_field]])
class TestsWithServer(TestCaseWithServer):
"""Module tests against our HTTP server/handler (and database)."""
-
- def test_do_POST_condition(self) -> None:
- """Test POST /condition and its effect on the database."""
- form_data = {'title': 'foo', 'description': 'foo'}
- self.check_post(form_data, '/condition', 302, '/condition?id=1')
- self.assertEqual(1, len(Condition.all(self.db_conn)))
- form_data['delete'] = ''
- self.check_post(form_data, '/condition?id=', 404)
- self.check_post(form_data, '/condition?id=2', 404)
- self.check_post(form_data, '/condition?id=1', 302, '/conditions')
- self.assertEqual(0, len(Condition.all(self.db_conn)))
-
- def test_do_GET(self) -> None:
- """Test /condition and /conditions response codes."""
- form_data = {'title': 'foo', 'description': 'foo'}
- self.check_post(form_data, '/condition', 302, '/condition?id=1')
+ checked_class = Condition
+
+ def test_fail_POST_condition(self) -> None:
+ """Test malformed/illegal POST /condition requests."""
+ # check incomplete POST payloads
+ valid_payload = {'title': '', 'description': ''}
+ self.check_minimal_inputs('/condition', valid_payload)
+ # check valid POST payload on bad paths
+ self.check_post(valid_payload, '/condition?id=foo', 400)
+ # check cannot delete depended-upon Condition
+ self.post_exp_cond([], {})
+ for key in ('conditions', 'blockers', 'enables', 'disables'):
+ self.post_exp_process([], {key: [1]}, 1)
+ self.check_post({'delete': ''}, '/condition?id=1', 500)
+ self.post_exp_process([], {}, 1)
+ self.post_exp_day([], {'new_todo': '1'})
+ for key in ('conditions', 'blockers', 'enables', 'disables'):
+ self.post_exp_todo([], {key: [1]}, 1)
+ self.check_post({'delete': ''}, '/condition?id=1', 500)
+
+ def test_POST_condition(self) -> None:
+ """Test (valid) POST /condition and its effect on GET /condition[s]."""
+ url_single, url_all = '/condition?id=1', '/conditions'
+ exp_single, exp_all = ExpectedGetCondition(1), ExpectedGetConditions()
+ all_exps = [exp_single, exp_all]
+ # test valid POST's effect on single /condition and full /conditions
+ self.post_exp_cond(all_exps, {}, post_to_id=False)
+ self.check_json_get(url_single, exp_single)
+ self.check_json_get(url_all, exp_all)
+ # test (no) effect of invalid POST to existing Condition on /condition
+ self.check_post({}, url_single, 400)
+ self.check_json_get(url_single, exp_single)
+ # test effect of POST changing title, description, and activeness
+ self.post_exp_cond(all_exps, {'title': 'bar', 'description': 'oof',
+ 'is_active': 1})
+ self.check_json_get(url_single, exp_single)
+ # test POST sans 'is_active' setting it negative
+ self.post_exp_cond(all_exps, {})
+ self.check_json_get(url_single, exp_single)
+ # test deletion POST's effect, both to return id=1 into empty single,
+ # full /conditions into empty list
+ self.check_json_get(url_single, exp_single)
+ self.post_exp_cond(all_exps, {'delete': ''}, redir_to_id=False)
+ exp_single.set('is_new', True)
+ self.check_json_get(url_single, exp_single)
+ self.check_json_get(url_all, exp_all)
+
+ def test_GET_condition(self) -> None:
+ """More GET /condition testing, especially for Process relations."""
+ # check expected default status codes
self.check_get_defaults('/condition')
- self.check_get('/conditions', 200)
+ # check 'is_new' set if id= absent or pointing to not-yet-existing ID
+ exp = ExpectedGetCondition(None)
+ exp.set('is_new', True)
+ self.check_json_get('/condition', exp)
+ exp = ExpectedGetCondition(1)
+ exp.set('is_new', True)
+ self.check_json_get('/condition?id=1', exp)
+ # make Condition and two Processes that among them establish all
+ # possible ConditionsRelations to it, check /condition displays all
+ exp = ExpectedGetCondition(1)
+ self.post_exp_cond([exp], {}, post_to_id=False)
+ for i, p in enumerate([('conditions', 'disables'),
+ ('enables', 'blockers')]):
+ self.post_exp_process([exp], {k: [1] for k in p}, i+1)
+ self.check_json_get('/condition?id=1', exp)
+
+ def test_GET_conditions(self) -> None:
+ """Test GET /conditions."""
+ # test empty result on empty DB, default-settings on empty params
+ exp = ExpectedGetConditions()
+ self.check_json_get('/conditions', exp)
+ # test 'sort_by' default to 'title' (even if set to something else, as
+ # long as without handler) and 'pattern' get preserved
+ exp.set('pattern', 'bar')
+ self.check_json_get('/conditions?sort_by=foo&pattern=bar&foo=x', exp)
+ exp.set('pattern', '')
+ # test non-empty result, automatic (positive) sorting by title
+ post_cond1 = {'is_active': 0, 'title': 'foo', 'description': 'oof'}
+ post_cond2 = {'is_active': 0, 'title': 'bar', 'description': 'rab'}
+ post_cond3 = {'is_active': 1, 'title': 'baz', 'description': 'zab'}
+ for i, post in enumerate([post_cond1, post_cond2, post_cond3]):
+ self.post_exp_cond([exp], post, i+1, post_to_id=False)
+ self.check_filter(exp, 'conditions', 'sort_by', 'title', [2, 3, 1])
+ # test other sortings
+ self.check_filter(exp, 'conditions', 'sort_by', '-title', [1, 3, 2])
+ self.check_filter(exp, 'conditions', 'sort_by', 'is_active', [1, 2, 3])
+ self.check_filter(exp, 'conditions', 'sort_by', '-is_active',
+ [3, 2, 1])
+ exp.set('sort_by', 'title')
+ # test pattern matching on title
+ exp.lib_del('Condition', 1)
+ self.check_filter(exp, 'conditions', 'pattern', 'ba', [2, 3])
+ # test pattern matching on description
+ exp.lib_wipe('Condition')
+ exp.set_cond_from_post(1, post_cond1)
+ self.check_filter(exp, 'conditions', 'pattern', 'of', [1])