From a0288291a3e4cd6a2e6d936abbc4b560595f3485 Mon Sep 17 00:00:00 2001 From: CasVT Date: Thu, 19 Jan 2023 21:29:15 +0100 Subject: [PATCH] Fixes issue #8 --- backend/custom_exceptions.py | 12 +- backend/db.py | 10 + backend/notification_service.py | 12 +- backend/templates.py | 151 ++++++++++++ backend/users.py | 12 + frontend/api.py | 108 ++++++++- frontend/static/css/add_edit.css | 2 + frontend/static/css/general.css | 21 ++ ...{reminders.css => reminders_templates.css} | 17 +- frontend/static/js/add.js | 3 + frontend/static/js/general.js | 20 ++ frontend/static/js/notification.js | 21 +- frontend/static/js/reminders.js | 1 + frontend/static/js/templates.js | 217 ++++++++++++++++++ frontend/templates/reminders.html | 122 +++++++--- tests/api_test.py | 2 +- 16 files changed, 679 insertions(+), 52 deletions(-) create mode 100644 backend/templates.py rename frontend/static/css/{reminders.css => reminders_templates.css} (91%) create mode 100644 frontend/static/js/templates.js diff --git a/backend/custom_exceptions.py b/backend/custom_exceptions.py index 9ddad62..9534954 100644 --- a/backend/custom_exceptions.py +++ b/backend/custom_exceptions.py @@ -28,7 +28,13 @@ class NotificationServiceNotFound(Exception): class NotificationServiceInUse(Exception): """The notification service is wished to be deleted but a reminder is still using it""" - api_response = {'error': 'NotificationServiceInUse', 'result': {}, 'code': 400} + def __init__(self, type: str=''): + self.type = type + super().__init__(self.type) + + @property + def api_response(self) -> Dict[str, Any]: + return {'error': 'NotificationServiceInUse', 'result': {'type': self.type}, 'code': 400} class InvalidTime(Exception): """The time given is in the past""" @@ -58,3 +64,7 @@ class InvalidKeyValue(Exception): @property def api_response(self) -> Dict[str, Any]: return {'error': 'InvalidKeyValue', 'result': {'key': self.key, 'value': self.value}, 'code': 400} + +class TemplateNotFound(Exception): + """The template was not found""" + api_response = {'error': 'TemplateNotFound', 'result': {}, 'code': 404} diff --git a/backend/db.py b/backend/db.py index a8598f1..d63c297 100644 --- a/backend/db.py +++ b/backend/db.py @@ -105,6 +105,16 @@ def setup_db() -> None: FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (notification_service) REFERENCES notification_services(id) ); + CREATE TABLE IF NOT EXISTS templates( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + title VARCHAR(255) NOT NULL, + text TEXT, + notification_service INTEGER NOT NULL, + + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (notification_service) REFERENCES notification_services(id) + ); CREATE TABLE IF NOT EXISTS config( key VARCHAR(255) PRIMARY KEY, value TEXT NOT NULL diff --git a/backend/notification_service.py b/backend/notification_service.py index 9dbb0dd..f92df6b 100644 --- a/backend/notification_service.py +++ b/backend/notification_service.py @@ -80,11 +80,19 @@ class NotificationService: # Check if no reminders exist with this service cursor = get_db() cursor.execute( - "SELECT id FROM reminders WHERE notification_service = ? LIMIT 1", + "SELECT 1 FROM reminders WHERE notification_service = ? LIMIT 1;", (self.id,) ) if cursor.fetchone(): - raise NotificationServiceInUse + raise NotificationServiceInUse('reminder') + + # Check if no templates exist with this service + cursor.execute( + "SELECT 1 FROM templates WHERE notification_service = ? LIMIT 1;", + (self.id,) + ) + if cursor.fetchone(): + raise NotificationServiceInUse('template') cursor.execute( "DELETE FROM notification_services WHERE id = ?", diff --git a/backend/templates.py b/backend/templates.py new file mode 100644 index 0000000..efe22ab --- /dev/null +++ b/backend/templates.py @@ -0,0 +1,151 @@ +#-*- coding: utf-8 -*- + +from sqlite3 import IntegrityError +from typing import List + +from backend.custom_exceptions import (NotificationServiceNotFound, + TemplateNotFound) +from backend.db import get_db + +class Template: + """Represents a template + """ + def __init__(self, template_id: int): + self.id = template_id + + exists = get_db().execute("SELECT 1 FROM templates WHERE id = ? LIMIT 1;", (self.id,)).fetchone() + if not exists: + raise TemplateNotFound + + def get(self) -> dict: + """Get info about the template + + Returns: + dict: The info about the template + """ + template = get_db(dict).execute(""" + SELECT + id, + title, text, + notification_service + FROM templates + WHERE id = ?; + """, + (self.id,) + ).fetchone() + + return dict(template) + + def update(self, + title: str = None, + notification_service: int = None, + text: str = None + ) -> dict: + """Edit the template + + Args: + title (str): The new title of the entry. Defaults to None. + notification_service (int): The new id of the notification service to use to send the reminder. Defaults to None. + text (str, optional): The new body of the template. Defaults to None. + + Returns: + dict: The new template info + """ + cursor = get_db() + + data = self.get() + new_values = { + 'title': title, + 'notification_service': notification_service, + 'text': text + } + for k, v in new_values.items(): + if v is not None: + data[k] = v + + try: + cursor.execute(""" + UPDATE templates + SET title=?, notification_service=?, text=? + WHERE id = ?; + """, ( + data['title'], + data['notification_service'], + data['text'], + self.id + )) + except IntegrityError: + raise NotificationServiceNotFound + + return self.get() + + def delete(self) -> None: + """Delete the template + """ + get_db().execute("DELETE FROM templates WHERE id = ?;", (self.id,)) + return + +class Templates: + """Represents the template library of the user account + """ + def __init__(self, user_id: int): + self.user_id = user_id + + def fetchall(self) -> List[dict]: + """Get all templates + + Returns: + List[dict]: The id, title, text and notification_service + """ + templates: list = list(map(dict, get_db(dict).execute(""" + SELECT + id, + title, text, + notification_service + FROM templates + WHERE user_id = ? + ORDER BY title, id; + """, + (self.user_id,) + ).fetchall())) + + return templates + + def fetchone(self, id: int) -> Template: + """Get one template + + Args: + id (int): The id of the template to fetch + + Returns: + Template: A Template instance + """ + return Template(id) + + def add( + self, + title: str, + notification_service: int, + text: str = '' + ) -> Template: + """Add a template + + Args: + title (str): The title of the entry + notification_service (int): The id of the notification service to use to send the reminder. + text (str, optional): The body of the reminder. Defaults to ''. + + Returns: + Template: The info about the template + """ + try: + id = get_db().execute(""" + INSERT INTO templates(user_id, title, text, notification_service) + VALUES (?,?,?,?); + """, + (self.user_id, title, text, notification_service) + ).lastrowid + except IntegrityError: + raise NotificationServiceNotFound + + return self.fetchone(id) diff --git a/backend/users.py b/backend/users.py index 5030ffe..f5a313f 100644 --- a/backend/users.py +++ b/backend/users.py @@ -6,6 +6,7 @@ from backend.db import get_db from backend.notification_service import NotificationServices from backend.reminders import Reminders from backend.security import generate_salt_hash, get_hash +from backend.templates import Templates ONEPASS_USERNAME_CHARACTERS = 'abcedfghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.!@$' ONEPASS_INVALID_USERNAMES = ['reminders','api'] @@ -52,6 +53,17 @@ class User: self.notification_services_instance = NotificationServices(self.user_id) return self.notification_services_instance + @property + def templates(self) -> Templates: + """Get access to the templates of the user account + + Returns: + Templates: Templates instance that can be used to access the templates of the user account + """ + if not hasattr(self, 'templates_instance'): + self.templates_instance = Templates(self.user_id) + return self.templates_instance + def edit_password(self, new_password: str) -> None: """Change the password of the account diff --git a/frontend/api.py b/frontend/api.py index 2087975..fd3cf60 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -15,6 +15,7 @@ from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue, from backend.notification_service import (NotificationService, NotificationServices) from backend.reminders import Reminders, reminder_handler +from backend.templates import Template, Templates from backend.users import User, register_user api = Blueprint('api', __name__) @@ -451,7 +452,7 @@ def api_reminders_query(): result = g.user_data.reminders.search(query) return return_api(result) -@api.route('/reminders/', methods=['GET','PUT','DELETE']) +@api.route('/reminders/', methods=['GET','PUT','DELETE']) @error_handler @auth def api_get_reminder(r_id: int): @@ -515,3 +516,108 @@ def api_get_reminder(r_id: int): elif request.method == 'DELETE': reminders.fetchone(r_id).delete() return return_api({}) + +#=================== +# Template endpoints +#=================== + +@api.route('/templates', methods=['GET', 'POST']) +@error_handler +@auth +def api_get_templates(): + """ + Endpoint: /templates + Description: Manage the templates + Requires being logged in: Yes + Methods: + GET: + Description: Get a list of all templates + Returns: + 200: + The id, title, notification_service and text of every template + POST: + Description: Add a template + Parameters (body (content-type: application/json)): + title (required): the title of the template + notification_service (required): the id of the notification service to use to send the notification + text: the body of the template + Returns: + 200: + The info about the new template entry + 400: + KeyNotFound: One of the required parameters was not given + """ + templates: Templates = g.user_data.templates + + if request.method == 'GET': + result = templates.fetchall() + return return_api(result) + + elif request.method == 'POST': + data = request.get_json() + title = extract_key(data, 'title') + notification_service = extract_key(data, 'notification_service') + text = extract_key(data, 'text', check_existence=False) + + result = templates.add(title=title, + notification_service=notification_service, + text=text) + return return_api(result.get(), code=201) + +@api.route('/templates/', methods=['GET', 'PUT', 'DELETE']) +@error_handler +@auth +def api_get_template(t_id: int): + """ + Endpoint: /templates/ + Description: Manage a specific template + Requires being logged in: Yes + URL Parameters: + : + The id of the template + Methods: + GET: + Returns: + 200: + All info about the template + 404: + No template found with the given id + PUT: + Description: Edit the template + Parameters (body (content-type: application/json)): + title: The new title of the entry. + notification_service: The new id of the notification service to use to send the reminder. + text: The new body of the template. + Returns: + 200: + Template updated successfully + 404: + No template found with the given id + DELETE: + Description: Delete the template + Returns: + 200: + Template deleted successfully + 404: + No template found with the given id + """ + template: Template = g.user_data.templates.fetchone(t_id) + + if request.method == 'GET': + result = template.get() + return return_api(result) + + elif request.method == 'PUT': + data = request.get_json() + title = extract_key(data, 'title', check_existence=False) + notification_service = extract_key(data, 'notification_service', check_existence=False) + text = extract_key(data, 'text', check_existence=False) + + result = template.update(title=title, + notification_service=notification_service, + text=text) + return return_api(result) + + elif request.method == 'DELETE': + template.delete() + return return_api({}) diff --git a/frontend/static/css/add_edit.css b/frontend/static/css/add_edit.css index 861e828..966a0e5 100644 --- a/frontend/static/css/add_edit.css +++ b/frontend/static/css/add_edit.css @@ -40,6 +40,7 @@ gap: .75rem; border: 2px solid var(--color-gray); + box-shadow: var(--default-shadow); font-size: 1.1rem; } @@ -62,6 +63,7 @@ border: 2px solid var(--color-gray); border-radius: 4px; + box-shadow: var(--default-shadow); } .repeat-bar > *, diff --git a/frontend/static/css/general.css b/frontend/static/css/general.css index 5fd9f80..dabaaa5 100644 --- a/frontend/static/css/general.css +++ b/frontend/static/css/general.css @@ -264,6 +264,27 @@ nav > div > button svg { color: var(--color-light); } +.tab-selector { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + gap: 1rem; + + padding-top: 1rem; +} + +.tab-selector > button { + border: 2px solid var(--color-gray); + padding: .5rem 1rem; + transition: background-color .3s ease-in-out; +} + +.tab-selector > button[data-selected="true"] { + background-color: var(--color-gray); +} + @media (max-width: 543px) { .window-container { margin-left: 0; diff --git a/frontend/static/css/reminders.css b/frontend/static/css/reminders_templates.css similarity index 91% rename from frontend/static/css/reminders.css rename to frontend/static/css/reminders_templates.css index 4915b49..b5d1e7b 100644 --- a/frontend/static/css/reminders.css +++ b/frontend/static/css/reminders_templates.css @@ -46,7 +46,8 @@ } /* REMINDER LIST */ -#reminder-list { +#reminder-list, +#template-list { --gap: 1rem; --entry-width: 13rem; max-width: 43rem; @@ -60,13 +61,15 @@ padding: 1rem; } -#add-entry { +#add-entry, +#add-template { justify-content: center; align-items: center; gap: .5rem; } -#add-entry svg { +#add-entry svg, +#add-template svg { height: 2rem; width: 2rem; } @@ -95,7 +98,8 @@ background-color: var(--color-gray); } -div.entry.fit { +div.entry.fit, +button.entry.fit { flex-grow: 1; } @@ -160,6 +164,11 @@ div.entry.fit { opacity: 1; } +#delete-template { + border-color: var(--color-error); + color: var(--color-error); +} + @media (max-width: 543px) { header > div { transform: translateX(0); diff --git a/frontend/static/js/add.js b/frontend/static/js/add.js index 238e260..e400f9c 100644 --- a/frontend/static/js/add.js +++ b/frontend/static/js/add.js @@ -52,6 +52,7 @@ function addReminder() { function showAdd() { if (!document.getElementById('add-entry').classList.contains('error')) { + loadTemplates(force=false); showWindow('add'); } else { showWindow('notification'); @@ -61,6 +62,7 @@ function showAdd() { function closeAdd() { hideWindow(); setTimeout(() => { + document.getElementById('template-selection').value = document.querySelector('#template-selection option[selected]').value; inputs.title.value = ''; inputs.time.value = ''; inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value; @@ -89,6 +91,7 @@ function toggleRepeated() { // code run on load document.getElementById('add-form').setAttribute('action', 'javascript:addReminder();'); +document.getElementById('template-selection').addEventListener('change', e => loadTemplate()); document.getElementById('normal-button').addEventListener('click', e => toggleNormal()); document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated()); document.getElementById('close-add').addEventListener('click', e => closeAdd()); diff --git a/frontend/static/js/general.js b/frontend/static/js/general.js index 270b43e..b410fb9 100644 --- a/frontend/static/js/general.js +++ b/frontend/static/js/general.js @@ -30,6 +30,23 @@ function hideWindow() { }); }; +function showTab(tab_id, button_id, load_function=null) { + document.querySelectorAll(`.tab-selector > button`).forEach(e => { + if (e.id === button_id) { + e.dataset.selected = "true" + } else { + e.dataset.selected = "false" + }; + }); + document.querySelectorAll(`#home > div:not(.tab-selector)`).forEach(e => { + e.classList.add('hidden'); + }); + document.getElementById(tab_id).classList.remove('hidden'); + if (load_function !== null) { + load_function(force=false); + }; +}; + // code run on load const api_key = sessionStorage.getItem('api_key'); @@ -44,6 +61,9 @@ document.getElementById('notification-services-button').addEventListener('click' document.getElementById('settings-button').addEventListener('click', e => showWindow('settings')); document.getElementById('logout-button').addEventListener('click', e => logout()); +document.getElementById('reminders-selector').addEventListener('click', e => showTab('reminder-tab', 'reminders-selector')); +document.getElementById('templates-selector').addEventListener('click', e => showTab('template-tab', 'templates-selector', loadTemplates)); + const edit_icon = ''; const delete_icon = ''; const save_icon = ''; diff --git a/frontend/static/js/notification.js b/frontend/static/js/notification.js index c57f3d6..9596eab 100644 --- a/frontend/static/js/notification.js +++ b/frontend/static/js/notification.js @@ -11,8 +11,12 @@ function fillNotificationSelection() { .then(json => { if (json.result.length) { document.getElementById('add-entry').classList.remove('error', 'error-icon'); - [document.getElementById('notification-service-input'), - document.getElementById('notification-service-edit-input')].forEach(options => { + [ + document.getElementById('notification-service-input'), + document.getElementById('notification-service-edit-input'), + document.getElementById('notification-service-template-input'), + document.getElementById('notification-service-template-edit-input') + ].forEach(options => { options.innerHTML = ''; json.result.forEach(service => { const entry = document.createElement('option'); @@ -95,10 +99,11 @@ function deleteService(id) { fetch(`/api/notificationservices/${id}?api_key=${api_key}`, { 'method': 'DELETE' }) - .then(response => { + .then(response => response.json()) + .then(json => { // catch errors - if (!response.ok) { - return Promise.reject(response.status); + if (json.error !== null) { + return Promise.reject(json); }; row.remove(); @@ -107,12 +112,12 @@ function deleteService(id) { }; }) .catch(e => { - if (e === 401) { + if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') { window.location.href = '/'; - } else if (e === 400) { + } else if (e.error === 'NotificationServiceInUse') { const delete_button = row.querySelector('button[title="Delete"]'); delete_button.classList.add('error-icon'); - delete_button.title = 'The notification service is still in use by a reminder'; + delete_button.title = `The notification service is still in use by a ${e.result.type}`; } else { console.log(e); }; diff --git a/frontend/static/js/reminders.js b/frontend/static/js/reminders.js index 8a82012..484f80f 100644 --- a/frontend/static/js/reminders.js +++ b/frontend/static/js/reminders.js @@ -134,3 +134,4 @@ setInterval(fillList, 60000); document.getElementById('search-form').setAttribute('action', 'javascript:search();'); document.getElementById('clear-button').addEventListener('click', e => clearSearch()); document.getElementById('add-entry').addEventListener('click', e => showAdd()); +document.getElementById('add-template').addEventListener('click', e => showWindow('add-template')); diff --git a/frontend/static/js/templates.js b/frontend/static/js/templates.js new file mode 100644 index 0000000..cb9672f --- /dev/null +++ b/frontend/static/js/templates.js @@ -0,0 +1,217 @@ +const template_inputs = { + 'title': document.getElementById('title-template-input'), + 'notification-service': document.getElementById('notification-service-template-input'), + 'text': document.getElementById('text-template-input') +}; + +const edit_template_inputs = { + 'title': document.getElementById('title-template-edit-input'), + 'notification-service': document.getElementById('notification-service-template-edit-input'), + 'text': document.getElementById('text-template-edit-input') +}; + +function loadTemplates(force=true) { + const table = document.getElementById('template-list'); + if (!force && !!table.querySelector('button:not(#add-template)')) { + return + }; + + fetch(`/api/templates?api_key=${api_key}`) + .then(response => { + // catch errors + if (!response.ok) { + return Promise.reject(response.status); + }; + return response.json(); + }) + .then(json => { + const select_list = document.getElementById('template-selection'); + select_list.querySelectorAll('option:not([selected])').forEach(e => e.remove()); + json.result.forEach(template => { + const entry = document.createElement('option'); + entry.value = template.id; + entry.innerText = template.title; + + select_list.appendChild(entry); + }); + + table.querySelectorAll('button:not(#add-template)').forEach(e => e.remove()); + json.result.forEach(template => { + const entry = document.createElement('button'); + entry.classList.add('entry'); + entry.addEventListener('click', e => showEditTemplate(template.id)); + + const title = document.createElement('h2'); + title.innerText = template.title; + entry.appendChild(title); + + table.appendChild(entry); + + if (title.clientHeight < title.scrollHeight) { + entry.classList.add('expand'); + }; + }); + table.querySelectorAll('button:not(#add-template)').forEach(template => template.classList.add('fit')); + }) + .catch(e => { + if (e === 401) { + window.location.href = '/'; + } else { + console.log(e); + }; + }); +}; + +function loadTemplate() { + const id = document.getElementById('template-selection').value; + if (id === "0") { + inputs.title.value = ''; + inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value; + inputs.text.value = ''; + } else { + fetch(`/api/templates/${id}?api_key=${api_key}`) + .then(response => { + // catch errors + if (!response.ok) { + return Promise.reject(response.status); + }; + return response.json(); + }) + .then(json => { + inputs.title.value = json.result.title; + inputs.notification_service.value = json.result.notification_service; + inputs.text.value = json.result.text; + }) + .catch(e => { + if (e === 401) { + window.location.href = '/'; + } else { + console.log(e); + }; + }); + }; +}; + +function addTemplate() { + const data = { + 'title': template_inputs.title.value, + 'notification_service': template_inputs["notification-service"].value, + 'text': template_inputs.text.value + }; + fetch(`/api/templates?api_key=${api_key}`, { + 'method': 'POST', + 'headers': {'Content-Type': 'application/json'}, + 'body': JSON.stringify(data) + }) + .then(response => { + // catch errors + if (!response.ok) { + return Promise.reject(response.status); + }; + + loadTemplates(); + closeAddTemplate(); + return + }) + .catch(e => { + if (e === 401) { + window.location.href = '/'; + } else { + console.log(e); + }; + }); +}; + +function closeAddTemplate() { + hideWindow(); + setTimeout(() => { + template_inputs.title.value = ''; + template_inputs['notification-service'].value = document.querySelector('#notification-service-template-input option[selected]').value; + template_inputs.text.value = ''; + }, 500); +}; + +function showEditTemplate(id) { + fetch(`/api/templates/${id}?api_key=${api_key}`) + .then(response => { + // catch errors + if (!response.ok) { + return Promise.reject(response.status); + }; + return response.json(); + }) + .then(json => { + document.getElementById('template-edit-form').dataset.id = id; + edit_template_inputs.title.value = json.result.title; + edit_template_inputs['notification-service'].value = json.result.notification_service; + edit_template_inputs.text.value = json.result.text; + showWindow('edit-template'); + }) + .catch(e => { + if (e === 401) { + window.location.href = '/'; + } else { + console.log(e); + }; + }); +}; + +function saveTemplate() { + const id = document.getElementById('template-edit-form').dataset.id; + const data = { + 'title': edit_template_inputs.title.value, + 'notification_service': edit_template_inputs['notification-service'].value, + 'text': edit_template_inputs.text.value + }; + fetch(`/api/templates/${id}?api_key=${api_key}`, { + 'method': 'PUT', + 'headers': {'Content-Type': 'application/json'}, + 'body': JSON.stringify(data) + }) + .then(response => { + // catch errors + if (!response.ok) { + return Promise.reject(response.status); + }; + loadTemplates(); + hideWindow(); + }) + .catch(e => { + if (e === 401) { + window.location.href = '/'; + } else { + console.log(e); + }; + }); +}; + +function deleteTemplate() { + const id = document.getElementById('template-edit-form').dataset.id; + fetch(`/api/templates/${id}?api_key=${api_key}`, { + 'method': 'DELETE' + }) + .then(response => { + // catch errors + if (!response.ok) { + return Promise.reject(response.status); + }; + + loadTemplates(); + hideWindow(); + }) + .catch(e => { + if (e === 401) { + window.location.href = '/'; + } else { + console.log(e); + }; + }); +}; + +// code run on load + +document.getElementById('template-form').setAttribute('action', 'javascript:addTemplate();'); +document.getElementById('close-template').addEventListener('click', e => closeAddTemplate()); +document.getElementById('template-edit-form').setAttribute('action', 'javascript:saveTemplate()'); +document.getElementById('close-edit-template').addEventListener('click', e => hideWindow()); +document.getElementById('delete-template').addEventListener('click', e => deleteTemplate()); diff --git a/frontend/templates/reminders.html b/frontend/templates/reminders.html index 85b0a7d..3d4eee6 100644 --- a/frontend/templates/reminders.html +++ b/frontend/templates/reminders.html @@ -6,7 +6,7 @@ - + @@ -16,6 +16,7 @@ + Reminders - Noted @@ -74,49 +75,71 @@
-
- -
- -
+
+ +
-
- + + +
+ +
+
+ + +

Add a notification service first!

+ +
+
+

Add a reminder

+
@@ -270,6 +293,35 @@
+
+

Add a template

+
+ + + + +
+ + +
+ +
+
+
+

Edit a template

+
+
+ + + +
+ + + +
+
+
+
diff --git a/tests/api_test.py b/tests/api_test.py index 9ac73d8..c062c4d 100644 --- a/tests/api_test.py +++ b/tests/api_test.py @@ -40,9 +40,9 @@ class Test_API(unittest.TestCase): self.assertEqual(result(AccessUnauthorized), return_api(**AccessUnauthorized.api_response)) self.assertEqual(result(ReminderNotFound), return_api(**ReminderNotFound.api_response)) self.assertEqual(result(NotificationServiceNotFound), return_api(**NotificationServiceNotFound.api_response)) - self.assertEqual(result(NotificationServiceInUse), return_api(**NotificationServiceInUse.api_response)) self.assertEqual(result(InvalidTime), return_api(**InvalidTime.api_response)) self.assertEqual(result(InvalidURL), return_api(**InvalidURL.api_response)) + self.assertEqual(result(NotificationServiceInUse, 'test'), return_api(**NotificationServiceInUse('test').api_response)) self.assertEqual(result(KeyNotFound, 'test'), return_api(**KeyNotFound('test').api_response)) self.assertEqual(result(InvalidKeyValue, 'test', 'value'), return_api(**InvalidKeyValue('test', 'value').api_response)) with self.assertRaises(TypeError):