diff --git a/README.md b/README.md
index a68d4ff..68a735b 100644
--- a/README.md
+++ b/README.md
@@ -196,11 +196,12 @@ The use of environment variables allow you to provide over-rides to default sett
| `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:
- Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. However for the path for the container is `/config`.
| `APPRISE_STATELESS_URLS` | For a non-persistent solution, you can take advantage of this global variable. Use this to define a default set of Apprise URLs to notify when using API calls to `/notify`. If no `{KEY}` is defined when calling `/notify` then the URLs defined here are used instead. By default, nothing is defined for this variable.
| `APPRISE_STATEFUL_MODE` | This can be set to the following possible modes:
📌 **hash**: This is also the default. It stores the server configuration in a hash formatted that can be easily indexed and compressed.
📌 **simple**: Configuration is written straight to disk using the `{KEY}.cfg` (if `TEXT` based) and `{KEY}.yml` (if `YAML` based).
📌 **disabled**: Straight up deny any read/write queries to the servers stateful store. Effectively turn off the Apprise Stateful feature completely.
+| `APPRISE_CONFIG_LOCK` | Locks down your API hosting so that you can no longer delete/update/access stateful information. Your configuration is still referenced when stateful calls are made to `/notify`. The idea of this switch is to allow someone to set their (Apprise) configuration up and then as an added security tactic, they may choose to lock their configuration down (in a read-only state). Those who use the Apprise CLI tool may still do it, however the `--config` (`-c`) switch will not successfully reference this access point anymore. You can however use the `apprise://` plugin without any problem ([see here for more details](https://github.com/caronc/apprise/wiki/Notify_apprise_api)). This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.
| `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk (`hash` mode only).
| `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more than one host.
| `BASE_URL` | Those who are hosting the API behind a proxy that requires a subpath to gain access to this API should specify this path here as well. By default this is not set at all.
| `LOG_LEVEL` | Adjust the log level to the console. Possible values are `CRITICAL`, `ERROR`, `WARNING`, `INFO`, and `DEBUG`.
-| `DEBUG` | This defaults to `False` however can be set to `True` if defined with a non-zero value (such as `1`).
+| `DEBUG` | This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.
## Development Environment
@@ -249,10 +250,10 @@ A scenario where you want to poll the API for your configuration:
```bash
# A simple example of the Apprise CLI
# pulling down previously stored configuration
-apprise --body="test message" --config=http://localhost:8000/get/{KEY}
+apprise -vvv --body="test message" --config=http://localhost:8000/get/{KEY}
```
-You can also leverage the `import` parameter supported in Apprise configuration files.
+You can also leverage the `import` parameter supported in Apprise configuration files if `APPRISE_CONFIG_LOCK` isn't set on the server you're accessing:
```nginx
# Linux users can place this in ~/.apprise
@@ -266,14 +267,41 @@ Now you'll just automatically source the configuration file without the need of
```bash
# Configuration is automatically loaded from our server.
-apprise --body="my notification"
+apprise -vvv --body="my notification"
```
If you used tagging, then you can notify the specific service like so:
```bash
# Configuration is automatically loaded from our server.
-apprise --tag=devops --body="Tell James GitLab is down again."
+apprise -vvv --tag=devops --body="Tell James GitLab is down again."
+```
+
+
+If you're server has the `APPRISE_CONFIG_LOCK` set, you can still leverage [the `apprise://` plugin](https://github.com/caronc/apprise/wiki/Notify_apprise_api) to trigger our pre-saved notifications:
+```bash
+# Swap {KEY} with your apprise key you configured on your API
+apprise -vvv --body="There are donut's in the front hall if anyone wants any" \
+ apprise://localhost:8000/{KEY}
+```
+
+Alternatively we can set this up in a configuration file and even tie our local tags to our upstream ones like so:
+```nginx
+# Linux users can place this in ~/.apprise
+# Windows users can place this info in %APPDATA%/Apprise/apprise
+
+# Swap {KEY} with your apprise key you configured on your API
+devteam=apprise://localhost:8000/{KEY}?tags=devteam
+
+# the only catch is you need to map your tags on the local server to the tags
+# you want to pass upstream to your Apprise server using this method.
+# In the above we tied the local keyword `friends` to the apprise server using the tag `friends`
+```
+
+We could trigger our notification to our friends now like:
+```bash
+# Trigger our service:
+apprise -vvv --tag=devteam --body="Guys, don't forget about the audit tomorrow morning."
```
### AppriseConfig() Pull Example
diff --git a/apprise_api/api/context_processors.py b/apprise_api/api/context_processors.py
index 00fae63..1756fca 100644
--- a/apprise_api/api/context_processors.py
+++ b/apprise_api/api/context_processors.py
@@ -23,6 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from .utils import ConfigCache
+from django.conf import settings
def stateful_mode(request):
@@ -30,3 +31,10 @@ def stateful_mode(request):
Returns our loaded Stateful Mode
"""
return {'STATEFUL_MODE': ConfigCache.mode}
+
+
+def config_lock(request):
+ """
+ Returns the state of our global configuration lock
+ """
+ return {'CONFIG_LOCK': settings.APPRISE_CONFIG_LOCK}
diff --git a/apprise_api/api/forms.py b/apprise_api/api/forms.py
index 6831734..e9098e6 100644
--- a/apprise_api/api/forms.py
+++ b/apprise_api/api/forms.py
@@ -90,6 +90,7 @@ class AddByConfigForm(forms.Form):
label=_('Configuration'),
widget=forms.Textarea(),
max_length=4096,
+ required=False,
)
def clean_format(self):
diff --git a/apprise_api/api/templates/config.html b/apprise_api/api/templates/config.html
index b697122..b718091 100644
--- a/apprise_api/api/templates/config.html
+++ b/apprise_api/api/templates/config.html
@@ -8,13 +8,14 @@
{% blocktrans %}To get started, the first thing you want to do is define your configuration. Do this by - clicking - on the Configuration tab. + clicking on the Configuration tab. {% endblocktrans %}
+ {% blocktrans %}At this time, the administrator of this server has locked down all configuration. This means + That pre-created configuration is securely hidden for the purpose of notification transmission only. + + New configuration can not be set, and existing configuration can not be modified or viewed. + {% endblocktrans %}
+
+ {% blocktrans %}The following command would cause apprise to directly notify all of your services:{% endblocktrans %}
+
+
apprise --body="Test Message" \
+ apprise{% if request.is_secure %}s{% endif %}://{{request.META.HTTP_HOST}}{{BASE_URL}}/{{key}}?tags=all
+
+ {% if not CONFIG_LOCK %}
+
{% blocktrans %}The following command would cause apprise to retrieve the configuration loaded and
send a test notification to all of your added services:{% endblocktrans %}
apprise --body="Test Message" --tag=all \
--config={{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/get/{{key}}
+
+ {% endif %}
{% blocktrans %}Define your configuration below:{% endblocktrans %}
+ {% else %} +{% blocktrans %}Access to your configuration has been disabled by your administrator.{% endblocktrans %} + + {% endif %}
{% blocktrans %}
@@ -102,7 +129,8 @@
{% endblock %}
{% block jsfooter %}
-async function update() {
+{% if STATEFUL_MODE != 'disabled' %}
+async function main_init(){
// disable the notification tab until we know for certain
// a notification is possible
@@ -117,273 +145,322 @@ async function update() {
document.querySelector('#url-list').textContent = ''
document.querySelector('#url-list-progress').style.display = null;
+{% if not CONFIG_LOCK %}
// Ensure no-config sections are visible
document.querySelector('.no-config')
.style.display = null;
+{% endif %}
- // perform our status check
- let response = await fetch('{% url "get" key %}', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json;charset=utf-8'
+ // perform a tag retrieval; start with 'all'
+ let tags = ['all'];
+
+ let jsonResponse = await fetch('{% url "json_urls" key %}/?privacy=1', {
+ method: 'GET',
+ })
+ if(jsonResponse.status != 200) {
+ // Take an early exit
+ document.querySelector('#url-list-progress').style.display = 'none';
+ document.querySelector('#url-list').textContent = '{% trans "An error occurred retrieving the list of loaded Apprise URL(s)" %}'
+ return;
+ }
+
+ // Initialize our tags making it easy for an end user to
+ // choose from. Tags are based off ones found in the saved
+ // configuration.
+ const data = await jsonResponse.json();
+ let external_data = tags.concat(data.tags).reduce(function(result, item) {
+ result[item] = null;
+ return result;
+ }, {})
+
+ M.Chips.init(document.querySelectorAll('.chips'), {
+ placeholder: 'Optional Tag',
+ secondaryPlaceholder: 'Another Tag',
+ autocompleteOptions: {
+ data: external_data,
+ minLength: 0
},
+ onChipAdd: function(e, chip) {
+ var $this = this;
+ $this.chipsData.forEach(function(e, index) {
+ if(!(e.tag in external_data))
+ $this.deleteChip(index);
+ })
+ }
});
- if(response.status == 204)
- {
- // no problem; we simply have no content to retrieve
- return '';
- }
- else if(response.status == 200)
- {
- // configuration found
+ // Now build our our loaded list of configuration for our welcome page
+ let urlList = document.createElement('ul');
- // Remove our restrictions on sending notifications
- document.querySelector('.config-overview li a[href="#notify"]')
- .parentNode.classList.remove('disabled');
+ // Create a list item for each url retrieved
+ data.urls.forEach(function (entry) {
+ let code = document.createElement('code');
+ let li = document.createElement('li');
+ code.textContent = entry.url;
+ li.setAttribute('class', 'card-panel');
+ li.appendChild(code);
- // get our results
- let result = await response.json();
+ // Get our tags associate with the URL
+ entry.tags.forEach(function (tag) {
+ let chip = document.createElement('div');
+ chip.setAttribute('class', 'chip');
+ chip.textContent = `🏷️ ${tag}`;
+ li.appendChild(chip);
+ });
- // Set our configuration so it's visible
- document.querySelector('#id_config').value = result.config;
- // Set our format
- document.querySelector('#id_format').value = result.format;
+ urlList.appendChild(li);
+ });
- // dispatch our event to update our select box
- if (typeof(Event) === 'function') {
- var event = new Event('change');
- } else { // for IE11
- var event = document.createEvent('Event');
- event.initEvent('change', true, true);
- }
- document.querySelector('#id_format').dispatchEvent(event);
+ // Store our new list
+ document.querySelector('#url-list-progress').style.display = 'none';
+ document.querySelector('#url-list').textContent = ''
+ if(urlList.childNodes.length > 0) {
// Ensure has-config sections are visible
document.querySelector('.has-config')
.style.display = null;
+ // Remove our restrictions on sending notifications
+ document.querySelector('.config-overview li a[href="#notify"]')
+ .parentNode.classList.remove('disabled');
+
+ {% if not CONFIG_LOCK %}
// Disable any no-config entries
document.querySelector('.no-config')
.style.display = 'none';
+ {% endif %}
- // perform a tag retrieval; start with 'all'
- let tags = ['all'];
+ // Save our list to the screen
+ document.querySelector('#url-list').appendChild(urlList);
- let jsonResponse = fetch('{% url "json_urls" key %}?privacy=1', {
- method: 'GET',
- }).then(function(jsonResponse) {
- return jsonResponse.json();
-
- }).then(function (data) {
- // Initialize our tags making it easy for an end user to
- // choose from. Tags are based off ones found in the saved
- // configuration.
- let external_data = tags.concat(data.tags).reduce(function(result, item) {
- result[item] = null;
- return result;
- }, {})
-
- M.Chips.init(document.querySelectorAll('.chips'), {
- placeholder: 'Optional Tag',
- secondaryPlaceholder: 'Another Tag',
- autocompleteOptions: {
- data: external_data,
- minLength: 0
- },
- onChipAdd: function(e, chip) {
- var $this = this;
- $this.chipsData.forEach(function(e, index) {
- if(!(e.tag in external_data))
- $this.deleteChip(index);
- })
- }
- });
-
- // Now build our our loaded list of configuration for our welcome page
- let urlList = document.createElement('ul');
-
- // Create a list item for each url retrieved
- data.urls.forEach(function (entry) {
- let code = document.createElement('code');
- let li = document.createElement('li');
- code.textContent = entry.url;
- li.setAttribute('class', 'card-panel');
- li.appendChild(code);
-
- // Get our tags associate with the URL
- entry.tags.forEach(function (tag) {
- let chip = document.createElement('div');
- chip.setAttribute('class', 'chip');
- chip.textContent = `🏷️ ${tag}`;
- li.appendChild(chip);
- });
-
- urlList.appendChild(li);
- });
-
- // Store our new list
- document.querySelector('#url-list-progress').style.display = 'none';
- document.querySelector('#url-list').textContent = ''
- if(urlList.childNodes.length > 0) {
- document.querySelector('#url-list').appendChild(urlList);
- } else {
- document.querySelector('#url-list').textContent = '{% trans "There are no Apprise URL(s) loaded." %}'
- }
-
- }).catch(function (err) {
- // There was an error
- document.querySelector('#url-list-progress').style.display = 'none';
- document.querySelector('#url-list').textContent = '{% trans "An error occurred retrieving the list of loaded Apprise URL(s)" %}'
+ {% if not CONFIG_LOCK %}
+ //
+ // Load our configuration now into the configuration tab
+ //
+ let response = await fetch('{% url "get" key %}', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json;charset=utf-8'
+ },
});
- return response;
+ if(response.status == 204)
+ {
+ // no problem; we simply have no content to retrieve
+ return '';
+ }
+ else if(response.status == 200)
+ {
+ // configuration found
+
+ // get our results
+ let result = await response.json();
+
+ // Set our configuration so it's visible
+ document.querySelector('#id_config').value = result.config;
+ // Set our format
+ document.querySelector('#id_format').value = result.format;
+
+ // dispatch our event to update our select box
+ if (typeof(Event) === 'function') {
+ var event = new Event('change');
+ } else { // for IE11
+ var event = document.createEvent('Event');
+ event.initEvent('change', true, true);
+ }
+ document.querySelector('#id_format').dispatchEvent(event);
+ }
+ {% endif %}
+ } else {
+ document.querySelector('#url-list').textContent = '{% trans "There are no Apprise URL(s) loaded." %}'
}
- // if we reach here, we failed
+
return null;
}
-update();
+function config_init() {
+ // over-ride manual submit for a nicer user experience
+ document.querySelector('#addconfig').onsubmit = function(event) {
+ event.preventDefault();
+ const form = this;
+ const body = new URLSearchParams(new FormData(form));
-// over-ride manual submit for a nicer user experience
-document.querySelector('#addconfig').onsubmit = function(event) {
- event.preventDefault();
- const form = this;
- const body = new URLSearchParams(new FormData(form));
+ content = document.querySelector('#id_config')
+ .value.replace(/^\s+|\s+$/gm,'');
+ if(content.length) {
+ // perform our status check
+ let response = fetch('{% url "add" key %}', {
+ method: 'POST',
+ body: body,
+ }).then(function(response) {
+ if(response.status == 200)
+ {
+ // update our settings
+ main_init();
- // perform our status check
- let response = fetch('{% url "add" key %}', {
- method: 'POST',
- body: body,
- }).then(function(response) {
- if(response.status == 200)
- {
- // update our settings
- update();
+ // user notification
+ Swal.fire(
+ '{% trans "Save" %}',
+ '{% trans "Successfully saved the specified URL(s)." %}',
+ 'success'
+ );
+ } else if(response.status == 500) {
+ // Disk issue
+ Swal.fire(
+ '{% trans "Save" %}',
+ '{% trans "There was an issue writing the configuration to disk. Check your file permissions and try again." %}',
+ 'error'
+ );
+ } else {
+ // user notification
+ Swal.fire(
+ '{% trans "Save" %}',
+ '{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}',
+ 'error'
+ );
+ }
+ });
+ } else {
+ // Perform Delete
+ // perform our status check
+ let response = fetch('{% url "del" key %}', {
+ method: 'POST',
+ body: body,
+ }).then(function(response) {
+ if(response.status == 200 || response.status == 204)
+ {
+ // update our settings
+ main_init();
- // user notification
- Swal.fire(
- '{% trans "Save" %}',
- '{% trans "Successfully saved the specified URL(s)." %}',
- 'success'
- );
- } else if(response.status == 500) {
- // Disk issue
- Swal.fire(
- '{% trans "Save" %}',
- '{% trans "There was an issue writing the configuration to disk. Check your file permissions and try again." %}',
- 'error'
- );
- } else {
- // user notification
- Swal.fire(
- '{% trans "Save" %}',
- '{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}',
- 'error'
- );
- }
- });
- return false;
-}
-
-// over-ride manual submit for a nicer user experience
-document.querySelector('#donotify').onsubmit = function(event) {
- event.preventDefault();
-
- const chipElement = document.querySelector('.chips');
-
- chipElement.querySelector('.chips');
- const chipInput = document.querySelector('.chips > input');
- if(chipInput.value) {
- // This code just handles text typed in the tag section that was
- // not submitted. This forces any lingering un-committed text
- // into a tag just prior to it's submission
- const ev = new KeyboardEvent('keydown', {
- altKey:false,
- bubbles: true,
- cancelBubble: false,
- cancelable: true,
- charCode: 0,
- code: "Enter",
- composed: true,
- ctrlKey: false,
- currentTarget: null,
- defaultPrevented: true,
- detail: 0,
- eventPhase: 0,
- isComposing: false,
- isTrusted: true,
- key: "Enter",
- keyCode: 13,
- location: 0,
- metaKey: false,
- repeat: false,
- returnValue: false,
- shiftKey: false,
- type: "keydown",
- which: 13
- });
- chipInput.dispatchEvent(ev);
- }
-
- // store tags (as comma separated string) from materialize chip type into form
- document.querySelector('#id_tag').value = M.Chips.getInstance(chipElement).chipsData.reduce(
- function(s, a){
- s.push(a.tag)
- return s;
- }, []).join(",")
-
- const form = this;
- const body = new URLSearchParams(new FormData(form));
-
- // perform our notification
- Swal.fire(
- '{% trans "Notification" %}',
- '{% trans "Sending notification(s)..." %}',
- );
- Swal.showLoading()
- let response = fetch('{% url "notify" key %}', {
- method: 'POST',
- body: body,
- headers: {
- 'Accept': 'text/html',
- 'X-Apprise-Log-Level': 'info'
+ // user notification
+ Swal.fire(
+ '{% trans "Delete" %}',
+ '{% trans "Successfully removed configuration." %}',
+ 'success'
+ );
+ } else {
+ // user notification
+ Swal.fire(
+ '{% trans "Delete" %}',
+ '{% trans "There was an issue removing the configuration." %}',
+ 'error'
+ );
+ }
+ });
}
- }).then(function(response) {
- response.text().then(function (html) {
- if(response.status == 200)
- {
- // user notification
- Swal.fire(
- '{% trans "Notification" %}',
- '{% trans "Successfully sent the notification(s)." %}' + html,
- 'success'
- );
- } else if(response.status == 424) {
- // user notification
- Swal.fire(
- '{% trans "Notification" %}',
- '{% trans "One or more of the notification(s) were not sent." %}' + html,
- 'warning'
- );
- } else {
- // user notification
- Swal.fire(
- '{% trans "Notification" %}',
- '{% trans "Failed to send the notification(s)." %}' + html,
- 'error'
- );
- }
- });
- });
- return false;
+ return false;
+ }
}
+
+function notify_init() {
+ // over-ride manual submit for a nicer user experience
+ document.querySelector('#donotify').onsubmit = function(event) {
+ event.preventDefault();
+
+ const chipElement = document.querySelector('.chips');
+
+ chipElement.querySelector('.chips');
+ const chipInput = document.querySelector('.chips > input');
+ if(chipInput.value) {
+ // This code just handles text typed in the tag section that was
+ // not submitted. This forces any lingering un-committed text
+ // into a tag just prior to it's submission
+ const ev = new KeyboardEvent('keydown', {
+ altKey:false,
+ bubbles: true,
+ cancelBubble: false,
+ cancelable: true,
+ charCode: 0,
+ code: "Enter",
+ composed: true,
+ ctrlKey: false,
+ currentTarget: null,
+ defaultPrevented: true,
+ detail: 0,
+ eventPhase: 0,
+ isComposing: false,
+ isTrusted: true,
+ key: "Enter",
+ keyCode: 13,
+ location: 0,
+ metaKey: false,
+ repeat: false,
+ returnValue: false,
+ shiftKey: false,
+ type: "keydown",
+ which: 13
+ });
+ chipInput.dispatchEvent(ev);
+ }
+
+ // store tags (as comma separated string) from materialize chip type into form
+ document.querySelector('#id_tag').value = M.Chips.getInstance(chipElement).chipsData.reduce(
+ function(s, a){
+ s.push(a.tag)
+ return s;
+ }, []).join(",")
+
+ const form = this;
+ const body = new URLSearchParams(new FormData(form));
+
+ // perform our notification
+ Swal.fire(
+ '{% trans "Notification" %}',
+ '{% trans "Sending notification(s)..." %}',
+ );
+ Swal.showLoading()
+ let response = fetch('{% url "notify" key %}', {
+ method: 'POST',
+ body: body,
+ headers: {
+ 'Accept': 'text/html',
+ 'X-Apprise-Log-Level': 'info'
+ }
+ }).then(function(response) {
+ response.text().then(function (html) {
+ if(response.status == 200)
+ {
+ // user notification
+ Swal.fire(
+ '{% trans "Notification" %}',
+ '{% trans "Successfully sent the notification(s)." %}' + html,
+ 'success'
+ );
+ } else if(response.status == 424) {
+ // user notification
+ Swal.fire(
+ '{% trans "Notification" %}',
+ '{% trans "One or more of the notification(s) were not sent." %}' + html,
+ 'warning'
+ );
+ } else {
+ // user notification
+ Swal.fire(
+ '{% trans "Notification" %}',
+ '{% trans "Failed to send the notification(s)." %}' + html,
+ 'error'
+ );
+ }
+ });
+ });
+ return false;
+ }
+}
+
+/* Initialize our page */
+main_init();
+{% if not CONFIG_LOCK %}
+/* Initialze our configuration */
+config_init();
+{% endif %}
+notify_init();
+{% endif %}
{% endblock %}
{% block onload %}
+{% if STATEFUL_MODE != 'disabled' %}
{{ block.super }}
document.querySelector('label [for="id_tag"]')
-
{
// create a new div with the class 'chips' assigned to it
const element = document.createElement('div')
@@ -395,4 +472,5 @@ document.querySelector('label [for="id_tag"]')
// Hide tag field since we use the pretty Materialize Chip setup instead
document.querySelector('#id_tag').style.display = 'none';
+{% endif %}
{% endblock %}
diff --git a/apprise_api/api/tests/test_add.py b/apprise_api/api/tests/test_add.py
index 1f22912..edc0cd2 100644
--- a/apprise_api/api/tests/test_add.py
+++ b/apprise_api/api/tests/test_add.py
@@ -25,6 +25,7 @@
from django.test import SimpleTestCase
from apprise import ConfigFormat
from unittest.mock import patch
+from django.test.utils import override_settings
from ..forms import AUTO_DETECT_CONFIG_KEYWORD
import json
@@ -38,6 +39,19 @@ class AddTests(SimpleTestCase):
response = self.client.get('/add/**invalid-key**')
assert response.status_code == 404
+ @override_settings(APPRISE_CONFIG_LOCK=True)
+ def test_save_config_by_urls_with_lock(self):
+ """
+ Test adding a configuration by URLs with lock set won't work
+ """
+ # our key to use
+ key = 'test_save_config_by_urls_with_lock'
+
+ # We simply do not have permission to do so
+ response = self.client.post(
+ '/add/{}'.format(key), {'urls': 'mailto://user:pass@yahoo.ca'})
+ assert response.status_code == 403
+
def test_save_config_by_urls(self):
"""
Test adding an configuration by URLs
@@ -99,6 +113,22 @@ class AddTests(SimpleTestCase):
)
assert response.status_code == 200
+ # Test with JSON (and no payload provided)
+ response = self.client.post(
+ '/add/{}'.format(key),
+ data=json.dumps({}),
+ content_type='application/json',
+ )
+ assert response.status_code == 400
+
+ # Test with XML which simply isn't supported
+ response = self.client.post(
+ '/add/{}'.format(key),
+ data='