Skip to content

Commit c6c6c72

Browse files
committed
Add unit tests for empty plugins upload
1 parent 90bfc04 commit c6c6c72

File tree

3 files changed

+447
-4
lines changed

3 files changed

+447
-4
lines changed

qgis-app/plugins/forms.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,20 @@ def clean_package_name(self):
222222
"Package name must start with a letter and can contain only ASCII letters, digits, '-' or '_'."
223223
)
224224
)
225-
if Plugin.objects.filter(package_name__iexact=package_name).exists():
225+
existing = Plugin.objects.filter(package_name__iexact=package_name).first()
226+
if existing:
226227
raise ValidationError(
227228
_("A plugin with a similar package name (%s) already exists.")
228-
% package_name
229+
% existing.package_name
229230
)
230231
return package_name
231232

232233
def clean_name(self):
233234
name = self.cleaned_data.get("name", "").strip()
234-
if Plugin.objects.filter(name__iexact=name).exists():
235+
existing = Plugin.objects.filter(name__iexact=name).first()
236+
if existing:
235237
raise ValidationError(
236-
_("A plugin with a similar name (%s) already exists.") % name
238+
_("A plugin with a similar name (%s) already exists.") % existing.name
237239
)
238240
return name
239241

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
"""
2+
Tests for creating empty plugins (without versions)
3+
"""
4+
5+
import os
6+
from unittest.mock import patch
7+
8+
from django.contrib.auth.models import User
9+
from django.core.files.uploadedfile import SimpleUploadedFile
10+
from django.test import Client, TestCase, override_settings
11+
from django.urls import reverse
12+
from plugins.forms import PluginCreateForm
13+
from plugins.models import Plugin, PluginVersion
14+
15+
16+
def do_nothing(*args, **kwargs):
17+
pass
18+
19+
20+
TESTFILE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "testfiles"))
21+
22+
23+
class PluginCreateEmptyTestCase(TestCase):
24+
"""Test creating empty plugins without versions"""
25+
26+
fixtures = [
27+
"fixtures/auth.json",
28+
]
29+
30+
@override_settings(MEDIA_ROOT="api/tests")
31+
def setUp(self):
32+
self.client = Client()
33+
self.url = reverse("plugin_create_empty")
34+
35+
# Create a test user
36+
self.user = User.objects.create_user(
37+
username="testuser",
38+
password="testpassword",
39+
email="test@example.com",
40+
first_name="Test",
41+
last_name="User",
42+
)
43+
44+
@patch("plugins.views.plugin_notify", new=do_nothing)
45+
def test_create_empty_plugin_get_request(self):
46+
"""Test GET request to create empty plugin page"""
47+
self.client.login(username="testuser", password="testpassword")
48+
49+
response = self.client.get(self.url)
50+
self.assertEqual(response.status_code, 200)
51+
self.assertIsInstance(response.context["form"], PluginCreateForm)
52+
self.assertContains(response, "Create an empty plugin")
53+
54+
def test_create_empty_plugin_requires_login(self):
55+
"""Test that anonymous users are redirected to login"""
56+
response = self.client.get(self.url)
57+
self.assertEqual(response.status_code, 302)
58+
self.assertIn("/accounts/login/", response.url)
59+
60+
@patch("plugins.views.plugin_notify", new=do_nothing)
61+
def test_create_empty_plugin_with_valid_data(self):
62+
"""Test creating an empty plugin with valid data"""
63+
self.client.login(username="testuser", password="testpassword")
64+
65+
data = {
66+
"package_name": "MyTestPlugin",
67+
"name": "My Test Plugin",
68+
}
69+
70+
response = self.client.post(self.url, data)
71+
72+
# Should redirect to token list
73+
self.assertEqual(response.status_code, 302)
74+
self.assertTrue(Plugin.objects.filter(package_name="MyTestPlugin").exists())
75+
76+
plugin = Plugin.objects.get(package_name="MyTestPlugin")
77+
self.assertEqual(plugin.name, "My Test Plugin")
78+
self.assertEqual(plugin.created_by, self.user)
79+
self.assertEqual(plugin.author, "Test User")
80+
self.assertEqual(plugin.email, "test@example.com")
81+
self.assertIn("Placeholder", plugin.description)
82+
83+
# Should have no versions yet
84+
self.assertEqual(plugin.pluginversion_set.count(), 0)
85+
86+
# Should redirect to plugin detail page
87+
expected_url = reverse("plugin_detail", args=[plugin.package_name])
88+
self.assertRedirects(response, expected_url)
89+
90+
@patch("plugins.views.plugin_notify", new=do_nothing)
91+
def test_create_empty_plugin_invalid_package_name_characters(self):
92+
"""Test that invalid package_name characters are rejected"""
93+
self.client.login(username="testuser", password="testpassword")
94+
95+
invalid_names = [
96+
"my plugin", # spaces
97+
"my/plugin", # slash
98+
"my.plugin", # dot
99+
"my@plugin", # special char
100+
"123plugin", # starts with digit
101+
"_myplugin", # starts with underscore
102+
]
103+
104+
for invalid_name in invalid_names:
105+
data = {
106+
"package_name": invalid_name,
107+
"name": "My Test Plugin",
108+
}
109+
110+
response = self.client.post(self.url, data)
111+
self.assertEqual(response.status_code, 200) # Form error, not redirect
112+
self.assertFalse(Plugin.objects.filter(package_name=invalid_name).exists())
113+
self.assertFormError(
114+
response,
115+
"form",
116+
"package_name",
117+
"Package name must start with a letter and can contain only ASCII letters, digits, '-' or '_'.",
118+
)
119+
120+
@patch("plugins.views.plugin_notify", new=do_nothing)
121+
def test_create_empty_plugin_valid_package_name_characters(self):
122+
"""Test that valid package_name characters are accepted"""
123+
self.client.login(username="testuser", password="testpassword")
124+
125+
valid_names = [
126+
("MyPlugin", "My Test Plugin One"),
127+
("my_plugin", "My Test Plugin Two"),
128+
("my-plugin", "My Test Plugin Three"),
129+
("MyPlugin123", "My Test Plugin Four"),
130+
]
131+
132+
for valid_name, plugin_name in valid_names:
133+
data = {
134+
"package_name": valid_name,
135+
"name": plugin_name,
136+
}
137+
138+
response = self.client.post(self.url, data)
139+
self.assertEqual(response.status_code, 302) # Successful redirect
140+
self.assertTrue(Plugin.objects.filter(package_name=valid_name).exists())
141+
142+
@patch("plugins.views.plugin_notify", new=do_nothing)
143+
def test_create_empty_plugin_duplicate_package_name(self):
144+
"""Test that duplicate package_name is rejected (case-insensitive)"""
145+
self.client.login(username="testuser", password="testpassword")
146+
147+
# Create first plugin
148+
data = {
149+
"package_name": "MyPlugin",
150+
"name": "My First Plugin",
151+
}
152+
response = self.client.post(self.url, data)
153+
self.assertEqual(response.status_code, 302)
154+
155+
# Try to create with same package_name (different case)
156+
data = {
157+
"package_name": "myplugin", # lowercase
158+
"name": "My Second Plugin",
159+
}
160+
response = self.client.post(self.url, data)
161+
self.assertEqual(response.status_code, 200) # Form error
162+
self.assertFormError(
163+
response,
164+
"form",
165+
"package_name",
166+
"A plugin with a similar package name (MyPlugin) already exists.",
167+
)
168+
169+
# Should still only have one plugin
170+
self.assertEqual(
171+
Plugin.objects.filter(package_name__iexact="myplugin").count(), 1
172+
)
173+
174+
@patch("plugins.views.plugin_notify", new=do_nothing)
175+
def test_create_empty_plugin_duplicate_name(self):
176+
"""Test that duplicate name is rejected (case-insensitive)"""
177+
self.client.login(username="testuser", password="testpassword")
178+
179+
# Create first plugin
180+
data = {
181+
"package_name": "MyPlugin1",
182+
"name": "My Awesome Plugin",
183+
}
184+
response = self.client.post(self.url, data)
185+
self.assertEqual(response.status_code, 302)
186+
187+
# Try to create with same name (different case)
188+
data = {
189+
"package_name": "MyPlugin2",
190+
"name": "my awesome plugin", # different case
191+
}
192+
response = self.client.post(self.url, data)
193+
self.assertEqual(response.status_code, 200) # Form error
194+
self.assertFormError(
195+
response,
196+
"form",
197+
"name",
198+
"A plugin with a similar name (My Awesome Plugin) already exists.",
199+
)
200+
201+
# Should still only have one plugin with that name
202+
self.assertEqual(
203+
Plugin.objects.filter(name__iexact="my awesome plugin").count(), 1
204+
)
205+
206+
@patch("plugins.views.plugin_notify", new=do_nothing)
207+
@patch("plugins.tasks.generate_plugins_xml", new=do_nothing)
208+
@patch("plugins.validator._check_url_link", new=do_nothing)
209+
@patch("plugins.security_utils.run_security_scan", new=do_nothing)
210+
def test_upload_version_after_creating_empty_plugin(self):
211+
"""Test that a version can be uploaded after creating an empty plugin"""
212+
self.client.login(username="testuser", password="testpassword")
213+
214+
# Create empty plugin
215+
data = {
216+
"package_name": "test_modul",
217+
"name": "Test Module Plugin",
218+
}
219+
response = self.client.post(self.url, data)
220+
self.assertEqual(response.status_code, 302)
221+
222+
plugin = Plugin.objects.get(package_name="test_modul")
223+
self.assertEqual(plugin.pluginversion_set.count(), 0)
224+
225+
# Now upload a version
226+
valid_plugin = os.path.join(TESTFILE_DIR, "valid_plugin.zip_")
227+
with open(valid_plugin, "rb") as file:
228+
uploaded_file = SimpleUploadedFile(
229+
"valid_plugin.zip_", file.read(), content_type="application/zip"
230+
)
231+
232+
upload_url = reverse("version_create", args=[plugin.package_name])
233+
response = self.client.post(
234+
upload_url,
235+
{
236+
"package": uploaded_file,
237+
},
238+
)
239+
240+
self.assertEqual(response.status_code, 302)
241+
242+
# Plugin should now have one version
243+
plugin.refresh_from_db()
244+
self.assertEqual(plugin.pluginversion_set.count(), 1)
245+
246+
version = plugin.pluginversion_set.first()
247+
self.assertEqual(version.version, "0.0.1")
248+
249+
# Metadata should be updated from the package
250+
self.assertNotIn("Placeholder", plugin.description)
251+
252+
@patch("plugins.views.plugin_notify", new=do_nothing)
253+
def test_create_empty_plugin_user_without_email(self):
254+
"""Test creating plugin when user has no email"""
255+
# Create user without email
256+
user_no_email = User.objects.create_user(
257+
username="noemail", password="testpassword"
258+
)
259+
self.client.login(username="noemail", password="testpassword")
260+
261+
data = {
262+
"package_name": "NoEmailPlugin",
263+
"name": "No Email Plugin",
264+
}
265+
266+
response = self.client.post(self.url, data)
267+
self.assertEqual(response.status_code, 302)
268+
269+
plugin = Plugin.objects.get(package_name="NoEmailPlugin")
270+
# Should have a default email
271+
self.assertEqual(plugin.email, "noreply@example.com")
272+
273+
@patch("plugins.views.plugin_notify", new=do_nothing)
274+
def test_create_empty_plugin_user_without_full_name(self):
275+
"""Test creating plugin when user has no first/last name"""
276+
# Create user without first/last name
277+
user_no_name = User.objects.create_user(
278+
username="noname", password="testpassword", email="test@example.com"
279+
)
280+
self.client.login(username="noname", password="testpassword")
281+
282+
data = {
283+
"package_name": "NoNamePlugin",
284+
"name": "No Name Plugin",
285+
}
286+
287+
response = self.client.post(self.url, data)
288+
self.assertEqual(response.status_code, 302)
289+
290+
plugin = Plugin.objects.get(package_name="NoNamePlugin")
291+
# Should use username as author
292+
self.assertEqual(plugin.author, "noname")
293+
294+
def tearDown(self):
295+
self.client.logout()

0 commit comments

Comments
 (0)