2018-03-19 16:26:10 +00:00
|
|
|
import os
|
2019-03-20 18:48:36 +00:00
|
|
|
import shutil
|
2018-03-19 16:26:10 +00:00
|
|
|
|
2019-07-10 14:34:33 +00:00
|
|
|
import sys
|
2020-04-02 20:58:58 +00:00
|
|
|
|
2018-07-30 15:55:46 +00:00
|
|
|
from django.contrib.auth.models import User
|
2018-02-23 22:48:32 +00:00
|
|
|
from django.test import Client
|
|
|
|
from rest_framework import status
|
|
|
|
|
2019-03-20 18:48:36 +00:00
|
|
|
from app.models import Plugin
|
2018-07-30 15:55:46 +00:00
|
|
|
from app.models import Project
|
|
|
|
from app.models import Task
|
2019-07-10 14:34:33 +00:00
|
|
|
from app.plugins import UserDataStore, enable_plugin
|
2018-03-19 16:26:10 +00:00
|
|
|
from app.plugins import get_plugin_by_name
|
2019-07-10 14:34:33 +00:00
|
|
|
from app.plugins import sync_plugin_db, get_plugins_persistent_path
|
2018-07-30 15:55:46 +00:00
|
|
|
from app.plugins.data_store import InvalidDataStoreValue
|
2019-07-10 14:34:33 +00:00
|
|
|
from app.plugins.pyutils import parse_requirements, compute_file_md5, requirements_installed
|
2018-02-23 22:48:32 +00:00
|
|
|
from .classes import BootTestCase
|
2018-05-13 17:30:44 +00:00
|
|
|
|
2018-02-23 22:48:32 +00:00
|
|
|
|
|
|
|
class TestPlugins(BootTestCase):
|
|
|
|
def setUp(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def test_core_plugins(self):
|
|
|
|
client = Client()
|
|
|
|
|
2020-04-02 18:29:27 +00:00
|
|
|
# We cannot access public files core plugins (plugin is disabled)
|
2018-03-02 16:30:16 +00:00
|
|
|
res = client.get('/plugins/test/file.txt')
|
2020-04-02 18:29:27 +00:00
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
2018-02-23 22:48:32 +00:00
|
|
|
|
2020-04-02 18:29:27 +00:00
|
|
|
# Cannot access mount point (plugin is disabled)
|
2018-03-02 16:30:16 +00:00
|
|
|
res = client.get('/plugins/test/app_mountpoint/')
|
2020-04-02 18:29:27 +00:00
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
2018-03-02 16:30:16 +00:00
|
|
|
|
2019-07-10 14:34:33 +00:00
|
|
|
# No python packages have been installed (plugin is disabled)
|
|
|
|
self.assertFalse(os.path.exists(get_plugins_persistent_path("test", "site-packages")))
|
|
|
|
|
|
|
|
enable_plugin("test")
|
|
|
|
|
2020-04-02 18:29:27 +00:00
|
|
|
# Python packages have been installed
|
|
|
|
self.assertTrue(os.path.exists(get_plugins_persistent_path("test", "site-packages")))
|
|
|
|
|
|
|
|
# We can access public files core plugins (without auth)
|
|
|
|
res = client.get('/plugins/test/file.txt')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
# We mounted an endpoint
|
|
|
|
res = client.get('/plugins/test/app_mountpoint/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
2021-06-08 15:51:16 +00:00
|
|
|
self.assertTemplateUsed(res, 'coreplugins/test/templates/app.html')
|
2020-04-02 18:29:27 +00:00
|
|
|
|
2018-07-30 15:55:46 +00:00
|
|
|
# Form was rendered correctly
|
2020-04-02 18:29:27 +00:00
|
|
|
self.assertContains(res,
|
|
|
|
'<input type="text" name="testField" class="form-control" required id="id_testField" />',
|
|
|
|
count=1, status_code=200, html=True)
|
2018-07-30 15:55:46 +00:00
|
|
|
|
2018-03-02 16:30:16 +00:00
|
|
|
# It uses regex properly
|
|
|
|
res = client.get('/plugins/test/app_mountpoint/a')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
# Querying a page should show the included CSS/JS files
|
|
|
|
client.login(username='testuser', password='test1234')
|
|
|
|
res = client.get('/dashboard/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
self.assertContains(res, "<link href='/plugins/test/test.css' rel='stylesheet' type='text/css'>", html=True)
|
|
|
|
self.assertContains(res, "<script src='/plugins/test/test.js'></script>", html=True)
|
|
|
|
|
|
|
|
# And our menu entry
|
|
|
|
self.assertContains(res, '<li><a href="/plugins/test/menu_url/"><i class="test-icon"></i> Test</a></li>', html=True)
|
|
|
|
|
2018-03-19 16:26:10 +00:00
|
|
|
# A node_modules directory has been created as a result of npm install
|
|
|
|
# because we have a package.json in the public director
|
|
|
|
test_plugin = get_plugin_by_name("test")
|
|
|
|
self.assertTrue(os.path.exists(test_plugin.get_path("public/node_modules")))
|
|
|
|
|
2020-04-02 20:58:58 +00:00
|
|
|
# This is a persistent plugin
|
|
|
|
self.assertTrue(test_plugin.is_persistent())
|
|
|
|
|
2018-07-30 15:55:46 +00:00
|
|
|
# A webpack file and build directory have been created as a
|
|
|
|
# result of the build_jsx_components directive
|
|
|
|
self.assertTrue(os.path.exists(test_plugin.get_path("public/webpack.config.js")))
|
|
|
|
self.assertTrue(os.path.exists(test_plugin.get_path("public/build")))
|
|
|
|
self.assertTrue(os.path.exists(test_plugin.get_path("public/build/component.js")))
|
|
|
|
|
|
|
|
# Test task view
|
|
|
|
user = User.objects.get(username="testuser")
|
|
|
|
project = Project.objects.get(owner=user)
|
|
|
|
task = Task.objects.create(project=project, name="Test")
|
|
|
|
client.logout()
|
|
|
|
|
|
|
|
# Cannot see the task view without logging-in
|
|
|
|
res = client.get('/plugins/test/task/{}/'.format(task.id))
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
client.login(username='testuser', password='test1234')
|
|
|
|
res = client.get('/plugins/test/task/{}/'.format(task.id))
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertContains(res, str(task.id))
|
|
|
|
|
|
|
|
# Test dynamic script
|
|
|
|
res = client.get('/plugins/test/app_dynamic_script.js')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertTrue(res.content.decode('utf-8') == '') # Empty
|
|
|
|
|
|
|
|
res = client.get('/plugins/test/app_dynamic_script.js?print=1')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertTrue(res.content.decode('utf-8') == "console.log('Hello WebODM');") # Empty
|
|
|
|
|
2019-07-10 14:34:33 +00:00
|
|
|
# Check that the plugins media dirs have been created
|
|
|
|
self.assertTrue(os.path.exists(get_plugins_persistent_path()))
|
|
|
|
self.assertTrue(os.path.exists(get_plugins_persistent_path("test", "site-packages")))
|
|
|
|
self.assertEqual(get_plugins_persistent_path("test", "site-packages"), test_plugin.get_python_packages_path())
|
|
|
|
|
|
|
|
# Check MD5 install has been created
|
|
|
|
self.assertTrue(os.path.exists(test_plugin.get_python_packages_path("install_md5")))
|
|
|
|
with open(test_plugin.get_python_packages_path("install_md5"), "r") as f:
|
|
|
|
md5 = f.read().strip()
|
|
|
|
self.assertTrue(len(md5) > 20)
|
|
|
|
self.assertEqual(md5, compute_file_md5(test_plugin.get_path("requirements.txt")))
|
|
|
|
|
|
|
|
self.assertTrue(requirements_installed(test_plugin.get_path("requirements.txt"), test_plugin.get_python_packages_path()))
|
|
|
|
|
|
|
|
# Test python imports context
|
|
|
|
self.assertFalse(test_plugin.get_python_packages_path() in sys.path)
|
|
|
|
with test_plugin.python_imports():
|
|
|
|
self.assertTrue(test_plugin.get_python_packages_path() in sys.path)
|
|
|
|
self.assertFalse(test_plugin.get_python_packages_path() in sys.path)
|
|
|
|
|
|
|
|
# Parse requirements test
|
|
|
|
self.assertEqual(parse_requirements(test_plugin.get_path("requirements.txt"))[0], "pyodm")
|
|
|
|
|
|
|
|
# Current plugin test
|
|
|
|
self.assertEqual(test_plugin.get_current_plugin_test(), test_plugin)
|
|
|
|
|
|
|
|
|
2018-07-30 15:55:46 +00:00
|
|
|
def test_plugin_datastore(self):
|
2019-07-10 14:34:33 +00:00
|
|
|
enable_plugin("test")
|
2018-07-30 15:55:46 +00:00
|
|
|
test_plugin = get_plugin_by_name("test")
|
|
|
|
user = User.objects.get(username='testuser')
|
|
|
|
other_user = User.objects.get(username='testuser2')
|
|
|
|
|
|
|
|
uds = test_plugin.get_user_data_store(user)
|
|
|
|
other_uds = test_plugin.get_user_data_store(other_user)
|
|
|
|
gds = test_plugin.get_global_data_store()
|
|
|
|
|
|
|
|
# No key
|
|
|
|
self.assertFalse(uds.has_key('mykey'))
|
|
|
|
|
|
|
|
# Default value works
|
|
|
|
self.assertTrue(uds.get_string('mykey', 'default') == 'default')
|
|
|
|
|
|
|
|
# Still no key should have been added
|
|
|
|
self.assertFalse(uds.has_key('mykey'))
|
|
|
|
|
|
|
|
# Add key
|
|
|
|
(object, created) = uds.set_string('mykey', 'value')
|
|
|
|
self.assertTrue(object.string_value == 'value')
|
|
|
|
self.assertTrue(created)
|
|
|
|
self.assertTrue(uds.has_key('mykey'))
|
|
|
|
|
|
|
|
# Key is not visible in global datastore
|
|
|
|
self.assertFalse(gds.has_key('mykey'))
|
|
|
|
|
|
|
|
# Key is not visible in another user's data store
|
|
|
|
self.assertFalse(other_uds.has_key('mykey'))
|
|
|
|
|
|
|
|
# Key is not visible in another's plugin data store
|
|
|
|
# for the same user
|
|
|
|
other_puds = UserDataStore('test2', user)
|
|
|
|
self.assertFalse(other_puds.has_key('mykey'))
|
|
|
|
|
|
|
|
# Deleting a non-existing key return False
|
|
|
|
self.assertFalse(uds.del_key('nonexistant'))
|
|
|
|
|
|
|
|
# Deleting a valid key returns True
|
|
|
|
self.assertTrue(uds.del_key('mykey'))
|
|
|
|
self.assertFalse(uds.has_key('mykey'))
|
|
|
|
|
|
|
|
# Various data types setter/getter work
|
|
|
|
uds.set_int('myint', 5)
|
|
|
|
self.assertTrue(uds.get_int('myint') == 5)
|
|
|
|
|
|
|
|
uds.set_float('myfloat', 10.0)
|
|
|
|
self.assertTrue(uds.get_float('myfloat', 50.0) == 10.0)
|
|
|
|
|
|
|
|
uds.set_bool('mybool', True)
|
|
|
|
self.assertTrue(uds.get_bool('mybool'))
|
|
|
|
|
|
|
|
uds.set_json('myjson', {'test': 123})
|
|
|
|
self.assertTrue('test' in uds.get_json('myjson'))
|
|
|
|
|
|
|
|
# Invalid types
|
|
|
|
self.assertRaises(InvalidDataStoreValue, uds.set_bool, 'invalidbool', 5)
|
|
|
|
|
2019-03-20 18:48:36 +00:00
|
|
|
def test_toggle_plugins(self):
|
|
|
|
c = Client()
|
|
|
|
c.login(username='testuser', password='test1234')
|
|
|
|
|
|
|
|
# Cannot toggle plugins as normal user
|
|
|
|
res = c.get('/admin/app/plugin/test/disable/', follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/login/?next=/admin/app/plugin/test/disable/')
|
|
|
|
|
|
|
|
c.login(username='testsuperuser', password='test1234')
|
|
|
|
|
2019-07-10 14:34:33 +00:00
|
|
|
enable_plugin("test")
|
|
|
|
|
2019-03-20 18:48:36 +00:00
|
|
|
# Test plugin is enabled
|
|
|
|
res = c.get('/admin/app/plugin/')
|
|
|
|
self.assertContains(res, '<a class="button" href="#" disabled>Enable</a>')
|
|
|
|
self.assertContains(res, "<script src='/plugins/test/test.js'></script>")
|
|
|
|
|
|
|
|
# Disable
|
|
|
|
res = c.get('/admin/app/plugin/test/disable/', follow=True)
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
# Test active vs. non-active flag for get_plugin_by_name
|
|
|
|
self.assertTrue(get_plugin_by_name("test") is None)
|
|
|
|
self.assertFalse(get_plugin_by_name("test", only_active=False) is None)
|
|
|
|
|
|
|
|
# Test plugin has been disabled
|
|
|
|
self.assertContains(res, '<a class="button" href="#" disabled>Disable</a>')
|
|
|
|
self.assertNotContains(res, "<script src='/plugins/test/test.js'></script>")
|
|
|
|
|
|
|
|
# Re-enable
|
|
|
|
res = c.get('/admin/app/plugin/test/enable/', follow=True)
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
|
|
|
|
def test_plugin_functions(self):
|
|
|
|
# Check db/fs syncing
|
2021-06-08 15:51:16 +00:00
|
|
|
if os.path.exists('coreplugins/test_copy'):
|
2019-03-20 18:48:36 +00:00
|
|
|
print("Removing plugins/test_copy")
|
2021-06-08 15:51:16 +00:00
|
|
|
shutil.rmtree('coreplugins/test_copy')
|
2019-03-20 18:48:36 +00:00
|
|
|
|
|
|
|
sync_plugin_db()
|
|
|
|
self.assertTrue(Plugin.objects.filter(pk='test_copy').count() == 0)
|
|
|
|
|
2021-06-08 15:51:16 +00:00
|
|
|
shutil.copytree('coreplugins/test', 'coreplugins/test_copy')
|
2019-03-20 18:48:36 +00:00
|
|
|
|
|
|
|
sync_plugin_db()
|
|
|
|
self.assertTrue(Plugin.objects.filter(pk='test_copy').count() == 1)
|
|
|
|
|
2021-06-08 15:51:16 +00:00
|
|
|
shutil.rmtree('coreplugins/test_copy')
|
2019-03-20 18:48:36 +00:00
|
|
|
sync_plugin_db()
|
|
|
|
self.assertTrue(Plugin.objects.filter(pk='test_copy').count() == 0)
|
|
|
|
|
|
|
|
# Get manifest works and parses JSON
|
2019-07-10 14:34:33 +00:00
|
|
|
p = get_plugin_by_name("test", only_active=False)
|
2019-03-20 18:48:36 +00:00
|
|
|
self.assertEqual(p.get_manifest()['author'], "Piero Toffanin")
|
|
|
|
|
|
|
|
|
2020-04-02 20:58:58 +00:00
|
|
|
def test_plugin_loading(self):
|
|
|
|
c = Client()
|
|
|
|
|
|
|
|
plugin_file = open("app/fixtures/testabc_plugin.zip", 'rb')
|
|
|
|
bad_dir_plugin_file = open("app/fixtures/bad_dir_plugin.zip", 'rb')
|
|
|
|
missing_manifest_plugin_file = open("app/fixtures/missing_manifest_plugin.zip", 'rb')
|
|
|
|
|
|
|
|
# Cannot upload new plugins anonymously
|
|
|
|
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/login/?next=/admin/app/plugin/actions/upload/')
|
|
|
|
self.assertFalse(os.path.exists(get_plugins_persistent_path("testabc")))
|
|
|
|
plugin_file.seek(0)
|
|
|
|
|
|
|
|
# Cannot upload plugins as a normal user
|
|
|
|
c.login(username='testuser', password='test1234')
|
|
|
|
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/login/?next=/admin/app/plugin/actions/upload/')
|
|
|
|
self.assertFalse(os.path.exists(get_plugins_persistent_path("testabc")))
|
|
|
|
self.assertEqual(Plugin.objects.filter(pk='testabc').count(), 0)
|
|
|
|
plugin_file.seek(0)
|
|
|
|
|
|
|
|
# Can upload plugin as an admin
|
|
|
|
c.login(username='testsuperuser', password='test1234')
|
|
|
|
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/app/plugin/')
|
|
|
|
messages = list(res.context['messages'])
|
|
|
|
self.assertTrue('Plugin added successfully' in str(messages[0]))
|
|
|
|
self.assertTrue(os.path.exists(get_plugins_persistent_path("testabc")))
|
|
|
|
plugin_file.seek(0)
|
|
|
|
|
|
|
|
# Plugin has been added to db
|
|
|
|
self.assertEqual(Plugin.objects.filter(pk='testabc').count(), 1)
|
|
|
|
|
|
|
|
# This is not a persistent plugin
|
|
|
|
self.assertFalse(get_plugin_by_name('testabc').is_persistent())
|
|
|
|
|
|
|
|
# Cannot upload the same plugin again (same name)
|
|
|
|
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/app/plugin/')
|
|
|
|
messages = list(res.context['messages'])
|
|
|
|
self.assertTrue('already exist' in str(messages[0]))
|
|
|
|
plugin_file.seek(0)
|
|
|
|
|
|
|
|
# Can access paths (while being logged in)
|
|
|
|
res = c.get('/plugins/testabc/hello/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
res = c.get('/api/plugins/testabc/hello/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
# Can access public paths as logged-in, (per plugin directive)
|
|
|
|
res = c.get('/plugins/testabc/file.txt')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
c.logout()
|
|
|
|
|
|
|
|
# Can still access the paths as anonymous
|
|
|
|
res = c.get('/plugins/testabc/hello/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
res = c.get('/api/plugins/testabc/hello/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
# But not the public paths as anonymous (per plugin directive)
|
|
|
|
res = c.get('/plugins/testabc/file.txt')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
# Cannot delete plugin as normal user
|
|
|
|
c.login(username='testuser', password='test1234')
|
|
|
|
res = c.get('/admin/app/plugin/testabc/delete/', follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/login/?next=/admin/app/plugin/testabc/delete/')
|
|
|
|
|
|
|
|
# Can delete plugin as admin
|
|
|
|
c.login(username='testsuperuser', password='test1234')
|
|
|
|
res = c.get('/admin/app/plugin/testabc/delete/', follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/app/plugin/')
|
|
|
|
messages = list(res.context['messages'])
|
|
|
|
|
|
|
|
# No errors
|
|
|
|
self.assertEqual(len(messages), 0)
|
|
|
|
|
|
|
|
# Directories have been removed
|
|
|
|
self.assertFalse(os.path.exists(get_plugins_persistent_path("testabc")))
|
|
|
|
|
|
|
|
# Cannot access the paths as anonymous
|
|
|
|
res = c.get('/plugins/testabc/hello/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
res = c.get('/api/plugins/testabc/hello/')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
res = c.get('/plugins/testabc/file.txt')
|
|
|
|
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
# Try to add malformed plugins files
|
|
|
|
res = c.post('/admin/app/plugin/actions/upload/', {'file': missing_manifest_plugin_file}, follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/app/plugin/')
|
|
|
|
messages = list(res.context['messages'])
|
|
|
|
self.assertTrue('Cannot load plugin' in str(messages[0]))
|
|
|
|
self.assertFalse(os.path.exists(get_plugins_persistent_path("test123")))
|
|
|
|
self.assertEqual(Plugin.objects.filter(pk='test123').count(), 0)
|
|
|
|
missing_manifest_plugin_file.seek(0)
|
|
|
|
|
|
|
|
res = c.post('/admin/app/plugin/actions/upload/', {'file': bad_dir_plugin_file}, follow=True)
|
|
|
|
self.assertRedirects(res, '/admin/app/plugin/')
|
|
|
|
messages = list(res.context['messages'])
|
|
|
|
self.assertTrue('Cannot load plugin' in str(messages[0]))
|
|
|
|
missing_manifest_plugin_file.seek(0)
|
|
|
|
|
|
|
|
plugin_file.close()
|
|
|
|
missing_manifest_plugin_file.close()
|
|
|
|
bad_dir_plugin_file.close()
|
2019-03-20 18:48:36 +00:00
|
|
|
|